Verified against Claude Code · 2026-08-05
Design a custom exception hierarchy callers can actually catch selectively
A prompt for a library-level exception hierarchy with a single common base, semantically distinct subclasses, and preserved exception chaining, instead of one flat CustomError class that forces every caller into the same broad except block regardless of what actually went wrong.
The prompt
Ready to copy — highlighted parts are example details you can swap.
Design a custom exception hierarchy for the library or module described below. The goal is a hierarchy callers can actually catch selectively — not one flat exception class that forces every caller to catch everything the same way regardless of what actually failed. MODULE A library called "orderkit" that parses and validates e-commerce order export files. FAILURE MODES A malformed file that can't be parsed at all, a row with an invalid discount code, a row referencing a product ID that doesn't exist, a network timeout when checking a product ID against a remote catalog. CALLER NEEDS A malformed file should abort the whole import immediately; an invalid discount code or missing product should be logged and that one row skipped; a network timeout should trigger a retry, not a skip. REQUIREMENTS 1. Define one base exception for this module/library (e.g. class OrderKitError(Exception)) that every other exception in it inherits from, so a caller who genuinely wants to catch anything this module can raise has one type to catch — but no caller should be forced to use that broad catch if A malformed file should abort the whole import immediately; an invalid discount code or missing product should be logged and that one row skipped; a network timeout should trigger a retry, not a skip. shows they need to react differently to different failures. If The main application defines AppError(Exception) at app/core/errors.py, and every internal library is expected to raise subclasses of it. names a project-wide base exception this library is expected to fit under, inherit from that instead of Exception directly, so a caller catching the whole application's error base still catches this module's failures too. 2. For each distinct failure mode in A malformed file that can't be parsed at all, a row with an invalid discount code, a row referencing a product ID that doesn't exist, a network timeout when checking a product ID against a remote catalog., create a specifically named subclass, not a generic one reused for multiple unrelated situations — a caller catching InvalidDiscountCodeError should never also silently catch a database connectivity failure because both happened to reuse the same exception type out of laziness. 3. Give each exception class the specific attributes a catcher would need to act on the failure programmatically, not just a human-readable message string — e.g. an InvalidDiscountCodeError should carry the actual code that was rejected as a real attribute, not only buried inside a formatted message a caller would have to regex out. 4. When raising a custom exception in response to catching a different underlying exception (a database driver's own exception, say), use raise CustomError(...) from original_exception, never a bare raise CustomError(...) that discards the original — losing the original exception's traceback and type is losing real debugging information for no benefit. 5. Decide deliberately whether any of A malformed file that can't be parsed at all, a row with an invalid discount code, a row referencing a product ID that doesn't exist, a network timeout when checking a product ID against a remote catalog. should be a subclass of a relevant Python builtin (a ValueError-like input problem might reasonably subclass ValueError too, using multiple inheritance, so callers who only know standard exception types still catch it correctly) versus purely custom — state the reasoning per exception, don't apply the same rule to all of them by default. 6. Document, in each exception class's docstring, exactly when it's raised and what a caller can safely assume about the state of the system after catching it (was anything partially written? is it safe to retry?). OUTPUT FORMAT 1. The exception hierarchy, as a class diagram in text (indented to show inheritance) before the code. 2. The exception classes with their attributes and docstrings. 3. One example: a caller that catches two different subclasses differently, and one that catches the base class broadly, showing both are genuinely supported.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Requiring a specifically named subclass per distinct failure mode, rather than one flat exception type reused everywhere, directly targets the actual cost of a flat hierarchy: a caller who wants to retry on a transient network timeout but skip-and-log on a bad discount code cannot express that distinction at all if both failures raise the same OrderKitError, and is forced into either catching everything the same way (retrying a permanent validation failure pointlessly) or parsing the exception's message string to guess what actually happened, which is exactly the kind of brittle string-matching a typed exception hierarchy exists to make unnecessary. The requirement to attach real attributes, not just a message string, matters because the caller_needs field almost always implies programmatic action on the failure, not just logging it for a human to read later — a caller that needs to log "which discount code was rejected" needs exc.code as an actual attribute it can read, and forcing it to regex a formatted message string to extract that value is fragile in a way that breaks the moment someone tweaks the message wording for readability, with no compiler or type checker to catch the mismatch. The raise ... from original_exception requirement preserves something Python's exception chaining specifically exists to provide: the __cause__ attribute and the "The above exception was the direct cause of the following exception" traceback section, which is often the only trace of what actually went wrong at the lowest level — a bare re-raise of a new exception type discards this, so when a caller's InvalidDiscountCodeError turns out to actually be masking a database connection blip that made a lookup fail, that real cause is gone from the traceback entirely, and debugging has to start over from nothing rather than one traceback frame back. The deliberate builtin-subclassing decision matters because it changes who can catch the exception without importing this specific library at all — a caller doing a broad except ValueError for input-validation problems across an entire codebase will silently also catch an InvalidDiscountCodeError if it multiply-inherits from ValueError, which is sometimes exactly the intended, helpful behavior and sometimes an unwanted surprise, so it has to be a stated decision per exception rather than an accident of how the class happened to be written.
Verified against
Claude Code Sonnet 4.6 · 2026-08-05
Changelog
- 2026-08-05 — Initial publish, verified against Claude Code (Sonnet 4.6) on Python 3.12.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
