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 inside 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.

One iterable side_effect across four calls A timeline of four calls against a mock configured with side_effect equal to the list one, two. The first call yields one, the second yields two, the third raises StopIteration because the iterator is exhausted, and a fourth call in a reused fixture raises again because the cursor is never rewound. One iterable side_effect across four calls call 1 yields 1 cursor advances call 2 yields 2 iterable empty call 3 StopIteration no fallback value reuse still exhausted cursor outlives test reset_mock(side_effect=True) clears the cursor; plain reset_mock() does not.
An iterable side_effect carries a cursor, and that cursor outlives the test unless the mock is rebuilt or reset — which is why the failure usually surfaces in the second parametrized case, not the first.
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 in Mock.__call__ An ordered short-circuit ladder: Mock.__call__ inspects side_effect first, testing exception, then callable, then iterable in that order, and only reaches return_value when side_effect is None or a callable returns mock.DEFAULT. Mock.__call__ resolution order side_effect is inspected first and short-circuits — checks run left to right 1 · exception class or instance → raise it 2 · callable call with same args → its result 3 · iterable next() yields value → may raise StopIteration 4 · None no override left → return_value bridge: a callable returning mock.DEFAULT falls through to return_value
Mock checks side_effect first and short-circuits on its type in order; return_value is consulted only on branch 4 (side_effect is None) or when a callable side_effect 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.

Diagnosing a mock that returns the wrong thing

When a mock hands back something you did not configure, three attributes localise the fault in under a minute, and they are worth checking in this order because each rules out a whole branch of the precedence chain.

Start with mock.side_effect. If it is anything other than None, return_value is not in play at all and no amount of reconfiguring it will change the result. A truthy side_effect you did not set usually came from a shared fixture or a patch decorator applied higher in the stack.

Next read mock.call_count against the length of the iterable you supplied. A count that already equals the length means the next call raises StopIteration; a count of zero when you expected calls means the code under test never reached the collaborator, and the value you are inspecting came from somewhere else entirely.

Finally inspect mock.mock_calls, which records children and their arguments in call order. A call recorded against mock.method() when you configured mock itself is the classic wrong-level configuration: the child mock has its own return_value and inherits nothing from its parent.

Python
import unittest.mock as mock

def diagnose(m: mock.Mock) -> dict:
    """Snapshot the three fields that explain any unexpected mock result."""
    return {
        "side_effect": repr(m.side_effect),   # not None -> return_value is bypassed
        "call_count": m.call_count,           # >= len(iterable) -> next call raises
        "calls": [str(c) for c in m.mock_calls[:5]],  # which child was really called
    }

api = mock.Mock(return_value="static")
api.side_effect = [1]
api()
print(diagnose(api))
# {'side_effect': '<list_iterator ...>', 'call_count': 1, 'calls': ['call()']}

Reading m.side_effect after the first call shows a list_iterator, not the list you assigned: Mock converts an iterable to an iterator on assignment, which is precisely why the cursor survives between tests.

Configuration lives on exactly one of two axes, and mixing them is what produces the surprises above.

Which axis to configure for a given need Three panels: a static reply configured with return_value, a scripted sequence configured with a callable side_effect, and a raised error configured with an exception side_effect, each listing what to set and what to assert. Which axis to configure for a given need Static reply set return_value leave side_effect None assert on call args safe to reuse Scripted sequence callable side_effect explicit tail value never raises StopIteration rebuild per test Raised error exception side_effect checked before all else pytest.raises in test clear with None
Pick one column per call: the middle one is the only place a sequence belongs, and it should always define its own tail.

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. 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