TypeID fields for Django models: type-prefixed, k-sortable UUIDv7 identifiers.
Project description
django-typeid
A Django model field implementing TypeID: globally-unique, k-sortable, type-prefixed identifiers like user_01h455vb4pex5vsknk084sn02q, stored in a native UUIDField column.
Status: Stable. No warranty, see LICENSE.txt.
Table of Contents
- What is a TypeID?
- Why use TypeIDs?
- Installation
- Usage
- API reference
- Related projects
- Tips and tricks
- Maintainer notes
- Changelog
What is a TypeID?
A TypeID is a type-prefixed, globally-unique identifier. If you've used an API like Stripe's, you've seen the shape:
user_01h455vb4pex5vsknk084sn02q
└──┘ └────────────────────────┘
prefix suffix
- The prefix identifies the record type. It's lowercase ASCII
[a-z_], at most 63 characters, and starts and ends with a letter. It may also be empty. - The suffix is a 128-bit UUIDv7 rendered as exactly 26 characters of Crockford base32, an alphabet that omits the ambiguous letters
i,l,o, andu.
TypeID is a cross-language standard, with implementations in Go, Rust, Python, TypeScript, and more, so the ids your Django app emits are parseable everywhere else in your stack.
Why use TypeIDs?
- Readability: Primary keys are supposedly "anonymous", but they still show up in URLs, logfiles, support tickets, and query output. It's much faster to understand what you're looking at when the identifier says what it is.
- Conflict and accident prevention: When every id is typed, whole classes of mistakes become impossible.
HTTP DELETE /users/invoice_01h455vb4pex5vsknk084sn02qfails fast. - Globally unique and k-sortable: Unlike a database sequence, ids can be generated anywhere, before the row is written, without coordination. Because UUIDv7 leads with a millisecond timestamp, ids also sort roughly by creation time, which keeps index locality much better than UUIDv4.
- Compact storage: The value is stored in a native
UUIDFieldcolumn (16 bytes on backends that support it), not as text.
For a more detailed look at this pattern, see Stripe's "Object IDs: Designing APIs for Humans".
Installation
Requirements
This package supports and is tested against the latest patch versions of:
- Python: 3.12, 3.13, 3.14
- Django: 4.2, 5.2, 6.0
- MySQL: 8.0+
- PostgreSQL: 14+
- SQLite: 3.9.0+
All database backends are tested with the latest versions of their drivers. SQLite is also tested on GitHub Actions' latest macOS virtual environment.
Instructions
pip install django-typeid
Usage
Declare the field, giving it a prefix:
from django.db import models
from django_typeid import TypeIDField
class User(models.Model):
id = TypeIDField(primary_key=True, prefix='user')
That's the whole setup. New rows get a freshly generated UUIDv7, and the value reads back as a TypeID string:
>>> u = User.objects.create()
>>> u.id
'user_01h455vb4pex5vsknk084sn02q'
>>> found_user = User.objects.filter(id='user_01h455vb4pex5vsknk084sn02q').first()
>>> found_user == u
True
The field accepts TypeID strings, uuid.UUID objects, and raw UUID strings interchangeably, both when assigning and when querying:
>>> import uuid
>>> u = User.objects.create(id=uuid.UUID('01890a5d-ac96-774b-bcce-b302099a8057'))
>>> u.id
'user_01h455vb4pex5vsknk084sn02q'
>>> User.objects.filter(id='01890a5d-ac96-774b-bcce-b302099a8057').first() == u
True
The library is validated against the spec's official valid and invalid conformance vectors.
Parameters
TypeIDField takes every parameter a normal UUIDField does, plus one of its own:
prefix: The type prefix, e.g.useroracct. Must follow the TypeID rules: lowercase ASCII[a-z_], at most 63 characters, starting and ending with a letter. Defaults to the empty string, which produces a bare 26-character id with no separator. Note: this library does not ensure the prefix you provide is unique within your project. You should ensure that.
Everything else about the format is fixed by the spec, and therefore not configurable: the encoding is always Crockford base32, the separator is always _, and the suffix is always zero-padded to exactly 26 characters. Passing encoding= or sep= raises ImproperlyConfigured.
The default value is a UUIDv7 generated by uuid7(). Pass your own default= to override it.
Using an existing UUIDField column
TypeIDField is backed by a plain Django UUIDField, so adopting it on a model that already uses UUID primary keys is a display-layer change: the generated migration only alters the field's arguments, and no data is rewritten.
class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4) # before
id = TypeIDField(primary_key=True, prefix='user') # after
Existing rows keep whatever UUIDs they already have. Those render as valid TypeIDs, since the suffix is just a base32 encoding of the 128-bit value; only newly generated ids will be UUIDv7 (and therefore k-sortable).
Registering URLs
When installing routes that must match a specific TypeID, use the get_url_converter() helper to install a Django custom path converter.
Using this method ensures that only valid id strings for that field will be presented to your view.
Example:
# models.py
class User(models.Model):
id = TypeIDField(primary_key=True, prefix='user')
# urls.py
from . import models
from django.urls import path, register_converter
from django_typeid import get_url_converter
# Register the pattern for `User.id` as "user_id". You should do this once for
# each unique TypeID field.
register_converter(get_url_converter(models.User, 'id'), 'user_id')
urlpatterns = [
path('users/<user_id:id>', views.user_detail),
...
]
# views.py
def user_detail(request, id):
user = models.User.objects.get(id=id)
...
Django REST Framework
Django REST Framework (DRF) works mostly without issue with django-typeid. However, an additional step is needed so that DRF treats the field as a string, not a UUID, in serializers.
You can use the included utility function to monkey patch DRF. It is safe to call this method multiple times.
from django_typeid import monkey_patch_drf
monkey_patch_drf()
Field attributes
The following attributes are available on the field once constructed.
.validate_string(strval)
Checks whether strval is a legal value for the field, throwing django_typeid.MalformedTypeIDError if not.
.re
A compiled regex which can be used to validate a string.
.re_pattern
A string regex pattern which can be used to validate a string. Unlike the pattern used in re, this pattern does not include the leading ^ and trailing $ boundary characters, making it easier to use in things like Django url patterns.
You probably don't need to use this directly, instead see get_url_converter().
Utility methods
These utility methods are provided on the top-level django_typeid module.
get_url_converter(model_class, field_name)
Returns a Django custom path converter for field_name on model_class.
See Registering URLs for example usage.
uuid7()
Returns a new UUIDv7 (time-ordered), per RFC 9562. This is the field's default value generator. It uses the standard library's uuid.uuid7() on Python 3.14+, and a built-in implementation on older versions.
Errors
django.db.utils.ProgrammingError
Thrown when attempting to access or query this field using an illegal value. Some examples of this situation:
- Providing an id with the wrong prefix (e.g.
id="acct_..."whereid="invoice_..."is expected). - Providing a string with illegal characters in it, or of the wrong length.
- Providing a value that is neither a string nor a
uuid.UUID.
You can consider these situations analogous to providing a wrongly-typed object to any other field type, for example SomeModel.objects.filter(id=object()).
You can avoid this situation by validating inputs first. See Field attributes.
🚨 Warning: The string value of a TypeID must always be treated as an exact value. Just like you would never modify the contents of a UUID, a TypeID string must never be translated, re-interpreted, or changed by a client.
django_typeid.MalformedTypeIDError
A subclass of ValueError, raised by .validate_string(strval) when the provided string is invalid for the field's configuration. Its base class, django_typeid.TypeIDError, is the root of the library's error hierarchy.
API reference
Complete reference documentation for every public field, function, and error, generated from the library's docstrings, lives in docs/api.md.
Related projects
If you like the shape of these ids but want them backed by an ordinary integer AutoField, see django-spicy-id. It provides drop-in replacements for Django's AutoField that simulate typed ids: the stored value is still a database-generated integer, but it is displayed and queried as a prefixed string like user_8M0kX, in your choice of encoding and separator.
The two libraries solve similar problems with different tradeoffs:
django-typeid |
django-spicy-id |
|
|---|---|---|
| Backing column | UUIDField (128-bit) |
AutoField / BigAutoField |
| Value generated by | your app, before insert | the database, on insert |
| Format | fixed by the TypeID spec | configurable encoding, separator, padding |
| Interoperable with other TypeID implementations | yes | no |
| Ids reveal row counts or insert order | no | yes, unless randomize is used |
They can be used side by side in the same project.
Tips and tricks
Don't change the prefix
Changing prefix after you have started using the field should be considered a breaking change for any external callers.
Although the stored UUIDs are never changed, any ids you previously handed out will no longer be accepted by the field, and clients that stored them will find they no longer resolve.
Maintainer notes
Release instructions and other notes for maintainers live in docs/maintainer-notes.md.
Changelog
See CHANGELOG.md for a summary of changes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_typeid-0.9.0.tar.gz.
File metadata
- Download URL: django_typeid-0.9.0.tar.gz
- Upload date:
- Size: 16.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1b17958279e6ddf999cde76b821e881a2d1e1c334ca61fcf3a36ca3f8d9a1ef
|
|
| MD5 |
903ba19d1e93f664e5876f224283c7b4
|
|
| BLAKE2b-256 |
184e8b6774df02d30b6357bffb4f479e1dd58ea7a590e5fe34847d09b0f9a2bf
|
Provenance
The following attestation bundles were made for django_typeid-0.9.0.tar.gz:
Publisher:
publish.yml on mik3y/django-typeid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_typeid-0.9.0.tar.gz -
Subject digest:
b1b17958279e6ddf999cde76b821e881a2d1e1c334ca61fcf3a36ca3f8d9a1ef - Sigstore transparency entry: 2252035413
- Sigstore integration time:
-
Permalink:
mik3y/django-typeid@3ebf177fa2710346461ae2de262eea9e2ce76ae8 -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/mik3y
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3ebf177fa2710346461ae2de262eea9e2ce76ae8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_typeid-0.9.0-py3-none-any.whl.
File metadata
- Download URL: django_typeid-0.9.0-py3-none-any.whl
- Upload date:
- Size: 21.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f451763a3ab6ba2bd5abe04f0d3ddeba326d489bdb1e596b89a2cadea8fb99b2
|
|
| MD5 |
83e41a7bfca1a13930c98ec3d0fc6ef1
|
|
| BLAKE2b-256 |
b41bda645e35d05533a48510317adb86614e298e66cddfb7ba1d9d536ae19388
|
Provenance
The following attestation bundles were made for django_typeid-0.9.0-py3-none-any.whl:
Publisher:
publish.yml on mik3y/django-typeid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_typeid-0.9.0-py3-none-any.whl -
Subject digest:
f451763a3ab6ba2bd5abe04f0d3ddeba326d489bdb1e596b89a2cadea8fb99b2 - Sigstore transparency entry: 2252035524
- Sigstore integration time:
-
Permalink:
mik3y/django-typeid@3ebf177fa2710346461ae2de262eea9e2ce76ae8 -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/mik3y
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3ebf177fa2710346461ae2de262eea9e2ce76ae8 -
Trigger Event:
push
-
Statement type: