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.
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| django_ulidfield-0.1.0.tar.gz | 5.1 kB | Without provenance | Details for django_ulidfield-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_ulidfield-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for django_ulidfield-0.1.0-py3-none-any.whl |
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
|
Security for django-ulidfield 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.