Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Result and match

Project overview and reading path

An importer reads integer labels. A malformed row should be reported while valid rows continue. The caller needs both the parsed value and a way to recognize rejection.

Typical Python

def parse_label(raw: str) -> int:
    return int(raw)


labels = [parse_label(raw) for raw in ("7", "cat", "2")]

The annotation exposes only success. "cat" raises ValueError at runtime and interrupts the import. Returning -1 instead would still look like a valid integer to the checker.

Alternative

Result: TypeAlias = Ok[T] | Err[E]


def parse_label(raw: str) -> Result[int, InvalidLabel]:
    try:
        return Ok(LABEL.validate_python(raw))
    except ValidationError:
        return Err(InvalidLabel(raw, "not a nonnegative integer"))


outcome = parse_label("cat")
match outcome:
    case Ok(value=label):
        description = f"class {label}"
    case Err(error=error):
        description = f"rejected {error.raw!r}: {error.reason}"
    case _:
        assert_never(outcome)

Source

parse_label("7") returns Ok(7); parse_label("cat") returns an Err carrying the rejected input and reason. describe_label uses match to consume either outcome. It produces "class 7" or a rejection message.

Result and match work together: the union exposes failure, matching narrows the payload, and assert_never checks that no variant is forgotten. Assigning the result directly to int, or reading .value before narrowing, is rejected. A Raises: docstring cannot provide that checked contract.

Rust’s standard library provides Result<T, E> as an enum. Python does not provide the equivalent, so this example uses an ordinary union of two small frozen dataclasses, without inheritance or a third-party package. Ok and Err give a reusable shape when an operation has one success type and an expected error. Some later lessons use a direct union such as JobMetadata | InvalidMetadata: naming the domain variants directly keeps those examples focused. Both designs require narrowing before the success value is used; match plus assert_never checks coverage of either closed union.

Use Result for supported retry, fallback, correction, or record rejection. Use exceptions when the operation has no recovery path and must unwind. Translate specific recoverable library exceptions at the boundary; do not disguise bugs. Deliberately raising for a broken internal invariant is analogous to Rust’s panic!: the operation cannot produce a valid result under that condition. Python can still raise unexpected exceptions, and a caller can discard the whole result. No return annotation proves exception freedom.

The parser accepts nonnegative integer-valued text, including "7.0"; it does not know the dataset’s class count. The two frozen variants freeze their own fields, not mutable payloads. Keep this small union rather than adding a result framework or unchecked unwrap methods.