forked from blshaer/python-by-example
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path02_custom_exceptions.py
More file actions
408 lines (330 loc) · 12.1 KB
/
02_custom_exceptions.py
File metadata and controls
408 lines (330 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
"""
================================================================================
File: 02_custom_exceptions.py
Topic: Creating and Using Custom Exceptions
================================================================================
This file demonstrates how to create your own exception classes in Python.
Custom exceptions make your code more expressive and allow you to handle
specific error conditions in a meaningful way.
Key Concepts:
- Why use custom exceptions
- Creating exception classes
- Exception with custom attributes
- Exception chaining
- Best practices
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Why Custom Exceptions?
# -----------------------------------------------------------------------------
print("--- Why Custom Exceptions? ---")
# Built-in exceptions are generic
# Custom exceptions:
# - Are more descriptive
# - Can carry additional data
# - Allow specific handling
# - Document error conditions
print("Benefits of custom exceptions:")
print(" - More meaningful error messages")
print(" - Carry context-specific data")
print(" - Enable targeted exception handling")
print(" - Self-documenting code")
# -----------------------------------------------------------------------------
# 2. Basic Custom Exception
# -----------------------------------------------------------------------------
print("\n--- Basic Custom Exception ---")
# Inherit from Exception (or a more specific built-in)
class InvalidAgeError(Exception):
"""Raised when an age value is invalid."""
pass
# Using the custom exception
def set_age(age):
"""Set age with validation."""
if age < 0:
raise InvalidAgeError("Age cannot be negative")
if age > 150:
raise InvalidAgeError("Age seems unrealistically high")
return age
# Catching custom exception
try:
set_age(-5)
except InvalidAgeError as e:
print(f"Caught InvalidAgeError: {e}")
try:
set_age(200)
except InvalidAgeError as e:
print(f"Caught InvalidAgeError: {e}")
# -----------------------------------------------------------------------------
# 3. Custom Exception with Attributes
# -----------------------------------------------------------------------------
print("\n--- Exception with Custom Attributes ---")
class ValidationError(Exception):
"""Raised when validation fails."""
def __init__(self, field, value, message):
self.field = field
self.value = value
self.message = message
# Call parent constructor with full message
super().__init__(f"{field}: {message} (got: {value})")
def to_dict(self):
"""Convert error to dictionary (useful for API responses)."""
return {
"field": self.field,
"value": self.value,
"message": self.message
}
# Using the enhanced exception
def validate_email(email):
"""Validate email format."""
if not email:
raise ValidationError("email", email, "Email is required")
if "@" not in email:
raise ValidationError("email", email, "Invalid email format")
return True
try:
validate_email("invalid-email")
except ValidationError as e:
print(f"Error: {e}")
print(f"Field: {e.field}")
print(f"Value: {e.value}")
print(f"As dict: {e.to_dict()}")
# -----------------------------------------------------------------------------
# 4. Exception Hierarchy
# -----------------------------------------------------------------------------
print("\n--- Exception Hierarchy ---")
# Create a base exception for your module/application
class AppError(Exception):
"""Base exception for the application."""
pass
class DatabaseError(AppError):
"""Database-related errors."""
pass
class ConnectionError(DatabaseError):
"""Database connection errors."""
pass
class QueryError(DatabaseError):
"""Query execution errors."""
pass
class AuthenticationError(AppError):
"""Authentication-related errors."""
pass
class PermissionError(AppError):
"""Permission-related errors."""
pass
# Now you can catch broadly or specifically
def database_operation():
raise QueryError("Invalid SQL syntax")
try:
database_operation()
except DatabaseError as e:
# Catches ConnectionError and QueryError
print(f"Database error: {e}")
except AppError as e:
# Catches all app errors
print(f"App error: {e}")
# -----------------------------------------------------------------------------
# 5. Exception Chaining
# -----------------------------------------------------------------------------
print("\n--- Exception Chaining ---")
class ProcessingError(Exception):
"""Error during data processing."""
pass
def parse_config(data):
"""Parse configuration data."""
try:
# Simulating a parsing error
if not data:
raise ValueError("Empty data")
return {"parsed": data}
except ValueError as e:
# Chain the original exception
raise ProcessingError("Failed to parse config") from e
try:
parse_config("")
except ProcessingError as e:
print(f"Processing error: {e}")
print(f"Caused by: {e.__cause__}")
# Using 'from None' to suppress chaining
def simple_error():
try:
raise ValueError("original")
except ValueError:
raise RuntimeError("new error") from None # Hides original
# -----------------------------------------------------------------------------
# 6. Practical Example: User Registration
# -----------------------------------------------------------------------------
print("\n--- Practical Example: User Registration ---")
class RegistrationError(Exception):
"""Base class for registration errors."""
pass
class UsernameError(RegistrationError):
"""Username-related errors."""
def __init__(self, username, reason):
self.username = username
self.reason = reason
super().__init__(f"Username '{username}': {reason}")
class PasswordError(RegistrationError):
"""Password-related errors."""
def __init__(self, issues):
self.issues = issues
super().__init__(f"Password issues: {', '.join(issues)}")
class EmailError(RegistrationError):
"""Email-related errors."""
pass
def validate_username(username):
"""Validate username."""
if len(username) < 3:
raise UsernameError(username, "Too short (min 3 chars)")
if not username.isalnum():
raise UsernameError(username, "Must be alphanumeric")
# Check if username exists (simulated)
existing = ["admin", "root", "user"]
if username.lower() in existing:
raise UsernameError(username, "Already taken")
return True
def validate_password(password):
"""Validate password strength."""
issues = []
if len(password) < 8:
issues.append("too short")
if not any(c.isupper() for c in password):
issues.append("needs uppercase")
if not any(c.islower() for c in password):
issues.append("needs lowercase")
if not any(c.isdigit() for c in password):
issues.append("needs digit")
if issues:
raise PasswordError(issues)
return True
def register_user(username, password, email):
"""Register a new user."""
try:
validate_username(username)
validate_password(password)
# validate_email(email) - already defined above
print(f" ✓ User '{username}' registered successfully!")
return True
except RegistrationError as e:
print(f" ✗ Registration failed: {e}")
return False
# Test registration
print("Registration attempts:")
register_user("ab", "password123", "test@example.com")
register_user("admin", "Password1", "test@example.com")
register_user("newuser", "weak", "test@example.com")
register_user("newuser", "StrongPass123", "test@example.com")
# -----------------------------------------------------------------------------
# 7. Context Manager with Exceptions
# -----------------------------------------------------------------------------
print("\n--- Exception in Context Manager ---")
class ManagedResource:
"""Resource with cleanup that handles exceptions."""
def __init__(self, name):
self.name = name
def __enter__(self):
print(f" Acquiring resource: {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" Releasing resource: {self.name}")
if exc_type is not None:
print(f" Exception occurred: {exc_type.__name__}: {exc_val}")
# Return True to suppress the exception
# Return False to propagate it
return False # Don't suppress exceptions
# Using the context manager
print("Context manager with exception:")
try:
with ManagedResource("database") as resource:
print(f" Using {resource.name}")
raise RuntimeError("Something went wrong!")
except RuntimeError:
print(" Caught exception outside")
# -----------------------------------------------------------------------------
# 8. Re-raising Exceptions
# -----------------------------------------------------------------------------
print("\n--- Re-raising Exceptions ---")
def process_data(data):
"""Process data with logging and re-raise."""
try:
# Processing that might fail
if not data:
raise ValueError("Empty data")
return data.upper()
except ValueError as e:
print(f" Logging error: {e}")
raise # Re-raise the same exception
try:
process_data("")
except ValueError as e:
print(f"Caught re-raised exception: {e}")
# -----------------------------------------------------------------------------
# 9. Exception Documentation
# -----------------------------------------------------------------------------
print("\n--- Exception Documentation ---")
class APIError(Exception):
"""
Base exception for API errors.
Attributes:
status_code: HTTP status code
message: Human-readable error message
error_code: Application-specific error code
Example:
>>> raise APIError(404, "Resource not found", "NOT_FOUND")
"""
def __init__(self, status_code: int, message: str, error_code: str = None):
"""
Initialize API error.
Args:
status_code: HTTP status code (e.g., 400, 404, 500)
message: Human-readable error description
error_code: Optional application error code
"""
self.status_code = status_code
self.message = message
self.error_code = error_code
super().__init__(message)
def to_response(self):
"""Convert to API response format."""
return {
"error": {
"code": self.error_code,
"message": self.message,
"status": self.status_code
}
}
# Using documented exception
try:
raise APIError(404, "User not found", "USER_NOT_FOUND")
except APIError as e:
print(f"API Error Response: {e.to_response()}")
# -----------------------------------------------------------------------------
# 10. Best Practices
# -----------------------------------------------------------------------------
print("\n--- Best Practices ---")
best_practices = """
1. Inherit from Exception, not BaseException
- BaseException includes SystemExit, KeyboardInterrupt
2. Create a hierarchy for related exceptions
- Base exception for your module/app
- Specific exceptions inherit from base
3. Add useful attributes
- Include context that helps debugging
- Provide methods for serialization (API responses)
4. Document your exceptions
- What conditions trigger them
- What attributes they have
- How to handle them
5. Use meaningful names
- End with 'Error' or 'Exception'
- Be specific: UserNotFoundError, not JustError
6. Don't over-catch
- except Exception: catches too much
- Be specific about what you expect
7. Re-raise when appropriate
- Log and re-raise for debugging
- Transform to appropriate exception type
8. Use exception chaining
- Use 'from' to preserve original exception
- Helps with debugging complex systems
"""
print(best_practices)