Type safety
How static checking (mypy) and runtime class-definition validation work together in PyMediate.
PyMediate's type safety comes from two different mechanisms working together, and they don't catch the same mistakes. Knowing which is which tells you what mypy will and won't surface before you run your code.
Static checking with mypy
mediator.send() infers its return type from the request's Request[ResponseT] type parameter, so mypy (and your editor) know the exact response type at the call site — no casts, no Any:
from dataclasses import dataclass
from pymediate import Request, Handler, Mediator, Services
@dataclass
class UserResponse:
user_id: int
username: str
@dataclass
class CreateUserRequest(Request[UserResponse]):
username: str
email: str
class CreateUserHandler(Handler[CreateUserRequest]):
def __call__(self, request: CreateUserRequest) -> UserResponse:
return UserResponse(user_id=1, username=request.username)
services = Services()
services.add(CreateUserHandler())
mediator = Mediator(services.provider())
response = mediator.send(CreateUserRequest(username="alice", email="alice@example.com"))
reveal_type(response) # note: Revealed type is "UserResponse"mypy also enforces that a handler's __call__ accepts the request type declared in Handler[RequestT] — get this wrong and mypy reports a Liskov substitution violation, like any incompatible method override:
class BadHandler(Handler[CreateUserRequest]):
def __call__(self, request: SomeOtherRequest) -> UserResponse:
...
# error: Argument 1 of "__call__" is incompatible with supertype "Handler";
# supertype defines the argument type as "CreateUserRequest" [override]Runtime checking at class-definition time
A handler's return type isn't checked by mypy the same way — a mismatched return type is a ResponseTypeMismatchError, raised by PyMediate's own __init_subclass__ validation rather than caught statically:
class BadHandler(Handler[CreateUserRequest]):
def __call__(self, request: CreateUserRequest) -> WrongResponse: # mypy: no error here
return WrongResponse(...)
# ResponseTypeMismatchError: Response type mismatch in BadHandler
# (raised the moment this class is defined, i.e. at import time)This still catches the mistake early — the error fires as soon as the module is imported, before any request is ever sent — just not during a mypy run. The same hook also validates that __call__ exists, takes exactly one parameter besides self, and is sync or async as expected (InvalidHandlerSignatureError), and that a request actually inherits from Request[ResponseType] (InvalidRequestTypeError). See error handling for the full hierarchy.
In short: wrong parameter type → mypy catches it. Wrong return type or malformed signature → PyMediate catches it at import time. Both happen before a request is ever processed; neither requires a test run to surface.
Running mypy on your own code
PyMediate itself is checked with mypy --strict, and similar settings work well for application code built on it:
[mypy]
python_version = 3.12
disallow_untyped_defs = false
check_untyped_defs = true
warn_return_any = trueA few things that keep mypy effective on PyMediate-based code:
- Always parameterize
Request.class CreateUserRequest(Request):(no type argument) type-checks, but you lose response-type inference entirely. - Prefer dataclasses over hand-written
__init__. mypy infers field types directly from dataclass annotations; a hand-rolled__init__is just as valid at runtime but gives mypy less to work with. See dataclasses with PyMediate. - Avoid
Anyin handler signatures. It silences exactly the override checking described above — a handler typeddef __call__(self, request: Any) -> Anywill never get an LSP violation from mypy, no matter what it actually does.
How PyMediate tests its own type safety
If you're contributing to PyMediate itself, tests/mypy/snippets/ holds small standalone scripts pinning down the library's type-checking contract: everything under valid/ must pass mypy --strict, everything under errors/ must fail it. The files in errors/ are deliberately broken — a type error there is the test working, not a bug to fix.
Next steps
- Requests and responses — why
Request[ResponseT]is the source of the inference - Error handling — the runtime validation exception hierarchy
- Testing — testing handlers and mediators directly