Python's unittest.mock module (usable from pytest too, no separate library required) is built around Mock and MagicMock, plus a patch mechanism for swapping out real objects during a test.
from unittest.mock import Mock, MagicMock
mock_client = Mock()
mock_client.get_user.return_value = {"id": 1, "name": "Ana"}
result = mock_client.get_user(1)
assert result == {"id": 1, "name": "Ana"}
mock_client.get_user.assert_called_once_with(1)
Mock auto-creates any attribute or method you access on it, returning another Mock by default, and records every call for later assertions (assert_called_once_with, call_count, call_args). MagicMock is the same thing with Python's "magic methods" (__len__, __iter__, __enter__, and so on) also pre-configured, which matters the moment you need to mock something used with len(), in a for loop, or as a context manager — a plain Mock doesn't support those out of the box.
monkeypatchpytest's built-in monkeypatch fixture swaps out an attribute, environment variable, or dictionary entry for the duration of one test, and automatically reverts it afterward — no manual save-and-restore required:
def get_api_key():
import os
return os.environ["API_KEY"]
def test_get_api_key(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-123")
assert get_api_key() == "test-key-123"
# API_KEY is automatically restored to whatever it was before this test, once it ends
def test_disable_network_call(monkeypatch):
def fake_get(url):
return Mock(status_code=200, json=lambda: {"ok": True})
monkeypatch.setattr("requests.get", fake_get)
result = fetch_status("https://example.com")
assert result["ok"] is True
The automatic cleanup is the whole point: hand-rolled patching (os.environ["API_KEY"] = "..." and manually resetting it in a finally) is easy to get wrong, especially the moment a test fails partway through and skips its own cleanup. monkeypatch guarantees the revert happens regardless of how the test ends.
This is one of the most common real mistakes anyone runs into with Python mocking, and it trips up experienced developers just as often as beginners. When module service.py does from utils import fetch_data and later calls fetch_data(), patching utils.fetch_data does nothing for service.py — because service.py already has its own local name, fetch_data, bound to the original function at import time. Patching the name inside utils doesn't touch the separate copy of that binding sitting in service's own namespace.
# utils.py
def fetch_data():
return "real data"
# service.py
from utils import fetch_data # service.fetch_data is now its OWN name, bound at import time
def get_report():
return fetch_data()
from unittest.mock import patch
# WRONG — patches utils.fetch_data, but service.py never looks there again after import
def test_get_report_wrong():
with patch("utils.fetch_data", return_value="fake"):
assert get_report() == "fake" # FAILS — still returns "real data"
# RIGHT — patch the name where it's actually looked up: inside the service module
def test_get_report_correct():
with patch("service.fetch_data", return_value="fake"):
assert get_report() == "fake" # passes
The rule to remember: patch the name where it's used, not where it's defined. If service.py imported the function by name (from utils import fetch_data), patch service.fetch_data. If instead it had done import utils and called utils.fetch_data() at the call site, patching utils.fetch_data would work fine, because in that case service.py looks the name up on the utils module fresh every time, rather than holding its own separate copy of the binding.