One base app exception plus specific subclasses, carrying structured context
Design an application exception tree with a single root, for example AppError, that subclasses Exception. Under it, group by domain or layer, such as ValidationError, NotFoundError, and ExternalServiceError. Each concrete error carries structured context as keyword arguments so logs and API responses can be built without string parsing. The root class is what callers can catch as a boundary; specific classes are what internal code raises and handles. That gives you a stable contract: the boundary catches AppError and maps it to a status code, while internal code catches the narrow type it can actually recover from. Do not inherit from Exception directly in dozens of unrelated places, and do not swallow the original exception; preserve it with raise ... from ... so the cause chain remains intact.
One root class per package or service; internal code raises specific subclasses.
Carry structured fields in init, such as code, resource_id, retryable, and details.
Keep the hierarchy shallow; three levels is usually enough. Deep hierarchies make catching ambiguous.
Map to transport at the boundary, not at the raise site. Let the web or gRPC layer translate AppError subclasses to status codes.
Common mistake: raising Exception or a generic string, which forces callers into string matching and hides recoverable categories.
Common mistake: catching a broad AppError and re-raising a different type without raise ... from ..., which loses the original traceback.
Version note: exception groups (PEP 654) arrived in 3.11 and add an except* syntax for handling multiple concurrent failures, which is worth using in concurrent code.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience