Skip to main content

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)

Source distribution for dealcode 1.0.1
File Size Uploaded
dealcode-1.0.1.tar.gz 14.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dealcode 1.0.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page