django-typeid
A Django model field implementing TypeID: globally-unique, k-sortable, type-prefixed identifiers like user_01h455vb4pex5vsknk084sn02q, stored in a native UUIDField column.
# models.py
from django_typeid import TypeIDField
class Invoice(models.Model):
id = TypeIDField(primary_key=True, prefix='invoice')
>>> Invoice.objects.create()
<Invoice: Invoice object (invoice_01h455vb4pex5vsknk084sn02q)>
>>> Invoice.objects.get(pk='invoice_01h455vb4pex5vsknk084sn02q')
<Invoice: Invoice object (invoice_01h455vb4pex5vsknk084sn02q)>
You can swap TypeIDField in for an existing UUIDField without a data migration, since the underlying column is unchanged. Your code starts seeing TypeID strings where it saw uuid.UUID objects; see Using an existing UUIDField column.
Are you stuck with numeric
AutoField/BigAutoFieldprimary keys and want the same shape of ids? See our sister library django-spicy-id.
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).
The database is unaffected, but a few things do change on the Python side:
- Reads give you a string, not a
uuid.UUID. This is the change most likely to break existing code:user.idis now'user_01h455vb4pex5vsknk084sn02q', so anything calling.hexor.inton it, or handing it to something that expects a UUID object, needs updating. (One wrinkle: a new, unsaved instance still holds the rawuuid.UUIDdefault until it is saved.) - Ids you have already handed out keep working. The field accepts raw UUID strings and
uuid.UUIDobjects for lookups, soUser.objects.get(pk='01890a5d-ac96-774b-bcce-b302099a8057')still resolves. Existing URLs, bookmarks, and API clients do not break. - New rows default to UUIDv7. If you would rather keep generating UUIDv4, pass
default=uuid.uuid4explicitly, at the cost of k-sortability. - DRF responses change shape. Call
monkey_patch_drf()so the field serializes as a string. Serialized output then carries TypeIDs where it used to carry bare UUIDs, which is a breaking change for your API consumers. - Forms and the admin need no changes. The field supplies its own form field, which accepts and normalizes the string format.
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.
Metadata
Release files for django-typeid 0.9.1
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_typeid-0.9.1.tar.gz | 17.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_typeid-0.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.2 kB
Release files / django_typeid-0.9.1.tar.gz
| Download URL | django_typeid-0.9.1.tar.gz |
|---|---|
| Size | 17.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1519258235775133b607de09b4df289091c204a248f58b1adf8519549ef76bcf
|
|
BLAKE2b-256 checksum How to use checksums |
503cb499a78f6757697d300a76f3022b819f5b4be24c91fe3ac30a63400e4da1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 6, 2026.
Transparency logRelease files / django_typeid-0.9.1-py3-none-any.whl
| Download URL | django_typeid-0.9.1-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1e19b4f0392422a2a64f5f767dd70e2a5c6fb2e8c0db83eebb5f462991bba8a0
|
|
BLAKE2b-256 checksum How to use checksums |
18baf760a4da5644bb88c0f93df8ac1f4b4e1c030eb9a81de2f3d1fcd2470727
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 6, 2026.
Transparency log