12.10.2026
🐍 Python 3.15 and free-threaded Python 3.14t / 3.15t wheels
⚡ Up to 2x faster DAG-CBOR encode, up to 10% faster decode
| Encode | v3.4.1 | v3.5.0 | Speedup |
|---|---|---|---|
citm_catalog |
1.04 ms | 0.52 ms | 2.01x (+101%) |
github |
0.05 ms | 0.03 ms | 1.78x (+78%) |
twitter |
0.47 ms | 0.27 ms | 1.74x (+74%) |
canada |
0.83 ms | 0.64 ms | 1.29x (+29%) |
| Decode | v3.4.1 | v3.5.0 | Speedup |
|---|---|---|---|
canada |
4.22 ms | 3.82 ms | 1.10x (+10%) |
torture_cids |
63.5 ms | 59.0 ms | 1.08x (+8%) |
github |
0.07 ms | 0.06 ms | 1.06x (+6%) |
atproto_repo.car |
65.0 ms | 61.8 ms | 1.05x (+5%) |
twitter |
0.77 ms | 0.75 ms | 1.02x (+2%) |
citm_catalog |
1.71 ms | 1.71 ms | 1.00x (+0%) |
❗ Behavior changes: -0.0 is now rejected on decode and encoded as 0.0, as the DAG-CBOR spec requires; encoding deeply nested data raises RecursionError instead of crashing; malformed CAR header types raise ValueError.
- Add Python 3.15 support; bump PyO3 0.29 and deps, adopt Ruff 0.17 default rules by @MarshalX in #129
- Add free-threaded wheels for Python 3.14t and 3.15t by @MarshalX in #130
- Ship
py.typedso type checkers pick up the bundled stubs by @MarshalX - Optimize DAG-CBOR encode by reusing a thread-local buffer and validating CIDs without
Multihashconstruction by @MarshalX in #116 - Optimize DAG-CBOR decode by reading each CBOR header once instead of re-decoding it per value by @MarshalX in #118
- Optimize DAG-CBOR decode by reading multi-byte header arguments at a fixed width by @MarshalX in #121
- Optimize DAG-CBOR encode by sorting map entries on a pooled stack and borrowing container items by @MarshalX in #122
- Optimize DAG-CBOR encode/decode by dispatching leaf values inline and recursing only for containers by @MarshalX in #123
- Forbid negative zero in DAG-CBOR: reject
-0.0on decode and encode it as0.0by @MarshalX in #126 - Raise
RecursionErrorinstead of overflowing the stack when encoding deeply nested DAG-CBOR by @MarshalX in #128 - Document raised exceptions and normalize CAR header type errors to
ValueErrorby @MarshalX in #127 - Restructure single-file
lib.rsinto per-domain modules by @MarshalX in #114 - Replace
_PyDict_SetItem_KnownHashwith publicPyDict_SetItemby @MarshalX in #124 - Fix decode error handling:
PyListleak, unbounded preallocation, and swallowed errors indecode_dag_cbor_multiby @MarshalX in #117 - Fix list and
bytearrayraces under free-threading by @MarshalX in #130 - Fix Windows build after PyO3 0.29 by @MarshalX in #131
- Fix
encode_multibasedocs:'u'is base64url, not base58btc by @MarshalX
27.05.2026
This is a focused decode-side optimization. The numbers below come from pytest-benchmark on Apple M1 / CPython 3.14 (PGO-built wheels), comparing v3.4.0 → v3.4.1 head-to-head.
| Fixture | v3.4.0 | v3.4.1 | Speedup |
|---|---|---|---|
canada |
4.75 ms | 4.43 ms | 1.07x (+7%) |
github |
69 µs | 66 µs | 1.05x (+5%) |
twitter |
790 µs | 766 µs | 1.03x (+3%) |
citm_catalog |
1.79 ms | 1.77 ms | 1.01x (+1%) |
The decoder used to fetch the Python recursion limit on every recursive step. We now read it once per top-level decode call and pass it through, closing the last decode gap with cbrrr (we're within 0.7% of it on canada and 30–40% ahead on the others).
26.05.2026
❗ Up to ~2x faster DAG-CBOR encode and decode!
This release is all about throughput. The numbers below come from pytest-benchmark on an Apple M1 / CPython 3.14, comparing v3.3.2 → v3.4.0 head-to-head.
| Fixture | v3.3.2 | v3.4.0 | Speedup |
|---|---|---|---|
github |
138 µs | 70 µs | 1.96x (+96%) |
twitter |
1.44 ms | 0.80 ms | 1.81x (+81%) |
citm_catalog |
2.87 ms | 1.81 ms | 1.59x (+59%) |
canada |
5.49 ms | 4.77 ms | 1.15x (+15%) |
| Fixture | v3.3.2 | v3.4.0 | Speedup |
|---|---|---|---|
canada |
2.31 ms | 1.17 ms | 1.98x (+98%) |
citm_catalog |
2.25 ms | 1.26 ms | 1.79x (+79%) |
twitter |
902 µs | 635 µs | 1.42x (+42%) |
github |
75.4 µs | 69.7 µs | 1.08x (+8%) |
- Optimize DAG-CBOR encode/decode and CAR block parsing by @MarshalX in #104
- Optimize DAG-CBOR string decode with an ASCII fast path by @MarshalX in #105
- Optimize DAG-CBOR map decode with a key-intern cache and
KnownHashinsert by @MarshalX in #106 - Optimize int encode with a direct
PyLongdigit read by @MarshalX in #107
- Bump to Rust 1.95 and PyO3 0.28 by @MarshalX in #102
- Drop the macos-13 PGO runner; cross-compile x86_64 macOS wheels from arm64 by @MarshalX in #108
- Bump CI actions (Node 24) and
pytest-codspeedv5 by @MarshalX in #103
- Add a head-to-head benchmark against other Python DAG-CBOR libs by @MarshalX in #109
- Update README and copyright years by @MarshalX in #110
05.12.2025
- Fix DASL testing CI by @MarshalX in #99
- Fix leading zero handling of CIDs in DAG-CBOR by @MarshalX in #98
03.12.2025
- Add DASL testing harnesses to CI workflow by @MarshalX in #97
- Fix error handling in array and map length checks by @MarshalX in #95
- Fix error handling in CID and ULEB128 processing functions by @MarshalX in #96
01.12.2025
❗ Almost x2 speed for CAR decoding (Bluesky firehose), ~25-35% faster DAG-CBOR decoding, and ~17% faster CID decoding!
I do have more ideas on what to try to speed it up even further! For example, applying BOLT on top of the existing PGO.
⚡ Optimizations
- Huge performance gain for DAG-CBOR decoding and CID by replacing deprecated Rust crate
libipldwithcbor4iiby @vmx in #80 and even contributed upstream! - Optimize DAG-CBOR encoding for arrays and maps by @MarshalX in #88
- Optimize CAR decoding by reducing allocations by @MarshalX in #90
- Optimize executable size (
panic = "abort") by @MarshalX in #86
Other
- Add
pytest-benchmarkGitHub Actions workflow by @MarshalX in #82 - Add lint and format check GHA workflow by @MarshalX in #83
- Add CONTRIBUTING.md by @MarshalX in #85
- Update CodSpeed by @MarshalX in #81
- Fix formatting in profiling by @MarshalX in #84
🏎️ Performance comparison. Before (v3.2.0) VS After (v3.3.0):
You can find more benchmark results here: #92
22.10.2025
❗ Possible breaking change for you: dropped support for PyPy 3.9 and PyPy 3.10 (both no longer supported upstream) in #77. CPython is still supported from 3.8 to 3.14.
- Add Python 3.14 support by @moreati in #74
- Update Rust to 1.90 by @MarshalX in #75
- Update PyO3 to 0.27.1 by @MarshalX in #75
- Fix repository URL specification in
pyproject.tomlfile by @moreati in #73 - Fix clippy warnings by @MarshalX in #76
- Fix CodSpeed by @MarshalX in #78
24.06.2025
- Bump rust to 1.87.0 by @MarshalX in #67
- Specify Rust version using env variable in workflows by @MarshalX in #67
- Fix bignum encoding by @MarshalX in #66
20.06.2025
- Add type stubs by @zzstoatzz in #56
- Add PyPy 3.11 support by @MarshalX in #63
- Update PyO3 to 0.25.1 with performance optimizations by @MarshalX in #63
- Update README by @MarshalX in #64
- Fix panic on encoding map with integer keys by @MarshalX in #60
- Fix panic on non UTF-8 string by @MarshalX in #59
- Fix smallest and largest CBOR integer roundtrip by @MarshalX in #58
- Fix decoding of infinities and NaNs according to specs by @MarshalX in #62
- Fix handling of concatenated objects / CBOR sequence by @MarshalX in #65
18.02.2025
- Update PyO3 to 0.23.4 by @MarshalX in #54
- Specify the dynamic version in
pyproject.tomlby @fxcoudert in #53 - Fix stack overflow when decoding deeply nested structures like arrays and maps by @MarshalX in #51
Possible breaking change for you: dropped support for PyPy 3.8
24.10.2024
❗ Breaking changes
🆕 Features
Other
- Update PyO3 to 0.22.5 by @MarshalX in #50
- Update
decode_carsignature,bytesCIDs instead ofstrin README by @snarfed in #49
07.09.2024
❗ Breaking changes
CID in CAR block keys and CID DAG-CBOR are now in raw byte form instead of stringified one!
⚡ Bunch of new optimizations
- Optimize duplicate check by @MarshalX in #34
- Optimize type checking by @MarshalX in #37
- Optimize keys duplication check by @MarshalX in #39
- Optimize array creation by @MarshalX in #40
- Optimize string encoding by @MarshalX in #41
🆕 Features
- Add CID encoding (
encode_cid(bytes) -> str) by @MarshalX in #38 - Add DAG-CBOR profiling by @MarshalX in #35
Other
- Fix upload to PyPI by @MarshalX in #33
- Update CodSpeed to v3 by @MarshalX in #47
- Update PyO3 to 0.22.2 by @MarshalX in #48
- Lock rust toolchain on v1.76.0 because higher versions segfaults during PGO gather by @MarshalX in #46
🏎️ Performance comparison. Before (v1.2.3) VS After (v2.0.0):
Raw bench results
Before (v1.2.3):
Hello World Decode:
===================
libipld : 144 ns
Hello World Encode:
===================
libipld : 150 ns
Realistic Decode Tests:
=======================
canada.json.dagcbor libipld : 5.11 ms (197.31 MB/s)
citm_catalog.json.dagcbor libipld : 3.43 ms (95.31 MB/s)
github.json.dagcbor libipld : 0.17 ms (270.36 MB/s)
twitter.json.dagcbor libipld : 2.15 ms (178.96 MB/s)
Realistic Decode Car Tests:
===========================
atproto_repo.car libipld : 119.97 ms (154.22 MB/s)
Realistic Encode Tests:
=======================
canada.json.dagcbor libipld : 1.19 ms (848.65 MB/s)
citm_catalog.json.dagcbor libipld : 1.66 ms (196.42 MB/s)
github.json.dagcbor libipld : 0.11 ms (429.15 MB/s)
twitter.json.dagcbor libipld : 0.87 ms (443.77 MB/s)
After (v2.0.0):
Hello World Decode:
===================
libipld : 124 ns
Hello World Encode:
===================
libipld : 116 ns
Realistic Decode Tests:
=======================
canada.json.dagcbor libipld : 4.65 ms (216.57 MB/s)
citm_catalog.json.dagcbor libipld : 2.96 ms (110.37 MB/s)
github.json.dagcbor libipld : 0.15 ms (311.00 MB/s)
twitter.json.dagcbor libipld : 1.75 ms (219.74 MB/s)
Realistic Decode Car Tests:
===========================
atproto_repo.car libipld : 92.44 ms (200.13 MB/s)
Realistic Encode Tests:
=======================
canada.json.dagcbor libipld : 1.00 ms (1011.47 MB/s)
citm_catalog.json.dagcbor libipld : 1.60 ms (203.57 MB/s)
github.json.dagcbor libipld : 0.06 ms (733.96 MB/s)
twitter.json.dagcbor libipld : 0.56 ms (686.31 MB/s)
Test stand: M1 Pro, Python 3.12.
I highly recommend using Python 3.12. The lower version will be slower.
P.S. Breaking change not only gives a performance boost but also aligns implementations with other languages.
11.03.2024
⚡ Thanks to PGO and new Bound API in pyo3 v0.21 I achieved huge performance boost ones again! The pyo3 team did an awesome work! Many thanks to all contributors! Also, I implemented own CAR reader that gives +11%
- Migrate to pyo3 v0.21 Bound API by @MarshalX in #23
- Add PGO-optimized wheels by @MarshalX in #30, #21
- Add own implementation of CAR reader by @MarshalX in #28
- Add IPLD Codec Fixtures by @MarshalX in #19
- Add CAR and CID benchmarks by @MarshalX in #20
- Improve work with numbers by @MarshalX in #25
- Use refs in args and Bound in return type where possible by @MarshalX in #31
🏎️ Performance comparison. Before (v1.2.2) VS After (v1.2.3):
Raw bench results
Before (v1.2.2):
Hello World Decode:
===================
libipld : 275 ns
Hello World Encode:
===================
libipld : 237 ns
Realistic Decode Tests:
=======================
canada.json.dagcbor libipld : 7.51 ms (134.15 MB/s)
citm_catalog.json.dagcbor libipld : 4.97 ms (65.71 MB/s)
github.json.dagcbor libipld : 0.23 ms (201.43 MB/s)
twitter.json.dagcbor libipld : 2.70 ms (142.51 MB/s)
Realistic Decode Car Tests:
===========================
atproto_repo.car libipld : 170.09 ms (108.77 MB/s)
Realistic Encode Tests:
=======================
canada.json.dagcbor libipld : 1.89 ms (533.01 MB/s)
citm_catalog.json.dagcbor libipld : 2.86 ms (114.02 MB/s)
github.json.dagcbor libipld : 0.14 ms (335.64 MB/s)
twitter.json.dagcbor libipld : 1.07 ms (358.30 MB/s)
After (v1.2.3):
Hello World Decode:
===================
libipld : 130 ns
Hello World Encode:
===================
libipld : 143 ns
Realistic Decode Tests:
=======================
canada.json.dagcbor libipld : 5.24 ms (192.11 MB/s)
citm_catalog.json.dagcbor libipld : 3.49 ms (93.51 MB/s)
github.json.dagcbor libipld : 0.18 ms (257.51 MB/s)
twitter.json.dagcbor libipld : 2.11 ms (182.31 MB/s)
Realistic Decode Car Tests:
===========================
atproto_repo.car libipld : 123.72 ms (149.55 MB/s)
Realistic Encode Tests:
=======================
canada.json.dagcbor libipld : 1.22 ms (823.65 MB/s)
citm_catalog.json.dagcbor libipld : 1.69 ms (192.80 MB/s)
github.json.dagcbor libipld : 0.11 ms (410.94 MB/s)
twitter.json.dagcbor libipld : 0.80 ms (481.72 MB/s)
Test stand: M1 Pro, Python 3.12.
I highly recommend using Python 3.12. The lower version will be slower.
02.03.2024
- Add unit tests and benchmarks by @MarshalX in #11
- Add CodSpeed by @MarshalX in #14
- Fix encode_multibase input data type by @MarshalX in #12
- Fix DAG CBOR string encoding by @MarshalX in #13
- Fix duplicated map keys check in decode_dag_cbor by @MarshalX in #16
- Fix non-string map keys check in decode_dag_cbor by @MarshalX in #17
- Fix DAG-CBOR map keys order for encoding and decoding by @MarshalX in #18
🏎️ Performance comparison
Before (v1.2.1):
canada.json.dagcbor libipld : 1.98 ms (508.99 MB/s)
citm_catalog.json.dagcbor libipld : 3.96 ms (82.46 MB/s)
twitter.json.dagcbor libipld : 1.92 ms (200.10 MB/s)After (v1.2.2):
canada.json.dagcbor libipld : 2.04 ms (493.43 MB/s)
citm_catalog.json.dagcbor libipld : 2.76 ms (118.31 MB/s)
twitter.json.dagcbor libipld : 1.03 ms (371.93 MB/s)CodSpeed Performance Report: #18#issuecomment-1974061926
27.02.2024
Encoding results compared to v1.2.0:
- canada.json.dagcbor x20.6
- citm_catalog.json.dagcbor x4.7
- twitter.json.dagcbor x4.1
26.02.2024
- Add
encode_dag_cborby @MarshalX in #5 - Add profiling by @MarshalX in #6
- Improve performance of DAG-CBOR decoding (remove a lot of unnecessary allocs and drops) by @MarshalX in #8
Decoding results compared to v1.1.1:
- citm_catalog.json.dagcbor x2
- canada.json.dagcbor x1.64
- twitter.json.dagcbor x1.29
Decoding results compared to v1.1.0:
- citm_catalog.json.dagcbor x4.9
- canada.json.dagcbor x4.6
- twitter.json.dagcbor x4.37
24.02.2024
- Add error handling by @MarshalX in #4
- Improve performance (at least x2 boost for CAR and DAG-CBOR decoding) by @MarshalX in #4
- Fix CID decoding in DAG-CBOR by @MarshalX in #4
10.01.2024
27.10.2023
13.09.2023
The first stable release
- Decode CID (
decode_cid(str) -> dict) - Decode DAG-CBOR (
decode_dag_cbor(bytes) -> dict,decode_dag_cbor_multi(bytes) -> list[dict]) - Decode CAR (
decode_car(bytes) -> tuple[dict, dict[str, dict]]). Returns a header and blocks mapped by CID.
by @MarshalX
