skip to content

What three kinds of value can unittest.mock's side_effect take?

level: middleimportance: must knowfreq 72%

answer

  1. How a mock stops being a constant
  2. Three shapes it will accept
  3. One raises, one is consumed, one is called
  4. Runs out and signals exhaustion
  5. A sentinel that defers to return_value

basics

~20 s

side_effect accepts an exception class or instance to raise, an iterable that supplies one result per call, or a callable invoked with the same arguments the mock received. It takes priority over return_value unless the callable returns unittest.mock.DEFAULT.

solid answer

~40 s

`side_effect` is what turns a mock from a constant into a script. Assign an exception class or instance and every call raises it. Assign an iterable and each call consumes the next item, raising `StopIteration` once it runs out — and any exception classes or instances *inside* that iterable are raised rather than returned, which is how you mix failures and successes. Assign a callable and it is invoked with exactly the arguments the mock received, so it can branch on them or accumulate state. `side_effect` is consulted before `return_value`, so a callable's result normally wins; the exception is the `unittest.mock.DEFAULT` sentinel, which means "fall back to `return_value`". Setting `side_effect = None` removes the script entirely.

code

python · 20 lines
python
from unittest.mock import Mock, DEFAULT

m = Mock(return_value="fallback")

m.side_effect = TimeoutError("upstream slow")
try:
    m()
except TimeoutError as exc:
    print("raised:", exc)

m.side_effect = [1, ValueError("bad row"), 3]
print(m())
try:
    m()
except ValueError as exc:
    print("raised from the iterable:", exc)
print(m())

m.side_effect = lambda use_default: DEFAULT if use_default else "computed"
print(m(True), m(False))

go deeper

for a junior

Recall that side_effect is how a mock raises instead of returning, and that assigning an exception class or instance makes every call to that mock fail.

for a middle

Explain all three forms and their precedence over return_value: exception raised, iterable consumed one item per call, callable invoked with the same arguments. Know that exceptions inside an iterable are raised.

for a senior

Demonstrate using side_effect to get real error-path coverage, and show awareness of the exhaustion trap — StopIteration is an Exception, so a broad except in the code under test hides an over-short script.

for a principal

Own the line between scripting and fidelity. A callable side_effect that grows conditionals is a fake wearing a mock's clothes; decide when the team should promote it to a real in-memory implementation shared across suites.

`return_value` answers "what does this mock give back?" with a single object. `side_effect` answers the harder question: "what does this mock *do* when called?" It accepts three shapes, and knowing all three is the difference between testing only the happy path and testing the error paths that actually break production. ### Form 1 — an exception Assign either an exception class or an exception instance and every call raises it: ```python client.evaluate.side_effect = TimeoutError client.evaluate.side_effect = TimeoutError("upstream slow") ``` A class is raised as-is and Python instantiates it with no arguments, so a class whose constructor requires arguments must be supplied as an instance instead. Prefer an instance when the message or attached attributes matter to the code under test — for example when your handler reads something off the exception before deciding to retry. This is the single most common use of `side_effect`: without it, driving an error path means either a real broken dependency or a hand-written fake that raises. ### Form 2 — an iterable Assign any iterable and the mock takes `iter()` of it at assignment time; each call consumes the next item. Plain items are returned; items that are exception classes or instances are *raised*. That mixing is the key trick: ```python client.evaluate.side_effect = [TimeoutError("peak"), {"enabled": True}] ``` The first call fails, the second succeeds — exactly the shape of a retry test. Two hazards follow. First, the iterable does not repeat or extend: one call past the end raises `StopIteration`. Second, `StopIteration` is a subclass of `Exception`, so a broad `except Exception:` in the code under test swallows it, and an off-by-one script can turn into an infinite retry loop rather than a clean failure. Since PEP 479 (Python 3.7), a `StopIteration` escaping a generator body is converted to `RuntimeError`, so the same mistake inside generator code surfaces as a confusing unrelated error. Keep the iterable exactly as long as the number of calls you expect. ### Form 3 — a callable Assign a function and the mock calls it with precisely the arguments it received, returning whatever the function returns. This is the general case, and it covers everything the other two forms cannot: results that depend on the arguments, results that depend on accumulated state, and side effects proper — recording into a list, flipping a flag, writing to a fake store. ```python def evaluate(key): if key.startswith("legacy-"): raise KeyError(key) return {"enabled": key != "kill-switch"} ``` A bare `Mock` performs no signature checking, so the arguments are forwarded verbatim; a mismatch shows up as a `TypeError` raised by your own callable. If the callable raises, that exception propagates out of the mock call exactly as if the collaborator had raised it. ### The DEFAULT sentinel A callable `side_effect` normally overrides `return_value`. Returning `unittest.mock.DEFAULT` opts back in: the mock returns its configured `return_value` instead of the sentinel. That is what makes hybrid configuration readable — one small callable special-cases the two arguments the test cares about and hands everything else back to a bland default: ```python from unittest.mock import DEFAULT m.side_effect = lambda key: KeyError(key) if key == "missing" else DEFAULT ``` Note that *returning* an exception instance is not the same as raising it — only iterable entries and directly-assigned exceptions are raised, so a callable that wants to fail must `raise`. ### Precedence and clearing The order is fixed: `side_effect` first, `return_value` second. If `side_effect` is not `None`, its result decides the call, unless a callable returned `DEFAULT`. To go back to a constant, set `side_effect = None`; to clear both scripted results at once, `mock.reset_mock(return_value=True, side_effect=True)` — a plain `reset_mock()` clears only recorded usage and deliberately leaves the configuration standing. ### Why interviewers ask this Happy-path tests are easy and everyone writes them. `side_effect` is the mechanism that makes timeouts, partial failures, empty results and exhausted resources cheap to test, and a candidate who knows only `return_value` almost always has a suite with no error-path coverage. The three forms map neatly onto three test intents: always fail, fail then recover, and behave like a small real implementation. All of this behaves identically across Python 3.10–3.14. ### Choosing between the three forms A useful rule of thumb: assign an exception when the whole test is about failure; assign an iterable when the test is about a *sequence* and its length is short and obvious; assign a callable when the result depends on the arguments or on state accumulated across calls. Iterables are the most fragile of the three, because their length quietly encodes how many times the collaborator is expected to be called — a refactor that adds one legitimate call breaks the test for a reason unrelated to its subject. Callables are the most durable and the most expensive to read, and past a couple of branches they are really a hand-written fake that would be clearer written as one.

  • What happens when a callable side_effect returns unittest.mock.DEFAULT?
    The mock returns its own `return_value` instead of the sentinel. That lets one callable special-case the few argument combinations a test cares about and delegate everything else to a bland configured default. It is also why `DEFAULT` cannot be used as legitimate test data — returning it always means "defer".
  • Both return_value and side_effect are set on a mock. Which one decides the call?
    `side_effect` — it is consulted first. An exception is raised, an iterable supplies the next item, and a callable's return value is used. `return_value` is reached only when `side_effect` is `None`, or when a callable `side_effect` returns `unittest.mock.DEFAULT`. Assigning `side_effect = None` restores plain `return_value` behaviour.
  • Does a mock check that a callable side_effect matches the call it received?
    A bare `Mock` performs no signature checking: the arguments are forwarded verbatim, so a mismatch surfaces as a `TypeError` raised inside your own callable rather than as a mock-level failure. Signature enforcement comes from building the double against a spec, which is a separate concern from scripting what it does.

saying these in an interview costs you the question

  • Thinks side_effect only means raising an exception
  • Expects a list side_effect to repeat after exhaustion
  • Believes return_value overrides a set side_effect
  • Misses that exceptions inside an iterable are raised
  • Returns DEFAULT expecting the sentinel object itself
  • Assumes the callable receives no call arguments

context