How It Works¶
pytest-uuid provides seamless UUID mocking that "just works" with both import patterns:
This page explains how pytest-uuid achieves this, and why it uses a proxy-based approach.
The Challenge: Python's Import System¶
When you write from uuid import uuid4, Python creates a direct reference to the function in your module's namespace:
# mymodule.py
from uuid import uuid4 # mymodule.uuid4 -> uuid.uuid4 (at import time)
def generate_id():
return uuid4() # Calls mymodule.uuid4, NOT uuid.uuid4
If a mocking library only patches uuid.uuid4, modules that used from uuid import uuid4 still have their original reference—they bypass the mock entirely.
How Other Libraries Handle This¶
Most mocking libraries (unittest.mock, pytest-mock) require you to patch where it's used:
# You must know and patch every location
mocker.patch("mymodule.uuid4", return_value=...)
mocker.patch("other_module.uuid4", return_value=...)
This is explicit and predictable, but requires you to know every module that imports uuid4.
pytest-uuid's Approach¶
pytest-uuid takes a different philosophy: patch once, work everywhere. When the plugin loads, it installs a permanent proxy at uuid.uuid4. Any code that imports uuid4—whether before or after the proxy is installed—gets this proxy function.
The Solution: Permanent Proxy¶
pytest-uuid uses a proxy function that replaces uuid.uuid4 at plugin initialization:
┌─────────────────────────────────────────────────────────┐
│ Plugin Load (pytest_load_initial_conftests) │
│ │
│ 1. Save original uuid.uuid4 function │
│ 2. Replace uuid.uuid4 with proxy function │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Any Code (conftest.py, test files, production code) │
│ │
│ from uuid import uuid4 # Gets the proxy! │
│ uuid4() # Calls proxy, not original │
└─────────────────────────────────────────────────────────┘
Why This Works¶
The proxy is installed in pytest_load_initial_conftests, which runs before conftest files are loaded. This means:
- When conftest.py does
from uuid import uuid4, it gets the proxy - When test files do
from uuid import uuid4, they get the proxy - When production code is imported and does
from uuid import uuid4, it gets the proxy - Even Pydantic models with
default_factory=uuid4capture the proxy
How the Proxy Works¶
# Simplified version of what happens in _proxy.py
_original_uuid4 = None
_generator_stack = [] # Thread-safe stack of generators
_generator_lock = threading.Lock()
def _proxy_uuid4():
with _generator_lock:
if _generator_stack:
generator = _generator_stack[-1]
if generator is not None:
return generator() # Use the test generator
return _original_uuid4() # Use real uuid4
def install_proxy():
global _original_uuid4
_original_uuid4 = uuid.uuid4
uuid.uuid4 = _proxy_uuid4
When you use freeze_uuid:
__enter__pushes a generator onto the stack- All
uuid.uuid4()calls go through the proxy → generator __exit__pops the generator from the stack
Call Flow¶
Test code calls uuid4()
│
▼
uuid.uuid4 (actually the proxy)
│
▼
Generator stack check
│
┌────┴────┐
│ │
▼ ▼
Generator Original
active? uuid.uuid4
│
▼
Return deterministic UUID
Thread Safety¶
Both UUID generation and call tracking are fully thread-safe:
UUID Generation:
- The proxy uses a thread-safe global stack protected by a lock
- All threads see the same active generator
- Lock protects stack operations (push/pop/read)
- Generator is called outside the lock to avoid holding it during user code
Call Tracking:
- All tracking properties (
call_count,calls,generated_uuids, etc.) use per-instance locks - Multiple threads can safely call UUID functions and have their calls tracked accurately
- Lock hold time is minimized by creating
UUIDCalldataclasses outside the critical section
For parallel test execution with pytest-xdist, each worker is a separate process with its own proxy and stack, so there's no cross-worker interference.
Thread Ordering
If your test code spawns threads that call uuid.uuid4() concurrently, all threads will use the same generator. The UUID values will still be deterministic (from the generator), but the order in which threads receive UUIDs depends on thread scheduling.
Nested Contexts¶
The stack-based approach supports nested freeze_uuid contexts:
with freeze_uuid(seed=42):
uuid.uuid4() # Uses seed=42 generator
with freeze_uuid(seed=99):
uuid.uuid4() # Uses seed=99 generator (inner)
uuid.uuid4() # Back to seed=42 generator
Each context pushes its generator onto the stack; __exit__ pops it off.
Compatibility¶
The proxy approach is compatible with common testing libraries:
| Library | Compatible | Notes |
|---|---|---|
| moto | Yes | Uses decorator/context manager patching |
| freezegun | Yes | Patches loaded modules |
| responses | Yes | Socket-level patching |
| pytest-mock | Yes | Thin wrapper over unittest.mock |
| httpretty | Yes | Socket-level patching |
| Pydantic | Yes | default_factory=uuid4 works correctly |
The proxy coexists peacefully with other mocking tools because it only affects uuid.uuid4 and delegates to the original when not in a freeze context.
Advantages Over Import Hook¶
Previous versions of pytest-uuid used an import hook to intercept module imports and patch uuid4 references. The proxy approach offers several advantages:
| Aspect | Import Hook | Proxy |
|---|---|---|
| Complexity | ~200 lines, wraps builtins.__import__ |
~100 lines, simple delegation |
| Edge cases | Stale patches, late imports | None—proxy handles all cases |
| Pydantic | Required special handling | Works automatically |
| Performance | Scanned every import | Zero overhead per import |
| Debugging | Complex stack traces | Simple stack traces |
Summary¶
| Technique | Purpose |
|---|---|
| Permanent proxy | Replace uuid.uuid4 once at plugin load |
| Thread-safe stack | Support nested contexts and thread safety |
| Early hook | Install proxy before conftest loads |
This combination ensures that freeze_uuid "just works" regardless of how or when modules import uuid4.
Further Reading¶
- Source code:
_proxy.py- The proxy implementation - Source code:
plugin.py- Plugin hooks and fixtures