UUID v7 Python: Time-Ordered UUIDs in Python
Python's `uuid` module covered versions 1, 3, 4 and 5 for two decades. v7 arrived in Python 3.14; before that a package or a dozen lines of your own fills the gap.
Python 3.14 and later
import uuid
uuid.uuid7() # standard library, Python 3.14+
On older versions the uuid6 package provides uuid6.uuid7() with the same output format, so switching later is a one-line change.
The standard-library version is monotonic within a millisecond. After the timestamp it spends 42 bits on a counter and 32 on fresh randomness: the counter starts at a random value in each new millisecond and goes up by one for every further UUID in it, so even a tight loop yields sorted values. If the system clock steps backwards, it keeps using the last timestamp plus one millisecond instead of going back in time.
Writing it yourself
The layout is a 48-bit millisecond timestamp, the version nibble, 12 random bits, the variant bits, then 62 more random bits:
import os, time, uuid
def uuid7() -> uuid.UUID:
ms = int(time.time() * 1000)
rand = os.urandom(10)
b = bytearray(ms.to_bytes(6, "big") + rand)
b[6] = (b[6] & 0x0F) | 0x70 # version 7
b[8] = (b[8] & 0x3F) | 0x80 # RFC 9562 variant
return uuid.UUID(bytes=bytes(b))
The two bit operations are what make it a valid UUID rather than 128 random-ish bits. This version does not guarantee ordering within a single millisecond — the libraries add a counter for that.
Reading the timestamp back
The first six bytes are the creation time, which is occasionally useful for debugging and is also the privacy caveat:
from datetime import datetime, timezone
def created_at(u: uuid.UUID) -> datetime:
ms = int.from_bytes(u.bytes[:6], "big")
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
On Python 3.14 you rarely need that helper: for a version 7 UUID, u.time already returns the Unix timestamp in milliseconds. Converting to a string is just as short. str(u) gives the 36-character canonical form, u.hex the 32 digits without hyphens, and uuid.UUID(text) parses either back, including uppercase and braces. json.dumps does not know the type and fails with TypeError: Object of type UUID is not JSON serializable, so convert with str() before serialising.
With Django and SQLAlchemy
Both support a native UUID column, so pass the callable as the default and let the database store 16 bytes:
# Django
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
# SQLAlchemy
id = mapped_column(Uuid, primary_key=True, default=uuid7)
Pass the function, not uuid7() — calling it in the field definition evaluates once at import and gives every row the same id, which is a classic Django mistake.
Frequently asked questions
Is uuid7 in the Python standard library?
Yes, from Python 3.14. Use the uuid6 package on earlier versions.
Can I extract the creation time?
Yes — the first 48 bits are the Unix millisecond timestamp.
Why does every row get the same UUID in Django?
The default was called instead of passed. Use default=uuid7, not default=uuid7().