mrkeyoor.com_
Thu 06 Aug 10:54 UTC
PyPIUtilsupdated 06 Aug 2026

cbor2

cbor2 encodes and decodes CBOR, the binary data format defined in RFC 8949. Think of CBOR as JSON with a byte-level encoding and a type system that actually matches what programs hold in memory: integers stay integers, binary blobs travel as bytes instead of base64, dict keys can be any type instead of only strings, and the result is usually smaller than the equivalent JSON. The API is deliberately the same shape as the stdlib json and pickle modules: dumps, loads, dump, load. Beyond the basics it knows the registered CBOR semantic tags, so datetimes, Decimals, Fractions, UUIDs, sets, IP addresses, and compiled regexes round-trip as themselves. Since version 6.0 the whole encoder and decoder is a Rust extension rather than the old C plus pure-Python pair.

Verdict

The default CBOR library for Python, and correctly so: complete tag coverage, a stdlib-shaped API, and an unusually careful maintainer who keeps shipping decoder-correctness fixes. Reach for it when a spec or a binary-heavy payload calls for CBOR, not as a general replacement for JSON, and read the 6.0 migration notes before upgrading from 5.x.

API stability3/5The project follows semantic versioning and 5.x was stable for years, but 6.0 in April 2026 rewrote the codec in Rust and shipped eight labelled backward-incompatible changes, including new tag_hook and object_hook signatures and the removal of the cbor2.encoder and cbor2.decoder modules. Basic dumps and loads code is unaffected; anything with custom hooks is not.
Docs4/5readthedocs covers basic usage, a full table of which semantic tag maps to which Python type in both directions, and a customizing page with working examples for semantic_decoders, tag_hook, object_hook, and cyclic value sharing. The version history explains each bug in plain terms. What is missing is a single 5.x to 6.x migration page.
Maintenance5/5Last push 1 August 2026, six releases between April and August 2026, and only 16 open issues (18 counting PRs). Alex Gronholm also maintains anyio and APScheduler, and recent releases are almost entirely decoder-correctness fixes for malformed input, which is the right thing for a parser to be spending its time on.
Ecosystem4/5Around 16.3 million downloads a week and the package almost every Python COSE, WebAuthn, and CoAP library depends on. Only 302 GitHub stars, which understates it; the flip side is that CBOR itself is a smaller world than JSON, so tooling around the format is limited to the bundled CLI and a handful of spec-level libraries.

Use it if

  • You are implementing a protocol that specifies CBOR: COSE and CWT tokens, WebAuthn and FIDO2 attestation objects, CoAP payloads, or a firmware update manifest
  • You are shipping binary fields (image bytes, hashes, signatures, sensor readings) and want to stop paying the 33 percent base64 tax that JSON forces on you
  • Your dict keys are not strings: CBOR maps take integers, tuples, or bytes as keys and give them back with the same type, where json silently stringifies them
  • You need values that JSON has no type for, such as timezone-aware datetimes, exact Decimals, sets, or UUIDs, and you would rather not invent a per-field encoding convention
Skip it if

Setup reality

pip install cbor2 and you are done on any mainstream platform: version 6.1.4 publishes 49 wheels covering CPython 3.10 through 3.15 plus PyPy, on manylinux, musllinux, macOS arm64 and x86_64, and Windows. There are no Python dependencies at all. The trap is the source fallback. Since 6.0 the extension is written in Rust, so if pip cannot find a wheel for your interpreter or platform it will try to build from source and demand a full Rust toolchain, which surprises people on Alpine images with an unusual Python or on freshly released Python versions. Pin the version and use a wheel-only install in CI if you want that failure to be loud. The other setup cost is conceptual: you have to decide up front whether to enable canonical encoding, value sharing, and string referencing, because those flags change the bytes on the wire and other CBOR implementations may not read them.

Patterns

Round-trip a Python object to CBOR bytesencode-decode-bytes

import cbor2

payload = cbor2.dumps({"id": 7, "tags": ["a", "b"], "blob": b"\x00\x01"})
print(type(payload))          # <class 'bytes'>

back = cbor2.loads(payload)
print(back)                   # {'id': 7, 'tags': ['a', 'b'], 'blob': b'\x00\x01'}

Same four function names as json and pickle. Two things json cannot do show up immediately: bytes survive as bytes with no base64 step, and a non-string dict key such as 7 comes back as the integer 7 rather than the string '7'.

Write to and read from a file without building the whole bufferfile-stream-roundtrip

import cbor2

with open("out.cbor", "wb") as fp:
    cbor2.dump({"records": list(range(1000))}, fp)

with open("out.cbor", "rb") as fp:
    data = cbor2.load(fp)

dump and load take a binary file object, so open with 'wb' and 'rb'. Passing a text-mode file gives a TypeError about needing bytes. load reads exactly one CBOR item and leaves the stream positioned after it.

Encode datetimes without hitting the naive-datetime errordatetime-timezone

from datetime import datetime, timezone
import cbor2

naive = datetime(2026, 8, 1, 12, 0)
# cbor2.dumps(naive) raises a CBOREncodeError: no timezone

data = cbor2.dumps(naive, timezone=timezone.utc)
compact = cbor2.dumps(naive, timezone=timezone.utc, datetime_as_timestamp=True)
print(cbor2.loads(data))      # always timezone-aware on the way back

CBOR has no representation for a naive datetime, so the encoder makes you supply a default timezone instead of guessing. datetime_as_timestamp=True writes a numeric epoch (tag 1) which is smaller but throws away the original offset, so everything decodes as UTC.

Teach the encoder about your own classcustom-type-encoder

import cbor2

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

def encode_point(encoder: cbor2.CBOREncoder, value: Point) -> None:
    encoder.encode_semantic(4000, [value.x, value.y])

data = cbor2.dumps(Point(4, 5), encoders={Point: encode_point})

The encoders mapping matches the exact type, not subclasses. Use the default= keyword instead if you want one catch-all callback for every unrecognised object. Pick a tag number outside the IANA-assigned ranges so you do not collide with a registered meaning.

Decode an unregistered semantic tag back into your classtag-hook-catch-all

import cbor2

def tag_hook(tag: cbor2.CBORTag, immutable: bool):
    if tag.tag == 4000:
        return Point(*tag.value)
    return tag

point = cbor2.loads(data, tag_hook=tag_hook)

The signature changed in 6.0. It used to be (decoder, tag); it is now (tag, immutable). Code copied from a pre-2026 blog post will fail with a confusing argument error. Always return the tag unchanged for numbers you do not handle, or unknown tags disappear.

Override how a specific registered tag decodesoverride-semantic-decoder

from datetime import datetime, timezone
import cbor2

def decode_epoch(item: int, immutable: bool) -> datetime:
    return datetime.fromtimestamp(item, timezone.utc)

payload = cbor2.dumps(cbor2.CBORTag(1, 1363896240))
print(cbor2.loads(payload, semantic_decoders={1: decode_epoch}))

semantic_decoders is for tags cbor2 already handles and you want handled differently; tag_hook is for tags it does not know. Supplying any semantic_decoders slows down every tagged value slightly, because each one now needs a trip back into the interpreter.

Serialize a structure that points back at itselfcyclic-value-sharing

import cbor2

node = {"name": "root"}
node["self"] = node

# cbor2.dumps(node) raises: cyclic data structure detected
data = cbor2.dumps(node, value_sharing=True)

back = cbor2.loads(data)
assert back["self"] is back

Without value_sharing the encoder refuses rather than looping forever. Turning it on tags every potentially shared list and dict, which costs bytes, and very few non-Python CBOR implementations understand the shared-value tags, so keep it to internal formats.

Produce deterministic bytes for signing or hashingcanonical-encoding

import cbor2

a = cbor2.dumps({"b": 1, "a": 2}, canonical=True)
b = cbor2.dumps({"a": 2, "b": 1}, canonical=True)
assert a == b

Without canonical=True map keys are written in dict insertion order, so two equal dicts can serialize to different bytes and break a signature check. Canonical mode sorts keys and uses the shortest encoding for each number, which is what COSE, CWT, and content-addressed storage expect.

Put limits on a decoder fed by the networkharden-untrusted-input

import io
import cbor2

raw = untrusted_bytes[:1_000_000]          # cap the size yourself first
decoder = cbor2.CBORDecoder(
    io.BytesIO(raw),
    max_depth=32,
    allow_indefinite=False,
    allow_duplicate_keys=False,
)
obj = decoder.decode()

The README states the library has not been tested against malicious input. max_depth defaults to 400 (added in 5.9.0 under CVE-2026-26209); lower it to whatever your schema actually nests. Rejecting indefinite-length items and duplicate map keys closes off two classes of parser-mismatch bug between you and the sender.

Read many CBOR items concatenated in one streamdecode-sequence

import io
import cbor2

buf = io.BytesIO(cbor2.dumps([1, 2]) + cbor2.dumps([3, 4]))
decoder = cbor2.CBORDecoder(buf)

while True:
    try:
        print(decoder.decode())
    except cbor2.CBORDecodeEOF:
        break

cbor2.load reads exactly one item, so a log file of appended records needs a CBORDecoder held open across calls. CBORDecodeEOF is the clean end-of-stream signal; a plain CBORDecodeError means the bytes are actually malformed.

Look at CBOR bytes from the shellcli-inspect

# hex on stdin, pretty JSON out
echo a16568656c6c6f65776f726c64 | xxd -r -ps | cbor2 --pretty

# base64 input
echo ggEC | python -m cbor2.tool --decode

# a stream of concatenated items
echo ggECggMEggUG | cbor2 -d --sequence

The cbor2 console script and python -m cbor2.tool are the same tool and ship with the package. The JSON it prints is explicitly lossy: bytes, non-string map keys, and tagged values all get flattened, so use it to eyeball a payload, never to convert one.

Alternatives

PackageRegistryPick it when
msgpackPyPIYour peers already speak MessagePack; it is a similar compact binary format with wider cross-language library support but no registered tag registry for things like Decimal.
orjsonPyPIYou want speed and small code changes but the format must stay JSON so browsers, logs, and existing services keep working.
msgspecPyPIYou want schema-typed structs validated on decode across JSON, MessagePack, YAML, and TOML, and you are willing to define the schema instead of decoding to plain dicts.
cbornpmYou are writing the other end of the same protocol in Node and need a CBOR codec that interoperates with what cbor2 emits.