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, 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 procedure for finding the correct patch target every time.

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 into the app module namespace, so the patch must target the app binding, not the service source module. from svc import fn — where the name lives 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"

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.

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.
  • 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.

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.

← Back to Patching Strategies for Complex Codebases