UUIDs (Universally Unique Identifiers) provide unique IDs without coordination. Essential for distributed systems, database keys, and API resources.

UUID Versions

import uuid
 
# UUID v4: Random (most common)
random_id = uuid.uuid4()
print(random_id)  # e.g., 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
 
# UUID v1: Timestamp + MAC address
time_based = uuid.uuid1()
 
# UUID v3: MD5 hash of namespace + name
md5_based = uuid.uuid3(uuid.NAMESPACE_DNS, 'example.com')
 
# UUID v5: SHA-1 hash of namespace + name (preferred over v3)
sha1_based = uuid.uuid5(uuid.NAMESPACE_DNS, 'example.com')

When to Use Each Version

VersionUniquenessSortablePrivacyUse Case
v1GuaranteedYesLeaks MACInternal systems
v3DeterministicNoSafeReproducible IDs
v4RandomNoSafeGeneral purpose
v5DeterministicNoSafeReproducible IDs

UUID Properties

import uuid
 
u = uuid.uuid4()
 
# String representations
print(str(u))           # 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
print(u.hex)            # 'f47ac10b58cc4372a5670e02b2c3d479'
print(u.urn)            # 'urn:uuid:f47ac10b-58cc-4372-a567-0e02b2c3d479'
 
# Components
print(u.version)        # 4
print(u.variant)        # RFC_4122
print(u.bytes)          # b'\xf4z\xc1\x0bX\xccCr\xa5g\x0e\x02\xb2\xc3\xd4y'
print(u.int)            # 324644427883734833893486402954248461433
 
# Time component (v1 only)
u1 = uuid.uuid1()
print(u1.time)          # Timestamp
print(u1.node)          # MAC address

Creating UUIDs from Strings

import uuid
 
# From string
u = uuid.UUID('f47ac10b-58cc-4372-a567-0e02b2c3d479')
 
# From hex (no dashes)
u = uuid.UUID(hex='f47ac10b58cc4372a5670e02b2c3d479')
 
# From bytes
u = uuid.UUID(bytes=b'\xf4z\xc1\x0bX\xccCr\xa5g\x0e\x02\xb2\xc3\xd4y')
 
# From integer
u = uuid.UUID(int=324644427883734833893486402954248461433)
 
# Validation
def is_valid_uuid(s: str) -> bool:
    try:
        uuid.UUID(s)
        return True
    except ValueError:
        return False

Deterministic UUIDs

Generate the same UUID for the same input:

import uuid
 
# Built-in namespaces
uuid.NAMESPACE_DNS    # For domain names
uuid.NAMESPACE_URL    # For URLs
uuid.NAMESPACE_OID    # For OIDs
uuid.NAMESPACE_X500   # For X.500 DNs
 
# Same input = same UUID
id1 = uuid.uuid5(uuid.NAMESPACE_DNS, 'example.com')
id2 = uuid.uuid5(uuid.NAMESPACE_DNS, 'example.com')
print(id1 == id2)  # True
 
# Custom namespace
MY_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, 'myapp.example.com')
 
def user_uuid(username: str) -> uuid.UUID:
    """Generate deterministic UUID for username."""
    return uuid.uuid5(MY_NAMESPACE, username)
 
print(user_uuid('alice'))  # Always same UUID for 'alice'

Database Primary Keys

import uuid
from dataclasses import dataclass, field
 
@dataclass
class User:
    name: str
    email: str
    id: uuid.UUID = field(default_factory=uuid.uuid4)
 
# SQLAlchemy
from sqlalchemy import Column
from sqlalchemy.dialects.postgresql import UUID
 
class User(Base):
    __tablename__ = 'users'
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

Short IDs from UUIDs

import uuid
import base64
 
def uuid_to_short(u: uuid.UUID) -> str:
    """Convert UUID to short URL-safe string."""
    return base64.urlsafe_b64encode(u.bytes).rstrip(b'=').decode()
 
def short_to_uuid(s: str) -> uuid.UUID:
    """Convert short string back to UUID."""
    # Add padding back
    padding = 4 - len(s) % 4
    if padding != 4:
        s += '=' * padding
    return uuid.UUID(bytes=base64.urlsafe_b64decode(s))
 
# Usage
u = uuid.uuid4()
short = uuid_to_short(u)
print(f"UUID: {u}")
print(f"Short: {short}")  # 22 chars instead of 36
 
restored = short_to_uuid(short)
print(u == restored)  # True

Sortable UUIDs (ULID-style)

UUIDs aren't naturally sortable. Here's a time-prefixed approach:

import uuid
import time
import struct
 
def time_uuid() -> str:
    """Generate time-sortable UUID-like identifier."""
    # Timestamp prefix (milliseconds)
    timestamp = int(time.time() * 1000)
    time_bytes = struct.pack('>Q', timestamp)[-6:]  # 48 bits
    
    # Random suffix
    random_bytes = uuid.uuid4().bytes[:10]
    
    # Combine and format like UUID
    combined = time_bytes + random_bytes
    hex_str = combined.hex()
    
    return f"{hex_str[:8]}-{hex_str[8:12]}-{hex_str[12:16]}-{hex_str[16:20]}-{hex_str[20:]}"
 
# These sort chronologically
id1 = time_uuid()
time.sleep(0.01)
id2 = time_uuid()
print(id1 < id2)  # True

UUID Sets and Dicts

import uuid
 
# UUIDs are hashable
seen_ids = set()
u = uuid.uuid4()
seen_ids.add(u)
print(u in seen_ids)  # True
 
# As dict keys
users = {
    uuid.uuid4(): {'name': 'Alice'},
    uuid.uuid4(): {'name': 'Bob'},
}
 
for user_id, data in users.items():
    print(f"{user_id}: {data['name']}")

Nil UUID

The all-zeros UUID for representing absence:

import uuid
 
nil_uuid = uuid.UUID('00000000-0000-0000-0000-000000000000')
# or
nil_uuid = uuid.UUID(int=0)
 
def get_parent_id(item) -> uuid.UUID | None:
    """Return parent ID or None."""
    parent = item.get('parent_id')
    if parent == nil_uuid:
        return None
    return parent

Comparing UUIDs

import uuid
 
u1 = uuid.uuid4()
u2 = uuid.uuid4()
 
# Equality
print(u1 == u2)  # False (almost certainly)
 
# Compare with string
print(u1 == str(u1))  # False (different types!)
print(str(u1) == str(u1))  # True
 
# Ordering (lexicographic)
print(u1 < u2)  # Based on bytes comparison

Request Tracing

import uuid
import logging
from contextvars import ContextVar
 
request_id: ContextVar[str] = ContextVar('request_id')
 
class RequestIDFilter(logging.Filter):
    def filter(self, record):
        record.request_id = request_id.get('unknown')
        return True
 
def process_request(handler):
    """Middleware to add request ID."""
    def wrapper(request, *args, **kwargs):
        req_id = str(uuid.uuid4())
        request_id.set(req_id)
        request.headers['X-Request-ID'] = req_id
        return handler(request, *args, **kwargs)
    return wrapper

Bulk UUID Generation

import uuid
 
# Generate many UUIDs efficiently
def generate_uuids(count: int) -> list[uuid.UUID]:
    return [uuid.uuid4() for _ in range(count)]
 
# Pre-generate for batches
uuids = generate_uuids(1000)
 
# Assign to objects
for obj, uid in zip(objects, uuids):
    obj.id = uid

Best Practices

  1. Use v4 for random IDs: Secure and simple
  2. Use v5 for deterministic IDs: When you need same input → same output
  3. Store as native UUID: Don't store as string in databases
  4. Validate on input: Always parse user-provided UUIDs
  5. Consider collision probability: v4 has 2^122 possibilities—practically collision-free

UUIDs solve the "unique ID without coordination" problem. They're the standard choice for distributed systems and APIs.

React to this post: