A Python mock is a stand-in object you drop into a test so it never touches the real API (Application Programming Interface), database, or email server. This post walks through the patterns that make unittest.mock click: the @patch decorator, MagicMock, side_effect, return_value, and the one rule about WHERE to patch that trips up almost everyone.
“A test that talks to a database is not a unit test.”
Michael Feathers, Working Effectively with Legacy Code
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Advanced | Reading Time: 17 minutes
Here is the problem. Your function calls an external weather API. Your class reads from a database. Your method fires off a welcome email. You want to test the logic around those calls, but you do not want a real test run to hit a live server, charge a real account, or fail at midnight just because someone unplugged the database. So how do you test code that depends on things you cannot control?
You swap the real thing for a fake you do control. That fake is a mock. It looks like the real dependency from the outside, returns whatever data you tell it to, and quietly records how your code used it so you can check the call afterwards.
Think of a stunt double in a movie. The lead actor is expensive and you do not want them jumping off a real building for every take. So you bring in a stunt double who looks the same on camera and does exactly the move the director needs. A mock is the stunt double for your dependencies: same shape on the outside, fully scripted on the inside, no real risk.
Good news on the setup front: unittest.mock ships with Python, so there is nothing to install, and it plugs straight into pytest. Everything below was run on Python 3.14.6 with pytest 9.1.1.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Read the diagram top to bottom. Your test code is in charge. The function you are testing (the System Under Test) would normally reach out to a real dependency, an API, a database, or the file system. During the test you cut that wire and point it at a mock instead. The mock hands back data you chose, and afterwards your test asks the mock: were you called, and with the right arguments? That swap is the whole game, and it is what keeps unit tests fast and steady.
They do not break because a server is down or a file got deleted. The right side of the diagram also names the four common kinds of test double (stub, mock, spy, fake); we will meet a few of them in the examples below.
Table of Contents
Your First Python Mock
Start with the painful version. Here is a tiny module that calls a real weather API and then builds a friendly report from the temperature it gets back. The weather_report function is the part we actually want to test. The trouble is that it calls get_temperature, which fires a live HTTP (HyperText Transfer Protocol) request.
📄 weather_service.py: a function that calls an external API
import requests
def get_temperature(city: str) -> float:
"""Fetch current temperature from weather API."""
response = requests.get(
f"https://api.weather.example.com/current?city={city}"
)
response.raise_for_status()
data = response.json()
return data["temperature"]
def weather_report(city: str) -> str:
"""Generate a human-readable weather report."""
temp = get_temperature(city)
if temp > 30:
return f"{city}: {temp}C, hot day, stay hydrated."
elif temp > 15:
return f"{city}: {temp}C, pleasant weather."
else:
return f"{city}: {temp}C, bundle up, it is cold."
We do not want our test to depend on a live server. So we patch get_temperature with a mock and feed it whatever temperature we want. Then we check that weather_report picks the right message and that it called the dependency exactly once with the right city.
📄 test_weather.py: mocking the API call
from unittest.mock import patch
from weather_service import weather_report
@patch("weather_service.get_temperature")
def test_hot_weather_report(mock_get_temp):
mock_get_temp.return_value = 35.0
result = weather_report("Mumbai")
assert result == "Mumbai: 35.0C, hot day, stay hydrated."
mock_get_temp.assert_called_once_with("Mumbai")
@patch("weather_service.get_temperature")
def test_cold_weather_report(mock_get_temp):
mock_get_temp.return_value = 5.0
result = weather_report("Shimla")
assert result == "Shimla: 5.0C, bundle up, it is cold."
mock_get_temp.assert_called_once_with("Shimla")
▶ Output: pytest -v test_weather.py
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0 collected 2 items test_weather.py::test_hot_weather_report PASSED [ 50%] test_weather.py::test_cold_weather_report PASSED [100%] ============================== 2 passed in 0.32s ==============================
What happened here: @patch("weather_service.get_temperature") swapped out get_temperature for a MagicMock for the duration of the test, and passed that mock in as the mock_get_temp argument. We set mock_get_temp.return_value = 35.0, so the moment weather_report calls get_temperature("Mumbai"), it gets 35.0 back. No HTTP request, no network, no waiting. The first assert checks the message our logic built; assert_called_once_with("Mumbai") checks that the dependency was used correctly, exactly once, with the right city. When the test finishes, @patch puts the real function back automatically.
The Golden Rule: Patch Where It Is Used, Not Where It Is Defined
This is the one that burns everybody once. When you write from requests import get inside my_module.py, Python makes a fresh label called get that lives in my_module and points at the same function. After that, my_module never looks at requests.get again; it uses its own label.
Picture saving the number of your friend Vinay in your phone. You copy it from a shared office sheet into your contacts under the name “Vinay”. Later the office sheet gets a new number typed in. Your phone still dials the old one, because your contact is a separate copy of the label, not a live link to the sheet. Patching requests.get while your module calls its own get is the same mistake: you edited the sheet, but the code is dialing from its own contacts.
📄 my_module.py: it imports get into its own namespace
from requests import get # makes a 'get' label inside my_module
def fetch_data(url):
return get(url).json()
📄 test_patch_where.py: wrong target vs right target
from unittest.mock import patch
from my_module import fetch_data
# WRONG: patching where the name is DEFINED (requests.get)
@patch("requests.get")
def test_bad_patch_target(mock_get):
# my_module bound its own name 'get' at import time, so patching
# requests.get never touches the copy my_module is actually calling.
try:
fetch_data("http://example.com")
except Exception:
pass # a real call may have happened; we only care about the mock
assert mock_get.called is False
print("BAD -> was the mock used?", mock_get.called)
# RIGHT: patching where the name is USED (my_module.get)
@patch("my_module.get")
def test_good_patch_target(mock_get):
mock_get.return_value.json.return_value = {"data": "mocked"}
result = fetch_data("http://example.com")
print("GOOD -> was the mock used?", mock_get.called)
print("GOOD -> result:", result)
assert result == {"data": "mocked"}
▶ Output: pytest -v -s test_patch_where.py
test_patch_where.py::test_bad_patch_target BAD -> was the mock used? False
PASSED
test_patch_where.py::test_good_patch_target GOOD -> was the mock used? True
GOOD -> result: {'data': 'mocked'}
PASSED
============================== 2 passed in 0.71s ==============================
What happened here: In the WRONG test, mock_get.called comes back False. That single line is the proof: our mock was never touched, so fetch_data went straight to the real network (which is exactly the slow, flaky thing we were trying to avoid). In the RIGHT test, we patched my_module.get, the label the code actually uses, so mock_get.called is True and we get our scripted {'data': 'mocked'} back. The rule in one line: patch the name in the module that calls it, not the module that first defined it.
side_effect: Simulating Errors and Sequences
return_value hands back the same thing every time. But real dependencies are messier than that. Sometimes the network is down. Sometimes the first call fails and a retry works. For those cases you reach for side_effect, which is the more flexible cousin of return_value.
side_effect accepts three useful shapes. Give it an exception and the mock raises it. Give it a list and the mock returns one item per call, in order, like dealing cards off the top of a deck. Give it a function and the mock runs that function with whatever arguments came in.
📄 test_side_effects.py: raise an error, then return a sequence
from unittest.mock import patch, MagicMock
import pytest
import requests
from weather_service import get_temperature
# 1. side_effect can RAISE an exception
@patch("weather_service.requests.get")
def test_api_error_handling(mock_get):
mock_get.side_effect = ConnectionError("Network unreachable")
with pytest.raises(ConnectionError):
get_temperature("Pune")
# 2. side_effect can be a LIST: one item per call, in order
@patch("weather_service.requests.get")
def test_retry_behavior(mock_get):
# First response fails when we check the status
failure = MagicMock()
failure.raise_for_status.side_effect = requests.HTTPError("500 Server Error")
# Second response succeeds and returns real-looking data
success = MagicMock()
success.raise_for_status.return_value = None
success.json.return_value = {"temperature": 22.5}
mock_get.side_effect = [failure, success]
# First attempt: the 500 turns into an HTTPError
with pytest.raises(requests.HTTPError):
get_temperature("Nashik")
# Second attempt: same code path, now it works
temp = get_temperature("Nashik")
assert temp == 22.5
assert mock_get.call_count == 2
print("Recovered on retry, temperature =", temp)
▶ Output: pytest -v -s test_side_effects.py
test_side_effects.py::test_api_error_handling PASSED test_side_effects.py::test_retry_behavior Recovered on retry, temperature = 22.5 PASSED ============================== 2 passed in 0.35s ==============================
What happened here: The first test sets side_effect to a ConnectionError, so the mocked requests.get raises it the moment get_temperature calls it. We wrap that call in pytest.raises to assert the error bubbles up. The second test sets side_effect to a list of two fake responses. Call one returns the failure response, whose raise_for_status throws an HTTPError (a simulated 500). Call two returns the success response with a clean status and real-looking JSON (JavaScript Object Notation), so get_temperature returns 22.5. The call_count == 2 check confirms both responses were used, which is exactly how you prove a retry actually retried.
patch.object: Mocking One Method on an Instance
Sometimes you do not want to replace a whole function by its dotted path. You already have an object in hand, and you just want to swap out one of its methods. That is what patch.object is for. You hand it the object and the method name as a string, and it does the swap for you inside a with block. Think of a substitute teacher covering one period: the rest of the school day runs exactly as scheduled, and the moment that period ends, the regular teacher is back.
📄 test_patch_object.py: mock one method on an instance
from unittest.mock import patch
class UserRepository:
def get_by_id(self, user_id: int) -> dict:
# In real code, this queries a database
raise NotImplementedError("Requires database connection")
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_display_name(self, user_id: int) -> str:
user = self.repo.get_by_id(user_id)
return f"{user['first_name']} {user['last_name']}"
def test_get_display_name():
repo = UserRepository()
with patch.object(repo, "get_by_id") as mock_get:
mock_get.return_value = {
"first_name": "Niranjan",
"last_name": "Raut",
}
service = UserService(repo)
name = service.get_display_name(42)
assert name == "Niranjan Raut"
mock_get.assert_called_once_with(42)
print("display name =", name)
▶ Output: pytest -v -s test_patch_object.py
test_patch_object.py::test_get_display_name display name = Niranjan Raut PASSED ============================== 1 passed in 0.05s ==============================
What happened here: The real get_by_id would hit a database, so left alone it raises NotImplementedError. Inside the with patch.object(repo, "get_by_id") block, that one method on that one repo object becomes a mock that returns the record of a user named Niranjan Raut. UserService never knows the difference; it just gets a dictionary and builds the display name. The moment the with block ends, the real method snaps back. Notice we tested real logic (the f-string that joins first and last name) while mocking only the boundary (the database call). That is the sweet spot.
The Spy Pattern: Watching a Real Function
A plain mock replaces the real behaviour. A spy keeps the real behaviour and just watches. You pass wraps=the_real_function to patch, and now every call runs the genuine code AND gets recorded, so you can still ask “was this called, and with what?” afterwards.
It is like the dashcam in a car. The car still drives exactly as it normally would; the camera just sits there recording so you can replay what happened. A spy lets the real function do its real job while quietly keeping the receipts.
This one is a small standalone script rather than a pytest file, because the spy patches __main__.process_order, the name as it lives in the script you are running. Run it with py -3.14 spy_demo.py.
📄 spy_demo.py: keep the real behaviour, record the call
from unittest.mock import patch
def process_order(items):
total = sum(item["price"] for item in items)
if total > 100:
total *= 0.9 # 10 percent discount over 100
return round(total, 2)
# wraps=process_order means the mock calls the REAL function,
# but still records every call for us to inspect.
@patch("__main__.process_order", wraps=process_order)
def run(spy):
items = [{"price": 60}, {"price": 50}]
result = process_order(items)
assert result == 99.0 # 110 * 0.9 = 99.0 (real discount logic ran)
spy.assert_called_once_with(items)
print("result =", result)
print("call count =", spy.call_count)
run()
▶ Output: py -3.14 spy_demo.py
result = 99.0 call count = 1
What happened here: Because of wraps, the spy did not fake the answer. The real discount logic ran (110 minus 10 percent is 99.0), and the spy also recorded that it was called once with our items list. That is the difference from a normal mock: a mock would have returned whatever we told it to and skipped the real math entirely. Reach for a spy when you want the actual behaviour to happen but you also want to assert that it happened.
Do Not Do This
Mistake 1: setting up a mock but never checking it was called
This is the quiet one. Say a new user named Viraj signs up and process_signup is supposed to send him a welcome email. The test below is green, so you trust it. But it would stay green even if the email was never sent, because you never asserted the call happened. A mock that nobody checks is just decoration.
❌ Bad: no assertion, so the test proves nothing
from unittest.mock import MagicMock
from notifications import process_signup
# This passes even if process_signup forgets to send the email.
def test_sends_welcome_email():
fake_client = MagicMock()
process_signup(fake_client, "Viraj")
# Missing: fake_client.send.assert_called_once_with(...)
✅ Good: assert the interaction actually happened
from unittest.mock import MagicMock
from notifications import process_signup
def test_sends_welcome_email():
fake_client = MagicMock()
process_signup(fake_client, "Viraj")
fake_client.send.assert_called_once_with(
to="Viraj",
subject="Welcome!",
body="Hi Viraj, thanks for signing up.",
)
▶ Output: pytest -v test_mistakes.py (the good version)
test_mistakes.py::test_sends_welcome_email PASSED [100%] ============================== 1 passed in 0.05s ==============================
Why this matters: The bad version never calls assert_called_once_with, so it cannot tell the difference between “the email was sent correctly” and “the email was never sent at all”. The good version pins down exactly what should have happened: send was called once, with this recipient, subject, and body. Now the test fails the day someone breaks the signup flow, which is the entire point of having it.
Mistake 2: over-mocking, where you end up testing the mocks
The opposite trap. You mock validate, save, and notify, set return values for all of them, and the test goes green. But think about what it actually checked: nothing. Every interesting line was replaced by a mock that returns what you told it to. You wrote a test that tests your own test setup.
❌ Bad: everything mocked, no real logic left to test
@patch("my_module.notify")
@patch("my_module.save")
@patch("my_module.validate")
def test_over_mocked(mock_validate, mock_save, mock_notify):
mock_validate.return_value = True
mock_save.return_value = {"id": 1}
# This only proves the mocks return what you set. The real
# validate / save / notify logic never ran.
Why this matters: A useful test runs your real logic and mocks only the boundaries it touches, the API, the database, the email server. If you find yourself mocking almost everything just to get a function under test, that is usually the function telling you it does too much. Split it up, and the tests get easy.
Where You See This in Real Code
These are not academic patterns. They show up constantly in working test suites:
- Payment code: teams patch the Stripe or Razorpay client so tests never charge a real card. A mock returns a fake “payment succeeded” response, and the test checks that the order was marked paid.
- Anything time based: patching
datetimein the module that uses it (or a smallclock()helper) lets you pin “today” to a fixed date so a test for “is this subscription expired” gives the same answer every run. You cannot patchdatetime.datetime.nowdirectly, because attributes of built-in C types are read-only, which is one more reason the patch-where-it-is-used rule pays off. - Email and notifications: patch the SMTP (Simple Mail Transfer Protocol) or push client, then assert the right message went to the right person, exactly the welcome-email test from earlier.
- Third party APIs: patch
requests.getor anhttpxcall so the suite does not depend on someone else’s server being up, and so you can simulate their 500 errors on demand withside_effect.
One honest caveat: if you only ever need a canned return value and never check how it was called, the lighter-weight tool is pytest‘s own monkeypatch fixture. Reach for unittest.mock when you also want the call records, which is most of the time once your code talks to the outside world.
Wrapping Up
You now have the working Python mock toolkit: @patch to swap a dependency, return_value for the happy path, side_effect for errors and sequences, patch.object for a single method on a live object, and wraps when you want a spy that records without faking. Above everything sits the golden rule: patch where the name is used, not where it is defined, and always assert that the mock was actually called. Next in the series we step out of testing and into project hygiene with virtual environments and dependency management. And if you want to jump to any other topic, browse the full Python + AI/ML tutorial series home.
Frequently Asked Questions
What is the difference between a mock and a stub in Python?
A stub returns fixed data and does not verify calls. A mock returns data AND verifies that it was called with specific arguments. In Python’s unittest.mock, MagicMock can act as both: it returns what you set, and it provides assert_called_* methods for verification.
Where should I patch in Python, where the object is defined or where it is used?
Always patch where it is USED, not where it is defined. If my_module.py does from requests import get, patch my_module.get, not requests.get. The import creates a new reference inside my_module’s namespace, and that is the name the code actually calls.
What is MagicMock in Python?
MagicMock is a subclass of Mock that automatically supports magic methods like __str__, __len__, and __iter__. It is the default mock type used by @patch. You set return_value and side_effect to control what it does.
When should I use side_effect vs return_value in a Python mock?
Use return_value when the mock should return the same value every time. Use side_effect when you need to raise an exception, return different values on sequential calls (pass a list), or run custom logic (pass a function).
How do I avoid over-mocking in Python tests?
Only mock external boundaries such as APIs, databases, file systems, and network calls. Do not mock your own code’s internal logic. If you need to mock almost everything to test a function, the function probably has too many responsibilities and should be split up.
Try It Yourself
Build a NotificationService that takes two dependencies in its constructor: an EmailClient (with a send method) and a Logger (with an info method). When a customer named Prathamesh places an order and you call service.notify("Prathamesh", "Order shipped"), it should send the email and then log a line. Now write the tests:
- Pass in a
MagicMockfor each dependency. - Assert the email client’s
sendwas called once with the right recipient and message. - Assert the logger’s
infowas called. - Bonus: set
email_client.send.side_effect = ConnectionErrorand write a test that the service logs an error instead of crashing.
If you can do the bonus test, you have understood the whole post: mock the boundary, force it to fail, and prove your real code handles the failure gracefully.
Interview Questions on Python Mocking
Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.
Q: Your test suite is fast on your machine but hangs for two minutes in CI, and you suspect one test is quietly hitting a live API. How do you track it down and fix it?
The usual culprit is a patch aimed at the wrong target: patching where the function is defined instead of where it is used leaves the real call in place while the test stays green. Check each network-facing test with mock.called or assert_called_once; a mock that was never touched means the real dependency ran. A plugin like pytest-socket can block all real socket access so any escaped call fails loudly instead of hanging. Then fix the patch target to the namespace of the module under test.
Q: Why do experienced teams insist on autospec=True when patching?
A bare MagicMock accepts any attribute access and any call signature, so a typo like mock.sendd(...) or a call with the wrong arguments passes silently. With autospec=True, the mock is built from the real object’s API: touching a nonexistent attribute or calling with a wrong signature raises immediately. That keeps your tests honest when the real interface changes during a refactor, because the mocks break along with the code instead of lying to you.
Q: Your teammate Aditi renames a keyword argument from send(to=...) to send(recipient=...) without changing behaviour, and twenty tests using assert_called_once_with break. What does that tell you, and what would you do?
It tells you the tests are tightly coupled to the call signature, which is an implementation detail. Some coupling is the whole point of a mock-based test, but when one harmless rename breaks twenty tests, the expectation is duplicated everywhere. Move the assertion into one shared helper or fixture so the signature lives in a single place, and where the exact arguments do not matter, assert only the ones that do. For richer dependencies, consider a fake with real state instead of asserting every call.
Q: How do you mock reading a file opened with open()?
Use the mock_open helper from unittest.mock: patch("my_module.open", mock_open(read_data="line1\nline2")) gives you a mock that supports the context manager protocol, so with open(...) as f works and returns your canned data. As always, patch it in the namespace of the module under test. If the file handling is complex, writing a real temp file with pytest’s tmp_path fixture is often simpler and less brittle than mocking.
Q: With multiple stacked @patch decorators, in what order do the mock arguments arrive?
Bottom-up: the decorator closest to the function is applied first and supplies the first mock parameter. So @patch("m.a") stacked above @patch("m.b") gives you def test(mock_b, mock_a). Getting the order backwards is a classic silent bug, because both parameters are still mocks and the test can keep passing while asserting against the wrong one.
Q: When would you choose a fake, like an in-memory repository, over a MagicMock?
When the test cares about behaviour across multiple calls: for example, save(user) followed by get_by_id(user.id) should return what was just saved. A fake keeps consistent internal state, so that flow just works, while a MagicMock would force you to script every individual interaction. Mocks shine when you verify a single interaction at a boundary; fakes shine when the test needs the dependency to behave realistically.
Go deeper: the official Python documentation covers every edge case of this topic.
Related Posts
Previous: Python Test Driven Development: Red-Green-Refactor Workflow
Next: Python: Type Hints, Annotations, Union Types (X | None), Generics
Series Home: Python + AI/ML Tutorial Series

No comment