Monkey Patching#
Monkey patching means changing or extending behavior at runtime by replacing attributes on modules, classes, or instances — after the original code was defined. Tests use it to stub dependencies; libraries occasionally patch third-party bugs. It is powerful, fragile, and almost never belongs in production business logic.
How to use this page
Read after Modules and Packages and Unit Testing. Prefer Decorators or dependency injection when you control the source. Pair with Inspect Module for Introspection to verify patches.
At a glance
| Track | Python Advanced → Meta Programming |
| Sections | 11 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: What monkey patching is · Levels of patching · The canonical test use case · unittest.mock.patch patterns · Patching class methods and properties · Environment and import-time patching · Risks and why production code avoids patching · Thread safety and async · … (+3 more)
- What monkey patching is
- Levels of patching
- The canonical test use case
unittest.mock.patchpatterns- Patching class methods and properties
- Environment and import-time patching
- Risks and why production code avoids patching
- Thread safety and async
- Detecting and debugging patches
- Legitimate production examples (rare)
- Manual patch with guaranteed restore
What monkey patching is#
import math
_original_sqrt = math.sqrt
def safe_sqrt(x: float) -> float:
if x < 0:
return 0.0
return _original_sqrt(x)
math.sqrt = safe_sqrt
math.sqrt(4) # 2.0
math.sqrt(-1) # 0.0 — patched behavior
You mutated the namespace binding math.sqrt. Every subsequent math.sqrt(...) anywhere in the process sees the patch — including code that already imported sqrt:
Levels of patching#
| Level | Example | Scope |
|---|---|---|
| Module attribute | module.func = stub |
All importers of that module object |
| Class attribute | Cls.method = new_method |
All instances (existing and future) |
| Instance attribute | obj.method = new_method |
Single instance only |
| Builtin | builtins.len = ... |
Entire interpreter — never in prod |
class Greeter:
def greet(self) -> str:
return "hello"
g = Greeter()
def loud_greet(self) -> str:
return "HELLO"
Greeter.greet = loud_greet # class-level — affects g and new instances
g2 = Greeter()
g2.greet() # 'HELLO'
g3 = Greeter()
g3.greet = lambda self: "hi" # instance-level — only g3
The canonical test use case#
import datetime
def is_weekend() -> bool:
return datetime.datetime.today().weekday() >= 5
# test:
def test_is_weekend_monday(monkeypatch):
class FakeDate(datetime.datetime):
@classmethod
def today(cls):
return cls(2026, 7, 6) # Monday
monkeypatch.setattr(datetime, "datetime", FakeDate)
assert is_weekend() is False
pytest.MonkeyPatch (or unittest.mock.patch) saves originals and restores after the test — always patch with restoration.
from unittest.mock import patch
@patch("mymodule.datetime.datetime")
def test_with_mock(mock_dt):
mock_dt.today.return_value.weekday.return_value = 5
...
Patch where the name is used, not where it is defined
If mymodule.py does from datetime import datetime, patch mymodule.datetime, not datetime.datetime in the stdlib.
unittest.mock.patch patterns#
from unittest.mock import patch, MagicMock
# context manager — auto-restore on exit
with patch("requests.get") as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"ok": True}
# call code under test
# decorator
@patch("myapp.send_email")
def test_signup(mock_send):
mock_send.return_value = True
...
| Tool | Restores automatically? |
|---|---|
pytest.monkeypatch |
Yes, after test |
unittest.mock.patch |
Yes, on __exit__ / test end |
| Manual assignment | No — your responsibility |
Patching class methods and properties#
class API:
def fetch(self) -> dict:
return {"real": True}
def fake_fetch(self) -> dict:
return {"stub": True}
API.fetch = fake_fetch
Properties are trickier — you replace the descriptor on the class:
class Config:
@property
def timeout(self) -> int:
return 30
Config.timeout = property(lambda self: 5)
Existing instances and descriptors
Replacing a method on the class affects attribute lookup for instances that don't shadow the name. Instance __dict__ entries override class patches.
Environment and import-time patching#
import os
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
# Now import module that reads env at import time
Order matters: patch before the import if the module reads globals at import time. See Modules and Packages for import semantics.
Risks and why production code avoids patching#
Security and stability
Monkey patching is global mutable state. A patch in one library can break unrelated code in the same process. Malicious or careless patches to builtins, os, or subprocess are equivalent to code injection.
| Risk | Consequence |
|---|---|
| Global side effects | Heisenbugs across threads/async tasks |
| Upgrade fragility | Internal names change between versions |
| Hidden behavior | Readers can't trust source code alone |
| No type-checker support | Mypy sees original signatures |
| Async / threaded races | Mid-test patch leaks to parallel tests |
Prefer:
- Dependency injection (pass collaborators as arguments)
- Interfaces / protocols + test doubles
- Official extension points (hooks, plugins)
- Subclassing when the library supports it
Thread safety and async#
Patches are process-wide. In multi-threaded servers, a test patch without restoration can leak into request handlers. Use:
pytestwith isolated processes (pytest-xdistcareful configuration)unittest.mock.patchas context manager in scoped code- Never patch in module-level test fixtures without teardown
Detecting and debugging patches#
import math
import inspect
print(math.sqrt)
print(inspect.getfile(math.sqrt)) # May point to unexpected file if patched
# Compare with known original
assert math.sqrt is math.__dict__.get("sqrt") # Weak check
Inspect Module for Introspection helps audit what code is actually bound at runtime.
Legitimate production examples (rare)#
| Context | Example |
|---|---|
| Test suites | Stub I/O, time, randomness |
| Gevent/eventlet | Monkey-patches socket for cooperative I/O |
| Hotfix in locked deployment | Temporary patch until upstream fix ships |
Even gevent documents that patching is invasive — treat as framework infrastructure, not application pattern.
Manual patch with guaranteed restore#
from contextlib import contextmanager
@contextmanager
def patch_attr(obj, name, value):
original = getattr(obj, name)
setattr(obj, name, value)
try:
yield
finally:
setattr(obj, name, original)
with patch_attr(math, "sqrt", safe_sqrt):
math.sqrt(-1)
# math.sqrt restored here
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
from mod import func then patch mod.func |
Call site still uses old binding | Patch name in namespace where used |
| No teardown | Pollutes other tests / requests | patch, monkeypatch, or try/finally |
| Patching production for features | Unmaintainable globals | DI, hooks, subclass |
Patching builtins |
Breaks entire runtime | Forbidden outside isolated experiments |
| Instance vs class patch confusion | "Why didn't other instances change?" | Know MRO / instance dict precedence |
Mental model checklist#
- Why does
from math import sqrtstill see a patchedmath.sqrt? - Where should you apply a patch when the module under test uses
from x import y? - What is the scope difference between class-level and instance-level patches?
- Why is monkey patching especially dangerous in multi-threaded apps?
- What are three alternatives to monkey patching in application design?
What's next#
| Topic | Page |
|---|---|
| Unit testing | Unit Testing |
| Modules / imports | Modules and Packages |
| Decorators (compile-time wrapping) | Decorators |
| exec / eval (stronger danger) | Code Generations using exec() and eval() |
| Security practices | Managing Secrets |