03.06 — PEP 695 generics in Ocarina#
Ocarina leans hard on the generics syntax from PEP 695 (Python 3.12+). It’s what keeps the whole typed ecosystem from being a mess.
Three forms of PEP 695#
1. Generic classes#
@final
class Ok[T](_BaseResult):
value: T
error: None = None
class TestSuite[Driver]:
...
class Watcher[Driver]:
...
class ChainRunner[T]:
...
class ValidationStartBlock[T]:
...
class CliStore[TKeys: str]: # with bound
...Before PEP 695:
from typing import Generic, TypeVar
T = TypeVar("T")
class Ok(_BaseResult, Generic[T]):
value: T
error: None = None2. Parameterized type aliases#
type Result[T] = Ok[T] | Fail
type Thunk[T] = Callable[[], T]
type Action[T] = Thunk[Result[T]]
type FailureHandler = Callable[[Exception], None]
type Predicate[T] = Callable[[T], None]
type Effect = Callable[[], None]
type TestScenario[Driver] = Callable[[Driver, ILogger], Scenario[Driver]]
type TestScenarioFragment[Driver] = Callable[[Driver, ILogger], TestChain]
type TestChain = Sequence[ChainRunner[Any]]
type TestWatchers[Driver] = Sequence[Watcher[Driver]] | NoneBefore PEP 695:
from typing import TypeAlias, TypeVar, Callable, Sequence, Union
T = TypeVar("T")
Driver = TypeVar("Driver")
Result: TypeAlias = Union[Ok[T], Fail] # requires T in module-level scope
Thunk: TypeAlias = Callable[[], T]3. Generic functions#
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 chain_actions[T](first: ActionSuccess[T], *rest: ActionSuccess[T]) -> ChainRunner[T]:
...
def field[T](*, validate: ValidationChainBuilder[T]) -> _CliField[T]:
...
def bootstrap[T](*, test_cycle: TestCycle[T], ...) -> None:
...
def validate[T](value: T, *, name: str | None = None) -> ValidationStartBlock[T]:
...Before PEP 695, it was a pain.
bound (constrained TypeVar)#
TPOM = TypeVar("TPOM", bound="POMBase")One very specific case where declaring the type with TypeVar to bound it on another type (like an extends on a generic in TypeScript) still earns its keep.
You can also “bound” inline:
class CliStore[TKeys: str]:
...TKeys: str means “TKeys is a subtype of str” (notion of covariance).
TypeGuard (PEP 647)#
def is_ok[T](result: Result[T]) -> TypeGuard[Ok[T]]:
return isinstance(result, Ok)The TypeGuard[Ok[T]] tells the checker: “if this function returns True, then result has type Ok[T] in the True branch”:
result = some_action()
if is_ok(result):
print(result.value) # ✅ mypy knows result is Ok[T], so result.value existsWithout TypeGuard, result.value errors — mypy wouldn’t know it’s an Ok.
Self (PEP 673)#
class POMBase(ABC):
@abstractmethod
def verify(self, *, timeout: float | None = None) -> Self: ...
class HomePage(SeleniumTitleMixin, POMBase):
def open(self) -> Self: # ← Self, not HomePage
self._driver.get(self._url)
return selfLiteral (PEP 586)#
type Mode = Literal[
"fail-fast-on-first-smoke-campaigns-sequence-fail",
"wait-for-all-smoke-tests",
]
type SeleniumCliStoreKeys = Literal[
"driver_path", "profile_path", "browser", "headless", "workers",
"logger", "wait_timeout", "force_delete_tmp_dirs", "only", "exclude",
]
type SupportedSeleniumBrowser = Literal["chrome", "firefox", "edge", "safari"]
type SupportedLogger = Literal["terminal", "file", "terminal+file", "muted"]Literal[...] enables:
- Autocomplete in the IDE.
- Refusal of invalid values at compile time.
Protocol (PEP 544)#
class ScreenshotDriver(Protocol):
def save_screenshot(self, path: str) -> bool:
...
class SupportsWrite(Protocol[T_contra]):
def write(self, s: T_contra, /) -> Any: ...
class ITakeScreenshot[Driver](Protocol):
def __call__(self, driver: Driver, logger: ILogger, category: str) -> None: ...Protocol brings structural typing to Python: any object with the right interface qualifies, no explicit inheritance required. Close to duck typing, but statically verified by type checkers. Very handy for mocks, adapters, test fakes.
@final (PEP 591)#
@final
class Test[Driver]: ...
@final
class Watcher[Driver]: ...
@final
class ChainRunner[T]: ...
@final
@dataclass(frozen=True)
class Ok[T](_BaseResult): ...
@final
class TestCycle[Driver]: ...@final declares the class cannot be subclassed.
- The checker rejects
class MyOk(Ok[T]). - The checker can treat unions as sealed:
Result[T] = Ok[T] | Failexhaustively covers two cases (OkandFailare both@final). - The user is forced to compose instead of inherit.
Never (PEP 661)#
class _SilentArgumentParser(ArgumentParser):
def error(self, message: str) -> Never:
raise ValueError(message)Never declares that the function never returns — either it raises, or it loops forever. The checker can treat the flow as if nothing ever happens after this call.
Unpack (PEP 692)#
Used in ocarina-example:
# ocarina-example/lib/ext/selenium/humanize/proxy.py
class HumanizedDriver(WebDriver):
def __init__(
self, driver: WebDriver, **keyboard_config: Unpack[KeyboardConfig]
) -> None:
...KeyboardConfig is a TypedDict. Unpack[KeyboardConfig] lets you type **kwargs as the TypedDict’s keys without forcing them into the signature.
Consistency with the philosophy#
From the Holy Book (chapter “First feedbacks”):
The recent evolution of Python’s type system, which Ocarina leans deeply on, is part of the very reason it’s feasible at all.
Without PEP 695 + PEP 647 + PEP 673 + PEP 661 + PEP 692 + PEP 544 + PEP 586 + PEP 591, Ocarina’s DSL would be shaky. Python’s typing ecosystem is what makes the DSL possible.