Skip to main content

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.

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).

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_typeid-0.9.0.tar.gz (16.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_typeid-0.9.0-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

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

Hashes for django_typeid-0.9.0.tar.gz
Algorithm Hash digest
SHA256 b1b17958279e6ddf999cde76b821e881a2d1e1c334ca61fcf3a36ca3f8d9a1ef
MD5 903ba19d1e93f664e5876f224283c7b4
BLAKE2b-256 184e8b6774df02d30b6357bffb4f479e1dd58ea7a590e5fe34847d09b0f9a2bf

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_typeid-0.9.0.tar.gz:

Publisher: publish.yml on mik3y/django-typeid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for django_typeid-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f451763a3ab6ba2bd5abe04f0d3ddeba326d489bdb1e596b89a2cadea8fb99b2
MD5 83e41a7bfca1a13930c98ec3d0fc6ef1
BLAKE2b-256 b41bda645e35d05533a48510317adb86614e298e66cddfb7ba1d9d536ae19388

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_typeid-0.9.0-py3-none-any.whl:

Publisher: publish.yml on mik3y/django-typeid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page