03.07 — Discriminated unions + TypeGuard + @final = “sealed” unions#
The combo that makes
Result[T]andTestResultboth typed and usable without casts.
Discriminated union#
@final
@dataclass(frozen=True)
class Ok[T](_BaseResult):
value: T
error: None = None
@final
@dataclass(frozen=True)
class Fail(_BaseResult):
error: Exception = field(default_factory=lambda: Exception("Unknown error"))
type Result[T] = Ok[T] | Fail- Two constructors:
Ok[T]andFail. - Discriminant is the isinstance check (not a field like
tag: Literal["ok", "fail"]). - Both are
@final— no possible subtype. The union is exhaustive, i.e. “sealed”.
Narrowing, TypeGuard#
def is_ok[T](result: Result[T]) -> TypeGuard[Ok[T]]:
return isinstance(result, Ok)
def is_fail[T](result: Result[T]) -> TypeGuard[Fail]:
return isinstance(result, Fail)def handle_result(result: Result[int]) -> str:
if is_ok(result):
return f"Got: {result.value}" # mypy: result is Ok[int], so .value exists
if is_fail(result):
return f"Error: {result.error}" # mypy: result is Fail, so .error exists
# mypy can detect this branch is unreachable (exhaustiveness)_BaseResult#
class _BaseResult:
error: Exception | NoneThis base lets you access result.error without narrowing: