Design
A walkthrough of how fastgrpc is built.
Pipeline
The codegen layer is a sequence of pure functions over an in-memory ProtoFile IR (intermediate representation). Each step takes the IR in and returns either an updated IR or a final artifact.
@service-decorated classes
↓ inspector
ProtoFile IR (field numbers = 0)
↓ lock
ProtoFile IR (field numbers filled in from .fastgrpc.lock)
↓ validator
ProtoFile IR (validated)
↓ proto writer
.proto source string
↓ protoc compiler
_pb2.py + _pb2_grpc.py
The IR itself is never persisted — only the final .proto and .fastgrpc.lock are written to disk.
Why an intermediate representation
Three things force the framework to hold the whole picture in memory before writing the final .proto:
- Field numbers come from the lock, not from source order. The inspector emits messages with
number=0placeholders; the lock pass fills them in. - Messages must be defined before they're used. The user writes them in any order; the writer topologically sorts the IR before rendering.
- Validation needs the whole picture. Duplicate names, reserved field numbers, broken type references — all need a complete view to catch.
The IR itself is just five plain dataclasses (ir.py): ProtoFile, ProtoMessage, ProtoField, ProtoMethod, ProtoService. No visitor pattern, no AST nodes, no transformer passes — just data passed between pure functions.
Type introspection, not AST parsing
fastgrpc uses typing.get_type_hints() and inspect.getmembers() rather than parsing the user's source with the ast module.
| Runtime introspection (chosen) | AST parsing (rejected) | |
|---|---|---|
Resolves from models import User |
yes | no |
Handles Optional[T], list[T] |
yes | very hard |
| Forward references | yes | no |
| Pydantic / dataclass introspection | yes | no |
| Complexity | low | high |
typing.get_type_hints() already does everything an AST parser would need to reimplement — it returns real Python type objects, not strings.
Server wiring
fastgrpc never writes the servicer subclass to disk. Instead, server/wiring.py synthesizes it at runtime via type(...):
impl_cls = type(
f"{service.name}Impl",
(servicer_base,),
{method_name: wrapper_function for ...},
)
Each wrapper:
- Receives a proto request from grpcio
- Converts it to a dataclass (or extracts kwargs for synthesized requests)
- Calls the user's method
- Converts the result back to a proto
The conversion logic is in server/converter.py — a recursive walk over the type hints.
Sync/async detection
A single pass over the registry classifies each @rpc method:
| Detection | Result |
|---|---|
inspect.iscoroutinefunction(method) or inspect.isasyncgenfunction(method) |
async |
| Otherwise | sync |
If both classifications appear, fastgrpc raises ValidationError rather than guessing. See Async vs sync.
Hot reload
The Rust notify crate (via the watchfiles Python wrapper) provides kernel-native file events. On every change:
- The watcher fires
- fastgrpc evicts user modules from
sys.modules(framework modules are preserved) - The entry-point file is re-imported, which re-runs the decorators
- The full pipeline runs
- The previous server is stopped and replaced
See Watch mode for the full sequence.
Source layout
src/fastgrpc/
├── decorators.py # @service, @rpc, registry
├── app.py # App config + interceptor registry
├── exceptions.py # gRPC-status-mapped exceptions
├── codegen/
│ ├── ir.py # ProtoFile / ProtoMessage / ProtoField dataclasses
│ ├── inspector.py # Python types → IR
│ ├── lock.py # field-number stability
│ ├── validator.py # IR + registry sanity checks
│ ├── proto_writer.py # IR → .proto string
│ └── compiler.py # invokes grpcio-tools / protoc
├── server/
│ ├── runner.py # build pipeline + lifecycle
│ ├── wiring.py # servicer synthesis, sync/async detection
│ ├── converter.py # dataclass ↔ protobuf conversion
│ ├── watcher.py # watchfiles-based hot reload
│ └── reflection.py # gRPC server reflection
└── cli/
└── main.py # Typer CLI: dev, run, proto, compile
Roadmap
- TLS / mTLS support
- gRPC health-check protocol
- OpenTelemetry tracing interceptor
- HTTP/JSON transcoding (
fastgrpc gateway) - Pydantic v2 model support alongside dataclasses