Nov. 13, 2025

How to use UUIDv7 in Python, Django and PostgreSQL

Introduction

Python 3.14 and PostgreSQL 18, both released as stable in recent months, introduced first-class support for UUIDv7, which provides time ordering and better index locality.

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.

This article assumes that you are running Python 3.14 and PostgreSQL 18, since earlier versions do not provide UUIDv7 generation or timestamp extraction functions.

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.

UUIDv4 vs UUIDv7

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

  • Fully random
  • Very good uniqueness
  • Poor index locality in large tables
  • Inserts become random writes

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

UUIDv7 encodes a timestamp in its most significant bits, which means new identifiers follow a natural chronological order. This reduces index fragmentation and avoids the random write patterns typical of UUIDv4.

Because inserts land in predictable index regions instead of scattered locations, write performance improves significantly on large tables and high-throughput systems.

When to choose database generated UUIDv7

In real deployments, database generated UUIDv7 is usually the safest and most consistent option. PostgreSQL 18 provides a monotonic, timestamp-aware generator that avoids clock drift between hosts, guarantees stable ordering under concurrency and ensures that creation times always reflect the database’s timeline rather than the application server.

Python-side UUIDv7 generation is universal and works on all databases, but it shifts responsibility to the application layer and depends on the local system clock. It remains the only option for engines that cannot generate UUIDv7 internally, although MariaDB 11.7 has added support.

Choose database generated UUIDv7 when using PostgreSQL 18 or MariaDB 11.7. Choose Python-generated UUIDv7 when using SQLite, MySQL, Oracle or earlier versions of MariaDB and PostgreSQL.

What about SQLite, MySQL, MariaDB, Oracle?

Not all databases provide native support for UUIDv7. This is important to understand because it determines whether UUIDv7 should be generated in Python or by the database itself.

MariaDB

MariaDB historically supported only UUIDv1 generation, however, starting with MariaDB 11.7, support for UUIDv4 and UUIDv7 has been introduced, but timestamp extraction is not supported.

MySQL

MySQL provides UUIDv1 function but does not currently support UUIDv4 or UUIDv7. When using MySQL, UUIDv7 must be generated at the application layer (e.g. Python uuid.uuid7()).

Oracle

Oracle supports only UUIDv4, starting with Oracle 23ai, but it does not provide a native UUIDv7 generator or timestamp extraction functions. As with other databases lacking UUIDv7 support, UUIDv7 must be generated in Python when using Oracle.

PostgreSQL

PostgreSQL 18 provides native uuidv7() generation and uuid_extract_timestamp(), but it also support UUIDv4 since version 13.

SQLite

SQLite can store UUIDv7 values without issues, but it cannot generate them or extract their timestamp. When using SQLite, UUIDv7 must always be generated in Python using uuid.uuid7().

Because of these differences, Python-side UUIDv7 generation is the most portable option, while database-side generation is available only when using PostgreSQL 18 or MariaDB 11.7+.

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.

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

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

>>> record = Record.objects.create()
>>> record.creation_time.isoformat()
'2025-11-14T17:18:24.144000+00:00'

A generated datetime column can be very useful even though UUIDv7 already embeds a timestamp. PostgreSQL computes it at write time, Django manages it declaratively through the ORM and having a proper datetime field makes filtering, ordering, indexing and using the Django admin much simpler without requiring annotations or extra computation on the UUID value.

Summary

Use Python 3.14 uuid.uuid7 when:

  • identifiers must be generated inside application code
  • you are using databases that cannot generate UUIDv7 natively (such as SQLite, Oracle, MySQL or MariaDB)
  • deterministic local generation is useful

Use PostgreSQL 18uuidv7 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

These features require PostgreSQL 18’s native functions. SQLite and other engines cannot generate UUIDv7 or extract timestamps at the database level.

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 uuidv7 and uuid_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_default and GeneratedField.
  • 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: 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: django-uuid47

Further reading

This article builds on Django GeneratedField which I have covered in a dedicated series on my blog:

  1. Database generated columns⁽¹⁾: Django SQLite”
  2. Database generated columns⁽²⁾: Django PostgreSQL”
  3. Database generated columns⁽³⁾: GeoDjango PostGIS”

Frequently asked questions

Is UUIDv7 stable
Yes. It is part of the updated UUID specification and fully implemented in Python 3.14 and PostgreSQL 18.
Does Django support UUIDv7
Yes. Django works with UUIDv7 without custom fields because Python and PostgreSQL provide the required functions.
Can UUIDv7 be used as a primary key
Yes. 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 UUIDv7
This depends on your system. New projects can adopt UUIDv7 immediately while migrations require careful consideration.
Does UUIDv7 require PostgreSQL 18
Yes. Native support is available starting with PostgreSQL 18.
Do I need a custom Django field to use UUIDv7
No. UUIDField works correctly with both Python and PostgreSQL generated UUIDv7 values.
Is it safe to expose raw UUIDv7 values in public APIs
Not 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.
Does the timestamp inside UUIDv7 make separate datetime fields unnecessary
It depends on the use case. The embedded timestamp is enough for ordering, but a dedicated datetime column generated by PostgreSQL is often easier to query, index, filter or use in the admin, and Django can manage it declaratively through a GeneratedField without triggers or extra query computation.
Can SQLite/Oracle/MariaDB/MySQL generate UUIDv7
No. SQLite/Oracle/MariaDB/MySQL can store UUIDv7 but cannot generate it or extract its timestamp. Use Python 3.14 uuid.uuid7() when working with them.

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: