Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Python Get UUID: uuid4, uuid7, and Which One for a Primary Key

Sean

Platform Writer

Aug 04, 2026
8 min read

uuid.uuid4() is the answer for almost every case. The interesting question is what happens when you make that value a database primary key.

Python Get UUID: uuid4, uuid7, and Which One for a Primary Key

The uuid module is in the standard library, needs no installation, and the common case is a single function call. That part takes thirty seconds.

What deserves more attention is the version choice and the storage decision. Random UUIDs as primary keys have a well-documented performance characteristic that catches teams out at scale, and there is a newer version specifically designed to fix it.

Table of contents

Generating one

import uuid

# Random. The default choice for identifiers.
uid = uuid.uuid4()
print(uid)        # 3f2504e0-4f89-41d3-9a0c-0305e82c3301

# Useful representations
print(str(uid))   # '3f2504e0-4f89-41d3-9a0c-0305e82c3301'
print(uid.hex)    # '3f2504e04f8941d39a0c0305e82c3301'  (no dashes)
print(uid.bytes)  # 16 raw bytes -- the compact storage form
print(uid.int)    # 128-bit integer

# Parse from a string; raises ValueError if malformed
parsed = uuid.UUID('3f2504e0-4f89-41d3-9a0c-0305e82c3301')

# Accepts the dashless form too
parsed = uuid.UUID('3f2504e04f8941d39a0c0305e82c3301')

uuid4() draws from the operating system’s cryptographic random source, so the values are unpredictable as well as unique. That matters if identifiers appear in URLs — a sequential integer invites people to try the next number.

The UUID object is immutable and hashable, so it works as a dictionary key or in a set without conversion. Two objects parsed from the same string compare equal regardless of input case or dashes.

The versions and when each applies

  • uuid1 — timestamp plus the machine’s MAC address. Sortable by time, but it leaks your hardware address and the generation time.
  • uuid3 and uuid5 — deterministic, derived from a namespace and a name via MD5 and SHA-1 respectively. The same inputs always produce the same UUID. Prefer uuid5.
  • uuid4 — random. The default for identifiers where you want no correlation between values.
  • uuid6, uuid7, uuid8 — added in Python 3.14. uuid7 is timestamp-ordered and random, designed specifically for database keys.

The deterministic versions are genuinely useful and underused. When you need a stable identifier derived from something that already uniquely identifies a thing, uuid5 gives you idempotency for free — reimporting the same record produces the same key rather than a duplicate.

import uuid

# Same input, same output, every time and on every machine
uuid.uuid5(uuid.NAMESPACE_URL, 'https://example.com/users/42')
# UUID('a6c3f1f8-...')  -- stable across runs

# A private namespace for your own identifier scheme
MY_NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
uuid.uuid5(MY_NAMESPACE, 'customer:tenant-9:invoice-1041')

Avoid uuid1 unless you have a specific reason. It embeds the MAC address, which is an information disclosure in anything user-facing, and uuid7 gives you the time-ordering benefit without that cost.

Why random UUIDs hurt as primary keys

This is the part worth understanding before you commit a schema. It is not a Python problem — it is what a B-tree index does with random values.

Database indexes store keys in sorted order. Sequential keys always append to the rightmost page, which stays in memory and fills neatly. Random keys land in arbitrary pages spread across the whole index, so every insert touches a different page — likely not cached, and probably requiring a split.

The consequences compound as the index outgrows memory: worse cache hit rates, more page splits, more fragmentation, larger indexes, and slower writes. On a small table you will never notice. At tens of millions of rows it is very noticeable.

uuid7 fixes this by putting a millisecond timestamp in the high bits and random data in the low bits. Values generated close in time sort close together, so inserts cluster like sequential keys while staying unpredictable and independently generatable.

import uuid

# Python 3.14+
uid = uuid.uuid7()

# Earlier versions: pip install uuid6
# from uuid6 import uuid7
# uid = uuid7()

If you are choosing today and your database will get large, uuid7 is the better default for primary keys. uuid4 remains correct for anything where time-ordering would leak information you would rather not expose, such as public-facing tokens.

Storing UUIDs without wasting space

The storage decision matters as much as the version. The text form is 36 characters; the binary form is 16 bytes.

-- PostgreSQL: use the native type
CREATE TABLE users (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email      text NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- Not this: 36 bytes plus overhead, slower comparisons
-- id varchar(36) PRIMARY KEY

PostgreSQL’s uuid type stores 16 bytes and compares them as integers. Storing UUIDs as text costs more than double the space in the table, more again in every index, and every comparison becomes a string comparison. On a primary key that is referenced by foreign keys throughout the schema, the difference is substantial.

MySQL has no native UUID type. Use BINARY(16) with the conversion functions, and note the swap-flag argument that reorders time fields for better index locality with uuid1.

-- MySQL 8+
CREATE TABLE users (
  id BINARY(16) PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE
);

INSERT INTO users (id, email) VALUES (UUID_TO_BIN(?, 1), ?);
SELECT BIN_TO_UUID(id, 1) AS id, email FROM users;

In Python, most database drivers handle the conversion. psycopg adapts uuid.UUID objects to the Postgres uuid type directly, so you pass the object and read one back without manual string conversion.

Practical patterns

A few things that come up repeatedly in real code.

import json
import uuid

# UUIDs are not JSON-serialisable by default
json.dumps({'id': uuid.uuid4()})
# TypeError: Object of type UUID is not JSON serializable

# Fix with a custom encoder
class UUIDEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, uuid.UUID):
            return str(obj)
        return super().default(obj)

json.dumps({'id': uuid.uuid4()}, cls=UUIDEncoder)
import uuid

# Validate untrusted input rather than trusting the shape
def parse_uuid(value):
    try:
        return uuid.UUID(str(value))
    except (ValueError, AttributeError, TypeError):
        return None

# A shorter URL-safe form when 36 characters is too long
import base64

def short_id(uid):
    return base64.urlsafe_b64encode(uid.bytes).rstrip(b'=').decode()

def from_short_id(text):
    return uuid.UUID(bytes=base64.urlsafe_b64decode(text + '=='))

The base64 form gives you 22 characters instead of 36 while preserving all 128 bits. Useful for URLs and anything a person might have to copy, and it round-trips exactly.

One caution on uuid.uuid4() and forking. If a process generates UUIDs, forks, and both children continue generating, they draw from the OS random source independently and stay unique. This is safe. The same is not true of userspace pseudo-random generators seeded before the fork, which is a reason not to build your own.

How this fits the rest of the stack

The UUID-as-primary-key decision is really a database decision that happens to be made in application code, and its cost only becomes visible once the index no longer fits in memory. That is easier to reason about when the database is a line item you can see rather than something inherited. RunxBuild managed Postgres gives you the native uuid type and gen_random_uuid() without an extension to install, and the RunxBuild hosting calculator shows the database and its storage separately so index growth is a number rather than a surprise.

Useful related references:

FAQ

Which UUID version should I use in Python?

uuid4() for general-purpose random identifiers. uuid7() for database primary keys, because it is time-ordered and avoids random-insert index fragmentation. uuid5() when you need the same input to always produce the same identifier.

Why are UUID primary keys slower than integers?

B-tree indexes store keys in sorted order. Random UUIDs scatter inserts across the whole index instead of appending to one hot page, causing cache misses, page splits, and fragmentation. uuid7 avoids this by embedding a timestamp in the high bits.

How should I store a UUID in PostgreSQL?

Use the native uuid type, which stores 16 bytes and compares as integers. Storing it as varchar(36) more than doubles the space in the table and every index, and makes comparisons string operations.

Is uuid4 safe for security tokens?

It draws from the OS cryptographic random source, so the values are unpredictable. For dedicated security tokens the secrets module is more idiomatic and lets you choose the length explicitly.

Why can’t I JSON-encode a UUID?

json.dumps does not know the type. Convert with str(uid) or supply a custom encoder whose default method returns the string form for UUID instances.

#Python Get UUID#Python#UUID#Primary Keys#Database