You write @rpc.mutation on a Python function. Three layers later, TypeScript autocomplete shows .useMutation() instead of .useQuery(). This post traces that journey end-to-end.
Step 1: the Python decorator
On the server, @rpc.mutation is a thin wrapper around @rpc. It sets kind=ProcedureKind.MUTATION on the procedure object:
from pyrpc_core import rpc
@rpc.mutation
def update_user(user_id: int, name: str) -> dict:
...Under the hood, rpc.mutation calls the same register() as @rpc, but passes kind="mutation". The procedure stores it. No magic.
Step 2: introspection picks it up
When you run pyrpc codegen or hit GET /rpc, introspection serializes every procedure. Each one now includes a kind field:
{
"procedures": {
"get_user": { "params": [...], "result": "...", "kind": "query" },
"update_user": { "params": [...], "result": "...", "kind": "mutation" }
}
}The default is "query". Bare @rpc works exactly like before — no breaking change.
Step 3: codegen emits two things
The Jinja2 template in client.ts.j2 reads the schema and produces:
- The
Typesinterface (the same one you already know) - A
ProcedureKindstype and aprocedureKindsconst
export interface Types {
get_user: (userId: number) => Promise<User>
update_user: (userId: number, name: string) => Promise<User>
}
export type ProcedureKinds = {
get_user: "query"
update_user: "mutation"
}
export const procedureKinds = {
get_user: "query",
update_user: "mutation",
} as const satisfies ProcedureKindsThe type is for TypeScript. The const is for runtime — adapters need an actual value to compare against.
Step 4: the adapter reads kinds
You pass kinds: procedureKinds when creating the client. The adapter's Proxy handler checks the kind for each procedure name and returns the matching hook:
const api = createReactClient<Types>({
baseUrl: "...",
kinds: procedureKinds,
})
// get_user → "query" → useQuery
api.get_user.useQuery({ userId: 1 })
// update_user → "mutation" → useMutation
api.update_user.useMutation()Without kinds, every procedure gets both hooks. That is the backward-compatible path for existing code.
Why this matters
The kind is declared once on the server. It cannot drift from the TypeScript side. If you add @rpc.mutation to a procedure, the next codegen run automatically flips the hook from useQuery to useMutation in your frontend. No manual wiring.

pyRPC