Every package structure encodes assumptions about how components relate. In pyRPC, the dependency chain is pyrpc-core → pyrpc-cli → pyrpc-codegen. This was not an accident — it was the result of working through what each package is, what it needs, and what it should not need.
The direction tells a story: applications depend on core; core depends on CLI; CLI depends on codegen. Each arrow means "this package needs that package to function." Let us unpack why each arrow points where it does.
pyrpc-core: the runtime, nothing else
pyrpc-core is what you import in production. It provides:
Router— the procedure registry.@rpc/@model— decorators for registering procedures and models.handle_request— the core RPC interpreter.PyRPCAsgiApp— the ASGI transport layer.RPCClient— the Python client for calling remote servers.- Framework adapters for FastAPI, Flask, and standalone ASGI.
Core has the strictest dependency requirements: it must be lightweight, stable, and suitable for production deployment. It should not pull in typer, rich, uvicorn, or any other CLI-only dependency.
Core depends on pyrpc-cli because we want pip install pyrpc-coreto install the CLI. But the dependency is purely structural — core never imports pyrpc-cli at runtime. It is a packaging dependency, not a code dependency. The [project.dependencies] line ensures pip installs pyrpc-cli alongside pyrpc-core, but the Python import graph is flat: importing pyrpc_core does not trigger import pyrpc_cli.
This is an important distinction. A packaging dependency ensures the package is present on disk. A code dependency (import) ensures it is loaded in memory. They do not need to be the same. By keeping the import lazy, core remains fast to import and free of CLI baggage, while still ensuring the CLI is available when the user types pyrpc.
pyrpc-cli: the glue layer
pyrpc-cli is the developer-facing surface. It owns:
- The
pyrpcCLI entry point (typer app). - All commands:
dev,serve,inspect,codegen,pull,shell. - The
_DevConsoleand_ShellConsoleREPL consoles. - Config read/write for
[tool.pyrpc]in pyproject.toml. - File watcher integration (
watchfiles). - First-run setup prompts and framework adapter installation.
CLI depends on pyrpc-codegen directly because the codegencommand calls generate_typescript_client() from pyrpc-codegen. This is a hard code dependency — the import happens at CLI startup.
CLI depends on pyrpc-core lazily because only some commands need it.pyrpc codegen --url does not import core. pyrpc dev app.main does. The lazy import pattern means the CLI help text and fast commands are snappy even if core is large.
CLI also brings in the heavy tooling dependencies: typer,rich, uvicorn, watchfiles, httpx. These are appropriate here because they are only needed when running the CLI — not in production, not in library code.
pyrpc-codegen: the pure library at the bottom
pyrpc-codegen is the simplest package. It takes a Python dict describing RPC procedures and returns a string of TypeScript source code. That is it.
Its dependencies are minimal:
jinja2— for rendering TypeScript from templates.jsonschema-ts— for converting Pydantic model JSON Schema to TypeScript interfaces.
It has zero pyrpc dependencies. It does not import pyrpc-core, pyrpc-cli, or any other pyrpc package. It can be installed standalone, used in CI pipelines, or embedded in other tools that need to generate TypeScript from Python schema data.
Being at the bottom of the dependency chain means codegen never causes circular dependency problems. No matter how pyrpc-core or pyrpc-cli evolve, codegen stays stable and independent. This is the ideal position for a library: dependents grow downward toward you, you never reach up to them.
What happens when you pip install pyrpc-core
Here is the complete resolution chain:
pip install pyrpc-core
├── pyrpc-core 1.0.0
│ └── requires: pyrpc-cli >=1.0.0
│ └── pyrpc-cli 1.0.0
│ ├── requires: pyrpc-codegen >=1.0.0
│ │ └── pyrpc-codegen 1.0.0
│ │ ├── requires: jinja2 >=3.0
│ │ └── requires: jsonschema-ts >=0.1
│ ├── requires: typer >=0.12
│ ├── requires: rich >=13.0
│ ├── requires: uvicorn >=0.29
│ ├── requires: httpx >=0.27
│ └── requires: watchfiles >=0.21
└── (pyrpc-core does NOT import pyrpc-cli at module level)
→ Fast import, no CLI deps loaded until you type "pyrpc"The user gets everything in one command. The TypeScript codegen, the dev console, the watcher, the shell, the serve command — all of it is installed. And yet, importing pyrpc_core in production still takes the same time it always did because the CLI code is on disk but not in memory.
Design principles behind the chain
Three principles guided the final structure:
1. Production dependencies must not include development dependencies
When you deploy pyRPC to production, you should not install typer, rich, or watchfiles. These are developer tools. The dependency chain ensures that pyrpc-core (the production package) only declares a dependency on pyrpc-cli, but never imports it at runtime. Production deployments that only import pyrpc-core will never trigger the CLI dependency tree.
2. Libraries at the bottom, tools in the middle, applications at the top
Layered dependency graphs are easier to reason about than tangled ones. pyrpc-codegen is a pure library — no side effects, no network calls, no [project.scripts]. pyrpc-cli is a tool — it orchestrates libraries and adds interactivity. pyrpc-core is an application library — it is the top-level package that users install and import.
The chain flows naturally: applications import core, core ensures CLI is available, CLI orchestrates codegen. No package needs to know about the layer above it.
3. Lazy imports are an API contract, not an optimization
The lazy import of pyrpc-core inside pyrpc-cli is a deliberate design choice, not a performance hack. It means pyrpc-cli commands that do not need core (codegen, shell with URL) work without core installed. This enables workflows like:
# Frontend-only CI: regenerate types from deployed server pip install pyrpc-cli pyrpc codegen https://api.example.com/rpc
The lazy import declares: "I can work without core for most of my commands. If you need the full set of commands, install core too or let the dependency chain bring it in." This is an explicit capability boundary, not a hidden optimization.
What is next
With the dependency chain clean, the next features build directly on top:
- First-run setup —
pyrpc devwith no config prompts for framework and entry point, saves to[tool.pyrpc], installs adapter. - Adapter auto-install —
pip install pyrpc-core[{framework}]based on framework choice. --reconfigure— re-run setup prompts to switch frameworks or entry points.- Optional
pyrpc shellremoval — the shell lives entirely in pyrpc-cli and can be deprecated without touching core or codegen.
All 45 tests pass. The repo is at github.com/pyrpc/pyrpc.

pyRPC