python-ulid
A fully-typed Python library for generating and parsing ULIDs — sortable, URL-safe unique identifiers with UUID interop and Pydantic support.
Repository Health
Technical Analysis
python-ulid implements the ULID specification for Python: a 128-bit, lexicographically sortable identifier that’s interchangeable with UUID but encodes as a shorter, URL-safe, case-insensitive 26-character string using Crockford’s base32. The ULID class supports construction from timestamps, datetimes, UUIDs, integers, hex strings, and raw bytes, and converts cleanly to and from uuid.UUID (including UUIDv4-style and round-trippable UUIDv7-style representations), making it easy to adopt ULIDs into an existing UUID-based schema incrementally.
Generation is handled by a ULIDGenerator that samples a clock, sources entropy, and applies a pluggable monotonicity policy (strict, lax, or pure-random) so that IDs created within the same millisecond stay correctly ordered — the generator is thread-safe via an internal lock and can be swapped process-wide by reassigning ulid.default_generator. The package also ships an optional Pydantic v2 integration for direct use as a validated model field type, and a ulid CLI for building and inspecting ULIDs from the shell.
What You Get
- A fully-typed
ULIDclass with constructors from timestamp, datetime, UUID, int, hex string, and raw bytes, plus a universalULID.parse()entry point that infers the source format - Lossless conversion to and from
uuid.UUID, including UUIDv4-style and round-trippable UUIDv7-style representations - A configurable
ULIDGeneratorwith three interchangeable monotonicity policies (StrictMonotonicPolicy,LaxMonotonicPolicy,PureRandomPolicy) for controlling same-millisecond ordering behavior - Native Pydantic v2 support (via the
pydanticextra) soULIDcan be used directly as a validated, JSON-serializable model field type - A
ulidcommand-line tool (ulid build,ulid show) for generating and inspecting IDs from the shell, including stdin piping for scripted use
Common Use Cases
- Replacing auto-incrementing or UUID4 primary keys in databases with sortable, timestamp-embedded identifiers
- Generating request, trace, or event IDs that sort naturally by creation time in logs and message queues
- Migrating a UUID-based schema to ULIDs incrementally via the library’s UUID and UUIDv7 interop methods
- Validating and parsing ULID values directly in Pydantic request/response models
Under The Hood
Architecture
The package is deliberately flat: ulid/__init__.py holds the public ULID class plus the generation machinery (ULIDGenerator, the MonotonicityPolicy protocol and its StrictMonotonicPolicy/LaxMonotonicPolicy/PureRandomPolicy implementations), ulid/base32.py isolates the bit-level Crockford base32 encode/decode arithmetic behind pure functions, ulid/constants.py centralizes byte-length and range constants shared by both, and ulid/__main__.py wraps everything in an argparse-based CLI. A single module-level default_generator is what the bare ULID() constructor and ULID.from_* classmethods delegate to, so swapping generation behavior process-wide is a one-line reassignment rather than a refactor; nothing else in the codebase depends on that generator being the default instance, which keeps the abstraction replaceable.
Tech Stack
Pure-Python with zero required third-party runtime dependencies (a typing-extensions backport only on Python <3.11); an optional pydantic>=2.0 extra adds __get_pydantic_core_schema__/_pydantic_validate hooks for native model-field support. Packaging uses hatchling with hatch-vcs for git-tag-derived versioning and hatch-fancy-pypi-readme to assemble the PyPI long description from a slice of the reStructuredText README. Development tooling is unusually current: ruff for both linting and formatting, pyrefly in strict mode for type checking (alongside mypy config retained for compatibility), pytest with pytest-cov, and poethepoet task runner wiring it together under uv.
Code Quality
Tests in tests/test_ulid.py, tests/test_base32.py, and tests/test_cli.py cover the public ULID API, the base32 codec, and the CLI end-to-end, using freezegun to deterministically test timestamp- and monotonicity-dependent behavior — including explicit tests for same-millisecond ordering and randomness-exhaustion edge cases. Every public function and method carries type hints, disallow_untyped_defs is enforced via mypy config, and pyrefly check runs in strict preset mode. Error handling favors explicit ValueError/TypeError with descriptive messages over silent coercion. CI (lint-and-test.yml) runs the full check suite (lint, format, types, docs) plus tests across supported Python versions on every push.
API Design
The library leans on Python idioms rather than inventing new ones: ULID implements __str__, __int__, __bytes__, __lt__/__eq__/__hash__ (via functools.total_ordering) so instances behave like native sortable, hashable values, and cached properties (timestamp, datetime, hex, milliseconds) avoid recomputing derived state. The parse() classmethod removes the need for callers to know which from_* constructor applies to a given input type, and the generator/policy split cleanly separates the rarely-changed default path from the fully customizable one — a caller who only wants ULID() never has to see ULIDGenerator or MonotonicityPolicy at all.