Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ This repository continues the `overflowdigital` `cuid2` package (`b02c8ca`); ver

## [Unreleased]

### Changed

- Faster ID generation and cold-start imports via single-draw `getrandbits` MSB fixed-point entropy and letter generation (`< 2^-64` bias), leading-digit base36 SHA3-512 hash encoding, no per-instance lock for the default `Cuid()` and `create_id()` generator (its `random.SystemRandom` and counter are already thread-safe), reduced reference-count contention on free-threaded Python (`3.14t` and `3.15t`), removed `inspect`, `string`, and `secrets` imports, and a deferred `socket` import.
- `random_generator` is typed as `random.Random` instead of `_random.Random`, and random bits are drawn with its `.getrandbits(k)`. Generators without `.getrandbits(k)`, or whose class defines `.random()` but not `.getrandbits(k)` (such as `random.Random` subclasses following the custom-generator pattern in the `random` module documentation), keep one `.random()` call per character and produce the same IDs as in 2.2.0.

**Upgrade note:** IDs from a seeded generator that provides `.getrandbits(k)` (for example `functools.partial(random.Random, 42)`) differ from 2.2.0 for the same seed, so update any stored expected IDs in tests. Test doubles that create attributes on demand (such as `unittest.mock.Mock` or `MagicMock`) and stub only `.random` must stub `.getrandbits` instead.

## [2.2.0] - 2026-10-30

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ by [Parallel Drive](https://github.com/paralleldrive) to Python 3.

## What is CUID2?

* Secure: It's not feasible to guess the next ID (unguessability derives from the `secrets.SystemRandom` CSPRNG salt; per [RFC 9562 §8](https://www.rfc-editor.org/rfc/rfc9562.html#section-8), IDs are identifiers, not capability tokens or secrets).
* Secure: It's not feasible to guess the next ID (unguessability derives from the `random.SystemRandom` CSPRNG salt; per [RFC 9562 §8](https://www.rfc-editor.org/rfc/rfc9562.html#section-8), IDs are identifiers, not capability tokens or secrets).
* Collision resistant: It's extremely unlikely to generate the same ID twice.
* Horizontally scalable: Generate IDs on multiple machines without coordination.
* Offline-compatible: Generate IDs without a network connection.
Expand All @@ -41,7 +41,7 @@ For more information on the improvements of CUID2 over CUID, see the
* **123.6-Bit Default Keyspace (`length=24`)**: 1 leading lowercase letter ($26$) followed by 23 base36 characters ($36^{23}$) yields $26 \times 36^{23} \approx \mathbf{1.61 \times 10^{37}}$ possible IDs ($\approx \mathbf{123.6\text{ bits}}$ of entropy)—slightly more than UUIDv4's 122 random bits in a 33% shorter string (24 vs. 36 chars). Reaching a 50% birthday-bound collision probability requires $\sqrt{2 \ln(2) \times 26 \times 36^{23}} \approx \mathbf{4.7 \times 10^{18}}$ IDs (generating **149 million IDs/sec continuously for 1,000 years**).
* **Up to 165.0 Bits (`BIG_LENGTH = 32`)**: At `length=32`, the keyspace expands to $26 \times 36^{31} \approx \mathbf{4.54 \times 10^{49}}$ IDs ($\approx \mathbf{165.0\text{ bits}}$; 50% collision bound at $\approx 7.9 \times 10^{24}$ IDs).
* **Histogram De-Biasing**: Because $2^{512} \approx 1.34 \times 10^{154}$ is not an exact power of 36, the most-significant base36 digit of a 512-bit SHA3-512 digest is biased toward low digits; CUID2 drops that leading hash character (`[1:]`) and prepends an independent uniform `[a-z]` letter so every character position has a uniform distribution.
* **Thread, Pre-Fork, & Container Snapshot Resilience**: `Cuid`, `create_id`, `cuid_wrapper`, and `create_counter` are thread-safe (including on free-threaded Python, tested on `3.14t` and `3.15t`). Each generator records the process ID (`os.getpid()`) that built its state; the first ID generated in a forked child process (`multiprocessing`, Gunicorn, Celery prefork, and uWSGI workers, including uWSGI's default mode, which skips Python's `os.register_at_fork` hooks) rebuilds that state with a fresh lock, RNG state, random initial counter (`INITIAL_COUNT_MAX = 476,782,367`, giving $\approx 25,700$ concurrent hosts before a 50% chance of two hosts starting at the same counter value), and child-PID fingerprints (`os.getpid()`, `socket.gethostname()`, and environment variable keys). For cloned VM/container snapshots where no `fork()` syscall occurs, per-call `secrets.SystemRandom` (`os.urandom`) salt and `time.time_ns()` provide defense-in-depth against state collisions.
* **Thread, Pre-Fork, & Container Snapshot Resilience**: `Cuid`, `create_id`, `cuid_wrapper`, and `create_counter` are thread-safe (including on free-threaded Python, tested on `3.14t` and `3.15t`). Each generator records the process ID (`os.getpid()`) that built its state; the first ID generated in a forked child process (`multiprocessing`, Gunicorn, Celery prefork, and uWSGI workers, including uWSGI's default mode, which skips Python's `os.register_at_fork` hooks) rebuilds that state with a fresh RNG state, lock (when `random_generator` returns something other than a `random.SystemRandom` instance or `counter` is not the default; the default `random.SystemRandom` and counter are already thread-safe), random initial counter (`INITIAL_COUNT_MAX = 476,782,367`, giving $\approx 25,700$ concurrent hosts before a 50% chance of two hosts starting at the same counter value), and child-PID fingerprints (`os.getpid()`, `socket.gethostname()`, and environment variable keys). For cloned VM/container snapshots where no `fork()` syscall occurs, per-call `random.SystemRandom` (`os.urandom`) salt and `time.time_ns()` provide defense-in-depth against state collisions.
* **Polyglot ORM Compatibility (Prisma & Drizzle)**: Generates 24-character base36 primary keys compatible with TypeScript/Node.js services using Prisma (`@default(cuid(2))` / `@onCreate(cuid(2))`) or Drizzle (`@paralleldrive/cuid2`), and replaces the deprecated CUIDv1 (`cuid`) format.

| Property | CUID2 (`cuid2`) | UUIDv7 (`uuid.uuid7`) | UUIDv4 (`uuid.uuid4`) | ULID (`python-ulid`) | NanoID (`nanoid`) |
Expand Down
2 changes: 1 addition & 1 deletion local/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def test_cli_custom_fingerprint(
capsys: pytest.CaptureFixture[str],
) -> None:
"""Tests that -f/--fingerprint is hashed verbatim (like upstream) and rejects empty strings."""
hash_spy = mocker.spy(cuid2.utils, "create_hash")
hash_spy = mocker.spy(cuid2.utils, "_create_hash_slice")
cli_main(["-f", "host-a", "-n", "2"])
lines = capsys.readouterr().out.splitlines()
assert len(lines) == 2
Expand Down
Loading
Loading