Skip to content

Benchmarks

Median decode/encode of a 100 KB config on CPython 3.14, release build. Lower is faster; the ratio is against e-serde.

The corpus is the canonical tests/resources/sample.* file, grown to each size — the same bytes every rival parses. The rivals are each library's own recommended API.

Decode

loads

Format e-serde orjson msgspec pyyaml(C) rtoml configparser
JSON 0.17 ms 0.17 ms 0.17 ms — — —
YAML 2.20 ms — — 12.6 ms — —
TOML 1.53 ms — — — 2.29 ms —
INI 2.00 ms — — — — 22.3 ms
  • JSON — a tie by construction: e-serde is msgspec here. The C decoder holds the GIL.
  • YAML — ≈6× faster than PyYAML's C loader, 66× faster than ruamel. The billion-laughs pre-scan runs only when the input contains &, so unanchored docs pay nothing for it.
  • TOML — ahead of rtoml, its nearest Rust rival, and 40×+ over the pure-Python parsers.
  • JSONC — the one format e-serde does not lead: pyjson5 (Rust, dedicated) beats it ~×0.5. The gap is the price of exact big integers and raw number tokens (0.2.1 onward).
  • CSV — polars builds a DataFrame (its whole reason to exist); e-serde reaches the same list[dict] with per-column inference, faster, and GIL-detached. Encoding stays polars' advantage (a native Arrow writer): ~×1.8 behind on dumps, an honest trade for a config library.

Validate (type=)

typed

Decoding is only part of the job. Routing the tree through msgspec.convert adds ~×1.4 for a Struct, and stays ~3× ahead of an orjson → pydantic pipeline on the same payload.

Async (GIL-detachment probe)

async

Eight concurrent decodes of a 10 MB payload, fan-out vs serial-sync ratio:

Format async vs serial Why
YAML ×0.49 (2.0× faster) saphyr releases the GIL → parallel
TOML ×0.45 (2.2× faster) toml-rs releases the GIL → parallel
JSON ×1.03 (no gain) msgspec's C decoder holds the GIL

The native Rust codecs scale across cores; the C one does not. That is the whole argument for aloads on large payloads.

Memory

memory

Python-visible allocations (tracemalloc) per 100 KB decode. C/Rust buffers that never touch the Python allocator read low by design — that is a feature, not a measurement gap.