UUIDv7 in Django

Abenezer Belachew

Abenezer Belachew · November 11, 2024

6 min read

Alright, I'm not here to tell you that UUIDv7 is the best choice among UUID versions—that's up to your project's needs. But if you're curious about what makes UUIDv7 unique and want to set it up in Django, you're in the right place. UUIDv7 offers timestamp-based ordering, a feature UUIDv4 can't deliver out of the box. For a deeper dive into comparing UUID versions, I've linked some resources at the end of this post.

What is UUIDv7?

  • Like other UUIDs, UUIDv7 is a 128-bit unique identifier used to tag data. You're probably familiar with UUIDv4, the go-to for generating random IDs. Since v4 UUIDs are purely random, the chances of duplicates are tiny—which is great. The downside? UUIDv4 doesn't naturally sort, which can slow things down if you're using it as an index.

  • UUIDv7, on the other hand, is a lexicographically sortable UUID with a built-in timestamp. This allows UUIDs to sort by their creation time, which is perfect for faster lookups and better indexing performance in databases.

UUIDv7 Structure

0190163d-8694-739b-aea5-966c26f8ad91
└─timestamp─┘ │└─┤ │└───rand_b─────┘
             ver │var
              rand_a

source: Anton Zhiyanov

Bit SizeDescription
Timestamp48Unix timestamp in milliseconds (sortable by generation time)
Version4Version (set to 7 for UUIDv7)
Random Part A12Random component (rand_a), for uniqueness within the same millisecond
Variant2Defines the UUID layout as RFC 4122 compliant
Random Part B62Additional random component (rand_b), providing further uniqueness

UUIDv7 in Python

Here's what a uuidv7 function in python looks like, based on Anton Zhiyanov's post on UUIDv7 in 33 languages:

import os
import time

def uuidv7() -> uuid.UUID:
    """
    Generate a UUIDv7.
    """
    # Generate 16 random bytes as a base for the UUID
    value = bytearray(os.urandom(16))

    # Get the current timestamp in milliseconds since the Unix epoch
    timestamp = int(time.time() * 1000)

    # Insert the timestamp (48 bits) into the UUID
    value[0] = (timestamp >> 40) & 0xFF
    value[1] = (timestamp >> 32) & 0xFF
    value[2] = (timestamp >> 24) & 0xFF
    value[3] = (timestamp >> 16) & 0xFF
    value[4] = (timestamp >> 8) & 0xFF
    value[5] = timestamp & 0xFF

    # version and variant
    value[6] = (value[6] & 0x0F) | 0x70
    value[8] = (value[8] & 0x3F) | 0x80

    return uuid.UUID(bytes=bytes(value))

So how would you use this in Django?

Using UUIDv7 in Django

To use UUIDv7 in Django, you can create a custom field that extends Django's UUIDField and generates UUIDv7s based on the function above.

Step 1: Create a custom UUID field

app/fields.py
import os
import time
import uuid

from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone


def uuidv7() -> uuid.UUID:
    """
    Generate a UUIDv7.
    """
    # random bytes
    value = bytearray(os.urandom(16))

    # current timestamp in ms
    timestamp = int(time.time() * 1000)

    # timestamp
    value[0] = (timestamp >> 40) & 0xFF
    value[1] = (timestamp >> 32) & 0xFF
    value[2] = (timestamp >> 24) & 0xFF
    value[3] = (timestamp >> 16) & 0xFF
    value[4] = (timestamp >> 8) & 0xFF
    value[5] = timestamp & 0xFF

    # version and variant
    value[6] = (value[6] & 0x0F) | 0x70
    value[8] = (value[8] & 0x3F) | 0x80

    return uuid.UUID(bytes=bytes(value))


class UUIDField(models.UUIDField):
    """
    A custom UUID field that generates different UUID versions.
    """

    def __init__(
        self,
        primary_key: bool = True,
        version: int | None = None,
        editable: bool = False,
        *args,
        **kwargs
    ):
        if version:
            if version == 2:
                raise ValidationError("UUID version 2 is not supported.")
            if version < 1 or version > 7:
                raise ValidationError("UUID version must be between 1 and 7.")

            version_map = {
                1: uuid.uuid1,
                3: uuid.uuid3,
                4: uuid.uuid4,
                5: uuid.uuid5,
                7: uuidv7,
            }
            kwargs.setdefault("default", version_map[version])
        else:
            kwargs.setdefault("default", uuid.uuid4)

        kwargs.setdefault("editable", editable)
        kwargs.setdefault("primary_key", primary_key)
        super().__init__(*args, **kwargs)
  • This custom UUIDField accepts an optional version argument. If you pass version=7, it will use the uuidv7 function; otherwise, it defaults to uuid.uuid4. You can customize the default version or extend the dictionary to support other versions as needed.

Step 2: Use the custom UUID field in your models

  • You can use this custom field in your models like this:
app/models.py
from django.db import models
from app.fields import UUIDField

class MyModel(models.Model):
    id = UUIDField(primary_key=True, version=7)
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

And that's it! You now have a Django model that uses UUIDv7 as the primary key. Depending on your use case, you can use it as an index for faster lookups or for sorting by creation time.

Testing UUIDv7 in the Django Shell

  • After running migrations and creating some MyModel instances, you can check the generated UUIDs in the Django shell:
shell
>>> from app.models import MyModel
>>> ids = MyModel.objects.all().values_list('id', flat=True)
>>> for id in ids:
...     print(id)
... 
UUID('01927d8c-1645-7ab9-86b6-d351a982a38f')
UUID('0192628e-ef00-7c4f-bdd6-811ff48f0e9b')
UUID('01927d8b-de00-7e2a-9cf7-b86dcb6d7c44')
UUID('01927db9-1b51-7844-94a7-349f372bdc00')
UUID('01927dee-f946-79dc-aac0-ec2ebbd740c4')
UUID('01927e08-c241-7b1b-bf0a-a63413abbdf2')
UUID('0192628a-de38-75f8-8b18-c43496533ad8')

You can check the validity of your v7 UUIDs on uuid7.com.

Tip

  • A quick way to identify if a UUID might be version 7 is by looking at the position of the version field. In a UUIDv7, the version number (shown as 7) is located in the 13th character of the UUID string.

Here are some example UUIDv7s with the version 7 highlighted:

  • 01927d8c-1645-7ab9-86b6-d351a982a38f
  • 0192628e-ef00-7c4f-bdd6-811ff48f0e9b
  • 01927d8b-de00-7e2a-9cf7-b86dcb6d7c44
  • 01927db9-1b51-7844-94a7-349f372bdc00

In each case, the 7 at this position indicates a UUIDv7.

Conclusion

  • Extending Django's UUIDField gives you the flexibility to support different UUID versions, whether it's for legacy compatibility or future-proofing.
  • For most cases, UUIDv4 works perfectly fine. But if you need sortable, time-based IDs, UUIDv7 is a great option to consider.
  • Congrats! You're now equipped to tackle your (imaginary or real) scaling challenges with UUIDv7 in Django.

Resources

UUIDv7-Specific Resources:

General UUID and ULID Comparisons:

Additional Reading:

🚏️