When we started designing pyRPC's package architecture, we looked at how other frameworks organize their packages. The usual suspects were obvious: FastAPI, Flask, Django. But the most instructive comparison wasBetter Auth — a TypeScript authentication library that solves a packaging problem remarkably similar to ours.
This post is about how Better Auth's meta-package pattern works, why we could not use it directly in Python, and how we adapted the same idea into a three-package structure that respects Python's packaging constraints.
Better Auth's meta-package model
Better Auth is a TypeScript library for authentication. It has a core package, framework adapters, database adapters, and plugins. In npm, they ship this as a meta-package: you install better-authand it re-exports everything from its sub-packages.
// npm: one install, everything available
npm install better-auth
// The meta-package re-exports sub-packages
import { auth } from "better-auth" // core
import { react } from "better-auth/react" // React adapter
import { next } from "better-auth/next" // Next.js adapterThe beauty of this pattern is that the sub-packages can depend on each other in any direction — npm does not care about circular dependencies between packages that are always installed together. The meta-package just aggregates them. Developers get one install command and one version to track, and the library authors can split concerns across packages without worrying about install UX.
We wanted this exact pattern for pyRPC. One install command, everything works. The ideal was:
# Dream scenario: one meta-package pip install pyrpc # Gives you: core runtime + CLI + TypeScript codegen
Why it does not translate directly
Python packaging has a constraint that npm does not: no package namespaces without a meta-package on PyPI. You cannot publishpyrpc/core and pyrpc/cli as separate packages on PyPI — the / namespace separator does not exist. You get flat names:pyrpc-core, pyrpc-cli, pyrpc-codegen.
More importantly, Python packaging tools (pip, uv, poetry) cannot resolve circular dependencies. If pyrpc-core depends onpyrpc-cli and pyrpc-cli depends on pyrpc-core, you getResolutionImpossible. In npm, circular dependencies between sibling packages under a common meta-package are irrelevant because npm resolves the dependency tree before installation and does not care about cycles between co-installed packages.
This means we cannot have a true meta-package in Python. The closest we can get is a chain: one package depends on another, which depends on a third. The chain must be acyclic and the direction must be unambiguous.
The Better Auth model reimagined for Python
We mapped Better Auth's logical model onto Python's constraints:
| npm (Better Auth) | PyPI (pyRPC) |
|---|---|
better-auth (meta-package) | pyrpc-core (entry point, declares deps) |
better-auth/core | pyrpc-core (same package, contains Router, etc.) |
better-auth/react | pyrpc-cli (separate concern, depends on core + codegen) |
| no equivalent | pyrpc-codegen (pure library at bottom of chain) |
The key insight: we merged the meta-package role into pyrpc-core itself. pyrpc-core is both the runtime library and the package that declares the dependency on pyrpc-cli. When you install pyrpc-core, pip resolves the chain and pulls in pyrpc-cli and pyrpc-codegen transitively. One install command, everything present, no cycles.
The chain, visualized
Better Auth (npm):
better-auth (meta-package)
├── @better-auth/core (no deps on other better-auth packages)
├── @better-auth/react (depends on core)
└── @better-auth/next (depends on core + react)
→ Circular deps don't matter in npm
pyRPC (PyPI):
pip install pyrpc-core
└── pyrpc-core (runtime, also acts as entry point)
└── depends on: pyrpc-cli (CLI tooling)
└── depends on: pyrpc-codegen (pure library, no pyrpc deps)
→ Chain must be acyclic in PythonThe pyrpc-core package plays double duty: it is the runtime library (Router, decorators, adapters) and the logical equivalent of the meta-package (the thing you install to get everything). This is a pragmatic compromise: it works because pyrpc-core never imports pyrpc-cli at module level. The packaging dependency ensures pyrpc-cli is on disk, but the code dependency is lazy.
Why pyrpc-codegen is at the bottom
In the Better Auth model, a "codegen" package would likely depend on "core" to introspect types. That is what we originally had: pyrpc-codegen depended on pyrpc-core. But that creates a cycle when pyrpc-core (the meta-package equivalent) needs to depend on pyrpc-codegen.
The solution was to make pyrpc-codegen a pure transformation library that takes a dict and returns a string. It has no pyrpc dependencies at all. The introspect-and-codegen workflow is owned by pyrpc-cli, which depends on both pyrpc-core (for introspection) and pyrpc-codegen (for generation). The chain flows cleanly downward.
This is the Better Auth principle applied under Python constraints: separate concerns into packages, establish a clear dependency direction, and let the top-level package be the one-install entry point. The result is not as elegant as npm namespaces, but it is predictable, maintainable, and free of cycles.
The meta-package pattern in practice
If you are designing a Python multi-package project and want the one-install experience, here is the recipe:
- Identify the production package. This is what users will pip install. It is the runtime, the library, the thing they import. Call it
yourproject-coreoryourproject-lib. - Identify the developer tooling. CLI commands, code generators, dev servers, file watchers. These go in
yourproject-cli. - Identify pure libraries. Packages that transform data without knowing about your runtime. These go at the bottom of the dependency chain with no internal dependencies.
- Make the production package depend on the CLI. Not the other way. The production package is the entry point. It declares the CLI as a dependency so pip installs everything. Use lazy imports in the CLI to avoid loading core until needed.
- No internal dependencies on pure libraries. Pure libraries should not depend on any of your other packages. They are standalone.
This pattern gives you the Better Auth one-install UX within Python's packaging constraints. It is not a meta-package in the npm sense — it is a dependency chain — but it achieves the same goal for the end user.
All 45 tests pass. The repo is at github.com/pyrpc/pyrpc.

pyRPC