Isolation & Contracts

Where to Patch: Understanding mock.patch Targets

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 (patch lives 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 patch as a decorator/context manager.
How from-import binding decides the patch target A from-import copies the reference from the svc source module into app's namespace at import time. The code reads app.fn, so patch app.fn — the green target — while patching svc.fn misses the copy. from svc import fn — where the name lives the reference is copied into the caller at import time svc.py (source) def fn(): ... defines the object app.py (caller) from svc import fn app.fn = copied ref import time: reference copied patch("app.fn") ✓ the binding the code reads patch("svc.fn") misses the copy
A from-import copies svc.fn into app's namespace at import time, so the live binding the code resolves is app.fn — that is the patch target. Patching svc.fn leaves app's copy untouched.

Solution

Trace the import form in the module that calls the dependency, then patch the namespace that module reads from.

Python
# 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.

What each import form binds, and where a patch lands A sequence diagram with three lanes: the consuming module, the defining module, and mock.patch. A from-import copies the function object into the consumer namespace at import time, so patching the defining module leaves the consumer copy untouched, while patching the consumer name replaces the binding the code actually calls. What each import form binds, and where a patch lands consumer module defining module mock.patch from svc import fetch binds a copy patch("svc.fetch") - inert patch("consumer.fetch") works import svc; svc.fetch() keeps the lookup late, so patching the defining module does work.
A 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.

  1. Find the call site. Locate the module that actually invokes the dependency and note the exact line, e.g. result = fetch(url) in app.py. The target is anchored to this module, not to whatever library ultimately defines the object.
  2. Inspect the import form. Open that module's imports. from svc import fetch copied the reference into app at import time, so the live binding is app.fetch. import svc (then svc.fetch()) never copies anything; the binding stays on the svc module object.
  3. 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.
  4. Verify with assert_called. Assert the mock ran (m.assert_called_once()); if the assertion fails with Expected '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 grab creates app.grab; patch app.grab. An import svc as s with s.fetch() still resolves on the original svc module object, so patch svc.fetch.
  • Class methods are attributes of the class, not the caller. To replace a method on instances, patch module.ClassName.method (or use patch.object(ClassName, "method")); the binding lives on the class regardless of where instances are created.
  • Patching builtins and sys.modules needs 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=True does 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__.py does from .svc import fetch and app then does from pkg import fetch, the reference has been copied twice: pkg.fetch and app.fetch are both bindings, and app.run() reads app.fetch. Patch app.fetch. Patching pkg.fetch misses the same way patching svc.fetch does — one hop further out.
  • Patch ordering with stacked decorators is bottom-up. Multiple @patch decorators 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 patch that "does nothing" but raises no error is a target bug, not a mock bug. patch only 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. The assert_called check 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.

Choosing the patch target from the import form A decision diagram keyed on how the consuming module imported the collaborator: a from-import means patching the consumer namespace, a module import means patching the defining module, and an attribute reached through an instance means patching the class. Choosing the patch target from the import form How does the caller reach the name? from x import y patch consumer.y the copied binding import x; x.y() patch x.y lookup stays late obj.method() patch Class.method descriptor on the type patch.object removes the guesswork by taking the object itself rather than a dotted string.
Patch where the name is looked up at call time, which is not always where it was written.

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.

← Back to Patching Strategies for Complex Codebases