Monkey Patching in Python: What It Is and When to Use It
Learn what monkey patching changes at runtime, when it helps in tests, how to scope and restore patches, and when to use dependency injection instead.
Monkey patching changes an object, class, module, or name binding while a Python program is running, without editing the original source definition. It is most useful in tests when you need to replace an external dependency—such as an API call, environment value, or filesystem behavior—with controlled behavior. Keep the patch narrow, patch the name the code actually looks up, and make sure it is restored afterward.
Monkey patching is a technique, not a Python keyword or a single library. pytest’s monkeypatch fixture and the standard library’s unittest.mock.patch are two tools for temporary changes. For code you control in production, explicit dependencies are usually easier to understand and maintain than hidden global patches.
What monkey patching means
At runtime, Python lets code assign to attributes and names. Replacing a method on a class, setting an attribute on a module, or rebinding a name in a module can change what later code does. The original source file does not need to change. This can be helpful for tests and risky when the change leaks beyond its intended scope.
There are two related but distinct ideas:
- Monkey patching is the broad technique of changing runtime behavior.
monkeypatchandpatchare utilities that make particular changes and, when used in their supported scopes, restore the previous state.
When to use it
Use a temporary patch when a test needs to control a dependency or isolate a behavior that would otherwise be slow, nondeterministic, or external. Common examples include replacing an API call, setting an environment variable, changing a mapping, redirecting a module attribute, or controlling the current working directory.
- Prevent a unit test from making a real network request.
- Give a function a known environment setting.
- Replace a function imported into the module under test.
- Use a mock when the test must assert that a dependency was called with particular arguments.
For new code you own, consider passing the dependency into the function or object explicitly. That makes the dependency visible and avoids changing shared global state.
Choose pytest monkeypatch or unittest.mock.patch
| Need | Use | Why |
|---|---|---|
| Set an attribute, mapping entry, environment variable, import path, or working directory and undo it after a test | pytest monkeypatch |
The fixture offers focused helpers and automatically undoes its changes at test teardown. |
| Replace a target with a mock and inspect calls or arguments | unittest.mock.patch |
It can create a mock and scope the replacement to a context manager or decorator. |
| Limit an unusual or risky change to a small section | monkeypatch.context() or patch() as a context manager |
The limited scope makes cleanup predictable. |
These tools are not competing definitions of monkey patching. Both can temporarily change a binding. Choose the one that matches the operation and whether you need mock call assertions.
Use pytest monkeypatch for environment and function changes
Install pytest in your project environment with python -m pip install pytest. The following example uses a project module named settings.py and a test file named test_settings.py.
# settings.py
import os
def service_region():
return os.environ.get("SERVICE_REGION", "default")
# test_settings.py
from settings import service_region
def test_service_region_uses_test_value(monkeypatch):
monkeypatch.setenv("SERVICE_REGION", "test-region")
assert service_region() == "test-region"
pytest supplies the monkeypatch fixture automatically. It restores the environment variable after the test, including restoring its previous value if it existed. Run the test with python -m pytest -q.
You can also use the fixture to set or delete attributes and mapping entries, change the current directory, and prepend to sys.path. For example:
def test_config_mapping(monkeypatch):
config = {"endpoint": "https://real.example"}
monkeypatch.setitem(config, "endpoint", "https://test.example")
assert config["endpoint"] == "https://test.example"
def test_missing_environment_value(monkeypatch):
monkeypatch.delenv("OPTIONAL_TOKEN", raising=False)
# Call code that should behave as if OPTIONAL_TOKEN is absent.
The raising=False option permits deletion when the name may not exist; without it, a missing target normally raises an error. Use the default strict behavior when a missing target indicates a test setup mistake.
Patch the name the tested code looks up
Python imports can create another name bound to the same function. Patching the original module does not necessarily replace that separate imported name. Patch the lookup site used by the code under test.
# report.py
from os import getcwd
def current_report_directory():
return getcwd()
# test_report.py
import report
def test_current_report_directory(monkeypatch):
monkeypatch.setattr(report, "getcwd", lambda: "/tmp/reports")
assert report.current_report_directory() == "/tmp/reports"
Here, report.current_report_directory resolves report.getcwd, so that is the name to replace. If the module instead used import os and called os.getcwd(), patch report.os.getcwd. This lookup-site rule also applies to unittest.mock.patch.
Use unittest.mock.patch when call assertions matter
patch() can temporarily replace an attribute with a mock. The mock records calls so a test can check how the code interacted with the dependency.
# billing.py
import gateway
def charge(customer_id, amount):
return gateway.submit(customer_id, amount)
# test_billing.py
from unittest.mock import patch
import billing
def test_charge_submits_expected_values():
with patch("billing.gateway.submit", autospec=True, return_value="accepted") as submit:
result = billing.charge("cust-42", 1250)
assert result == "accepted"
submit.assert_called_once_with("cust-42", 1250)
Use autospec=True or a suitable spec when it fits the target interface. A permissive mock can keep accepting calls even after the real interface changes, so keep appropriate integration coverage for connections between components.
Keep patches scoped and reversible
- Identify the exact name or state the tested code reads.
- Apply the patch in the test or a small context manager.
- Run the behavior under test and make assertions about results or interactions.
- Allow the fixture or context manager to restore the prior state automatically.
pytest’s fixture undoes its changes during teardown. For a narrow patch inside a test, use monkeypatch.context():
def test_small_patch_scope(monkeypatch):
import report
original = report.getcwd
with monkeypatch.context() as patch:
patch.setattr(report, "getcwd", lambda: "/tmp/one-test")
assert report.current_report_directory() == "/tmp/one-test"
assert report.getcwd is original
With unittest.mock.patch, a with block or decorator similarly bounds the replacement. Avoid manual assignments that are not protected by cleanup logic: if an assertion or exception interrupts the test, shared state can remain altered.
When not to monkey patch
- Long-lived production behavior: a patch applied at import time can silently affect unrelated callers and make behavior depend on import order.
- Code you can redesign: pass a client, clock, storage object, or other dependency explicitly so callers and tests can choose the implementation.
- Changes to builtins: patching
open,compile, or other builtins can disrupt pytest itself or libraries used by the test runner. If unavoidable, tightly scope the change. - Mocks that stand in for an entire integration: mocks can miss interface drift. Use a suitable spec and retain tests that exercise real component connections where needed.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The real function still runs | The patch changed the defining module, while the tested code uses an imported alias or another lookup path. | Patch the name in the module under test, such as mymodule.getcwd. |
| A patch leaks into another test | State was changed manually or at module scope, outside fixture teardown or a context manager. | Use the pytest fixture or patch() context manager and keep the scope local. |
| pytest breaks in a surprising way | A builtin or a function used by pytest or a plugin was replaced too broadly. | Prefer patching an application-owned lookup site; if necessary, use a very small context and restore immediately. |
AttributeError or missing-target error during patch setup |
The target name is misspelled, absent, or looked up in the wrong namespace. | Inspect the module used by the code under test and correct the target. Use non-strict deletion only when absence is expected. |
| A mock accepts an invalid call | The mock is too flexible and does not constrain the real interface. | Use autospec or spec where suitable, and add integration coverage for the boundary. |
Performance, reliability, and cost
A patch changes in-memory Python state; the main engineering costs are test isolation and maintenance, rather than an external service charge. Keep patches local so test order, concurrency, and imports cannot create hidden dependencies. Tests that alter process-wide environment, working directory, module attributes, or import paths need particular care if they run concurrently in the same process.
For repeatable tests, set only the state required by the case, use automatic restoration, avoid broad patches to test-runner dependencies, and retain integration tests where mocks would hide mismatches. For durable application customization, explicit dependency injection usually makes reliability easier to reason about.
Or skip the browser setup
If a test or workflow needs a website screenshot, you can capture one with a single GET request instead of managing a browser. See the ScreenshotNeo API docs for the request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.
Sign up free for 1,000 screenshots a month, no card required.
Frequently asked questions
Is monkey patching a Python language feature?
No. It describes a runtime technique. pytest and unittest.mock provide tools for performing controlled temporary changes.
Is monkeypatching the same as mocking?
No. Mocking is one use of runtime replacement, often with an object that records interactions. Monkey patching also includes other changes, such as setting an environment variable or replacing an attribute without a mock.
Can I use pytest monkeypatch and unittest.mock.patch in one project?
Yes. Use the fixture for its convenient state changes and use patch() when a mock and its call assertions are useful. Keep each change scoped and reversible.


