For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Frameworks

Where should I patch in pytest?

Rule AR-35

Patch the name the code under test looks up, through monkeypatch or mocker, on the module object. Don't reassign module globals in a test.

monkeypatch.setattr swaps a name for the length of one test and puts it back afterwards. That makes it a good tool, but it only works on the name your code reads. If checkout.py does from payments.gateway import charge, then patching payments.gateway.charge changes an object checkout no longer reads. The general rule is on AR-18. This page covers how pytest applies it: which fixture to use, what the arguments mean, and what fails loudly and what doesn't.

Why it happens

Python binds a name where it is imported. After from payments.gateway import charge, checkout holds its own reference to the function, and replacing the attribute in payments.gateway doesn't touch it. The mock docs put the rule this way: "you patch where an object is _looked up_, which is not necessarily the same place as where it is defined." The pytest docs say the same thing for monkeypatch: "Prefer patching the reference that your code uses instead of patching the original object in the standard library."

What pytest gives you, from the monkeypatch how-to and the reference:

  • Undo is automatic. "All modifications will be undone after the requesting test function or fixture has finished."
  • **setattr(target, name, value, raising=True)** replaces an attribute. With raising=True, "Raises AttributeError if the attribute does not exist". Pass raising=False and a patch aimed at a missing name succeeds without effect on anything.
  • The target can be an object or a dotted string. The reference: "you can specify a string as target which will be interpreted as a dotted import path", for example monkeypatch.setattr("os.getcwd", lambda: "/"). With the object form, monkeypatch.setattr(checkout, "charge", fake), the module is a real reference, and a rename tool follows it. The attribute name is still a string.
  • Other methods: delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir and context().
  • Builtins. "It is not recommended to patch builtin functions such as open, compile, etc.", because pytest's own internals use them. monkeypatch.context() limits a patch to a block.
  • Outside a test. MonkeyPatch.context() returns a new object "which undoes any patching done inside the with block", and pytest.MonkeyPatch() can be used directly "for when the fixture is not available". For a fixture with a scope wider than function, use MonkeyPatch.context() inside it The monkeypatch how-to does not label the fixture's scope, but says "The same monkeypatch fixture is used across a single test function invocation" and that changes are undone after "the requesting test function or fixture has finished" (https://docs.pytest.org/en/stable/reference/reference.html), which describes function scope.

Bad and good

payments/checkout.py does from payments.gateway import charge and calls charge(customer_id, total).

python# bad: tests/test_checkout.py import payments.gateway from payments.checkout import checkout def test_checkout_charges_total(): original = payments.gateway.charge payments.gateway.charge = lambda *a, **k: payments.gateway.ChargeResult(ok=True, charge_id="ch_test") try: receipt = checkout("cus_42", [1200, 800]) finally: payments.gateway.charge = original assert receipt.charge_id == "ch_test"

This has three faults. It reassigns a module global by hand and relies on finally for the restore. It patches payments.gateway, which checkout doesn't read. And the lambda takes any arguments, so it accepts a call the real function would reject. The test runs the real charge and fails or goes to the network.

python# good: tests/test_checkout.py from unittest import mock import pytest from payments import checkout as checkout_module from payments import gateway @pytest.fixture def charge_mock(monkeypatch: pytest.MonkeyPatch) -> mock.Mock: fake = mock.create_autospec(gateway.charge) fake.return_value = gateway.ChargeResult(ok=True, charge_id="ch_test") # checkout.py did "from payments.gateway import charge": patch its own name. monkeypatch.setattr(checkout_module, "charge", fake) return fake def test_checkout_charges_total(charge_mock: mock.Mock): receipt = checkout_module.checkout("cus_42", [1200, 800]) charge_mock.assert_called_once_with("cus_42", 2000, "usd") assert receipt.charge_id == "ch_test"

monkeypatch.setattr(checkout_module, "charge", fake) names the module that looks the name up, restores it at the end of the test, and with the default raising=True fails at setup if checkout has no charge. create_autospec gives the stand-in the real signature: the mock docs say functions and methods "will have their arguments checked to ensure that they are called with the correct signature."

With the pytest-mock plugin the same patch is written through the mocker fixture:

pythondef test_checkout_charges_total(mocker): charge = mocker.patch("payments.checkout.charge", autospec=True) charge.return_value = gateway.ChargeResult(ok=True, charge_id="ch_test") ...

mocker comes from the pytest-mock plugin. Its usage page: "The mocker fixture has the same API as mock.patch, supporting the same arguments", and its examples use mocker.patch.object(os, 'listdir', autospec=True) (https://pytest-mock.readthedocs.io/en/latest/usage.html). The target string here follows the same where-to-look-it-up rule.

Trade-offs and when the other way is fine

  • **monkeypatch needs no extra package.** It ships with pytest, and covers attributes, dict items, environment variables, the working directory and sys.path. Use it for those. It doesn't give signature checking by itself, so pass it a create_autospec object or a typed fake class when the signature matters.
  • **mocker adds call recording and autospec in one call.** It is a plugin, so it is one more dependency: the plugin's page says "This pytest plugin provides a mocker fixture" and installs with pip install pytest-mock (https://pytest-mock.readthedocs.io/en/latest/). If the suite already uses unittest.mock, mock.patch as a context manager does the same job with the standard library.
  • A typed fake class beats either for a service used in many tests. CH-02 uses a FakeCharge callable. The test asserts on fake.calls and a checker can compare the fake's __call__ to the real function.
  • A dotted string is shorter, and a module object is easier to follow. Both undo the same way. Prefer the object form when the module is already imported in the test.
  • Dependency injection removes the patch. If checkout takes charge as a parameter with a default, the test passes a fake and nothing is patched. That costs a parameter in the code under test (AR-18).
  • Fixture scope matters. A function-scoped patch fixture is isolated. A wider scope shares one patch across tests, and with monkeypatch that needs MonkeyPatch.context() as above. Use that only for patches no test changes.

Enforce it

ToolSettingWhat it catchesChecked
pytestmonkeypatch.setattr(..., raising=True) (the default)a patch aimed at a name that doesn't exist; a moved or renamed target fails at setup2026-10-30
unittest.mockpatch(..., autospec=True) or create_autospec(fn)a call whose arguments the real signature rejects raises TypeError2026-10-30
ruffPT008 pytest-patch-with-lambda (select the PT family)a lambda as the replacement in a patch where return_value fits. A lambda that uses its arguments is not flagged2026-10-30 (from AR-18)
ruffPT019 pytest-fixture-param-without-valuea test taking a patch fixture only for its effect; use @pytest.mark.usefixtures("name")2026-10-30
pytestpytest --fixtureslists available fixtures"Show available fixtures, sorted by plugin appearance" (https://docs.pytest.org/en/stable/reference/reference.html)

Nothing in the pages read checks that a patch target is the lookup site rather than the definition site. A patch at the wrong site with raising=True still succeeds when the attribute exists, which is the case in CH-02. The way to catch it is a test that asserts the mock was called (assert_called_once_with), so a patch that doesn't take effect fails the test.

Rule block for AGENTS.md / CLAUDE.md

markdown## pytest patching - Patch the name the code under test looks up. If checkout.py has "from payments.gateway import charge", patch payments.checkout.charge. If it has "import payments.gateway" and calls payments.gateway.charge, patch payments.gateway.charge. - Use the monkeypatch fixture (or mocker). Never assign to module attributes in a test body, and never write try/finally to restore them. - Prefer monkeypatch.setattr(module_object, "name", fake) over a dotted string when the module is already imported. Keep raising=True (the default). - Give replacements the real signature: create_autospec(real_fn) or patch(..., autospec=True), or a typed fake class. No bare lambdas that accept any arguments. - Every patched call gets an assert on how it was called, so a patch that missed its target fails the test. - Patch builtins only inside monkeypatch.context(). Don't patch open or compile in a fixture that spans tests. - When moving a function or changing an import in production code, search the tests for the old dotted path and the old module's name in patch calls, and update them in the same change.

Edge cases

  • **from x import y captured before the patch.** The module that already imported y keeps the real one. Patch the importer's name.
  • Class attributes and methods. monkeypatch.setattr(SomeClass, "method", fake) replaces it for every instance until undo. The mock docs show that autospec copies the real signature, and that create_autospec(spec, instance=True) uses a class as the spec for an instance object; with instance=False, a class spec gives a mock whose return value (the instance) has the same spec (https://docs.python.org/3/library/unittest.mock.html).
  • Re-exports. The failure in CH-02: a re-export in payments/client.py kept monkeypatch.setattr("payments.client.charge", ...) succeeding while checkout read a different name.
  • **raising=False.** Use it only for names that legitimately may not exist, such as an optional attribute on a third-party object. It turns a misspelt target into silence.
  • Environment and time. setenv, delenv and chdir change inputs, not calls, and are fine in a named fixture (AR-34).
  • Async functions. The mock docs: "create_autospec() now returns an AsyncMock if the target is an async function" (since Python 3.8), and calling an AsyncMock gives an awaitable (https://docs.python.org/3/library/unittest.mock.html).
  • Overlap with AR-18. The lookup-site rule and the autospec rule are shared with AR-18. This page stands on its own for the pytest parts: monkeypatch versus mocker, raising, fixture scope, the object versus string target and the pytest rules. If the Lead reads it as too close, merge the shared "where to patch" paragraph into AR-18 and keep the pytest section as AR-34's second half.

What LLMGRAPH shows

LLMGRAPH does not list monkeypatch.setattr as a use of charge; the attribute name stays a string.

References

pytest (fetched 2026-10-30):

  • Monkeypatch how-to (setattr, raising, patch the reference your code uses, builtins, context(), undo): https://docs.pytest.org/en/stable/how-to/monkeypatch.html
  • Reference, MonkeyPatch class (dotted-string target, raising, context(), pytest.MonkeyPatch()): https://docs.pytest.org/en/stable/reference/reference.html
  • Fixtures how-to (fixture scope, requesting by name): https://docs.pytest.org/en/stable/how-to/fixtures.html

Python standard library (fetched 2026-10-30, carried over from AR-18; outside this work order's source list, listed for the quotes above):

  • unittest.mock, where to patch, autospec, create_autospec: https://docs.python.org/3/library/unittest.mock.html

Ruff: PT008 https://docs.astral.sh/ruff/rules/pytest-patch-with-lambda/ · PT019 https://docs.astral.sh/ruff/rules/pytest-fixture-param-without-value/

General rule: AR-18. Chimera: CH-02.

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: docs checks resolved
  • 2026-10-30: remaining checks closed (claims reduced to documented behaviour)
  • AR-18 Why does monkeypatching break agent edits?
  • AR-34 Why does my agent break pytest fixtures it can't see?
  • CH-02 Why do my pytest tests pass for a reason I can't see?

Chimeras that use this rule

  • CH-02 Why do my pytest tests pass for a reason I can't see?

Last verified

For agentsthis page as Markdown/agent-readable/ar-35.mdllms.txt