Random UUIDs are convenient identifiers: they can be generated without coordinating with a database, and the probability of collision is tiny. But a UUIDv4 primary key has one awkward property for ordered indexes: newly generated values are spread across the key space instead of tending toward the end of the index.

UUID version 7 keeps the decentralized 128-bit UUID shape while putting a Unix-epoch millisecond timestamp at the front. Python 3.14 adds uuid.uuid7() to the standard library, so applications no longer need a third-party package just to generate RFC 9562 UUIDv7 values.

That makes UUIDv7 useful, but not magical. It is a unique identifier with useful temporal ordering properties, not a transaction sequence number or a replacement for a trusted creation timestamp.

Generate a UUIDv7

The basic API is deliberately small:

import uuid

order_id = uuid.uuid7()

print(order_id)
print(order_id.version)  # 7

The result is the same uuid.UUID type used for other UUID versions, so existing code that accepts UUID objects often needs little or no change.

Python also exposes the embedded UUIDv7 timestamp through UUID.time:

from datetime import datetime, timezone
import uuid

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

print(created_at)

For UUIDv7, time is milliseconds since the Unix epoch. This differs from UUIDv1 and UUIDv6, whose time value uses 100-nanosecond intervals from the Gregorian epoch.

Why ordering improves

RFC 9562 places the 48-bit Unix timestamp in the most significant part of a UUIDv7. As time advances, the leading portion of newly generated identifiers therefore advances too.

That property matters because Python compares UUID objects by their 128-bit integer values:

import uuid

first = uuid.uuid7()
second = uuid.uuid7()

print(first < second)

Python’s implementation also uses a counter to provide monotonic UUID generation within a millisecond. That is helpful when one process generates several IDs faster than the system clock’s millisecond resolution.

For database keys, the practical attraction is locality. A B-tree index receiving roughly increasing keys tends to touch a narrower region than one receiving uniformly random UUIDv4 values. This can reduce page churn and fragmentation in some workloads.

I would still benchmark the actual database and write pattern before claiming a performance win. Storage engines, fill factors, buffering, concurrency, and key representation all affect the result.

UUIDv7 is not a global sequence

It is tempting to see a timestamp at the front and treat UUIDv7 as a globally ordered event number. That is too strong.

Consider two application servers:

server A clock: 12:00:00.120
server B clock: 12:00:00.090

If A handles an event before B but their clocks disagree, sorting the UUIDs can produce an order different from the real causal order. Clock corrections and virtualized environments make the same issue possible on one host over time.

Python’s within-millisecond monotonic behavior is an implementation property for generated values; it does not synchronize counters or clocks between machines.

If an application needs authoritative ordering, use a mechanism that actually establishes it: a database sequence, transaction log position, consensus-backed revision, or domain-specific ordering field.

UUIDv7 is better described as time ordered than globally sequential.

The timestamp is visible

A UUIDv4 does not directly reveal when it was created. UUIDv7 intentionally does.

Anyone holding a UUIDv7 can recover its millisecond timestamp:

import uuid
from datetime import datetime, timezone

value = uuid.UUID("01900000-0000-7000-8000-000000000000")
when = datetime.fromtimestamp(value.time / 1000, tz=timezone.utc)

That may leak operational information. Public identifiers can reveal approximate object creation times, traffic patterns, or the relative age of records.

This does not automatically make UUIDv7 unsuitable for public URLs, but it should be an explicit privacy decision. If creation time itself is sensitive, a random UUIDv4 or an opaque externally mapped identifier may be a better boundary.

Do not use the embedded time as authoritative data

Because the timestamp is convenient, code can start using it instead of storing created_at:

created_at = datetime.fromtimestamp(record.id.time / 1000, tz=timezone.utc)

I avoid making that the canonical creation time.

The UUID records generator-clock time, not necessarily database commit time or business-event time. An ID may be generated and then inserted minutes later. A transaction can roll back. A queued command can carry an ID long before the corresponding object exists.

Store an explicit timestamp when the timestamp has domain meaning:

CREATE TABLE orders (
    id UUID PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL,
    ...
);

The UUID timestamp remains useful for rough ordering, diagnostics, and partitioning decisions where its semantics are sufficient.

Keep UUIDs in native or binary form when possible

A UUID is 128 bits. Its familiar hyphenated text representation is larger:

import uuid

value = uuid.uuid7()

assert len(value.bytes) == 16
assert len(str(value)) == 36

If a database has a native UUID type, I generally use it. Otherwise a 16-byte binary representation can be preferable to a 36-character text key when the surrounding stack handles binary UUIDs cleanly.

Be careful with byte ordering when interoperating with systems that use nonstandard GUID layouts. Python’s normal UUID.bytes representation is big-endian UUID byte order; bytes_le exists for the alternate little-endian layout of the first fields.

For ordinary RFC UUID storage, do not casually swap between those forms.

Migrating from UUIDv4 does not require rewriting old IDs

A column can contain UUIDs of different versions unless an application or schema explicitly forbids it. That makes gradual migration possible:

import uuid


def new_id() -> uuid.UUID:
    return uuid.uuid7()

New rows receive UUIDv7 while existing UUIDv4 identifiers remain valid.

The important question is whether downstream code assumed version 4. Validation such as this becomes wrong after migration:

if value.version != 4:
    raise ValueError("unsupported UUID")

Check serializers, API validators, database constraints, fixtures, analytics jobs, and language-specific UUID libraries before changing the generator.

A mixed-version table also means that sorting all historical IDs does not suddenly reconstruct creation order. The ordering property only has the intended meaning among values whose layouts and generation semantics support it.

UUIDv6 and UUIDv8 solve different problems

Python 3.14 also adds uuid6() and uuid8().

UUIDv6 reorders the time-based UUIDv1 layout to improve database locality. Like UUIDv1, it can involve a node identifier. For a new application wanting Unix-time-oriented IDs, UUIDv7 is usually easier to reason about.

UUIDv8 is a custom-layout space. Python lets callers provide custom bit blocks, but its default generated blocks are not cryptographically secure. I would not choose UUIDv8 merely because its version number is newer.

UUIDv4 remains the straightforward choice when a cryptographically secure random UUID is desired and time ordering is unnecessary.

Parse untrusted UUIDs before trusting their version

At an API boundary, parsing and version validation are separate decisions:

import uuid


def parse_order_id(raw: str) -> uuid.UUID:
    try:
        value = uuid.UUID(raw)
    except (ValueError, AttributeError) as exc:
        raise ValueError("invalid order id") from exc

    if value.version != 7:
        raise ValueError("order id must be UUIDv7")

    return value

Whether the version check belongs there depends on the migration strategy. If old UUIDv4 IDs remain externally addressable, rejecting every non-v7 UUID would break legitimate records.

Also remember that UUID validity is not authorization. A well-formed UUID tells you nothing about whether the caller may access the object it names.

Test properties instead of exact values

Generated UUIDs are intentionally variable, so tests should focus on guarantees the application relies on:

import uuid


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

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

If the application relies on local ordering, test a batch:

def test_generated_ids_are_monotonic():
    values = [uuid.uuid7() for _ in range(1000)]

    assert values == sorted(values)

I would not turn a local monotonicity test into a claim about distributed ordering. For multi-node behavior, test the application’s real ordering mechanism under clock skew and concurrent writes.

Migration tests are valuable too: make sure old UUIDv4 records can still be loaded, serialized, routed, and authorized after the default generator changes.

A useful boundary

UUIDv7 sits in a practical middle ground. It preserves decentralized UUID generation and the familiar 128-bit representation while making generated values naturally time ordered. In Python 3.14, that capability is now one standard-library call away.

The design stays clean when each property is given the right job: use UUIDv7 as the identifier, use an explicit timestamp for business time, and use a real sequencing mechanism when correctness depends on total or causal order.

With those boundaries in place, UUIDv7 can improve key locality and operational readability without quietly turning an identifier into a clock, sequence, or authorization primitive.