If you've worked with FastAPI or Flask, you know the pattern: define a route, define a Pydantic model, wire up the request handler, document it with OpenAPI, and then write your frontend calls by hand — hoping the types match. It works, but there's a gap between your backend and frontend that you have to manage manually.
pyRPC closes that gap. It gives you a tRPC-style experience for Python backends: write a function, slap an @rpc decorator on it, and call it from TypeScript with full type safety. No OpenAPI codegen step, no manual type duplication, no runtime surprises.
The problem with traditional Python APIs
A typical FastAPI endpoint looks like this:
@app.post("/users")
async def get_user(user_id: int) -> User:
user = await db.fetch_user(user_id)
return userThen on the frontend, you write something like:
const res = await fetch("/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: 1 }),
})
const user = await res.json()
// user.name could be anything — no type safetyThe types are documented in OpenAPI, but you have to manually regenerate the TypeScript client, or write your own fetch wrappers, or rely on a codegen step that generates hundreds of lines of boilerplate. None of these are terrible, but they add friction to every change.
What tRPC showed us
tRPC proved that you don't need a separate API layer at all. Define a procedure, call it from the client — the types flow through automatically. No REST endpoints to design, no GraphQL schema to maintain, no codegen to run. The function is the API.
The TypeScript ecosystem embraced this immediately. But Python — despite being the language of choice for countless backends — had no equivalent. You either used raw REST, or you adopted GraphQL, or you accepted the type gap.
Enter pyRPC
pyRPC brings the same model to Python. Define your procedures with @rpc, mount them on your framework of choice, and call them from TypeScript with full type inference.
# server.py
from pyrpc_core import rpc, model
from pyrpc_fastapi import mount_fastapi
from fastapi import FastAPI
app = FastAPI()
@model
class User:
id: int
name: str
email: str
@rpc
def get_user(user_id: int) -> User:
return User(id=user_id, name="Alice", email="alice@example.com")
mount_fastapi(app)// client.ts
import { createClient } from "@pyrpc/client"
import type { Types } from "@pyrpc/types"
const client = createClient<Types>()
const user = await client.get_user(1)
// ^-- type: { id: number; name: string; email: string }The types are inferred from your Python code. Change a field in your model, and TypeScript immediately flags any mismatched usage. No codegen, no OpenAPI export, no manual sync.
The philosophy
pyRPC is built on a few core ideas:
- Dead simple install —
pip install pyrpc-coreand you're done. No config files, no boilerplate. - Works everywhere — FastAPI, Flask, or any ASGI server. Pick your framework, pyRPC adapts.
- Batteries included but modular — Core is tiny. Add adapters and codegen as you need them.
- Universal validation — Pydantic v2 under the hood, automatic for primitives and models.
- Type-safe bridge — Python to TypeScript, end to end, without leaving your editor.
Where we are
pyRPC is in alpha. The core protocol is stable, the FastAPI and Flask adapters work, the TypeScript client generates correct types, and the codegen tool produces production-ready contracts. We're still iterating on the developer experience, the adapter API surface, and the documentation.
Try it on the interactive playground, check out the sandbox architecture post, and open issues on GitHub if something breaks.