When we first set up pyRPC's package structure, we made a deliberate choice to split it into three separate packages: pyrpc-core, pyrpc-cli, and pyrpc-codegen. The reason was a circular dependency between core and codegen that prevented pip install from working in a mono-package design.
Today, we're reversing that decision. pyrpc-cli no longer exists as a separate package. Its source lives directly inside pyrpc-core. The dependency chain went from three packages back to two:
Before (3 packages): pyrpc-core → pyrpc-cli → pyrpc-codegen After (2 packages): pyrpc-core → pyrpc-codegen
pip install pyrpc-core now gives you the runtime, the CLI, and the codegen library - all in one command.
The original problem
The circular dependency was real: pyrpc-core needed introspection logic that lived in pyrpc-codegen, and pyrpc-codegen needed core types for its schema models. You couldn't install either without the other already being installed.
The classic fix - extract the CLI into a middle package - worked perfectly. pyrpc-cli became the bridge that both sides could depend on without cycles. We documented this in detail in our earlier post on circular dependency resolution.
What changed
The key insight came when we refactored pyrpc-codegen into a pure library. We stripped out all pyrpc-core dependencies - no runtime types, no router references, nothing. Just Jinja2 templates, JSON Schema input, and TypeScript output.
At that point, pyrpc-codegen had zero pyrpc imports. The circular dependency simply stopped existing. The middle package was no longer needed - and maintaining a separate package meant more CI time, more version bumps, and more cognitive load for contributors.
The trade-offs
The original reason for keeping the CLI separate was to avoid dragging typer, rich, uvicorn, and watchfiles into the core runtime. Users who only wanted the protocol library shouldn't have to install CLI dependencies.
We decided that trade-off was no longer worth it. Here's why:
- Every real user wants the CLI. In practice, everyone who installed pyrpc-core also installed pyrpc-cli. The split was theoretical purity at the cost of practical complexity.
- Extra dependencies are optional at runtime. The CLI entry point lazy-imports typer and rich. If you never call
pyrpc, those packages are loaded but never imported - a small disk footprint with zero import-time cost. - One package is simpler to maintain. One version number, one changelog, one CI pipeline. Contributors don't need to learn which package to edit.
The new package story
Here's what the landscape looks like now:
pip install pyrpc-core # → runtime + CLI + codegen (everything) npm install @pyrpc/client # → TypeScript client
For most users, pip install pyrpc-core is the only command they'll ever need. Frontend-only developers just npm install @pyrpc/client - the postinstall script fetches the schema from your server and generates types automatically. The pyrpc-codegen library is an internal dependency; you never install it directly.
What we learned
The three-package split was the right decision at the time - it solved a real packaging problem. But as the design evolved, the constraint that motivated the split disappeared. Revisiting old decisions when the underlying constraints change is not backtracking; it's engineering hygiene.
If you were using pyrpc-cli directly, your workflow doesn't change. The pyrpc command is still there, still does the same things. You just install one package instead of two.

pyRPC