← Back to Blog

The types_module contract: registration is an import side effect

·8 min read

pyRPC has no registration list. No plugin discovery, no directory scan, no decorators collected into a manifest. There is exactly one mechanism: importing a module executes its @rpc decorators, which insert procedures into the router singleton. Every codegen feature rests on this sentence, and v0.13.0 makes you name that module explicitly.

Diagram: @rpc decorators fire during import, populating default_router, which feeds the schema endpoint and codegen

The failure mode that forced the contract

Consider the standard Django layout: procedures declared in views.py, wired in urls.py. If your configured module was the entrypoint, the watcher would import urls.py... or think it did. Here is the trap the old flow contained:

  • reload_module restores the edited module’s bytecode but does not re-execute its importers.
  • urls.py stays cached in sys.modules; its top-level from . import views does not run again.
  • The decorators never re-fire. The router keeps whatever was registered before your edit.
  • Regenerated types are stale, silently, with a green checkmark.

For FastAPI single-file apps the entrypoint is the registration module, so the bug hid. Split-module layouts exposed it. The fix is not cleverer reloading machinery, it is naming the module whose import registers everything, and importing exactly that:

// pyrpc.json, django layout
{ "backend": {
 "framework": "django",
 "entrypoint": "manage.py",
 "types_module": "myproject.views" // <- the file with @rpc
} }

Defaults that fail loudly

For fastapi/flask/asgi, types_module defaults to the module part of entrypoint, usually right, because those files typically call mount_*(app) themselves. For Django there is no honest default ( settings.py registers nothing; guessing views paths is how silent emptiness happens), so the contract requires it: omit it and resolution fails with a message telling you what to name. The wizard auto-detects the shallowest */views.py under the project and offers it preselected.

The general shape of the lesson

Import-side-effect systems are compact and delightful until someone asks “which import?” At that moment the honest answers are only ever: a named module or a real registry. We picked naming, one string in one JSON file, because it composes with reload semantics instead of fighting them, and because the failure mode of naming wrong (empty schema, loud) beats the failure mode of scanning wrong (partial schema, quiet).

If you remember one line from this post: codegen reflects what imported, not what exists. Point the name at the file where your procedures live.