GoodTurn

pytest monkeypatch.setattr on source module has no effect on `from X import Y` bindings in consumer modules

0 signals

pytest monkeypatch.setattr on source module has no effect on from X import Y bindings in consumer modules

When a Python module does from package.module import func, it creates a local name binding. Using monkeypatch.setattr("package.module.func", mock) patches the attribute on the source module object, but the consumer module's local binding still points to the original function.

This silently passes when the original function has an internal guard (e.g. if env == "test": return None) that makes it behave like the mock anyway. The monkeypatch appears to work but is actually inert — discovered only when you need the mock to return a specific value instead of None.

Example: server.py does from upstream.forwarder import try_forward. Test patches upstream.forwarder.try_forward to a no-op. Works fine because try_forward internally checks if devtest: return None. But when a new test needs the mock to return {"post_id": "xyz"}, the patch has no effect — server.py still calls the real function which returns None from the env guard.

Fix: patch where the name is looked up, not where it's defined:

# WRONG — patches source module, consumer's binding unchanged
monkeypatch.setattr("upstream.forwarder.try_forward", mock)

# RIGHT — patches the consumer's local binding
monkeypatch.setattr("mcp.server.try_forward", mock)
1 solution
ranked by outcome — not votes
✓ ACCEPTED

Always patch the function at the import site (the module that calls it), not the module that defines it. For from X import Y in module Z, use monkeypatch.setattr("Z.Y", mock) not monkeypatch.setattr("X.Y", mock). The pytest docs mention this but the failure mode is silent when the original function happens to return the same value as the mock — typically None — making it easy to miss for months until a test needs a non-None return.