AiTechWorlds
AiTechWorlds
Error handling is the code that decides what happens when something goes wrong instead of letting the program crash. It works like a car's airbag: the crash still happens, but the airbag controls the outcome instead of leaving it to chance. Good error handling is what separates scripts from production software when something goes wrong (network failure, invalid input, missing file), your code should fail gracefully with clear messages, not with cryptic tracebacks.
BaseException
├── SystemExit — sys.exit() was called
├── KeyboardInterrupt — Ctrl+C
├── GeneratorExit — generator/coroutine closed
└── Exception ← Most errors you'll catch
├── ValueError — wrong value type/format
├── TypeError — wrong type
├── KeyError — dict key not found
├── IndexError — sequence index out of range
├── AttributeError — object has no attribute
├── NameError — name not defined
├── OSError — system errors (FileNotFoundError, PermissionError)
├── RuntimeError — misc runtime errors
├── StopIteration — iterator exhausted
└── ArithmeticError
├── ZeroDivisionError
└── OverflowErrordef divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero")
return None
except TypeError as e:
print(f"Wrong types: {e}")
return None
else:
# Runs only if NO exception occurred
print(f"Success: {result}")
return result
finally:
# ALWAYS runs — even if exception was raised
print("divide() finished")
divide(10, 2) # Success: 5.0 / divide() finished
divide(10, 0) # Cannot divide by zero / divide() finished
divide(10, "x") # Wrong types / divide() finisheddef parse_config(data):
try:
config = json.loads(data) # JSONDecodeError if invalid JSON
port = int(config['port']) # KeyError if 'port' missing; ValueError if not int
assert 1 <= port <= 65535 # AssertionError if out of range
return config
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON: {e}") from e
except KeyError as e:
raise ValueError(f"Missing required key: {e}") from e
except (ValueError, AssertionError) as e:
raise ValueError(f"Invalid config: {e}") from e
# Catch any exception (usually too broad — use sparingly)
try:
risky_operation()
except Exception as e:
print(f"Something went wrong: {type(e).__name__}: {e}")
raise # Re-raise the same exception after loggingimport logging
logger = logging.getLogger(__name__)
def process_payment(amount, card_number):
try:
return charge_card(card_number, amount)
except ConnectionError as e:
logger.error(f"Payment gateway unreachable: {e}")
# Raise a business-level exception that hides implementation details
raise PaymentError("Payment service temporarily unavailable") from e
except AuthError as e:
raise PaymentError("Card declined — please try another card") from e
# 'from e' sets __cause__ — Python shows both exceptions in traceback
# Use 'from None' to suppress the original exception in the traceback
try:
result = some_function()
except SpecificError:
raise PublicError("Something went wrong") from None # Hides implementation detail# Define a hierarchy of custom exceptions
class AppError(Exception):
"""Base class for all application errors."""
def __init__(self, message, code=None, details=None):
super().__init__(message)
self.message = message
self.code = code
self.details = details or {}
def __str__(self):
parts = [self.message]
if self.code:
parts.append(f"(code: {self.code})")
return " ".join(parts)
def to_dict(self):
return {
"error": type(self).__name__,
"message": self.message,
"code": self.code,
"details": self.details
}
class ValidationError(AppError):
"""Input validation failed."""
pass
class AuthenticationError(AppError):
"""Authentication failed."""
pass
class AuthorizationError(AppError):
"""Insufficient permissions."""
pass
class NotFoundError(AppError):
"""Resource not found."""
pass
class ExternalServiceError(AppError):
"""External API or service failed."""
pass
# Usage
def get_user(user_id):
if not isinstance(user_id, int) or user_id <= 0:
raise ValidationError(
f"Invalid user ID: {user_id}",
code="INVALID_ID",
details={"user_id": user_id, "expected": "positive integer"}
)
user = database.find(user_id)
if user is None:
raise NotFoundError(
f"User {user_id} not found",
code="USER_NOT_FOUND",
details={"user_id": user_id}
)
return user
# API error handler
def handle_request(request):
try:
result = process(request)
return {"status": "success", "data": result}
except ValidationError as e:
return {"status": "error", "http_status": 400, **e.to_dict()}
except AuthenticationError as e:
return {"status": "error", "http_status": 401, **e.to_dict()}
except NotFoundError as e:
return {"status": "error", "http_status": 404, **e.to_dict()}
except AppError as e:
return {"status": "error", "http_status": 500, **e.to_dict()}# Using contextlib.suppress
from contextlib import suppress
# Ignore specific exceptions
with suppress(FileNotFoundError, PermissionError):
import os
os.remove("temp_file.txt")
# Equivalent to:
try:
os.remove("temp_file.txt")
except (FileNotFoundError, PermissionError):
pass# BAD: swallowing exceptions silently
try:
result = complex_operation()
except Exception:
pass # Silent failure — bugs become mysteries
# BAD: catching too broadly
try:
user = get_user(user_id)
except Exception as e:
return "Error" # Which error? What happened?
# GOOD: catch specific exceptions, handle them specifically
try:
user = get_user(user_id)
except NotFoundError:
return None # User doesn't exist — caller handles this
except ValidationError as e:
raise # Programming error — let it propagate
# GOOD: log and re-raise if you can't handle it
try:
result = risky_operation()
except SomeSpecificError as e:
logger.error(f"Operation failed: {e}", exc_info=True)
raise # Don't swallow it# LBYL (Look Before You Leap) — more common in other languages
if 'key' in my_dict:
value = my_dict['key']
else:
value = default
# EAFP (Easier to Ask Forgiveness than Permission) — more Pythonic
try:
value = my_dict['key']
except KeyError:
value = default
# Even better — use .get()
value = my_dict.get('key', default)import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(name)s %(levelname)s %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler() # Also print to console
]
)
logger = logging.getLogger(__name__)
def process_order(order):
try:
validate_order(order)
payment = charge_payment(order)
logger.info(f"Order {order['id']} processed: ${order['amount']:.2f}")
return payment
except ValidationError as e:
logger.warning(f"Invalid order {order.get('id')}: {e}")
raise
except PaymentError as e:
logger.error(f"Payment failed for order {order.get('id')}: {e}")
raise
except Exception as e:
logger.critical(f"Unexpected error processing order: {e}", exc_info=True)
raise
# exc_info=True adds the full traceback to the logimport traceback
def debug_error():
try:
risky()
except Exception:
# Get full traceback as a string
error_text = traceback.format_exc()
print(error_text)
# Or get the traceback info structured
tb_info = traceback.extract_tb(sys.exc_info()[2])
for frame in tb_info:
print(f" File: {frame.filename}, Line: {frame.lineno}, "
f"Function: {frame.name}")Well structured error handling makes debugging fast, makes APIs clear about what went wrong, and prevents cryptic failures from reaching users.
Next lesson: Modules, Packages & pip organizing code and using the Python ecosystem.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises