monkeypatch is your secret weapon for testing code that interacts with the outside world or has hard-to-control dependencies. It lets you temporarily swap out functions, methods, or attributes with your own fakes, making your tests fast, deterministic, and focused.
Let’s say you have a module my_module.py with a function that fetches data from an external API:
# my_module.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
And you want to test a function that uses this, process_user_info:
# main.py
from my_module import get_user_data
def process_user_info(user_id):
data = get_user_data(user_id)
return f"User {user_id}: {data.get('name', 'N/A')}"
You don’t want to actually hit api.example.com during tests. This is where monkeypatch shines.
Here’s a test using monkeypatch:
# test_main.py
from main import process_user_info
def test_process_user_info(monkeypatch):
# Define a fake response for requests.get
class MockResponse:
def json(self):
return {"name": "Alice"}
def raise_for_status(self):
pass # Simulate a successful response
# Patch requests.get to return our mock response
monkeypatch.setattr("my_module.requests.get", lambda url, **kwargs: MockResponse())
# Now call the function that uses get_user_data
result = process_user_info(123)
assert result == "User 123: Alice"
This test works because monkeypatch.setattr("my_module.requests.get", ...) replaces the actual requests.get function within the my_module’s scope with our lambda that returns a MockResponse. When get_user_data is called inside process_user_info, it’s calling our fake requests.get, not the real one.
You can also patch attributes on objects. Imagine my_module.py has a class with a configuration attribute:
# my_module.py
class Settings:
API_KEY = "real_api_key_123"
def fetch_with_key():
# Uses Settings.API_KEY
return f"Fetching with key: {Settings.API_KEY}"
To test fetch_with_key without relying on the actual Settings.API_KEY:
# test_main.py
from main import fetch_with_key
def test_fetch_with_key(monkeypatch):
# Patch the API_KEY attribute on the Settings class
monkeypatch.setattr("my_module.Settings.API_KEY", "fake_key_456")
result = fetch_with_key()
assert result == "Fetching with key: fake_key_456"
Here, monkeypatch.setattr("my_module.Settings.API_KEY", "fake_key_456") directly replaces the API_KEY class attribute of Settings within the my_module’s context for the duration of the test.
The most surprising true thing about monkeypatch is that it doesn’t just replace things; it temporarily replaces them. Once the test function finishes, the original function or attribute is restored automatically. This is crucial because it prevents your test environment from being polluted by patches intended for a single test. The monkeypatch fixture manages this lifecycle for you.
Let’s say you have a class ExternalService in services.py and you want to test a method in app.py that uses it.
# services.py
class ExternalService:
def __init__(self, url):
self.url = url
def get_data(self, item_id):
print(f"Calling real external service at {self.url} for {item_id}")
# ... actual network call ...
return {"id": item_id, "value": "real_data"}
# app.py
from services import ExternalService
class App:
def __init__(self):
self.service = ExternalService("http://prod.service.com")
def process_item(self, item_id):
data = self.service.get_data(item_id)
return f"Processed {item_id}: {data['value'].upper()}"
To test App.process_item:
# test_app.py
from app import App
class MockExternalService:
def __init__(self, url):
# We can even ignore the URL passed during instantiation for testing
pass
def get_data(self, item_id):
print(f"Mock service returning data for {item_id}")
return {"id": item_id, "value": f"mock_value_{item_id}"}
def test_process_item(monkeypatch):
# Patch the ExternalService class itself *before* it's instantiated in App
monkeypatch.setattr("app.ExternalService", MockExternalService)
app_instance = App() # This will now instantiate MockExternalService
result = app_instance.process_item(456)
assert result == "Processed 456: MOCK_VALUE_456"
In this scenario, monkeypatch.setattr("app.ExternalService", MockExternalService) replaces the ExternalService class within the app module’s namespace. When App() is called, it imports and uses MockExternalService instead of the original ExternalService. This is powerful because it affects how the dependency is created within the code under test.
You can also use monkeypatch to temporarily modify the environment or delete items.
import os
def read_config_value(key):
return os.environ.get(key, "default")
def test_read_config_value(monkeypatch):
# Ensure the environment variable is not set for this test
monkeypatch.delenv("MY_CONFIG_KEY", raising=False)
assert read_config_value("MY_CONFIG_KEY") == "default"
# Set a new environment variable for the test
monkeypatch.setenv("MY_CONFIG_KEY", "test_value")
assert read_config_value("MY_CONFIG_KEY") == "test_value"
# Unset it explicitly (though monkeypatch does this automatically after the test)
monkeypatch.delenv("MY_CONFIG_KEY")
assert read_config_value("MY_CONFIG_KEY") == "default"
The monkeypatch fixture has three primary methods: setattr, setitem, and delitem, along with setenv and delenv for environment variables.
monkeypatch.setattr(target, value, raising=True): Replaces an attribute on an object or module.targetis a string like"module.ClassName.attribute_name"or"module.function_name".valueis what you’re replacing it with.monkeypatch.setitem(target, key, value): Replaces an item in a dictionary or list-like object.targetis a string like"module.dictionary_name".monkeypatch.delitem(target, key, raising=True): Deletes an item.monkeypatch.delenv(name, raising=True): Deletes an environment variable.monkeypatch.setenv(name, value, prepend=None): Sets an environment variable.
When patching a function or method, you often pass a lambda or a standalone function that mimics the signature and return type of the original. For classes, you can pass a mock class. The key is to target the name where it is looked up, not necessarily where it is defined. In app.py, ExternalService is looked up as app.ExternalService, so we patch it there. If app.py had from services import ExternalService as ES, and then used ES, we’d patch app.ES.
The one thing most people don’t realize is the precise string you pass to setattr matters greatly. It’s the fully qualified name of the object as seen by the module where the lookup occurs. If module_a.py imports func_b from module_b.py as from module_b import func_b, and module_a.py calls func_b(), you need to patch module_a.func_b. If module_a.py instead imported the whole module as import module_b and called module_b.func_b(), you would patch module_b.func_b. This distinction is critical and often the source of "it’s not patching!" confusion.
The next challenge you’ll face is when you need to patch multiple things in a single test, and their lifetimes or dependencies become complex.