Skip to main content
Back to Blog
Python
2026-06-15

pytest-mock vs monkeypatch in 2026: Which to Use & When

pytest-mock's mocker fixture vs pytest's built-in monkeypatch in 2026 — what each does, when to pick which, auto-teardown, spies, and full code examples.

pytest-mock vs monkeypatch in 2026: Which to Use & When

monkeypatch is pytest's built-in fixture for temporarily setting attributes, dictionary items, and environment variables, with automatic teardown after each test. pytest-mock is a third-party plugin that wraps Python's standard unittest.mock and exposes it as a mocker fixture, also with automatic teardown — its job is creating mocks and spies with assertable call records. Use monkeypatch when you just need to replace a value, env var, or attribute. Use mocker when you need a Mock/MagicMock you can later assert was called with specific arguments. Many test suites use both. This guide explains the difference and shows exactly when each fits.

The one-sentence rule

If you need to assert how something was called (assert_called_once_with, inspect call_args, count calls), reach for mocker from pytest-mock. If you only need to substitute a value or attribute and do not care about call inspection, reach for monkeypatch. Both clean up after the test automatically, so neither requires a manual try/finally or afterEach.

NeedUse
Replace os.environ["KEY"] for one testmonkeypatch.setenv
Replace an attribute/function with a fixed valuemonkeypatch.setattr
Replace a function and assert it was called with Xmocker.patch
Create a Mock/MagicMock objectmocker.Mock() / mocker.MagicMock()
Spy on a real function (run it and record calls)mocker.spy
Stub a method to return/raise somethingmocker.patch.object
Delete an attribute or env var temporarilymonkeypatch.delattr / delenv
chdir into a temp directorymonkeypatch.chdir

monkeypatch: the built-in for surgical replacements

monkeypatch ships with pytest — no install needed. Request it as a fixture argument. Every change it makes is reverted automatically when the test finishes, so tests stay isolated.

def test_uses_env(monkeypatch):
    monkeypatch.setenv("API_BASE", "https://test.local")
    assert build_url("/users") == "https://test.local/users"
    # API_BASE is restored automatically after the test

Its core methods:

monkeypatch.setattr(obj, "name", value)          # replace an attribute
monkeypatch.setattr("pkg.mod.func", replacement) # by dotted path (string target)
monkeypatch.delattr(obj, "name", raising=False)  # remove an attribute
monkeypatch.setitem(some_dict, "key", value)     # set a dict entry
monkeypatch.delitem(some_dict, "key")            # remove a dict entry
monkeypatch.setenv("VAR", "value")               # set an env var
monkeypatch.delenv("VAR", raising=False)         # unset an env var
monkeypatch.syspath_prepend(path)                # prepend to sys.path
monkeypatch.chdir(path)                           # change working dir

The classic use is replacing a function with a stub that returns a known value — without needing any assertion on how it was called:

def test_temperature_display(monkeypatch):
    def fake_read_sensor():
        return 21.5

    monkeypatch.setattr("weather.read_sensor", fake_read_sensor)
    assert format_temp() == "21.5°C"

The critical gotcha — and it applies to mocker.patch too — is patch where the name is looked up, not where it is defined. If weather.py does from sensors import read_sensor, you must patch weather.read_sensor (the name imported into weather), not sensors.read_sensor. Getting this wrong is the number-one reason a patch "doesn't take." For broader testing patterns across Python tooling, browse the QA skills directory.

pytest-mock: the mocker fixture over unittest.mock

pytest-mock is a plugin you install:

pip install pytest-mock

It does not invent a new mocking engine. It is a thin, ergonomic wrapper around the standard library's unittest.mock, exposed as a mocker fixture so you avoid nesting with mock.patch(...) context managers or stacking @patch decorators. Crucially, every patch made via mocker is undone automatically at the end of the test — you never call .stop() yourself.

def test_sends_email(mocker):
    send = mocker.patch("app.notifications.send_email")
    register_user("ada@example.com")
    send.assert_called_once_with("ada@example.com", subject="Welcome")

Because mocker.patch returns a MagicMock, you get the full assertion surface of unittest.mock:

mock_obj.assert_called()              # called at least once
mock_obj.assert_called_once()         # exactly once
mock_obj.assert_called_with(a, b=2)   # last call's args
mock_obj.assert_called_once_with(...) # exactly once, with these args
mock_obj.assert_not_called()
mock_obj.call_count                   # integer
mock_obj.call_args                    # last call
mock_obj.call_args_list               # every call

Set return values and side effects directly:

def test_retry_on_timeout(mocker):
    fetch = mocker.patch("app.client.fetch")
    fetch.side_effect = [TimeoutError, TimeoutError, {"ok": True}]  # fail twice, then succeed

    result = fetch_with_retry()
    assert result == {"ok": True}
    assert fetch.call_count == 3

mocker also exposes the constructors so you can build standalone mocks: mocker.Mock(), mocker.MagicMock(), mocker.patch.object(SomeClass, "method"), and mocker.create_autospec(target) for a mock whose signature matches the real object (autospec catches calls with the wrong arguments — highly recommended).

mocker.spy: run the real thing and record calls

A standout feature unique to pytest-mock is mocker.spy. It wraps a real method so it still executes but also records every call — perfect for "I want the real behavior and to assert it was invoked."

def test_cache_calls_loader_once(mocker):
    spy = mocker.spy(DataLoader, "load")  # real load() still runs

    cache = Cache(DataLoader())
    cache.get("key")
    cache.get("key")  # served from cache the second time

    spy.assert_called_once()  # loader hit the disk only once

monkeypatch cannot do this — it replaces, it does not wrap-and-observe. When you need both real execution and call assertions, mocker.spy is the tool. Note the spied object exposes spy_return and spy_exception so you can inspect what the real call produced.

patch.object and autospec: safer, more precise patches

Two mocker features deserve their own section because they prevent whole categories of bugs.

mocker.patch.object patches a method on a specific object or class rather than by a dotted string path. This is often clearer and refactor-safe, because your IDE and type checker can see the attribute — a renamed method becomes an error instead of a silently-missed patch.

def test_charge_uses_object_patch(mocker):
    # patch the method on the class directly — no fragile string path
    charge = mocker.patch.object(PaymentGateway, "charge", return_value={"id": "ch_1"})

    gateway = PaymentGateway()
    gateway.charge(amount=4200)

    charge.assert_called_once_with(amount=4200)

autospec makes a mock that matches the real object's signature. A plain MagicMock happily accepts any call, so a typo like charge(amont=4200) passes the test while breaking production. Autospec rejects calls that do not match the real signature:

def test_autospec_catches_bad_calls(mocker):
    charge = mocker.patch("billing.charge", autospec=True)
    # or: mocker.create_autospec(real_charge)

    billing.charge(amount=4200)        # ok — matches the real signature
    charge.assert_called_once_with(amount=4200)
    # billing.charge(amont=4200) would raise TypeError at call time

Autospec is widely recommended for any mock standing in for a real, stable interface. It costs nothing and turns "the test passed but the call was wrong" into an immediate failure. monkeypatch has no equivalent — another reason to reach for mocker when correctness of the call matters.

There is also mocker.stub(name="..."), a lightweight callable double useful as a named callback whose calls you want to assert, without patching anything:

def test_observer_notified(mocker):
    on_done = mocker.stub(name="on_done")
    run_task(callback=on_done)
    on_done.assert_called_once_with(result="ok")

Side-by-side: the same test both ways

Consider testing a function that calls time.sleep between retries. If you only need to neutralize the sleep, monkeypatch is the lighter choice:

def test_no_real_sleep(monkeypatch):
    monkeypatch.setattr("app.retry.time.sleep", lambda s: None)
    assert do_work() == "done"   # runs instantly, no assertion on sleep

But if you must verify the back-off schedule — that it slept for 1s, then 2s, then 4s — you need call inspection, so mocker wins:

def test_exponential_backoff(mocker):
    sleep = mocker.patch("app.retry.time.sleep")
    do_work()
    assert [c.args[0] for c in sleep.call_args_list] == [1, 2, 4]

Same target, different needs, different tool. This is the practical decision in a nutshell. For a deeper look at how mocking libraries compare across languages, see the framework comparison hub.

Patching environment and config: a monkeypatch sweet spot

Environment variables and sys.path are where monkeypatch clearly beats mocker, because mocker.patch.dict("os.environ", ...) is more verbose and monkeypatch has purpose-built helpers:

def test_reads_feature_flag(monkeypatch):
    monkeypatch.setenv("FEATURE_NEW_CHECKOUT", "1")
    monkeypatch.delenv("LEGACY_MODE", raising=False)
    assert is_new_checkout_enabled() is True

For config dictionaries, monkeypatch.setitem is similarly clean:

def test_overrides_setting(monkeypatch):
    monkeypatch.setitem(settings.CONFIG, "timeout", 1)
    assert client().timeout == 1

A realistic end-to-end example using both

A single test that combines both fixtures — monkeypatch for the environment, mocker for the asserted call:

def test_charge_customer(monkeypatch, mocker):
    # monkeypatch: make the env deterministic
    monkeypatch.setenv("STRIPE_KEY", "sk_test_123")

    # mocker: replace the network call AND assert how it was used
    charge = mocker.patch("billing.gateway.create_charge", return_value={"id": "ch_1"})

    receipt = charge_customer(customer_id="cus_9", amount_cents=4200)

    charge.assert_called_once_with(amount=4200, currency="usd", customer="cus_9")
    assert receipt.charge_id == "ch_1"

Each fixture does what it is best at: monkeypatch sets a value with no assertion, mocker provides an assertable mock. Both auto-revert, leaving the next test pristine.

CI usage

Neither fixture needs special CI handling — they are ordinary pytest features. The only setup step is installing the plugin so mocker exists:

- name: Install deps
  run: pip install pytest pytest-mock

- name: Test
  run: pytest --strict-markers

monkeypatch requires nothing extra since it is built in. If you adopt mocker.create_autospec widely, consider running with -W error so any unittest.mock deprecation surfaces in CI rather than slipping by.

Common errors and troubleshooting

The patch has no effect. You patched where the function is defined instead of where it is imported and used. If module a does from b import f, patch a.f, not b.f. This applies to both monkeypatch.setattr and mocker.patch.

fixture 'mocker' not found. pytest-mock is not installed. Run pip install pytest-mock; the fixture is provided by the plugin, not by pytest core.

A mock accepts calls with wrong arguments. A plain MagicMock accepts anything. Use mocker.create_autospec(target) or mocker.patch(..., autospec=True) so the mock enforces the real signature and fails on mismatched arguments.

Manually stopping a patch. You do not need to. The whole point of mocker (and monkeypatch) is automatic teardown after each test — calling .stop() yourself is unnecessary and can cause errors.

Trying to assert calls with monkeypatch. monkeypatch replaces values; it has no call-recording. If you find yourself writing a fake that counts its own invocations, switch to mocker.patch and use assert_called_once_with.

For more Python testing deep-dives and plugin comparisons, browse the blog.

Frequently Asked Questions

What is the difference between pytest-mock and monkeypatch?

monkeypatch is pytest's built-in fixture for temporarily replacing attributes, dict items, and environment variables, with automatic teardown — it substitutes values but cannot record how they were called. pytest-mock is a third-party plugin exposing a mocker fixture that wraps unittest.mock, giving you Mock/MagicMock objects with full call-assertion methods, also auto-cleaned up. Use monkeypatch for plain replacement and mocker when you must assert call arguments or counts.

When should I use mocker instead of monkeypatch?

Use mocker whenever you need to assert how something was called — assert_called_once_with, call_count, inspecting call_args_list — or when you need a Mock/MagicMock/spy object. Use monkeypatch when you only need to set a value, attribute, or environment variable and do not care about call inspection. If your fake function ends up manually counting its own calls, that is a signal to switch to mocker.

Does mocker need manual teardown like unittest.mock?

No. pytest-mock's entire value over raw unittest.mock is that every patch created through the mocker fixture is automatically undone when the test ends, so you never call .stop() or use a with block. monkeypatch also reverts all of its changes automatically after each test. This auto-teardown keeps tests isolated without boilerplate.

Why isn't my monkeypatch.setattr taking effect?

Almost always because you patched the wrong name. Python binds imported names into the importing module, so if app.py does from utils import helper, you must patch app.helper, not utils.helper. Patch the location where the name is looked up at call time, not where it is originally defined — this same rule applies to mocker.patch.

How do I spy on a function so it still runs but records calls?

Use mocker.spy(target, "method_name") from pytest-mock. Unlike mocker.patch, the spy lets the real method execute while recording every call, so you can run real behavior and still assert with assert_called_once() or inspect spy_return. monkeypatch cannot do this because it replaces rather than wraps, so mocker.spy is the right tool when you need both.

Do I need to install anything to use monkeypatch?

No. monkeypatch is a built-in pytest fixture, available out of the box with no extra package. pytest-mock and its mocker fixture, by contrast, must be installed with pip install pytest-mock. If you only use monkeypatch, a plain pytest install is enough.

pytest-mock vs monkeypatch in 2026: Which to Use & When | QASkills.sh