The single most common unittest.mock mistake is patching requests.get and watching the real HTTP request fire anyway — the exact failure that makes mocking network and HTTP calls flaky — because the code under test did from requests import get and now resolves get in its own module namespace, not in requests. The canonical rule is "patch where it's looked up, not where it's defined," and it falls directly out of how Python binds names: a from-import copies a reference into the importing module at import time, so patching the source module never touches that copy. This guide explains the binding mechanism, shows the two import forms side by side, and gives a repeatable procedure for finding the correct patch target every time — one that scales to the deep import graphs covered in patching strategies for complex codebases.
Prerequisites
- Python 3.x with
unittest.mock(patchlives in the standard library; examples target 3.11). - A clear mental model of module namespaces and import binding. The Patching Strategies for Complex Codebases overview frames the broader problem.
- Familiarity with
patchas a decorator/context manager.
Solution
Trace the import form in the module that calls the dependency, then patch the namespace that module reads from.
# svc.py — where the function is DEFINED
def fetch() -> str:
return "REAL network result"
# app.py — the module UNDER TEST
from svc import fetch # <-- copies the reference: app.fetch is now a name
def run() -> str:
return fetch() # resolves `fetch` in app's OWN namespace
# test_app.py
from unittest.mock import patch
import app
def test_patch_where_looked_up():
# CORRECT: patch the binding app actually resolves at call time.
with patch("app.fetch", return_value="FAKE") as m:
assert app.run() == "FAKE" # the mock replaced app.fetch
m.assert_called_once_with()
def test_patch_source_module_fails():
# WRONG: app already copied the reference; svc.fetch is a different binding.
with patch("svc.fetch", return_value="FAKE"):
# app.fetch still points at the original object -> real code runs.
assert app.run() == "REAL network result"
# --- Contrast: attribute-access import makes the source module the target. ---
# app_attr.py
import svc # keeps a live reference to the module object
def run_attr() -> str:
return svc.fetch() # resolves `fetch` on the svc module AT CALL TIME
def test_attribute_access_patches_source():
import app_attr
# Now patching the source works, because the lookup happens on svc.
with patch("svc.fetch", return_value="FAKE"):
assert app_attr.run_attr() == "FAKE"
The rule follows from what import actually does to names, and the two forms bind them in different places.
from-import copies the object into the consumer namespace, so the consumer name is the only one the call site reads.Why this works
from svc import fetch executes an assignment: it copies the current value of svc.fetch into app's module dictionary as app.fetch. From then on, app.run() looks up fetch in app's globals, never consulting svc again — so patch("svc.fetch", ...) rebinds a name nothing reads. patch works by setting an attribute on the object named by the dotted path, so you must point it at the exact namespace where the code resolves the name: app.fetch for a from-import, and svc.fetch for import svc followed by svc.fetch(), because that form defers the lookup to call time on the live module object.
Finding the target: a four-step procedure
When a patch silently does nothing, do not guess dotted paths — trace the name. This procedure resolves the target for any dependency, however deep the import graph.
- Find the call site. Locate the module that actually invokes the dependency and note the exact line, e.g.
result = fetch(url)inapp.py. The target is anchored to this module, not to whatever library ultimately defines the object. - Inspect the import form. Open that module's imports.
from svc import fetchcopied the reference intoappat import time, so the live binding isapp.fetch.import svc(thensvc.fetch()) never copies anything; the binding stays on thesvcmodule object. - Patch the lookup namespace. For the from-import,
patch("app.fetch"). For attribute access,patch("svc.fetch"). State it as a single rule: patch where the name is read, not where the object was defined. - Verify with
assert_called. Assert the mock ran (m.assert_called_once()); if the assertion fails withExpected 'fetch' to have been called, the real function executed and your target names the wrong namespace — return to step 2.
A fast way to confirm the binding without reading every import is to inspect the module dictionary at a REPL: import app; app.fetch shows the object app will actually resolve. If app.fetch is svc.fetch is True, both paths point at the same object now, but only patch("app.fetch") intercepts the copy that app.run() reads. When several call sites share one dependency, patch each caller's namespace, or refactor to import svc so a single patch("svc.fetch") covers them all.
Edge cases and failure modes
- Re-imports and aliases shift the target.
from svc import fetch as grabcreatesapp.grab; patchapp.grab. Animport svc as swiths.fetch()still resolves on the originalsvcmodule object, so patchsvc.fetch. - Class methods are attributes of the class, not the caller. To replace a method on instances, patch
module.ClassName.method(or usepatch.object(ClassName, "method")); the binding lives on the class regardless of where instances are created. - Patching builtins and
sys.modulesneeds different targeting.open,print, and module-level singletons resolve through builtins or the import system, not a plain copied name — see patching builtins and sys.modules safely. autospec=Truedoes not change the target, only the double's strictness. You still patch where the name is looked up; pairing the correct target with autospec strict mocking catches signature drift once the patch lands. This matters for mocking network and HTTP calls, where the wrong target silently lets real requests through.- Package re-exports add a third namespace. When
pkg/__init__.pydoesfrom .svc import fetchandappthen doesfrom pkg import fetch, the reference has been copied twice:pkg.fetchandapp.fetchare both bindings, andapp.run()readsapp.fetch. Patchapp.fetch. Patchingpkg.fetchmisses the same way patchingsvc.fetchdoes — one hop further out. - Patch ordering with stacked decorators is bottom-up. Multiple
@patchdecorators inject mocks as arguments in reverse order; a wrong assumption here looks like a wrong target. The mock that does not match its expected call is the misordered one, not necessarily the wrong namespace. - A
patchthat "does nothing" but raises no error is a target bug, not a mock bug.patchonly fails loudly when the dotted path itself is unresolvable (AttributeError: <module> does not have the attribute). A syntactically valid but logically wrong target — the source module instead of the caller — patches a real name that nothing reads, so the test passes against live code. Theassert_calledcheck in step 4 is what surfaces it.
When you cannot find the binding by reading, ask the interpreter: import consumer; print(consumer.fetch.__module__) tells you where the object was defined, while vars(consumer)["fetch"] shows the binding a patch would replace. If those two disagree, the second one is the patch target.
Frequently Asked Questions
Why does patching the function's source module not work? A from-import copies the reference into the importing module's namespace at import time. The code under test resolves the name in its own module, so patching the original source module leaves that copied binding untouched and the real function still runs.
What is the patch where it's looked up rule? Patch the name in the namespace where the code under test reads it, not where the object is defined. If module app imports a function from svc with from svc import fn, patch app.fn, because app.fn is the binding the code resolves.
Does import module then module.func avoid the binding problem? Yes. With import svc and a call to svc.fn, the code resolves fn on the svc module object at call time, so patching svc.fn works. The fragile case is from svc import fn, which copies the reference into the caller.
Why does my patch work in one test file and not another?
Because the two files import the collaborator differently, or because one of them imported the consuming module before the patch was applied and the other did not. Import order decides which binding exists at patch time; importlib.reload in one test and not the other produces exactly this asymmetry. Patch the binding the failing module actually reads, and prefer patch.object(consumer, "fetch") so the target is resolved from a real object rather than from a string.
Related guides
- For builtins, environment, and module-level singletons, read patching builtins and sys.modules safely.
- Once the target is right, lock the double's contract with autospec strict mocking.
- If the dependency can be injected instead of patched, injecting fakes vs mocks in constructors avoids target headaches entirely.
- When the patched object is awaited, choose the class via Mock vs MagicMock vs AsyncMock — when to use each.