0
# Exception Handling and Error Management
1
2
Comprehensive exception classes for handling various error conditions in command-line applications, with proper error reporting and user-friendly messages. Click's exception system provides structured error handling that maintains good user experience.
3
4
## Capabilities
5
6
### Base Exception Classes
7
8
Foundation exception classes that provide structured error handling for CLI applications.
9
10
```python { .api }
11
class ClickException(Exception):
12
"""
13
Base exception that Click can handle and show to user.
14
15
Attributes:
16
- exit_code: int, exit code when exception causes termination (default: 1)
17
- message: str, error message to display
18
"""
19
20
def __init__(self, message):
21
"""
22
Create a Click exception.
23
24
Parameters:
25
- message: Error message to display to user
26
"""
27
28
def format_message(self):
29
"""Format the error message for display."""
30
31
def show(self, file=None):
32
"""
33
Display the error message to user.
34
35
Parameters:
36
- file: File object to write to (defaults to stderr)
37
"""
38
39
class UsageError(ClickException):
40
"""
41
Exception that signals incorrect usage and aborts further handling.
42
43
Attributes:
44
- exit_code: int, exit code (default: 2)
45
- ctx: Context, context where error occurred
46
- cmd: Command, command that caused the error
47
"""
48
49
def __init__(self, message, ctx=None):
50
"""
51
Create a usage error.
52
53
Parameters:
54
- message: Error message
55
- ctx: Context where error occurred
56
"""
57
58
def show(self, file=None):
59
"""Show error with usage information."""
60
```
61
62
**Usage Examples:**
63
64
```python
65
@click.command()
66
@click.argument('filename')
67
def process_file(filename):
68
"""Process a file."""
69
import os
70
71
if not os.path.exists(filename):
72
raise click.ClickException(f'File "{filename}" does not exist.')
73
74
if not filename.endswith('.txt'):
75
raise click.UsageError('Only .txt files are supported.')
76
77
click.echo(f'Processing {filename}...')
78
79
# Custom exception handling
80
@click.command()
81
@click.pass_context
82
def custom_error(ctx):
83
"""Demonstrate custom error handling."""
84
try:
85
# Some operation that might fail
86
result = risky_operation()
87
except SomeError as e:
88
ctx.fail(f'Operation failed: {e}')
89
```
90
91
### Parameter-Related Exceptions
92
93
Exceptions for handling parameter validation and parsing errors.
94
95
```python { .api }
96
class BadParameter(UsageError):
97
"""
98
Exception for bad parameter values.
99
100
Attributes:
101
- param: Parameter, parameter object that caused error
102
- param_hint: str, alternative parameter name for error display
103
"""
104
105
def __init__(self, message, ctx=None, param=None, param_hint=None):
106
"""
107
Create a bad parameter error.
108
109
Parameters:
110
- message: Error message
111
- ctx: Current context
112
- param: Parameter that caused error
113
- param_hint: Alternative name for parameter in error
114
"""
115
116
class MissingParameter(BadParameter):
117
"""
118
Exception raised when a required parameter is missing.
119
120
Attributes:
121
- param_type: str, type of parameter ("option", "argument", "parameter")
122
"""
123
124
def __init__(self, message=None, ctx=None, param=None, param_hint=None, param_type=None):
125
"""
126
Create a missing parameter error.
127
128
Parameters:
129
- message: Custom error message
130
- ctx: Current context
131
- param: Missing parameter
132
- param_hint: Alternative parameter name
133
- param_type: Type of parameter for error message
134
"""
135
136
class BadArgumentUsage(UsageError):
137
"""Exception for incorrect argument usage."""
138
139
class BadOptionUsage(UsageError):
140
"""
141
Exception for incorrect option usage.
142
143
Attributes:
144
- option_name: str, name of the incorrectly used option
145
"""
146
147
def __init__(self, option_name, message, ctx=None):
148
"""
149
Create a bad option usage error.
150
151
Parameters:
152
- option_name: Name of the problematic option
153
- message: Error message
154
- ctx: Current context
155
"""
156
157
class NoSuchOption(UsageError):
158
"""
159
Exception when an option doesn't exist.
160
161
Attributes:
162
- option_name: str, name of the invalid option
163
- possibilities: list, suggested alternative options
164
"""
165
166
def __init__(self, option_name, message=None, possibilities=None, ctx=None):
167
"""
168
Create a no such option error.
169
170
Parameters:
171
- option_name: Name of invalid option
172
- message: Custom error message
173
- possibilities: List of suggested alternatives
174
- ctx: Current context
175
"""
176
```
177
178
**Usage Examples:**
179
180
```python
181
# Custom parameter validation
182
def validate_email(ctx, param, value):
183
"""Validate email parameter."""
184
import re
185
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', value):
186
raise click.BadParameter('Invalid email format', ctx, param)
187
return value
188
189
@click.command()
190
@click.option('--email', callback=validate_email, required=True)
191
def send_email(email):
192
"""Send email with validation."""
193
click.echo(f'Sending email to {email}')
194
195
# Custom parameter type with exceptions
196
class PortType(click.ParamType):
197
name = 'port'
198
199
def convert(self, value, param, ctx):
200
try:
201
port = int(value)
202
except ValueError:
203
self.fail(f'{value} is not a valid integer', param, ctx)
204
205
if port < 1 or port > 65535:
206
self.fail(f'{port} is not in valid range 1-65535', param, ctx)
207
208
return port
209
210
@click.command()
211
@click.option('--port', type=PortType(), default=8080)
212
def start_server(port):
213
"""Start server with port validation."""
214
click.echo(f'Starting server on port {port}')
215
216
# Handling missing parameters
217
@click.command()
218
@click.option('--config', required=True,
219
help='Configuration file (required)')
220
def run_with_config(config):
221
"""Command requiring configuration."""
222
click.echo(f'Using config: {config}')
223
```
224
225
### File-Related Exceptions
226
227
Exceptions for file operations and I/O errors.
228
229
```python { .api }
230
class FileError(ClickException):
231
"""
232
Exception when a file cannot be opened.
233
234
Attributes:
235
- ui_filename: str, formatted filename for display
236
- filename: str, original filename
237
"""
238
239
def __init__(self, filename, hint=None):
240
"""
241
Create a file error.
242
243
Parameters:
244
- filename: Name of problematic file
245
- hint: Additional hint for the error
246
"""
247
248
def format_message(self):
249
"""Format error message with filename."""
250
```
251
252
**Usage Examples:**
253
254
```python
255
@click.command()
256
@click.argument('input_file', type=click.File('r'))
257
@click.argument('output_file', type=click.File('w'))
258
def copy_file(input_file, output_file):
259
"""Copy file with error handling."""
260
try:
261
content = input_file.read()
262
output_file.write(content)
263
click.echo('File copied successfully')
264
except IOError as e:
265
raise click.FileError(input_file.name, str(e))
266
267
# Manual file handling with exceptions
268
@click.command()
269
@click.argument('filename')
270
def read_file(filename):
271
"""Read file with custom error handling."""
272
try:
273
with open(filename, 'r') as f:
274
content = f.read()
275
click.echo(content)
276
except FileNotFoundError:
277
raise click.FileError(filename, 'File not found')
278
except PermissionError:
279
raise click.FileError(filename, 'Permission denied')
280
except IOError as e:
281
raise click.FileError(filename, str(e))
282
```
283
284
### Control Flow Exceptions
285
286
Special exceptions for controlling application flow and termination.
287
288
```python { .api }
289
class Abort(RuntimeError):
290
"""
291
Internal exception to signal that Click should abort.
292
Used internally by Click for graceful termination.
293
"""
294
295
class Exit(RuntimeError):
296
"""
297
Exception that indicates the application should exit with a status code.
298
299
Attributes:
300
- exit_code: int, status code to exit with
301
"""
302
303
def __init__(self, code=0):
304
"""
305
Create an exit exception.
306
307
Parameters:
308
- code: Exit status code (0 for success)
309
"""
310
```
311
312
**Usage Examples:**
313
314
```python
315
@click.command()
316
@click.option('--force', is_flag=True, help='Force operation')
317
def dangerous_operation(force):
318
"""Perform dangerous operation."""
319
if not force:
320
click.echo('This is a dangerous operation!')
321
if not click.confirm('Are you sure?'):
322
raise click.Abort()
323
324
click.echo('Performing dangerous operation...')
325
326
@click.command()
327
@click.argument('exit_code', type=int, default=0)
328
def exit_with_code(exit_code):
329
"""Exit with specified code."""
330
click.echo(f'Exiting with code {exit_code}')
331
raise click.Exit(exit_code)
332
333
# Context methods for error handling
334
@click.command()
335
@click.pass_context
336
def context_errors(ctx):
337
"""Demonstrate context error methods."""
338
339
# ctx.fail() raises UsageError
340
if some_condition:
341
ctx.fail('Something went wrong with usage')
342
343
# ctx.abort() raises Abort
344
if another_condition:
345
ctx.abort()
346
347
# ctx.exit() raises Exit
348
if success_condition:
349
ctx.exit(0)
350
```
351
352
### Exception Handling Best Practices
353
354
Comprehensive examples showing proper exception handling patterns in Click applications.
355
356
```python
357
# Comprehensive error handling example
358
@click.group()
359
@click.option('--verbose', is_flag=True, help='Enable verbose output')
360
@click.pass_context
361
def cli(ctx, verbose):
362
"""Main CLI with error handling."""
363
ctx.ensure_object(dict)
364
ctx.obj['verbose'] = verbose
365
366
@cli.command()
367
@click.argument('config_file', type=click.Path(exists=True, readable=True))
368
@click.option('--output', type=click.Path(writable=True), required=True)
369
@click.pass_context
370
def process_config(ctx, config_file, output):
371
"""Process configuration file with comprehensive error handling."""
372
import json
373
import yaml
374
375
try:
376
# Try to determine file format
377
if config_file.endswith('.json'):
378
with open(config_file, 'r') as f:
379
config = json.load(f)
380
elif config_file.endswith(('.yml', '.yaml')):
381
with open(config_file, 'r') as f:
382
config = yaml.safe_load(f)
383
else:
384
raise click.UsageError(
385
'Configuration file must be JSON (.json) or YAML (.yml/.yaml)'
386
)
387
388
except json.JSONDecodeError as e:
389
raise click.FileError(config_file, f'Invalid JSON: {e}')
390
except yaml.YAMLError as e:
391
raise click.FileError(config_file, f'Invalid YAML: {e}')
392
except PermissionError:
393
raise click.FileError(config_file, 'Permission denied')
394
395
# Validate configuration
396
required_keys = ['name', 'version']
397
for key in required_keys:
398
if key not in config:
399
raise click.BadParameter(
400
f'Missing required key: {key}',
401
ctx=ctx,
402
param_hint='config_file'
403
)
404
405
# Process and write output
406
try:
407
processed = process_configuration(config)
408
with open(output, 'w') as f:
409
json.dump(processed, f, indent=2)
410
411
if ctx.obj['verbose']:
412
click.echo(f'Successfully processed {config_file} -> {output}')
413
414
except IOError as e:
415
raise click.FileError(output, f'Cannot write output: {e}')
416
except Exception as e:
417
# Catch-all for unexpected errors
418
raise click.ClickException(f'Unexpected error: {e}')
419
420
# Error recovery example
421
@cli.command()
422
@click.option('--retry', default=3, help='Number of retries')
423
@click.pass_context
424
def unreliable_operation(ctx, retry):
425
"""Operation with retry logic."""
426
import random
427
import time
428
429
for attempt in range(retry + 1):
430
try:
431
if random.random() < 0.7: # 70% chance of failure
432
raise Exception('Random failure')
433
434
click.echo('Operation succeeded!')
435
return
436
437
except Exception as e:
438
if attempt < retry:
439
if ctx.obj['verbose']:
440
click.echo(f'Attempt {attempt + 1} failed: {e}')
441
click.echo(f'Retrying in 1 second...')
442
time.sleep(1)
443
else:
444
raise click.ClickException(
445
f'Operation failed after {retry + 1} attempts: {e}'
446
)
447
```