Metadata-Version: 2.4
Name: django-typeid
Version: 0.9.0
Summary: TypeID fields for Django models: type-prefixed, k-sortable UUIDv7 identifiers.
Author: mike wakerly
Author-email: mike wakerly <opensource@hoho.com>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.12, <4
Description-Content-Type: text/markdown

# django-typeid

A Django model field implementing [TypeID](https://github.com/jetify-com/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](https://badge.fury.io/py/django-typeid.svg)](https://badge.fury.io/py/django-typeid)
[![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/django-typeid.svg)](https://pypi.python.org/pypi/django-typeid/) ![Test status](https://github.com/mik3y/django-typeid/actions/workflows/test.yml/badge.svg)

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [What is a TypeID?](#what-is-a-typeid)
- [Why use TypeIDs?](#why-use-typeids)
- [Installation](#installation)
  - [Requirements](#requirements)
  - [Instructions](#instructions)
- [Usage](#usage)
  - [Parameters](#parameters)
  - [Using an existing `UUIDField` column](#using-an-existing-uuidfield-column)
  - [Registering URLs](#registering-urls)
  - [Django REST Framework](#django-rest-framework)
  - [Field attributes](#field-attributes)
    - [`.validate_string(strval)`](#validate_stringstrval)
    - [`.re`](#re)
    - [`.re_pattern`](#re_pattern)
  - [Utility methods](#utility-methods)
    - [`get_url_converter(model_class, field_name)`](#get_url_convertermodel_class-field_name)
    - [`uuid7()`](#uuid7)
  - [Errors](#errors)
    - [`django.db.utils.ProgrammingError`](#djangodbutilsprogrammingerror)
    - [`django_typeid.MalformedTypeIDError`](#django_typeidmalformedtypeiderror)
- [API reference](#api-reference)
- [Related projects](#related-projects)
- [Tips and tricks](#tips-and-tricks)
  - [Don't change the prefix](#dont-change-the-prefix)
- [Maintainer notes](#maintainer-notes)
- [Changelog](#changelog)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## What is a TypeID?

A [TypeID](https://github.com/jetify-com/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](https://www.rfc-editor.org/rfc/rfc9562) rendered as exactly 26 characters of [Crockford base32](https://www.crockford.com/base32.html), 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"](https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a).

## 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:

```py
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:

```py
>>> 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:

```py
>>> 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`](https://github.com/jetify-com/typeid/blob/main/spec/valid.yml) and [`invalid`](https://github.com/jetify-com/typeid/blob/main/spec/invalid.yml) 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()`](#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.

```py
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](https://docs.djangoproject.com/en/5.2/topics/http/urls/#registering-custom-path-converters).

Using this method ensures that _only_ valid id strings for that field will be presented to your view.

Example:

```py
# models.py
class User(models.Model):
    id = TypeIDField(primary_key=True, prefix='user')
```

```py
# 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),
    ...
]
```

```py
# 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.

```py
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](https://docs.djangoproject.com/en/5.2/topics/http/urls/#registering-custom-path-converters) for `field_name` on `model_class`.

See [Registering URLs](#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`](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](https://github.com/mik3y/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`](docs/maintainer-notes.md).

## Changelog

See [`CHANGELOG.md`](https://github.com/mik3y/django-typeid/blob/main/CHANGELOG.md) for a summary of changes.
