We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 11 of 24
A generic Result[T, E]
Generic classes are what make repo.get(user_id) return a User instead of
an Any — at every call site, forever, without a cast. Repositories, result
wrappers, caches, connection pools, paginated responses: the generic container
is the single highest-leverage type in most application codebases, because one
correct annotation propagates type information through hundreds of call sites
that nobody has to annotate individually.
PEP 695 (3.12) replaced the ceremony:
# before
T = TypeVar("T")
E = TypeVar("E")
class Result(Generic[T, E]): ...
# now
class Result[T, E]: ...
The runtime enforces the migration rather than allowing both: writing
class A[T](Generic[T]) is a hard TypeError at import time. There is no
gradual mode here — a class is either PEP 695 or it is not.
The task
class Result[T, E]:
@classmethod
def ok(cls, value: T) -> "Result[T, E]": ...
@classmethod
def err(cls, error: E) -> "Result[T, E]": ...
def is_ok(self) -> bool: ...
def unwrap(self) -> T: ...
def unwrap_or(self, default: T) -> T: ...
def map[U](self, f: Callable[[T], U]) -> "Result[U, E]": ...
def safe_div(numerator: int, denominator: int) -> Result[int, str]: ...
def solve(values: list[int], divisor: int) -> tuple[list[str], list[bool], list[int]]: ...
unwrap raises ValueError on an err. unwrap_or returns the default
instead. map applies f to an ok value and short-circuits on an err,
carrying the error through unchanged — that is the entire point of the type.
Note map declares its own type parameter [U]: the method is generic
independently of the class.
safe_div returns Result.err("division by zero") for a zero divisor and
Result.ok(numerator // denominator) otherwise — Python’s floor division, so
-3 // 2 is -2 and 10 // -2 is -5.
solve builds one Result per value and returns three lists: each result
mapped through lambda n: f"<{n}>" then unwrap_or("n/a"); each is_ok();
and each unwrap_or(-1).
The interesting design problem
unwrap must return a T, and the err case has no T to return. The naive
storage — self._value: T | None — makes unwrap unimplementable without a
cast, because T might itself include None and the checker cannot tell an
absent value from a present None.
The idiomatic fix is to store presence in the shape rather than the value:
self._value: tuple[T] | tuple[()]
A one-tuple means “present”, the empty tuple means “absent”, and mypy narrows
the union on len(self._value) == 1. No sentinel, no Optional, no cast.
This trick — encoding an option in a container’s arity — is worth having in your
pocket; it is exactly how you write a total first() or a total pop().
Types
Under --strict, --disallow-any-generics rejects a bare Result annotation:
def f() -> Result is an error, Result[int, str] is not. That is the flag
that stops a generic class from quietly degrading into an untyped one.
Two static facts the design is supposed to guarantee, worth checking in your editor before you submit:
-
Result.ok(1).map(str).unwrap()revealsstr, notAnyand notint. -
Result.ok(1).unwrap() + "x"is a type error, becauseunwrapreturnsint.
Generic must not appear anywhere in your submission, and neither may a
module-scope TypeVar.
Forward references inside the class body do need quoting — Result is not bound
until the class statement finishes executing, so -> "Result[U, E]" is
required in 3.12. (PEP 649’s deferred annotations change this in 3.14, but not
for a module targeting 3.12.)
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.