Isolation & Contracts

Resolving side_effect and return_value Conflicts

A mock configured with mock = Mock(return_value="ok"); mock.side_effect = [1, 2] returns 1, then 2, then raises StopIteration instead of falling back to "ok" — and a callable side_effect that quietly returns None makes the mock return None no matter what return_value you set. Both surprises trace to the same rule: unittest.mock.Mock.__call__ evaluates side_effect before it ever looks at return_value. This guide pins down that precedence chain and gives you the deterministic patterns to stop side_effect from silently shadowing return_value.

Prerequisites

  • Python 3.8+ (the precedence rules below are unchanged since 3.6; AsyncMock requires 3.8).
  • unittest.mock from the standard library — no third-party packages required.
  • Optional: pytest 7.0+ if you reproduce the conflicts inside a test runner.

Solution

The fix is to understand the precedence chain, then commit to exactly one configuration axis per call. Mock.__call__ resolves a return in this fixed order:

  1. If side_effect is an exception class or instance, raise it.
  2. If side_effect is a callable, call it with the same args; if it returns the sentinel unittest.mock.DEFAULT, fall through to return_value, otherwise return its result.
  3. If side_effect is an iterable, call next() on it and return the yielded value (raising StopIteration when exhausted).
  4. Otherwise (side_effect is None), return return_value.

The code below demonstrates each branch and the one supported way to use both attributes together — the DEFAULT sentinel.

Python
import unittest.mock as mock

# --- Branch 4: side_effect is None, so return_value wins ---
m = mock.Mock(return_value="static")
assert m() == "static"               # no side_effect set -> falls through

# --- Branch 2/3: side_effect set, return_value is bypassed ---
m = mock.Mock(return_value="static")
m.side_effect = [1, 2]               # an iterable side_effect
assert m() == 1                      # next() on the iterable
assert m() == 2
try:
    m()                              # iterable exhausted...
except StopIteration:
    pass                             # ...raises, does NOT fall back to "static"

# --- The supported bridge: return mock.DEFAULT to use return_value ---
m = mock.Mock(return_value="fallback")
def route(value):
    # Return a real value for "live", otherwise defer to return_value.
    return "dynamic" if value == "live" else mock.DEFAULT
m.side_effect = route
assert m("live") == "dynamic"        # callable produced a value
assert m("other") == "fallback"      # callable returned DEFAULT -> return_value

# --- Exception branch takes priority over everything ---
m = mock.Mock(return_value="never")
m.side_effect = TimeoutError("upstream down")
try:
    m()
except TimeoutError:
    pass                             # raised before return_value is consulted

When you genuinely need a sequence of replies and a stable tail value, encode the whole policy in one callable rather than leaning on implicit fallback:

Python
import unittest.mock as mock

def sequenced(values, default):
    """A callable side_effect that yields `values` in order, then `default` forever."""
    it = iter(values)
    def _call(*args, **kwargs):
        try:
            return next(it)          # consume the scripted sequence
        except StopIteration:
            return default           # explicit, deterministic tail — never raises
    return _call

api = mock.Mock()
api.side_effect = sequenced([200, 200, 503], default=200)
assert [api() for _ in range(5)] == [200, 200, 503, 200, 200]

To reset a mock between reuses without leaving a stale iterator cursor behind, use the side_effect flag on reset_mock (added in Python 3.6):

Python
m = mock.Mock(side_effect=[1, 2])
m()                                  # consumes 1
m.reset_mock(side_effect=True)       # clears call history AND side_effect
m.side_effect = [9, 8]               # fresh iterable, cursor at start
assert m() == 9

The same precedence rules govern AsyncMock, but the value must be awaitable-compatible. A list side_effect is consumed by iteration (not by awaiting each item), and a callable side_effect may be sync or async. Wrapping a sequence in an async callable keeps behavior deterministic when the iterable runs dry:

Python
import asyncio
import unittest.mock as mock

def async_sequenced(values, default):
    it = iter(values)
    async def _call(*args, **kwargs):
        try:
            return next(it)
        except StopIteration:
            return default
    return _call

client = mock.AsyncMock()
client.side_effect = async_sequenced(["a", "b"], default="z")

async def main():
    return [await client(), await client(), await client()]

assert asyncio.run(main()) == ["a", "b", "z"]

Why this works

Mock.__call__ never merges the two attributes; it short-circuits on the first non-None side_effect it finds, and only an explicit None (or a callable returning mock.DEFAULT) lets control reach return_value. By collapsing all dynamic behavior into a single callable, you remove the implicit iterator state that causes StopIteration and the silent None returns that come from a callable forgetting to return a value. Choosing one axis per call makes the mock's output a pure function of its configuration rather than of hidden cursor position.

side_effect versus return_value precedence A decision flow showing Mock checks side_effect first, branching on exception, callable, and iterable before falling back to return_value. Mock.__call__ resolution order side_effect set? exception raise it callable call, use result iterable next(): may stop None / DEFAULT return_value return_value is reached only on the rightmost path
Mock checks side_effect first and branches on its type; return_value is consulted only when side_effect is None or a callable returns mock.DEFAULT.

Edge cases and failure modes

  • Iterable exhaustion across parametrized tests. A fixture that returns Mock(side_effect=[1, 2, 3]) keeps one iterator across @pytest.mark.parametrize runs; the third reuse raises StopIteration. Build the mock fresh per test (function-scoped fixture) or reset with reset_mock(side_effect=True).
  • Callable that forgets to return. A side_effect lambda used for its side effect only (e.g. logging) returns None, so the mock returns None and return_value is silently ignored. Return mock.DEFAULT explicitly when you want the fallback.
  • StopIteration inside a generator under test. If the code under test runs inside a generator, a StopIteration leaking from an exhausted side_effect is converted to RuntimeError by PEP 479. The traceback then points at the generator, not the mock — check mock.call_count against the iterable length.
  • AsyncMock with a non-awaitable result. A sync callable side_effect that returns a plain value works (AsyncMock wraps it), but returning a coroutine object that the caller never awaits produces a "coroutine was never awaited" warning. The debugging track covers this in tracing unawaited coroutine warnings.
  • reset_mock() without flags leaves config intact. Plain reset_mock() clears call_count and mock_calls but deliberately keeps side_effect and return_value. Pass side_effect=True / return_value=True to clear them too.

Frequently Asked Questions

Does setting side_effect to None restore return_value behavior? Yes. Assigning side_effect = None clears the override so the mock falls back to return_value. But if the mock previously held an exhausted iterable, that cursor is gone once you reassign, so the next call uses return_value cleanly. To also clear call history, pair it with reset_mock().

Why does my mock return None even though I set return_value? Because side_effect is set and takes precedence. If side_effect is a callable, its return value is used; if it returns None, the mock returns None and return_value is ignored. Wrap the fallback inside the callable, or set side_effect = None.

Can side_effect and return_value be used together? Not as a blend. Mock.__call__ checks side_effect first; if it is a callable, iterable, or exception, return_value is bypassed for that call. The one bridge is returning the sentinel unittest.mock.DEFAULT from a callable side_effect, which tells Mock to fall through to return_value.

These conflicts are easiest to avoid when the mock's interface is constrained in the first place — pairing this with autospec and strict mocking stops a typo'd attribute from silently absorbing your return_value, and the broader mechanics live in the deep dive into unittest.mock. When the conflicting mock is injected rather than patched, the constructor-level approach in dependency injection for testability makes the configuration explicit at the call site.

← Back to Autospec & Strict Mocking