pyRPC’s entire promise is that TypeScript types derived from Python survive the network boundary without drifting. Until last week, nothing verified the Python side of that promise statically: no mypy, no pyright, nothing. The TypeScript packages at least compiled through tsup builds, but no explicit gate failed when types broke. Both gaps are closed now, and both found real problems immediately.
The find: a transport lying about its contract
mypy’s first run over the five Python packages produced exactly thirteen errors in twenty-one files, and one of them was gold:
asgi.py:90: Argument 3 to "send_response" has incompatible type "dict[str, Any] | list[dict[str, Any]]"; expected "dict[str, Any]"
v0.13 introduced batched requests: handle_request accepts a list and returns one response per element. The ASGI transport passes the result straight to its serializer, which still declared dict-only. Python does not care (json.dumps is happy either way), so tests passed. But the annotation documented a contract that batching had silently broken. Widened to the union, with the comment trail pointing at the feature that changed it.
Fixing honestly instead of silencing
- Lazy globals. cli.py declared
default_router = Noneplaceholders for deferred imports. Replaced with annotation-only declarations carrying real types; the runtime assignments fill them in. - The _cwd stash. The dev server process carried its working directory as a dynamic attribute (
proc._cwd = ...) for config-triggered restarts. mypy rightly refused. Now there is a tiny_ServerProcess(Popen[bytes])subclass with a typedcwd, and the console reads it defensively for externally attached processes. - Invariants mypy cannot see. After the config block in dev(), spec and cfg_path are guaranteed non-None by construction. Two asserts with comments now state that invariant where the type system needs it. Asserts are runtime-checked honesty, not suppression.
- Untyped third parties (jsonc_edit, jsonschema_ts, django) get per-module ignore_missing_imports overrides rather than a global blindfold.
The TypeScript side
Each of the six npm packages gained a typecheck script running tsc --noEmit, aggregated by a root script and executed in CI after builds. All six passed on day one, which tracks with strict mode already being on; the value is that this stays true automatically. tsup emitting declarations during builds had been quietly serving as a weak proxy for this gate, and proxies erode.
Scope, stated plainly
Sources are covered; tests are not yet. That is a deliberate line drawn so the first gate ships fast and meaningful rather than slow and noisy. It is written down in CONTRIBUTING.md as a follow-up, because a scope you do not publish is a scope people assume is infinite.
For a framework whose debugging story begins with the phrase the types are generated from your Python signatures, checking our own types was overdue by exactly one shipped contract drift.

pyRPC