2025, Dec 10 05:00

Stop chaining context managers with and in Python: correct ways to stack mock.patch and mock.patch.dict in tests

Learn why using boolean and in a Python with statement breaks context managers. See correct ways to stack mock.patch and mock.patch.dict in unit tests.

When patching configuration during tests, it’s tempting to glue multiple context managers together with a boolean operator. It looks compact, but in Python that pattern doesn’t do what you think. If you’re using mock.patch.dict to inject values into a module-level mapping and need that patch only within a method, using and inside a with statement will silently break one of the patches.

Problem setup

The decorator-based approach works as expected because both patches are applied around the call. Here’s a minimal example in that style:

@mock.patch.dict("cfg.store", {"probe": 123})
@mock.patch("cfg.fetch_store", return_value=None, autospec=True)
def execute(self, patched_fetch, *args, **kwargs):
    return super().execute(*args, **kwargs)

But switching to a single with statement combined with and leads to trouble. Only the second context manager is actually entered; the dict patch isn’t managed correctly, so the test won’t see the injected value:

def execute(self, *args, **kwargs):
    with mock.patch.dict("cfg.store", {"probe": 123}) and mock.patch(
            "cfg.fetch_store", return_value=None, autospec=True
        ) as p_fetch:
        return super().execute(*args, **kwargs)

Why it breaks

In a with statement, and doesn’t combine context managers. It’s just the boolean operator. The first expression mock.patch.dict(...) is evaluated, then its truthiness is checked. If it’s truthy, Python evaluates and enters only the second expression as the real context manager. The result is that the function patch is active, but the dict patch is not actually entered or exited as a context, which is why the value injection doesn’t take effect.

By contrast, the decorator form works in this scenario, and so do proper with constructs, because they explicitly enter and exit each context manager in order.

The fix

Use comma-separated context managers in a single with statement, or nest with blocks. Both forms correctly enter the first context, then the second, and unwind them in reverse order.

def execute(self, *args, **kwargs):
    with mock.patch.dict("cfg.store", {"probe": 123}), \
         mock.patch("cfg.fetch_store", return_value=None, autospec=True) as p_fetch:
        return super().execute(*args, **kwargs)

Or, if you prefer explicit nesting:

def execute(self, *args, **kwargs):
    with mock.patch.dict("cfg.store", {"probe": 123}):
        with mock.patch("cfg.fetch_store", return_value=None, autospec=True) as p_fetch:
            return super().execute(*args, **kwargs)

You can also write multiple context managers inside parentheses instead of using a line continuation. This keeps the same semantics while improving readability:

def execute(self, *args, **kwargs):
    with (
        mock.patch.dict("cfg.store", {"probe": 123}),
        mock.patch("cfg.fetch_store", return_value=None, autospec=True) as p_fetch,
    ):
        return super().execute(*args, **kwargs)

Why this matters

Tests that manipulate configuration are often deceptively simple. A subtle misuse of the with statement can invalidate part of your setup without raising obvious errors. You might end up debugging code paths that ran with half-applied patches, which is noisy and time-consuming. Understanding that and is not a context-manager combiner helps prevent flaky behavior and keeps isolation airtight.

Takeaways

When you need to stack patches as context managers, never use and inside with. Use comma-separated managers in one with statement or nest with blocks. The decorator approach remains fine if it fits your test design, but when you need access to instance attributes like self.some_value inside the patched scope, the context-manager forms above are the right tool. Stick to these patterns and your patches will apply predictably, in order, and will be reliably undone at the end of the block.