Keys

A key is a str, an int, a bytes, or a tuple of those. Keys are stored in an order-preserving encoding, which is what makes iteration sorted and range scans possible.

Types

users["alice"] = {...}          # str
events[1699000000] = {...}      # int, full 64-bit range
blobs[b"\x01\x02"] = {...}      # bytes
edges[("a", "b")] = {...}       # tuple of the above

What is rejected

KeyResultWhy
1.5TypeErrorfloat ordering has NaN and -0.0 hazards
TrueTypeErrorbool is an int subclass; it would alias 1
NoneTypeErrorno meaningful position in the ordering
(1, (2, 3))TypeErrortuples are flat, not nested
2**63ValueErroroutside the 64-bit range

Rejection happens at write time, so a bad key never reaches storage.

Ordering

Components sort by type first, then by value: int < str < bytes. Strings compare by their UTF-8 bytes, and tuples compare componentwise.

c = db["c"]
for k in [b"q", "z", 5, -1, ("t", 1)]:
    c[k] = {}

list(c)
# [-1, 5, ('t', 1), 'z', b'q']

('t', 1) precedes 'z' because the comparison reaches the first component before anything else: 't' < 'z'. Tuple keys are not a separate category that sorts after scalars — they interleave, component by component.

A shorter key sorts before any key extending it:

[("a",), ("a", 1), ("a", 2), ("b",)]    # already in order

A scalar is a 1-tuple

"a" and ("a",) are the same key. The encoding treats a scalar as a one-component tuple, so there is a single code path and no ambiguity:

c[("a",)] = {"n": 1}
c["a"]                          # {'n': 1}
list(c)                         # ['a'] — decodes to the scalar

Ranges

range(start, stop) is half-open — start included, stop excluded — and either bound may be None for unbounded:

c.range("a", "m")               # 'a' <= key < 'm'
c.range(None, "m")              # everything below 'm'
c.range("a", None)              # everything from 'a' up

Both bounds must be the same key type; mixing them raises TypeError.

Prefixes

prefix(p) means two different things depending on what you pass, and both are useful:

logs.prefix("2026-08")          # str: keys of that type beginning with it
blobs.prefix(b"\x01")           # bytes: likewise
edges.prefix(("a",))            # tuple: keys whose leading components match

A string or bytes prefix matches on characters. A tuple prefix matches on whole components — ("a",) matches ("a", 1) and ("a", "b"), but never ("ab",).

Iteration order is not insertion order

This is the sharpest departure from dict. If you need insertion order, store a sequence number in the key:

events[(seq, "created")] = {...}