We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Error Handling with Exceptions, Retries and Warnings step 4 of 20
Build a Payment Error Hierarchy
In Catch Errors by Category, you caught errors from a payment family that
someone else wrote. Now you build the family yourself. You also write a
function that turns a failed charge into the right error. The gateway's answer is an HTTP
status code and a response body, which solve gets as data.
error = classify(402, {"decline_code": "expired_card"})
type(error).__name__ # "CardDeclinedError"
isinstance(error, PermanentPaymentError) # True
Build the family
The starter has PaymentError(Exception). Add four classes:
TransientPaymentError(PaymentError), with the constructor(message: str, *, retry_after: float). Store the delay inself.retry_after.PermanentPaymentError(PaymentError), with no extra fields.CardDeclinedError(PermanentPaymentError), with the constructor(message: str, *, decline_code: str). Store the code inself.decline_code.ConfigurationError(PaymentError, ValueError), so thatexcept ValueErrorcatches it too.
Each constructor passes the message to super().__init__(message). The
wording is up to you, because the tests check only classes and attributes.
Pick the error for a response
Write classify(status: int, body: Mapping[str, object]) -> PaymentError.
It returns the exception object and does not raise it. Apply the first rule
that fits:
- If
statusis below 400, return aConfigurationError. That status is not an error, so callingclassifywith it is a mistake in your setup. - If
statusis429, or 500 to 599 including both ends, return aTransientPaymentError. Ifbody.get("retry_after")is anintor afloat, convert it withfloat(). Otherwise use1.0. - If
statusis402, return aCardDeclinedError. Ifbody.get("decline_code")is a string, use it, even the empty string"". Otherwise use"unknown". - If
statusis401or403, return aConfigurationError. - For any other status, return a
PermanentPaymentError.
For retry_after, a bool counts as an int, because bool is a subclass
of int. So True becomes 1.0 and False becomes 0.0. Keep zero and
negative delays as they are. A string such as "2" gets 1.0.
Report each result
Write solve(events: Sequence[Mapping[str, object]]) -> list[dict[str, object]].
Return one record per event, in the same order, or [] for no events.
- Read
event["status"], so a missing status raisesKeyError. If the value is not anint, raiseTypeError. Aboolpasses this check, soTruecounts as status 1 and gets rule 1. - If the event's
bodyis a mapping, use it. If it is missing or has another type, use an empty dictionary. - Ignore any other keys.
Each record has 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 error's retry_after if it is transient, otherwise None |
"decline_code" |
the error's decline_code if it is a declined card, otherwise None |
isinstance matches parent classes too. So a declined card is permanent,
and a ConfigurationError is both a PaymentError and a ValueError.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.