We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modelling Data with Dataclasses step 23 of 28
Build a Generic Result Type with Ok and Err
A pipeline of steps can fail at any step. Instead of raising, each step
can return a result, which is either Ok(value) for a success or
Err(error) for a failure. You chain the steps with methods. Once a step
fails, the later steps skip their work, and you never write an
isinstance check between steps.
Ok(4).and_then(_halve).map(_double) # Ok(value=4)
Ok(3).and_then(_halve).map(_double) # Err(error='odd')
Build Ok[T] and Err[E] as generic frozen dataclasses with
slots=True. Use the class Ok[T]: syntax, and no Any or
# type: ignore. The starter defines the alias:
type Result[T, E] = Ok[T] | Err[E]
Write the methods
Ok[T] has one field, value: T, and Err[E] has one field,
error: E. Keep the generated repr, such as Ok(value=6) and
Err(error='odd').
| Method | On Ok[T] |
On Err[E] |
|---|---|---|
map(fn) |
Call fn(value) once and return Ok of the result |
Return self without calling fn |
map_err(fn) |
Return self without calling fn |
Call fn(error) once and return Err of the result |
and_then(fn) |
Call fn(value) once and return what it returns, an Ok or an Err |
Return self without calling fn |
unwrap_or(default) |
Return the value | Return default |
Let an exception from a callback propagate. These methods do not turn
exceptions into an Err.
Annotate the methods
- A method that returns
selfreturnsSelf. Ok.mapbuilds a newOkwhose value can have another type, so it returnsOk[U], whereUis a type parameter of the method.Ok.and_thenhas two type parameters of its own, one for the new value and one for the new error.Ok.unwrap_oris(default: T) -> T.Err.unwrap_orhas its own type parameter fordefaultand returns that type.- Type a callback that the method skips as
Callable[[Never], object].Neveris a type with no values, so it is a subtype of every type. Callable parameters are contravariant, so any function that takes one positional argument fits. The method must not call it.
Run the pipeline
The starter gives you the step functions. Write _apply(result, op) with
a match on op:
op |
Call | What a success becomes |
|---|---|---|
"double" |
result.map(_double) |
The value times two |
"dec" |
result.map(_decrement) |
The value minus one |
"halve" |
result.and_then(_halve) |
Ok(value // 2) for an even value, Err("odd") for an odd one |
"guard" |
result.and_then(_nonneg) |
Err("negative") for a negative value, otherwise unchanged |
"shout" |
result.map_err(_shout) |
Unchanged. An error becomes uppercase. |
- An unknown
opraisesValueError(f"unknown op: {op!r}"), even when the result is anErr. - An
Errskips the success steps, but_applystill handles each later op, so a later"shout"still runs.
solve(start, ops) starts from Ok(start), applies the ops in order, and
returns a dict with four keys:
"kind"is"ok"or"err"."payload"is the value or the error string."unwrap_or"isunwrap_or(-1)on the final result."repr"is thereprof the final result.
With start=3 and ops=["halve", "double", "shout"], "halve" gives
Err("odd"), "double" skips it, and "shout" gives Err("ODD"). So
solve returns
{"kind": "err", "payload": "ODD", "unwrap_or": -1, "repr": "Err(error='ODD')"}.
An empty ops list returns Ok(start) unchanged.
Why an Ok[int] is not an Ok[object]
With the class Ok[T]: syntax, mypy infers variance from how the class
uses T. Ok.unwrap_or takes a T and returns one, so Ok is
invariant, and mypy rejects an Ok[int] where an Ok[object] is
expected. Freezing the dataclass does not change that. In Err, E
appears only in the frozen field and inside the callback type of
map_err, so mypy infers Err as covariant. The pipeline uses
Ok[int] | Err[str] throughout, so variance never matters there.
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.