Skip to main content

django-ulidfield

A drop-in Django model field for storing sortable, time-encoded ULIDs as 26-character strings.

What are ULIDs?

ULIDs (Universally Unique Lexicographically Sortable Identifiers) are a modern alternative to UUIDs that combine the benefits of both sequential integers and random UUIDs. They consist of:

  • 48-bit timestamp (milliseconds since Unix epoch)
  • 80-bit randomness
    01AN4Z07BY             79KA1307SR9X4MV3
|----------------|    |------------------------|
       time                   randomness
      48bits                    80bits

ULIDs are encoded in base-32 (Crockford's Base32) resulting in 26-character strings that are:

  • Sortable by creation time
  • URL-safe (no special characters)
  • Case-insensitive
  • Compatible with UUID storage (128-bit)

Why ULIDs over UUIDs?

As explained in Brandur Leach's article "Identity Crisis: Sequence v. UUID as Primary Key", ULIDs solve several problems with traditional UUID v4:

Problems with Random UUIDs

  • Poor database performance: Random UUIDs cause index fragmentation and cache misses
  • High WAL overhead: More write-ahead log data due to scattered page updates
  • No temporal ordering: Can't sort by creation time

ULID Advantages

  • Time-ordered: ULIDs sort naturally by creation time
  • Better database performance: Sequential timestamp prefix reduces index fragmentation
  • Distributed generation: No single point of failure like auto-incrementing integers
  • Opaque to users: Prevents enumeration attacks and business intelligence leakage
  • UUID compatible: Can be stored in UUID columns when needed

Installation

pip install django-ulidfield

Or with Poetry:

poetry add django-ulidfield

Usage

Basic Usage

from django.db import models
from django_ulidfield import ULIDField

class Article(models.Model):
    id = ULIDField(primary_key=True)
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

# ULIDs are automatically generated
article = Article.objects.create(title="Hello World", content="...")
print(article.id)  # Output: 01AN4Z07BY79KA1307SR9X4MV3

Non-Primary Key Usage

class Order(models.Model):
    id = models.AutoField(primary_key=True)
    order_id = ULIDField()  # Unique by default
    customer_email = models.EmailField()
    total = models.DecimalField(max_digits=10, decimal_places=2)

Custom Configuration

class Document(models.Model):
    # Allow null values
    doc_id = ULIDField(null=True, blank=True)

    # Custom default function
    tracking_id = ULIDField(default=None, null=True)

    # Allow duplicates (not recommended)
    reference_id = ULIDField(unique=False)

Field Options

ULIDField inherits from Django's CharField and accepts all the same options, with these defaults:

  • max_length=26 (ULIDs are always 26 characters)
  • unique=True (ULIDs should be unique)
  • editable=False (ULIDs are typically auto-generated)
  • default=generate_ulid (automatically generates new ULIDs)
  • blank=False (ULIDs are required by default)

Validation

The field automatically validates that values are proper ULIDs:

# This will raise a ValidationError
invalid_article = Article(id=\"invalid-ulid\")
invalid_article.full_clean()  # ValidationError: 'invalid-ulid' is not a valid ULID

Database Considerations

Index Performance

ULIDs provide better database performance than random UUIDs because:

  • The timestamp prefix keeps new insertions clustered together
  • Reduces index page splits and cache misses
  • Minimizes write-ahead log (WAL) overhead

Storage

  • Database storage: 26 characters (can be optimized to 16 bytes in UUID columns)
  • Memory/JSON: 26-character string
  • URL-safe: Can be used directly in URLs

Migration from UUIDs

If you're migrating from UUIDs, you can:

  1. Direct replacement (new records only):
# Change this:
id = models.UUIDField(primary_key=True, default=uuid.uuid4)

# To this:
id = ULIDField(primary_key=True)
  1. Gradual migration (with a new field):
class MyModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)  # Keep existing
    ulid = ULIDField(null=True, blank=True)  # Add new field

Time Extraction

You can extract the timestamp from a ULID:

from ulid import ULID

# Get timestamp from ULID
ulid_obj = ULID.from_str(article.id)
timestamp = ulid_obj.timestamp()
datetime_obj = ulid_obj.datetime()

Development

Setup

git clone https://github.com/your-username/django-ulidfield
cd django-ulidfield
poetry install
poetry run pre-commit install

Running Tests

poetry run pytest

Code Quality

This project uses:

  • Ruff for linting and formatting
  • pytest for testing
  • pre-commit for code quality checks

Requirements

  • Python 3.9+
  • Django 4.2+
  • python-ulid 3.0.0+

License

MIT License - see LICENSE file for details.

Related Resources

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Release files for django-ulidfield 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-ulidfield 0.1.0
File Size Uploaded
django_ulidfield-0.1.0.tar.gz 5.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-ulidfield 0.1.0
File Interpreter ABI Platform
django_ulidfield-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 10.8 kB

Release files / django_ulidfield-0.1.0.tar.gz

Download URL django_ulidfield-0.1.0.tar.gz
Size 5.1 kB
Tags Source
SHA-256 checksum
How to use checksums
890d3143d604ff7f67647bff91482c5e6d8c8deaa77b272da37d9b1ae98f2418
BLAKE2b-256 checksum
How to use checksums
5f452006f76ed1d09b0a0a5ad9f2fb8823f8da0fa17de9ba8295a390d8150f9e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.1.3 CPython/3.12.3 Linux/6.11.0-1018-azure

Release files / django_ulidfield-0.1.0-py3-none-any.whl

Download URL django_ulidfield-0.1.0-py3-none-any.whl
Size 5.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad03f9c8c2677a56da66c8211d2834fa4ef54fe1b453916e9400061a7d173f03
BLAKE2b-256 checksum
How to use checksums
e9c39a69c26e00ce1d8f46bc6ccb9153f6e5468fbd4deacfc83001ee09b9948e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.1.3 CPython/3.12.3 Linux/6.11.0-1018-azure

Release history Release notifications | RSS feed

This release

0.1.0 This release

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