dealcode (Python)
Collision-free, random-looking codes from a counter. Python implementation of the dealcode spec.
Install
pip install dealcode
Requires Python ≥ 3.9. The only dependency is cryptography (PyCA), used for AES.
Quickstart
from dealcode import Dealcode
codec = Dealcode(key="0a1b...64-hex-chars-from-your-secret-manager")
codec.encode(0) # '767a5b' (6 hex chars)
codec.encode(1) # '421163' never collides with any other counter
codec.decode("421163") # 1
(The outputs shown are the real ones for this exact key and default config — with your own key every counter maps to a different, but equally stable, code.)
The key can be raw bytes (16/24/32 bytes are used as-is as an AES key) or any
string/bytes, which are deterministically expanded to an AES-256 key. Generate
one with openssl rand -hex 32 and keep it in your secret manager — the
mapping is stable only while the key (and every other option) stays fixed.
Options
Dealcode(
key, # bytes | str
alphabet="hex", # "dec" | "hex" | "base32" | "crockford" | "base36"
# | "base58" | "base62" | "base64url" | custom string
min_length=6, # codes start at this length...
max_length=None, # ...and grow one char at a time up to this (default: max for 2^63)
domain="", # namespace: same key, unrelated codes per domain
)
coupon = Dealcode(key, "crockford", domain="coupons") # human-friendly, e.g. '7Q4WKZ'
order = Dealcode(key, "dec", min_length=8, domain="orders") # digits only
fixed = Dealcode(key, "hex", min_length=16, max_length=16) # constant-length
decode raises InvalidCodeError for malformed input — wrong length,
characters outside the alphabet, or a value outside the issuable range.
A well-formed code always decodes to some counter, whether or not that
counter was ever issued (inherent to a permutation — see SPEC §7). Treat
decode as parsing, not proof of existence: look the counter up before acting
on it, and note that a one-character typo in a valid code can resolve to a
different valid counter — add rate limiting (and, for human-typed flows, an
existence check or your own check digit). encode raises RangeError
outside [0, codec.capacity), and construction mistakes (bad key, alphabet,
lengths, domain) raise ConfigError. All errors subclass DealcodeError
(a ValueError).
Preset alphabets normalize obvious typing variants before decoding — hex
is case-insensitive, and crockford also folds O → 0 and I/L → 1 —
but normalization never strips anything: separators and whitespace are not
removed. If you display codes grouped (XXXX-XXXX), strip the hyphens or
spaces in your app before calling decode.
Using it with your database
Dealcode does not talk to your database — it only turns a counter into a code. Any source of never-repeating integers works. With PostgreSQL:
CREATE SEQUENCE order_code_seq AS bigint MINVALUE 0 START WITH 0;
CREATE TABLE orders (
id bigint PRIMARY KEY, -- the counter
code text NOT NULL UNIQUE, -- safety net; alerts on config mistakes
...
);
import os
from sqlalchemy import text
from dealcode import Dealcode, InvalidCodeError
codec = Dealcode(key=os.environ["DEALCODE_KEY"], domain="orders")
def create_order(conn) -> str:
n = conn.execute(text("SELECT nextval('order_code_seq')")).scalar_one()
code = codec.encode(n)
conn.execute(
text("INSERT INTO orders (id, code) VALUES (:id, :code)"),
{"id": n, "code": code},
)
return code
def find_order(conn, code: str):
try:
n = codec.decode(code) # malformed codes never reach the DB
except InvalidCodeError:
return None
return conn.execute(text("SELECT * FROM orders WHERE id = :id"), {"id": n}).first()
Sequences never hand out the same number twice (even across concurrent transactions and rollbacks), so codes never collide. Gaps in the sequence are invisible — codes look random anyway.
If the UNIQUE constraint on code ever fires, do not retry: it means the
key/config changed for an existing namespace. Investigate.
Fixed-length cycling mode
For code shapes that must never grow — airline-PNR-style fixed-length codes —
CyclingDealcode (SPEC §11) fills the entire fixed-length space, and when it
is exhausted refills the same space through a different permutation instead
of adding a character:
from dealcode import CyclingDealcode
pnr = CyclingDealcode(key, "crockford", length=6, domain="bookings")
code = pnr.encode(n) # always exactly 6 chars; cycle = n // pnr.capacity
cycle = pnr.cycle_of(n) # which cycle a counter belongs to — store it with the code
n = pnr.decode(code, cycle=3) # the cycle is required context
Configuration mirrors Dealcode (key, alphabet, domain) with a single fixed
length: 2 <= length <= 128 and 100 <= radix**length <= 2**63.
Codes repeat across cycles (the space is being reused — that's the
point), so keep at most one cycle's codes live per uniqueness scope: retire
or expire cycle e before issuing from e+1, index with
UNIQUE(cycle, code) rather than UNIQUE(code), and store each live code's
cycle — decode needs it. Decoding with a wrong (but in-range) cycle is
not an error — it silently returns a different counter; the existence
check on the decoded counter is what catches it.
Thread safety & performance
A Dealcode instance is immutable and thread-safe; create one per namespace at
startup and reuse it. Encoding is ten AES-CBC-MAC rounds — tens of microseconds,
no allocation-heavy paths, O(1) in the counter value.
Running the tests
From the repository root:
pip install -e ./python pytest # or: export PYTHONPATH=python/src
python -m pytest python/tests
The suite covers the official NIST FF1 sample vectors, every shared
cross-language vector in
testvectors/,
and behavioural/edge cases.
License
MIT — see LICENSE.
Metadata
Release files for dealcode 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dealcode-1.0.1.tar.gz | 14.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dealcode-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.2 kB
Release files / dealcode-1.0.1.tar.gz
| Download URL | dealcode-1.0.1.tar.gz |
|---|---|
| Size | 14.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4e7e4e6a34b8772dd326828184f3b59cc5d6f87c8da41fa33d363bc98a2e9df
|
|
BLAKE2b-256 checksum How to use checksums |
58753ab446f955298bc61ff7b1dc59cdfc4a28bd61e92374509b5ed072be4e69
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency logRelease files / dealcode-1.0.1-py3-none-any.whl
| Download URL | dealcode-1.0.1-py3-none-any.whl |
|---|---|
| Size | 14.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a06ce70f3cb38d2d18cffe229059b6ac77bf60faa9bb5aa3c33817cf71e08614
|
|
BLAKE2b-256 checksum How to use checksums |
e4f15f7995e9e418a15864d538a31e8f1d29153382f905bafa8e18727c176f9c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency log