You can get most of the “ADT/state-machine reliability” benefits in Python by combining static checking + tagged unions + boundary validation:
Model states as tagged unions (Union + Literal + dataclass(frozen=True)), use match (Py3.10+) and add assert_never so type checkers complain when you forget a case.
Run Pyright (strict) or mypy –strict in CI so “illegal states” show up as build failures, not incidents.
Validate/parsing at boundaries (HTTP/queues) with Pydantic discriminated unions (tagged unions at runtime), then keep internals typed.
For expected failures, prefer an explicit Result (e.g., returns) over exceptions-as-control-flow.
Use Ruff for lint/consistency (it’s not a type checker, but pairs well with one).