We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Error Handling with Exceptions, Retries and Warnings step 20 of 20
Return Errors as Values with a Result Type
In Type Hints for Code That Can Fail, parse_count returned
Parsed | Invalid. Here you build the reusable version. Ok holds a success,
Err holds a failure, and attempt calls a function and turns the
exceptions you choose into an Err:
attempt(lambda: int("21"), (ValueError,)) # Ok(value=21)
attempt(lambda: int("x"), (ValueError,)) # Err(error=ValueError("invalid literal for int() with base 10: 'x'"))
attempt(lambda: int("x"), (KeyError,)) # raises the ValueError
Syntax you need
class Ok[T, E]:makesOka generic class.TandEare type parameters, placeholders that each use fills in, so anOk[int, ValueError]holds anint.@dataclass(frozen=True, slots=True), already in the starter, writes__init__,__repr__and__eq__from the fields.frozen=Truemakes the fields read-only, andslots=Truestores them without a per-instance dictionary.type Result[T, E] = Ok[T, E] | Err[T, E]gives the union a name.Callable[[T], U]is a function that takes aTand returns aU.Neveris the return type of a function that always raises, as in Mark Functions That Never Return.
Ok and Err
Ok[T, E] has one field, value: T. Err[T, E] has one field, error: E.
Each class takes both type parameters, although it stores only one, so its
methods can name the other side's type. Give both classes these methods:
| Method | On Ok |
On Err |
|---|---|---|
is_ok() -> bool |
Return True |
Return False |
map(fn) |
Call fn once on the value and return Ok of the result |
Return Err with the same error, without calling fn |
map_err(fn) |
Return Ok with the same value, without calling fn |
Call fn once on the error and return Err of the result |
unwrap() |
Return the value | Raise UnwrapError |
unwrap_or(default) |
Return the value | Return default |
mapchangesTto a new typeU, andmap_errchangesEto a new typeF. Declare each new parameter on its method, as indef map[U](self, fn: Callable[[T], U]) -> "Ok[U, E]".- The side that does not change may return a new wrapper around the same stored object.
- Let exceptions from
fnescape.mapandmap_errcatch nothing. unwrap_or(default: T) -> Treturns the value of anyOk, even0or"". Do not test truthiness.Err.unwrap()returnsNeverand raisesUnwrapError(f"unwrap() called on Err({self.error!r})"). Thenunwrap()on aResult[T, E]has the typeT.is_ok()returns a plainbool, which mypy cannot use to narrow the union. Checkisinstance(result, Err)before you read.error.
attempt
attempt(fn, catching) calls fn once, with no arguments.
- If
fnreturns, returnOkwith its value. - If
fnraises an exception that matches a class incatching, returnErrholding that same exception object, so its notes and other attributes survive. - Matching includes subclasses, as in an
exceptclause.(LookupError,)catches aKeyError, but(KeyError,)does not catch a plainLookupError. - Any other exception escapes unchanged. An empty tuple catches nothing.
E: BaseException in the signature is a bound. It lets a caller ask attempt
to catch any exception class, even KeyboardInterrupt, which inherits from
BaseException but not from Exception. solve below asks only for
ValueError and KeyError.
solve
solve(source, fallback) builds a producer function, runs it through
attempt with (ValueError, KeyError), and reports the outcome. The producer:
- raises when
source.get("kind") == "raise". It looks up the class namedsource.get("exc", "ValueError")inRAISABLEand raises it with the messagesource.get("message", ""). An unknown class name makes the lookup raiseKeyError, whichattemptcatches like any otherKeyError. - otherwise returns
int(source.get("value", "0")). Text that is not a whole number raisesValueError. - ignores any other keys.
On the Result that attempt returns, map the number to twice its value, then
to a string. Map the error to its class name, type(exc).__name__. Report on
that final Result.
Wrap the attempt call in try/except BaseException, so an exception that
escapes it becomes a report too. Keep that catch separate from the catching
inside attempt. Return exactly these keys:
| Key | Result returned | Exception escaped |
|---|---|---|
"outcome" |
"handled" |
"escaped" |
"escaped_type" |
None |
The exception's class name |
"is_ok" |
The final result's is_ok() |
None |
"err" |
The Err's error, now a class name, otherwise None |
None |
"unwrap_or" |
The success string, or fallback for an Err |
None |
"unwrap_or_type" |
"str" |
None |
"unwrapped" |
The success string, otherwise None |
None |
"unwrap_raised" |
"UnwrapError" for an Err, otherwise None |
None |
For {"value": "21"}, the success string is "42". For a caught
ValueError, err is "ValueError" and unwrap_or is fallback. A
TypeError or KeyboardInterrupt from the producer escapes attempt and
uses the second column.
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.