Learn how to use UUIDv7 today with stable releases of Python 3.14, Django 5.2 and PostgreSQL 18. A step by step guide showing how to generate UUIDv7 in Python, store them in Django models, use PostgreSQL native functions and build time ordered primary keys without writing SQL.
© 2025 Paolo Melchiorre CC BY-SA “Torre di Cerrano framed by the coastal maritime pine woods in Pineto, Italy.”
Introduction
UUIDv7 is finally available in the entire Python Django PostgreSQL stack. It provides time ordering, better index locality and easier reasoning about data creation compared to UUIDv4. All o…
Learn how to use UUIDv7 today with stable releases of Python 3.14, Django 5.2 and PostgreSQL 18. A step by step guide showing how to generate UUIDv7 in Python, store them in Django models, use PostgreSQL native functions and build time ordered primary keys without writing SQL.
© 2025 Paolo Melchiorre CC BY-SA “Torre di Cerrano framed by the coastal maritime pine woods in Pineto, Italy.”
Introduction
UUIDv7 is finally available in the entire Python Django PostgreSQL stack. It provides time ordering, better index locality and easier reasoning about data creation compared to UUIDv4. All of this can be used today with stable versions and without custom extensions.
In this article I walk through a practical example showing how UUIDv7 works in Python first and then in PostgreSQL using Django ORM and migrations only. No manual SQL is needed and every step shows real code and migration output.
What is a UUID
A UUID is a 128 bit identifier that aims to be globally unique without coordination. It works well in distributed systems because it removes the need for a central sequence generator. This makes UUIDs ideal for many modern architectures.
The most common version is UUIDv4 which is fully random. UUIDv7 improves on this by adding a timestamp prefix that makes identifiers sort in creation order. This helps index locality and makes inserts more efficient.
UUIDv4 vs UUIDv7
UUIDv4
- Fully random
- Very good uniqueness
- Poor index locality in large tables
- Inserts become random writes
- Hard to infer creation time
UUIDv7
- Timestamp based and time ordered
- Better index locality and more predictable writes
- Creation time can be extracted directly
- Suitable as a primary key in high write environments
Why ordering matters
Because a UUIDv7 begins with a timestamp, new values follow a natural ordering. Database indexes work better with ordered inserts since they reduce fragmentation and avoid many of the random writes caused by UUIDv4.
UUIDv7 is a strong alternative to traditional auto incrementing integer IDs in new Django projects because it combines global uniqueness with predictable ordering. While integers are simple and efficient, they require coordination across services and can complicate sharding or offline workflows. UUIDv7 keeps inserts sequential enough for good index locality while avoiding the bottlenecks of a central sequence. This makes it a practical default identifier for modern architectures where horizontal scaling or distributed processing are likely to appear later.
Preparing the environment
Python 3.14 is assumed to be available. The first step is creating a virtual environment and installing Django and Black, which will prepare the project structure and dependencies needed for the tutorial.
Creating the virtual environment
This step creates a clean virtual environment with Python 3.14 and installs the tools needed to start the Django project.
$ python3.14 -m venv .venv
$ source .venv/bin/activate
$ python -m pip install django black
Creating a Django project and the first UUIDv7 model
The next step is creating the Django project and application that will contain the model used in the UUIDv7 example.
Creating the Django project and application
Here we start a new Django project named uuidv7 and add an items app that will contain the models used in the examples.
$ python -m django startproject uuidv7
$ tree --noreport uuidv7/
uuidv7/
├── manage.py
└── uuidv7
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
$ cd uuidv7
$ python -m django startapp items
$ tree --noreport items/
items/
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
Enable the app in settings:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"items",
]
Creating the Item model using Python uuid.uuid7
This model uses Python 3.14 builtin uuid.uuid7 for primary key generation and adds a cached_property to extract the timestamp from the UUID. It demonstrates client side UUIDv7 usage without depending on the database.
import uuid
from datetime import datetime
from django.db import models
from django.utils.functional import cached_property
class Item(models.Model):
uuid = models.UUIDField(default=uuid.uuid7, primary_key=True)
@cached_property
def creation_time(self):
return datetime.fromtimestamp(self.uuid.time / 1000)
Creating the initial migration
This section generates the migration file that defines the database table for the Item model. The migration uses SQLite as the default backend and produces a straightforward schema containing only the UUID column.
$ python -m manage makemigrations
Migrations for 'items':
items/migrations/0001_initial.py
+ Create model Item
Inspecting the SQL for SQLite
This command shows the SQL that Django will run on SQLite, making it easier to understand how the Item model is represented in the database.
$ python -m manage sqlmigrate items 0001
BEGIN;
--
-- Create model Item
--
CREATE TABLE "items_item" (
"uuid" char(32) NOT NULL PRIMARY KEY
);
COMMIT;
Applying the migration
Now we apply the migration so the Item table is created in the local SQLite development database.
$ python -m manage migrate items 0001
Operations to perform:
Target specific migration: 0001_initial, from items
Running migrations:
Applying items.0001_initial... OK
Testing the Item model in Django shell
This example creates an Item instance and reads the timestamp encoded in its UUIDv7 value.
$ python -m manage shell
>>> item = Item()
>>> item.creation_time.isoformat()
'2025-11-14T16:52:55.879000'
Using UUIDv7 in PostgreSQL 18 with Django 5.2
PostgreSQL 18 adds native support for UUIDv7 and timestamp extraction, making it possible to generate the UUID directly in the database. This section shows how to configure Django to use PostgreSQL and how to create models that rely on these functions.
Installing psycopg
We install psycopg, the PostgreSQL driver used by Django, so the project can connect to a PostgreSQL 18 database.
$ python -m pip install psycopg[binary]
Configuring PostgreSQL in settings.py
The database configuration is updated to point Django to a PostgreSQL 18 instance instead of SQLite.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": "postgres",
"NAME": "uuidv7",
"PASSWORD": "postgres",
"PORT": "5432",
"USER": "postgres",
}
}
Creating the Record model with PostgreSQL generated UUIDv7
The following model uses Django db_default to call the uuidv7 function directly inside PostgreSQL. It results in the UUID being generated in the database during the insert operation.
Defining a Django database function for uuidv7
This Django function wrapper maps directly to PostgreSQL’s uuidv7 function so it can be used in model definitions.
class UUIDv7(models.Func):
function = 'uuidv7'
output_field = models.UUIDField()
Creating the Record model
The Record model uses PostgreSQL uuidv7 to generate the primary key at insert time.
class Record(models.Model):
uuid = models.UUIDField(db_default=UUIDv7(), primary_key=True)
Creating the migration for the Record model
We create the migration that defines the database table storing UUIDv7 values generated by PostgreSQL.
$ python -m manage makemigrations
Migrations for 'items':
items/migrations/0002_record.py
+ Create model Record
Inspecting the migration SQL for PostgreSQL
This command shows the SQL generated by Django for the Record model when using PostgreSQL.
$ python -m manage sqlmigrate items 0002
BEGIN;
--
-- Create model Record
--
CREATE TABLE "items_record" (
"uuid" uuid DEFAULT (uuidv7()) NOT NULL PRIMARY KEY
);
COMMIT;
Applying the migration
Applying this migration creates the items_record table in PostgreSQL using the uuidv7 default.
$ python -m manage migrate items 0002
Operations to perform:
Target specific migration: 0002_record, from items
Running migrations:
Applying items.0001_initial... OK
Applying items.0002_record... OK
Testing the Record model
Here we create a new Record and verify that PostgreSQL generated a UUIDv7 value.
$ python -m manage shell
>>> record = Record.objects.create()
>>> record.uuid
UUID('019a8359-f49d-7f56-8921-977e2c47242c')
PostgreSQL 18 offers uuid_extract_timestamp which can extract the timestamp encoded inside a UUIDv7. This section shows how to expose that value in Django using a GeneratedField.
This function wrapper exposes PostgreSQL’s uuid_extract_timestamp so it can be used in a GeneratedField.
class UUIDExtractTimestamp(models.Func):
function = "uuid_extract_timestamp"
output_field = models.DateTimeField()
Adding the creation_time generated column
The Record model is extended with a generated column that stores the timestamp extracted from the UUID.
class Record(models.Model):
uuid = models.UUIDField(db_default=UUIDv7(), primary_key=True)
creation_time = models.GeneratedField(
expression=UUIDExtractTimestamp("uuid"),
output_field=models.DateTimeField(),
db_persist=True,
)
Generating the migration for creation_time
This migration adds the creation_time column and configures PostgreSQL to compute it automatically.
$ python -m manage makemigrations
Migrations for 'items':
items/migrations/0003_record_creation_time.py
+ Add field creation_time to record
Inspecting the SQL for the generated column
This shows the SQL statement that defines the generated timestamp column in PostgreSQL.
$ python -m manage sqlmigrate items 0003
BEGIN;
--
-- Add field creation_time to record
--
ALTER TABLE "items_record"
ADD COLUMN "creation_time" timestamp with time zone
GENERATED ALWAYS AS (uuid_extract_timestamp("uuid")) STORED;
COMMIT;
Applying the migration
Running this migration creates the generated column and enables timestamp extraction for new rows.
$ python -m manage migrate items 0003
Operations to perform:
Target specific migration: 0003_record_creation_time, from items
Running migrations:
Applying items.0003_record_creation_time... OK
This final example inserts a new Record and checks the extracted timestamp stored in the generated column.
$ python -m manage shell
>>> record = Record.objects.create()
>>> record.creation_time.isoformat()
'2025-11-14T17:18:24.144000+00:00'
Summary
Use Python uuid.uuid7 when:
- identifiers must be generated inside application code
- SQLite or non PostgreSQL databases are used
- deterministic local generation is useful
Use PostgreSQL uuidv7 when:
- the database is responsible for generating identifiers
- multiple writers insert rows concurrently
- you want to expose creation time using uuid_extract_timestamp
- sequential inserts improve performance
Both methods are valid and can be mixed in the same project.
Key takeaways
- UUIDv7 provides ordered identifiers that improve index locality and reduce random writes compared to UUIDv4.
- Python 3.14 can generate UUIDv7 directly with
uuid.uuid7, making it easy to use even without database support. - PostgreSQL 18 adds native
uuidv7anduuid_extract_timestamp, allowing server side UUID generation and timestamp extraction without storing extra fields. - Django 5.2 integrates smoothly with both Python and PostgreSQL UUIDv7 through
db_defaultandGeneratedField. - UUIDv7 should not be exposed directly in public APIs because the embedded timestamp can reveal creation patterns or activity timing.
- UUIDv47 offers a reversible masking approach that keeps UUIDv7 internally while exposing a UUIDv4 looking value externally.
Considering the downsides of UUIDv7 and possible solutions
Drawbacks of exposing UUIDv7 to external users
UUIDv7 works very well inside a database, but it is not always ideal as a public identifier. Because the most significant bits contain a precise Unix timestamp, anyone who sees the UUID can infer when the record was created. This can reveal activity patterns, account creation times or traffic trends, which in some contexts may help correlation or de anonymization attacks. The random portion of the UUID remains intact, but the timestamp still leaks meaningful metadata. For these reasons UUIDv7 is generally recommended for internal use, while external facing APIs should avoid exposing it directly.
Masking UUIDv7 timestamps with UUIDv47
A practical solution to these issues is provided by the UUIDv47 approach. The idea is to keep a true UUIDv7 inside the database so that ordering and index locality are preserved, but to expose a masked representation externally. UUIDv47 applies a reversible SipHash based transformation to the timestamp field only, using a key derived from the random portion of the UUID. The result looks like a UUIDv4 to clients and hides the timing information, while still mapping deterministically and invertibly back to the original internal UUIDv7.
The project implementing this idea is available here: https://github.com/stateless-me/uuidv47
Django support through django uuid47
For Django users there is a dedicated integration package that implements UUIDv47 as a model field and handles configuration of the masking key. This allows applications to adopt masked UUIDv7 identifiers without altering the rest of the schema or the way models are defined.
The Django integration package is available here: https://pypi.org/project/django-uuid47/
Further reading
This article builds on Django GeneratedField which I have covered in a dedicated series on my blog:
- “Database generated columns⁽¹⁾: Django SQLite”
- “Database generated columns⁽²⁾: Django PostgreSQL”
- “Database generated columns⁽³⁾: GeoDjango PostGIS”
Frequently asked questions
Is UUIDv7 stableYes. It is part of the updated UUID specification and fully implemented in Python 3.14 and PostgreSQL 18.Does Django support UUIDv7Yes. Django works with UUIDv7 without custom fields because Python and PostgreSQL provide the required functions.Can UUIDv7 be used as a primary keyYes. It offers far better index locality than UUIDv4 and is well suited to primary keys in most applications.Should I migrate existing data from UUIDv4 to UUIDv7This depends on your system. New projects can adopt UUIDv7 immediately while migrations require careful consideration.Does UUIDv7 require PostgreSQL 18Yes. Native support is available starting with PostgreSQL 18.Do I need a custom Django field to use UUIDv7No. UUIDField works correctly with both Python and PostgreSQL generated UUIDv7 values.Is it safe to expose raw UUIDv7 values in public APIsNot always. UUIDv7 encodes a timestamp in its most significant bits and this can reveal when a record was created. For applications where identifiers are visible to untrusted users it is safer to use a masking scheme like UUIDv47 or expose a separate UUIDv4 instead.
Conclusions
UUIDv7 offers ordering, predictable inserts and timestamp extraction without losing UUID benefits. With Python 3.14, Django 5.2 and PostgreSQL 18 it can be used today without custom extensions or complex setup.
Discuss this article
If you want to comment or share feedback you can do so on:
- Hacker News
- Lobsters