Python 3.14 added uuid.uuid7(), giving applications a standard-library way to generate UUID version 7 identifiers defined by RFC 9562.

UUIDv7 is useful when an application wants a globally shaped 128-bit identifier while also putting creation time near the front of the identifier. That property can make newly generated values naturally cluster by time in systems that sort UUIDs by their binary or canonical value.

It is tempting to summarize UUIDv7 as “a sortable UUID.” That is directionally useful but incomplete. The timestamp has millisecond resolution, Python adds a counter for monotonicity within a millisecond, clocks can move, and separate processes do not become a distributed sequence generator merely because they all use UUIDv7.

This article makes those boundaries explicit.

Generate UUIDv7 with the standard library

On Python 3.14 or newer, generation is direct:

import uuid

identifier = uuid.uuid7()

print(identifier)
print(identifier.version)  # 7

The result is an ordinary immutable uuid.UUID object, so existing code that stores or serializes UUID objects can often use version 7 without inventing a new identifier type.

The important compatibility boundary is the Python version. uuid.uuid7() was added in Python 3.14. A library that still supports Python 3.13 or older cannot call it unconditionally.

If your project has a mixed deployment fleet, make the minimum runtime requirement explicit instead of discovering the incompatibility when an older worker reaches the call.

Understand what is encoded in the identifier

UUIDv7 carries a Unix-epoch timestamp in milliseconds. Python exposes that timestamp through the UUID object’s time attribute:

import uuid
from datetime import datetime, timezone

identifier = uuid.uuid7()
created_ms = identifier.time
created_at = datetime.fromtimestamp(created_ms / 1000, tz=timezone.utc)

print(created_at)

For a version 7 UUID, UUID.time is the 48-bit millisecond timestamp since the Unix epoch. This differs from versions 1 and 6, whose time representation uses 100-nanosecond intervals from the Gregorian epoch.

That distinction matters when generic code accepts several UUID versions. Do not interpret UUID.time without knowing which version produced the value.

A small helper can make the requirement visible:

from datetime import datetime, timezone
from uuid import UUID


def uuid7_datetime(value: UUID) -> datetime:
    if value.version != 7:
        raise ValueError("expected a UUIDv7 value")

    return datetime.fromtimestamp(value.time / 1000, tz=timezone.utc)

The timestamp is useful metadata, but it is not a substitute for every explicit timestamp column. More on that boundary later.

Why time ordering can help storage locality

A UUIDv4 is random. Consecutive values have no intentional relationship in their high-order bits:

import uuid

for _ in range(3):
    print(uuid.uuid4())

UUIDv7 puts its timestamp in the high-order portion of the identifier. Values generated as time advances therefore tend to sort in creation-time order.

That can be attractive for database indexes. Random primary keys can insert throughout a B-tree key space, while time-oriented keys tend to place recent inserts near one another. The exact performance effect depends on the database, UUID storage representation, index implementation, workload, fill settings, replication strategy, and concurrency. Benchmark the actual schema rather than assuming a universal improvement.

Also verify how the database orders UUID values. A standard textual UUID, a native UUID column, and a custom byte-swapped representation are not automatically equivalent ordering schemes.

If ordering behavior is part of the design, test it at the storage boundary you actually deploy.

Python makes same-millisecond generation monotonic

Milliseconds are coarse compared with modern request rates. A process can generate many identifiers before the clock reaches the next millisecond.

Python’s uuid.uuid7() implementation uses a 42-bit counter to guarantee monotonicity within a millisecond. That means a burst like this has useful ordering behavior within the generating process:

import uuid

values = [uuid.uuid7() for _ in range(10_000)]

assert values == sorted(values)
assert len(values) == len(set(values))

The counter is an implementation behavior documented by Python for portability across platforms that lack sub-millisecond clock precision.

Do not stretch that guarantee into a distributed one. Two application processes, containers, or machines maintain independent generator state. UUIDv7 gives them identifiers with time-oriented structure; it does not coordinate them into one gapless global sequence.

If a business rule requires a single authoritative order across writers, use a mechanism designed to establish that order, such as a database sequence, transaction commit position, or another coordination protocol.

Time order is not event order

An embedded timestamp answers approximately when an identifier was generated according to the generating system’s clock. It does not prove when a business event occurred, when a transaction committed, or which of two distributed operations happened first.

Consider two services:

service A clock: 12:00:00.120
service B clock: 12:00:00.080

If their clocks disagree, identifiers generated by service B may sort before identifiers from service A even when the real-world operation at B happened later.

Network delay creates another distinction. A request may receive its UUID before expensive work starts, while another request receives a later UUID and commits first.

Keep separate concepts separate:

  • use the UUID as identity;
  • use an explicit event timestamp for domain time;
  • use database or protocol ordering when commit/order semantics matter.

UUIDv7 can make identity conveniently time-oriented without becoming an ordering oracle.

Do not use the embedded time as a secret

A UUIDv7 intentionally exposes creation-time information. Anyone who can inspect the identifier can recover its millisecond timestamp.

That is often harmless for internal record identifiers, but it can be undesirable when creation time itself is sensitive. It can also reveal rough activity timing to clients who receive public identifiers.

More importantly, an identifier should not automatically be treated as an authorization credential. Knowing or guessing a record ID must not grant access to the record.

Keep authorization checks independent from identifier format:

def get_invoice(current_user, invoice_id):
    invoice = load_invoice(invoice_id)

    if not current_user.can_view(invoice):
        raise PermissionError("invoice is not accessible")

    return invoice

The UUID selects a resource. The authorization policy decides whether the caller may use it.

If you need an unguessable bearer secret, generate and manage a credential specifically for that purpose rather than relying on the fact that an identifier looks complicated.

Parse external UUIDs before trusting their version

uuid.UUID() accepts standard textual UUID representations:

import uuid

value = uuid.UUID("01941f29-7c00-7abc-8def-0123456789ab")
print(value.version)

Parsing proves that the input has a UUID representation. It does not prove that the application received the UUID version it expects.

When an API contract specifically requires UUIDv7, validate both:

import uuid


def parse_uuid7(text: str) -> uuid.UUID:
    try:
        value = uuid.UUID(text)
    except (ValueError, AttributeError) as exc:
        raise ValueError("invalid UUID") from exc

    if value.version != 7:
        raise ValueError("UUID must be version 7")

    return value

Whether an API should reject other UUID versions is a product and migration decision. A database that historically stores UUIDv4 may intentionally accept both during a transition.

Do not add version rejection merely because version 7 is now preferred for newly generated records.

Keep an explicit created_at when the domain needs one

Because UUIDv7 embeds a timestamp, a schema like this may look redundant:

id          UUID PRIMARY KEY
created_at  TIMESTAMP NOT NULL

Often it is not redundant.

The identifier timestamp records generation time. created_at can represent a deliberate application or database semantic: row insertion, transaction creation, imported source time, or another domain event. It is independently queryable and can have the precision, timezone handling, constraints, and indexing policy the application needs.

An explicit timestamp also survives future identifier migrations. If the system later changes key format, historical time semantics do not disappear into key-decoding logic.

Use the UUID timestamp when generation time is genuinely the information you need. Keep a dedicated column when time is part of the data model.

Avoid converting through floating point when exact milliseconds matter

The convenient conversion shown earlier divides milliseconds by 1000 and passes a floating-point number to datetime.fromtimestamp(). That is fine for many display and logging tasks.

When exact millisecond arithmetic matters, keep the integer until the final conversion:

from datetime import datetime, timedelta, timezone
from uuid import UUID

UNIX_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)


def uuid7_datetime_exact(value: UUID) -> datetime:
    if value.version != 7:
        raise ValueError("expected UUIDv7")

    return UNIX_EPOCH + timedelta(milliseconds=value.time)

This expresses the unit directly and avoids using a floating-point Unix timestamp as an intermediate representation.

Store UUIDs in a representation your database understands

Python offers several UUID representations:

import uuid

value = uuid.uuid7()

print(str(value))   # canonical text
print(value.hex)    # 32 hexadecimal characters
print(value.bytes)  # 16 bytes, big-endian field order
print(value.int)    # 128-bit integer

Prefer the database’s native UUID type when it provides the semantics and indexing behavior you need. Otherwise, choose a representation deliberately and keep it consistent across readers and writers.

Do not casually mix bytes and bytes_le. They encode fields differently. If one component writes one representation and another interprets it as the other, the reconstructed UUID changes.

A serialization round trip is a cheap test:

import uuid

original = uuid.uuid7()
restored = uuid.UUID(bytes=original.bytes)

assert restored == original

Test the equivalent round trip through the real database driver as well.

UUIDv7 does not replace UUIDv4 everywhere

UUIDv4 remains a simple choice when random identifiers are exactly what you want:

import uuid

identifier = uuid.uuid4()

Choose UUIDv7 when time-oriented layout is useful and exposing generation time is acceptable. Choose UUIDv4 when temporal ordering is unnecessary or undesirable.

Do not migrate merely because 7 is a newer version number. UUID versions encode different construction strategies; they are not a sequence of security or quality upgrades.

Python 3.14 also added UUID versions 6 and 8. Version 6 is a reordered time-based alternative related to version 1 and intended to improve database locality. Version 8 is for custom layouts. Neither is a reason to reach for a more configurable format when UUIDv7 already expresses the requirement.

For security-sensitive random UUID generation, Python documents uuid.uuid4() as cryptographically secure. Keep that distinction in mind instead of assuming every UUID generation function has the same security purpose.

Test properties instead of literal generated values

Generated UUIDs are intentionally dynamic, so tests should check contracts rather than hard-coded output:

import uuid


def test_new_id_is_uuid7():
    value = uuid.uuid7()

    assert isinstance(value, uuid.UUID)
    assert value.version == 7


def test_uuid7_round_trips_through_text():
    value = uuid.uuid7()

    assert uuid.UUID(str(value)) == value


def test_uuid7_exposes_millisecond_time():
    value = uuid.uuid7()

    assert isinstance(value.time, int)
    assert value.time > 0

If your application promises sortable storage, test that property through the storage representation rather than only with Python objects.

If your application supports multiple Python versions, test the declared minimum version in CI. A feature that works on a developer’s Python 3.14 installation is not enough when production still runs an older interpreter.

Treat ordering as a useful property, not a transaction guarantee

UUIDv7 gives Python applications a practical middle ground between random UUIDs and centrally allocated numeric sequences. It keeps the familiar UUID shape while putting millisecond time into the high-order bits, and Python’s implementation adds same-millisecond monotonic generation with a counter.

Use those properties where they help: index locality, roughly chronological identifiers, log inspection, and extracting generation time. Keep their limits visible too. Separate processes are not coordinated into one global sequence, clocks are not transaction order, the timestamp is visible to anyone who sees the identifier, and identity is not authorization.

With those boundaries in place, uuid.uuid7() is a small standard-library call that can simplify an otherwise surprisingly consequential identifier-design decision.