Python
Python's dynamic nature makes serialization uniquely challenging. While it excels at developer productivity, the runtime overhead of object instantiation and the Global Interpreter Lock (GIL) can severely bottleneck high-throughput data processing pipelines.
Runtime
What it is
This suite measures CPython, the usual C implementation of the Python interpreter. Other implementations such as PyPy are not used here. CPython compiles source to bytecode and then runs that bytecode in a virtual machine. Almost every value is a heap object with a type pointer and a reference count. That design makes Python easy to write and expensive to decode into, because even a small integer is a full object.
The Global Interpreter Lock (GIL) is a lock that lets only one thread run Python bytecode at a time inside a process. If a decode takes 10 ms of Python bytecode, the whole process is blocked for those 10 ms, unless the library releases the GIL while it works in C, C++, or Rust.
| This suite | |
|---|---|
| Interpreter | CPython 3.12 or newer. Not PyPy or Jython. |
| Host toolchain | uv (uv sync) |
| Prepare | ./scripts/install-host-requirements.sh python |
| Run | python/scripts/run-benchmarks.sh |
| Memory | Reference counting plus a cyclic garbage collector. The GIL is present. |
What this suite runs
python/pyproject.toml requires Python 3.12 or newer. The uv tool creates a local virtual environment and installs the packages listed in the lock file. The benchmark runner is ordinary CPython on the command line. It is not a web server such as FastAPI or Flask.
What changes the numbers
Libraries with a C or Rust core, such as orjson, msgspec, and protobuf, spend most of the timed path outside the interpreter. They can release the GIL while they parse bytes. Pure-Python paths, such as dill serialize and the official FlatBuffers Builder, stay inside the virtual machine and look much slower.
Deserializing into a dict is usually cheaper than building a class or a Pydantic model, because constructing Python objects is itself expensive. The tracemalloc tool under-counts allocations that happen inside C or Rust extensions.
Suite-specific gotchas
The official FlatBuffers Python package builds with a pure-Python Builder. The C++ and Rust FlatBuffers libraries are a different speed class. This suite measures the Python binding. See caveats.
pickle, cloudpickle, and dill work only in Python. Loading untrusted input with them can run arbitrary code.
These times cannot be ranked against another language.
Where to go next
The steps to install the toolchain and run the benchmark are in python/README.md. The language overview is The Python interpreter. For the GIL and garbage collection in latency, see Latency tails and GC.
Benchmark runner
- Directory:
python/(repository root) - Output: monorepo
logs/python/YYYY-MM-DD-HHMMSS.csv(Language=python, times in nanoseconds) - Runner:
python/scripts/run-benchmarks.sh(or project docs for modes) - Registration:
python/src/benchmark/runner.py - Modes:
bytesandstream
Serializers
| Log name | Category | Package | Native input (prepare_data) |
Stream mode | Notes |
|---|---|---|---|---|---|
| avro | Schema | fastavro |
record dict | adapted | Compact schemaless size; dict/union path slower than protobuf C++ |
| cbor2 | Binary | cbor2 |
dict | native | IETF CBOR (RFC 8949) |
| cloudpickle | Native | cloudpickle |
dataclass | native | Extended pickle; same security caveats |
| dill | Native | dill |
dataclass | native | Graphs/dynamics; ser much slower than pickle (pure-Python dispatch) |
| flatbuffers | Schema | flatbuffers |
dataclass → Builder | adapted | Python Builder ser is slow; deser is zero-copy GetRootAs view |
| json | JSON | stdlib | dict | adapted | Baseline text JSON |
| mashumaro | JSON | mashumaro |
dataclass | adapted | ORJSONEncoder/Decoder |
| msgpack | Binary | msgpack |
dict | native | Reference MessagePack |
| msgspec | JSON | msgspec |
Struct | native (encode_into) |
Typed array-like Structs |
| msgspec-msgpack | Binary | msgspec |
Struct | native | Same Struct path, MessagePack |
| orjson | JSON | orjson |
dict | adapted | Rust core; conversion untimed |
| pickle | Native | stdlib | dataclass | native | Cycles supported; unsafe untrusted |
| protobuf | Schema | protobuf |
Message | adapted | From suite protobuf schemas under schemas/v2/ / language generated modules |
| pydantic | JSON | pydantic |
BaseModel | adapted | Validation-oriented API models |
| rapidjson | JSON | python-rapidjson |
dict | adapted | C++ RapidJSON bindings |
| serpyco-rs | JSON | serpyco-rs + orjson |
dataclass | adapted | dump/load + orjson wire |
Call-path contract (fair timing)
Timed methods measure codec only on library-native values:
prepare(name, type)— encoders, schemas, buffers (untimed)prepare_data(obj, …)— dataclass → dict / Struct / Message / Model (untimed)serialize_*/deserialize_*— encode/decode only (timed)
FlatBuffers is the exception where Builder construction is the serialize API (no separate Message type). Stream mode is native when the library has a real file/stream API; otherwise adapted (bytes then write / read then bytes).
Caveats
Why avro size is great but ops/s lag protobuf
- Size is real: schemaless Avro omits field names (schema out-of-band).
- Speed is mostly library/runtime: timed path is only
schemaless_writer/readeron a pre-built dict + cachedparse_schema. fastavro (Cython) still loses to protobuf messageSerializeToStringby ~20–30× on nested types because of Python-dict field walks and null-unions. - Measurement: the benchmark runner no longer runs
tracemallocduring timed ser/des (it was inflating alloc-heavy codecs ~2–3×).
Why flatbuffers and dill ops/s look low
- flatbuffers (serialize): the official Python package builds with a pure-Python
Builder. Expect ~100×+ slower ser thanprotobufon the same POCO. C++/Rust FlatBuffers are a different performance class; this suite measures the Python binding. - flatbuffers (deserialize): timed path is zero-copy
GetRootAs+ a thin view. Full field materialization is not forced inside the timer (FlatBuffers' model: pay on field access). - dill (serialize): for ordinary importable dataclasses the wire size matches pickle, but dill's pure-Python
savepath (module/type discovery) is ~15–20× slower than Cpickle. That is inherent;byref/recursedo not close the gap on these data types. Prefer pickle when you do not need dill's dynamic-object features.
Other caveats
tracemallocunder-counts C/Rust extension allocations.- Fidelity is semantic, not strict type identity (dict vs dataclass, enum vs int, datetime ms truncation).
Benchmark runner: python/README.md. Serialization Categories.
Numbers
Measured numbers for this language live on the Dashboard (pre-filtered). Claim level is L1 (one machine, one session) — see Claims and replication.