We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Failure by Design step 2 of 18
Exception hierarchies as API design
Every library ships two APIs: the one people call, and the one they except.
The second one is usually designed by accident.
When a payment client raises ValueError from one code path, KeyError from
another and requests.HTTPError from a third, callers have exactly two
options: catch Exception — swallowing their own bugs along with your
failures — or couple themselves to your internals and break the day you swap
HTTP libraries. Neither is acceptable, and both are your fault, not theirs.
The fix is one package-level base class, subclassed along the axis callers actually branch on. That axis is almost never “which internal module raised it”. It is recoverability: do I retry this, or do I stop and tell the user?
Two rules that carry most of the weight:
-
Structured attributes, not encoded messages.
retry_afterbelongs on the exception as afloat. The moment a caller has to regex your message text to find out how long to wait, your message text is a public API you did not know you had. -
Inherit from a builtin as well, when it is honestly true.
class ConfigurationError(YourLibError, ValueError)means everyexcept ValueErrorhandler your users already wrote keeps working. That is how you introduce a hierarchy into a shipped library without a breaking change.
Build the hierarchy
-
PaymentError(Exception)— the single class that means “anything this library raises”. -
TransientPaymentError(PaymentError)— worth retrying. Constructor(message: str, *, retry_after: float); storeretry_afteras an attribute. -
PermanentPaymentError(PaymentError)— retrying changes nothing. -
CardDeclinedError(PermanentPaymentError)— constructor(message: str, *, decline_code: str). -
ConfigurationError(PaymentError, ValueError)— the caller wired the client up wrong.
Then the classifier
def classify(status: int, body: Mapping[str, object]) -> PaymentError:
Rules, applied in this order:
-
status < 400— calling a classifier on a success response is a bug in the caller, and a bug in the caller is aValueError. ReturnConfigurationError. -
status == 429, or500 <= status <= 599— returnTransientPaymentError.retry_afterisbody["retry_after"]when that value is anintor afloat, converted tofloat; otherwise1.0. -
status == 402— returnCardDeclinedError.decline_codeisbody["decline_code"]when that value is astr; otherwise"unknown". -
statusis401or403— returnConfigurationError. -
anything else — return
PermanentPaymentError.
Note that classify returns the error rather than raising it. That is
deliberate: it keeps the function pure, and it is how you would want to unit
test the mapping in a real client.
Then the probe
def solve(events: Sequence[Mapping[str, object]]) -> list[dict[str, object]]:
Each event is {"status": <int>, "body": <mapping>}. Classify it and append
one record per event, in order, with exactly these keys:
| key | value |
|---|---|
"type" |
type(error).__name__ |
"payment_error" |
isinstance(error, PaymentError) |
"value_error" |
isinstance(error, ValueError) |
"transient" |
isinstance(error, TransientPaymentError) |
"permanent" |
isinstance(error, PermanentPaymentError) |
"retry_after" |
the attribute if it is a transient error, else None |
"decline_code" |
the attribute if it is a declined card, else None |
Message strings are not asserted — the whole point is that callers branch on type and attribute, never on prose.
What the type checker is doing for you here
body: Mapping[str, object] hands you object, not Any. float(raw) on an
object is an error until you narrow it with isinstance. That is the type
system telling you the truth about a JSON body: you do not know what is in
there, so check.
Likewise classify is declared to return PaymentError, so .retry_after
does not exist as far as mypy is concerned until an isinstance narrows it —
which is exactly the discipline you want callers to inherit.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.