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:
- Direct replacement (new records only):
# Change this:
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
# To this:
id = ULIDField(primary_key=True)
- 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
- ULID Specification
- "Identity Crisis: Sequence v. UUID as Primary Key" - Deep dive on database identifier strategies
- python-ulid library
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)
| File | Size | Uploaded | |
|---|---|---|---|
| django_ulidfield-0.1.0.tar.gz | 5.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|