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;
AsyncMockrequires 3.8). unittest.mockfrom the standard library — no third-party packages required.- Optional:
pytest7.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:
- If
side_effectis an exception class or instance, raise it. - If
side_effectis a callable, call it with the same args; if it returns the sentinelunittest.mock.DEFAULT, fall through toreturn_value, otherwise return its result. - If
side_effectis an iterable, callnext()on it and return the yielded value (raisingStopIterationwhen exhausted). - Otherwise (
side_effect is None), returnreturn_value.
The code below demonstrates each branch and the one supported way to use both attributes together — the DEFAULT sentinel.
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.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:
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):
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:
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.
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.parametrizeruns; the third reuse raisesStopIteration. Build the mock fresh per test (function-scoped fixture) or reset withreset_mock(side_effect=True). - Callable that forgets to return. A
side_effectlambda used for its side effect only (e.g. logging) returnsNone, so the mock returnsNoneandreturn_valueis silently ignored. Returnmock.DEFAULTexplicitly when you want the fallback. StopIterationinside a generator under test. If the code under test runs inside a generator, aStopIterationleaking from an exhaustedside_effectis converted toRuntimeErrorby PEP 479. The traceback then points at the generator, not the mock — checkmock.call_countagainst the iterable length.AsyncMockwith a non-awaitable result. A sync callableside_effectthat 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. Plainreset_mock()clearscall_countandmock_callsbut deliberately keepsside_effectandreturn_value. Passside_effect=True/return_value=Trueto 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.
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.
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.
Related
- Deep dive into unittest.mock — the full
Mock.__call__resolution mechanics behind this precedence chain. - Autospec and strict mocking — lock the spec so an attribute typo cannot swallow your
return_value. - Dependency injection for testability — configure the double explicitly at the call site instead of patching in place.
- Controlling time and randomness in tests — a frequent source of sequential
side_effectlists that exhaust mid-test. - Tracing unawaited coroutine warnings — diagnose the
AsyncMockfailure mode called out above.
← Back to Autospec & Strict Mocking