python-ulid

A fully-typed Python library for generating and parsing ULIDs — sortable, URL-safe unique identifiers with UUID interop and Pydantic support.

Library
PyPI
v4.0.1
567stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
66/100Good
Development Activity68
Maintenance60
Community48
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
80/100Excellent
Architecture85
Code Quality88
Innovation55
Learning Curve90

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 ULID class with constructors from timestamp, datetime, UUID, int, hex string, and raw bytes, plus a universal ULID.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 ULIDGenerator with three interchangeable monotonicity policies (StrictMonotonicPolicy, LaxMonotonicPolicy, PureRandomPolicy) for controlling same-millisecond ordering behavior
  • Native Pydantic v2 support (via the pydantic extra) so ULID can be used directly as a validated, JSON-serializable model field type
  • A ulid command-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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search