Exceptions
try/except/else/finally, raise...from for chaining, and ExceptionGroup for handling multiple concurrent failures in Python 3.11+.
Errors in Python are exceptions - objects that inherit from BaseException (or usually from its subclass Exception). When something goes wrong, Python raises one of these objects. You catch it with a try / except block. The optional else clause runs only when try succeeds with no exception; finally runs no matter what.
The try block wraps code that might fail. Each except clause names the exception type (or types) it handles. You can catch multiple types in one clause with a tuple. The else clause - which many developers overlook - runs only when the try block completes without raising, which keeps "success" logic separate from "error" logic. finally always runs, making it the right place for cleanup.
import json
def parse_config(text: str) -> dict:
try:
data = json.loads(text)
except json.JSONDecodeError as err:
print(f"Invalid JSON: {err}")
return {}
except (TypeError, ValueError) as err:
print(f"Unexpected error: {err}")
return {}
else:
# Only runs when json.loads succeeded
print(f"Parsed {len(data)} keys")
return data
finally:
# Always runs, even if an exception propagated
print("parse_config done")
parse_config('{"key": "value"}')
# Parsed 1 keys
# parse_config done
parse_config("not json")
# Invalid JSON: ...
# parse_config doneWhen you catch an exception and raise a different one, you can preserve the original error as the "cause" using raise NewError(...) from original. Python then shows both errors in the traceback, which makes debugging much easier. Using raise NewError(...) without from discards the original context. The clause raise ... from None explicitly suppresses the cause - use it deliberately, not as a default.
Separately: except Exception: is the safe broad catch for user code, because it does not catch KeyboardInterrupt or SystemExit - those subclass BaseException directly and should propagate. Bare except: catches everything including KeyboardInterrupt and is almost always wrong.
class DatabaseError(Exception):
pass
def load_user(user_id: int) -> dict:
try:
# Simulate a low-level error
raise ConnectionError("socket timeout after 30s")
except ConnectionError as original:
# Wrap with domain context, preserve the cause
raise DatabaseError(f"Failed to load user {user_id}") from original
try:
load_user(42)
except DatabaseError as err:
print(err) # Failed to load user 42
print(err.__cause__) # socket timeout after 30s# Safe broad catch
try:
risky_operation()
except Exception as err:
# KeyboardInterrupt and SystemExit still propagate
log(err)
# Dangerous - catches KeyboardInterrupt too
try:
risky_operation()
except: # bare except - avoid
passPython 3.11 introduced ExceptionGroup for cases where multiple things fail at once - most commonly when several async tasks run concurrently and more than one raises an error. An ExceptionGroup holds a list of exceptions. The new except* syntax (note the asterisk) matches exception groups, letting you handle specific member types while letting others propagate.
import asyncio
async def might_fail(name: str, should_fail: bool) -> str:
await asyncio.sleep(0)
if should_fail:
raise ValueError(f"{name} failed")
return name
async def main() -> None:
# TaskGroup raises ExceptionGroup if any task fails
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(might_fail("a", False))
tg.create_task(might_fail("b", True))
tg.create_task(might_fail("c", True))
except* ValueError as eg:
for err in eg.exceptions:
print(f"Caught ValueError: {err}")
# Output:
# Caught ValueError: b failed
# Caught ValueError: c failed
asyncio.run(main())In production
except Exception: catches almost everything but does not catch KeyboardInterrupt or SystemExit (those subclass BaseException directly), so except Exception is the safe broad catch and bare except: is the dangerous one. Always chain with raise NewError(...) from original_error so the traceback preserves the cause - bare raise NewError(...) discards the original context and makes bugs much harder to trace. ExceptionGroup and except* (3.11+) handle the asyncio fan-out case where multiple tasks fail concurrently.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free