unittest.mock.patch() replaces an attribute reachable by a name. It does not search memory for every reference to the same object. If production code copied a reference into its own module, changing the exporter later leaves that copy untouched.
That explains the rule "patch where the name is looked up." The slogan is useful only when you can identify the lookup. Is the bytecode loading a module global, reading an attribute from a module object, resolving a class attribute, closing over a cell, or using a default argument captured at definition time?
The following experiments build that model without requiring a project layout. Temporary module objects stand in for imported modules, and standard-library mock APIs expose exactly which namespace changes.
Version note. Python blocks were run on CPython 3.14.7 using the standard library. Name-resolution rules are Python language guarantees.
unittest.mockdetails refer to Python 3.14. Bytecode instruction names and adaptive execution are CPython implementation details and are not patching APIs.
Experiment 1: assignment copies a reference
from types import SimpleNamespace
provider = SimpleNamespace(send=lambda: 'real')
consumer_send = provider.send
provider.send = lambda: 'fake'
assert provider.send() == 'fake'
assert consumer_send() == 'real'
consumer_send = provider.send binds the current function object to another name. Reassigning provider.send changes one namespace entry; it does not mutate the old function or find aliases.
from provider import send has the same binding shape at module import time. The consumer receives a global named send. Code that later executes send() reads the consumer's global dictionary, not provider.send. Patch consumer.send.
Python guarantee. Assignment binds a name to an object. Names in distinct namespaces can refer to the same object and can subsequently be rebound independently.
Experiment 2: module imports preserve attribute lookup
from types import SimpleNamespace
from unittest.mock import patch
provider = SimpleNamespace(send=lambda: 'real')
def deliver():
return provider.send()
with patch.object(provider, 'send', return_value='fake') as mocked:
assert deliver() == 'fake'
mocked.assert_called_once_with()
assert deliver() == 'real'
This resembles import provider followed by provider.send(). The function first resolves its global provider, then looks up send on that object at call time. Replacing the attribute affects the expression.
Neither import form is intrinsically better for tests. import provider can make the collaboration visible and patch target stable. A direct import can make local code shorter. The design question is whether dependencies and behavior remain clear, not whether everything is mockable.
Patch cleanup matters. The context manager restores the exact previous attribute even if the assertion raises. Manual reassignment without finally can contaminate later tests.
Experiment 3: inspect the actual global lookup
import dis
send = lambda: 'real'
def deliver():
return send()
assert deliver.__globals__['send'] is send
instructions = list(dis.get_instructions(deliver))
assert any(instruction.argval == 'send' for instruction in instructions)
deliver.__globals__ is the namespace used for global names in this function. That is often the fastest diagnostic: inspect the callable's defining module, then locate the attribute loaded by the expression.
On CPython 3.14, dis shows implementation-level instructions that may include specialized or fused forms depending on options and execution history. Do not write tests asserting exact opcodes. The language-level conclusion is simply that an unqualified send in a function body follows Python's local, enclosing, global, and builtins resolution rules.
When production code imports inside a function, read that function too. Import still consults sys.modules and then binds a local name; patching may require the imported module's attribute because the local module reference is acquired on every call.
Experiment 4: patching the exporter can miss the consumer
from types import ModuleType
from unittest.mock import patch
provider = ModuleType('provider')
provider.send = lambda: 'real'
consumer = ModuleType('consumer')
consumer.send = provider.send
exec('def deliver():\n return send()\n', consumer.__dict__)
with patch.object(provider, 'send', return_value='wrong target'):
assert consumer.deliver() == 'real'
with patch.object(consumer, 'send', return_value='right target') as mocked:
assert consumer.deliver() == 'right target'
mocked.assert_called_once_with()
The first patch succeeds mechanically but is behaviorally irrelevant. This is dangerous because a test can still pass for another reason and leave reviewers believing the dependency was isolated.
Assert that important mocks were called, but do not stop there. A call assertion proves interaction with the replacement; an output or state assertion proves the unit used that interaction to produce required behavior. Tests made only of mock calls tend to reproduce implementation structure rather than establish a useful contract.
Patch target strings such as 'package.consumer.send' are resolved when patch starts, not when the decorator is defined. The target module must be importable, and the final attribute normally must exist unless create=True is used.
Experiment 5: class lookup and instance shadowing differ
from unittest.mock import patch
class Client:
def request(self):
return 'real'
first = Client()
second = Client()
second.request = lambda: 'instance'
with patch.object(Client, 'request', autospec=True, return_value='class') as mocked:
assert first.request() == 'class'
assert second.request() == 'instance'
mocked.assert_called_once_with(first)
first.request is found on the class and bound as a method. second has an instance attribute with the same name, so that shadows the class descriptor. Patching the class does not erase the instance override.
Patch descriptors such as properties, static methods, and class methods on the class rather than an instance. PropertyMock is intended for properties. Python's descriptor protocol determines whether an instance dictionary can shadow an attribute; data descriptors take precedence over instance attributes.
autospec=True preserves the method signature enough that binding supplies first, allowing a useful receiver assertion. A plain Mock substituted for a method does not behave exactly like a function descriptor.
Experiment 6: closures do not read module globals
from unittest.mock import patch
def send():
return 'global'
def build_deliverer(sender):
def deliver():
return sender()
return deliver
deliver = build_deliverer(lambda: 'closed')
with patch(__name__ + '.send', return_value='patched'):
assert deliver() == 'closed'
assert deliver.__closure__[0].cell_contents() == 'closed'
sender is an enclosing-function variable stored in a closure cell. There is no lookup of this module's send, so that patch cannot matter. This is one reason explicit injection is powerful: the test can supply a dependency directly without global mutation.
CPython exposes cell objects through __closure__, but mutating closure internals is not a sensible testing seam. Construct the function with the collaborator you need. If production hides a dependency in a closure with no construction seam, reconsider the API rather than using implementation tricks.
Experiment 7: default arguments capture once
from unittest.mock import patch
def clock():
return 10
def stamp(now=clock):
return now()
with patch(__name__ + '.clock', return_value=99):
assert stamp() == 10
assert stamp(clock) == 99
Default expressions run when the def statement executes. stamp.__defaults__ retains the original function. Patching the global afterward does not replace that object.
Injecting dependencies through defaults can be convenient, but it surprises tests and readers when the default is a dynamic service. Prefer a sentinel default resolved inside the function, an object constructor dependency, or an explicit parameter at the application boundary. Do not mutate __defaults__ in tests; call the public seam.
This differs from a late global lookup:
from unittest.mock import patch
def clock():
return 10
def stamp(now=None):
if now is None:
now = clock
return now()
with patch(__name__ + '.clock', return_value=99):
assert stamp() == 99
Now clock is resolved when stamp runs, so module patching works. More importantly, callers can pass a clock explicitly.
Experiment 8: autospec catches interface drift
from unittest.mock import create_autospec
class Gateway:
def charge(self, account_id, cents, *, idempotency_key):
raise NotImplementedError
gateway = create_autospec(Gateway, instance=True)
gateway.charge.return_value = 'accepted'
assert gateway.charge('a-1', 500, idempotency_key='k-1') == 'accepted'
try:
gateway.charge('a-1', 500)
except TypeError:
pass
else:
raise AssertionError('missing keyword should fail')
An unrestricted Mock invents attributes on access and accepts any arguments. A misspelled method can make a test pass against an interface production never had. Autospeccing restricts attributes and validates signatures based on the specified object.
Autospec is not runtime type checking. It does not validate argument types or implement the collaborator's semantics. Properties inspected during recursive autospeccing should also be safe to access; introspection can trigger badly designed descriptors.
Use a small protocol or interface as the spec when the real client is enormous. That keeps the test double aligned with the dependency actually consumed.
Experiment 9: patch dictionaries with automatic restoration
import os
from unittest.mock import patch
original = os.environ.get('DEEPCUTS_MODE')
with patch.dict(os.environ, {'DEEPCUTS_MODE': 'test'}):
assert os.environ['DEEPCUTS_MODE'] == 'test'
assert os.environ.get('DEEPCUTS_MODE') == original
Environment variables, registries, and caches are mappings, so patch.dict expresses scoped mutation and restoration. In pytest, monkeypatch.setenv, setattr, and setitem provide fixture-managed restoration with the same core principle.
Restoration only repairs this process's mapping. A child process may inherit an earlier snapshot, and a library may have copied an environment value into a module global at import. Patch before that lookup or patch the copied configuration value where production reads it.
Avoid session-wide patches unless the state is truly a suite invariant. Broad patches hide dependencies and increase interactions between tests.
Better seams, fewer patches
Patch is ideal at narrow boundaries: clock, random source, transport client, subprocess launcher, or environment. It is poor as a way to reconstruct every internal call. If a test patches five private helpers in the module under test, it often tests the current decomposition rather than behavior.
Prefer injecting stable capabilities into a service object, passing values into pure functions, and using a small fake when collaborator behavior has meaningful state. Patch the constructor or factory at an outer assembly boundary when replacing expensive infrastructure. Keep the action and outcome visible in the test.
When a patch appears ineffective, follow this diagnostic order:
- Read the exact expression production executes.
- Identify whether it resolves a local, closure, global, builtin, or attribute.
- Inspect the defining module and import form.
- Check for captured defaults, aliases, instance shadowing, and import-time copies.
- Patch the narrow owner for the shortest possible scope.
- Assert both relevant interaction and observable result.
Patch lifetime must match concurrency lifetime
patch mutates process-global namespace state. If a test starts a thread or task that will perform the lookup later, leaving the patch context too early restores the real dependency before that lookup. Synchronize with the background operation and keep the context active until work has completed and been joined or awaited.
The reverse leak is also possible: a broad patch remains active while unrelated concurrent work uses the same module. Tests running in separate processes avoid cross-process patch state, but tasks and threads inside one test process share it. Prefer dependency injection when collaborators vary per concurrent request; one module global cannot safely represent two different fakes at once.
Context variables can carry request-local values through async task contexts, but they are not a general mock registry and have specific propagation rules for threads. Use them only when request context is part of production design.
Know what a mock cannot prove
A mock can validate calls against a signature and record interactions. It cannot prove that a real HTTP client serializes the same payload, that a database transaction has the expected isolation, or that a clock behaves across daylight-saving transitions. Keep a smaller set of contract and integration tests at those boundaries.
Overconfigured mocks are particularly fragile. If a test scripts ten consecutive return values and asserts an exact private call sequence, a harmless batching refactor breaks it while protocol incompatibility may remain invisible. Configure the minimum behavior needed to drive the public case. Assert domain output, durable state, or a narrow boundary request.
Fakes are useful when stateful behavior matters: an in-memory repository can enforce uniqueness and expose committed records. They still need contract tests against the real adapter, or they can drift into a friendlier system than production. The purpose of choosing patch, mock, stub, fake, or real dependency is not taxonomy. It is preserving the risk the test claims to cover while removing unrelated cost and nondeterminism.
Exercises
- Create two real temporary modules using
from x import value; patch exporter and consumer in turn and explain the results. - Patch a property correctly with
PropertyMock, then show why patching one instance is different. - Refactor a closure-captured HTTP client into an explicit constructor dependency without increasing patch count.
- Apply autospec to one existing test double and fix every interface mismatch it reveals.
- Find a test that patches an internal helper. Rewrite it around a boundary fake and compare what refactoring it tolerates.
Keep this model
Patch changes one binding in one namespace for one scope. Production behavior changes only if its runtime lookup reaches that binding. Direct imports create independent globals, module imports preserve attribute lookup, methods follow descriptor rules, closures read cells, and defaults retain definition-time objects.
Locate the lookup before writing the target string. Then ask whether patching is the clearest seam at all. Tests become more trustworthy when mocks constrain real interfaces and assertions establish outcomes, not merely when the green test reached a fake.