Until v0.13.0, pyrpc dev had one answer for every backend: uvicorn. FastAPI users shrugged, that is their normal. Flask users got an ASGI-wrapper detour. Django users were told to run manage.py runserver themselves in a second terminal. PR #141 replaced the one-size launcher with a resolver that launches each framework’s native dev server.
Why native matters
- Django is not just WSGI/ASGI. Settings loading, app registry initialization, and management commands are framework machinery. Running Django any other way re-implements pieces of
runserverbadly, static files handling and autoreload semantics being the classic victims. - Flask’s dev server already works. Wrapping a WSGI app in an ASGI bridge to serve it under uvicorn adds a dependency, a thread hop, and a second stack trace format to debug, to arrive at the same behavior
flask rungives you out of the box. - Error messages stay familiar. When your traceback says Flask or Django said something, it is because Flask or Django said it. No translation layer between you and your framework.
LaunchPlan: commands as data
The resolver does not spawn anything. It returns a frozen LaunchPlan, argv, optional env additions, optional working directory, and the caller spawns once:
plan = resolve_launch(spec, host=..., port=..., reload=..., base_cwd=...) proc = subprocess.Popen(plan.argv, cwd=plan.cwd or cwd, env=env)
Commands-as-data made the whole matrix unit-testable without mocking processes. Every row of this table is asserted literally in test_runners.py:
fastapi/asgi -> python -m uvicorn module:app --host H --port P [--reload] flask -> python -m flask --app module:app run --host H --port P [--reload] django -> python <path>/manage.py runserver H:P [--noreload] (cwd = manage.py dir)
Note the asymmetry: for Django the configured entry point is a filesystem path to manage.py, so the runner resolves it against the config file’s directory and sets cwd accordingly, because manage.py runserver only works from where your project lives.
Restart semantics come free
Because launch resolution is pure, the live config watcher reuses it verbatim: edit backend.framework while the session runs and the watcher diffs the parsed spec, terminates the old process, resolves a fresh plan, and relaunches. Switching fastapi to flask mid-session genuinely swaps runtimes.
The deeper principle: a tool should host your stack, not substitute for it. pyRPC owns type synchronization; the framework owns serving. v0.13.0 makes ownership lines explicit instead of convenient.

pyRPC