Skip to main content

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 / BigAutoField primary keys and want the same shape of ids? See our sister library django-spicy-id.

Status: Stable. No warranty, see LICENSE.txt.

PyPI version PyPI Supported Python Versions Test status

Table of Contents

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, and u.

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_01h455vb4pex5vsknk084sn02q fails 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 UUIDField column (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. user or acct. 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.id is now 'user_01h455vb4pex5vsknk084sn02q', so anything calling .hex or .int on it, or handing it to something that expects a UUID object, needs updating. (One wrinkle: a new, unsaved instance still holds the raw uuid.UUID default until it is saved.)
  • Ids you have already handed out keep working. The field accepts raw UUID strings and uuid.UUID objects for lookups, so User.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.uuid4 explicitly, 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_..." where id="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)

Source distribution for django-typeid 0.9.1
File Size Uploaded
django_typeid-0.9.1.tar.gz 17.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-typeid 0.9.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.9.1 This release

2 release files

0.9.0

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