The first time someone told me Python lets you modify a class or module after it’s already been defined — even classes from libraries you didn’t write — I assumed they were exaggerating. They weren’t. Monkey patching is real, it’s built into how dynamic Python’s object model is, and I’ve used it both to save myself from disaster during a production incident and to shoot myself in the foot during a debugging session that took way longer than it should have. This guide covers what monkey patching actually is, how it works under the hood, when it’s a legitimate tool, and when it’s a trap.
What Monkey Patching Actually Means
Monkey patching is modifying or extending code — a class, a module, or an object — at runtime, without touching its original source. Because Python classes and modules are just objects with mutable namespaces (usually backed by a __dict__), you can reach in and change them after the fact.
class Greeter:
def hello(self):
return "Hello"
def new_hello(self):
return "Hola"
Greeter.hello = new_hello
g = Greeter()
print(g.hello()) # Hola
Nothing in Greeter‘s original definition changed. I reassigned the hello name inside Greeter‘s namespace to point at a different function object. Every existing and future instance of Greeter now uses the new behavior, because method lookup happens dynamically at call time, not baked in at class-definition time.
Why This Works: Python’s Object Model
This is possible because classes in Python are, themselves, instances of type (or a metaclass), and their attributes live in a mutable mapping. There’s no compile-time binding that locks Greeter.hello to a specific function forever — Greeter.hello is just a dictionary lookup at attribute-access time. The same applies to modules: sys.modules['some_module'] is a live object you can mutate.
import math
original_sqrt = math.sqrt
def patched_sqrt(x):
print(f"Computing sqrt of {x}")
return original_sqrt(x)
math.sqrt = patched_sqrt
print(math.sqrt(16))
# Computing sqrt of 16
# 4.0
Every piece of code in the process that does import math; math.sqrt(...) after this patch runs the patched version — because math is a single shared module object cached in sys.modules, not re-imported fresh each time.
Legitimate Uses
1. Testing — unittest.mock.patch
By far the most common and defensible use of monkey patching is in tests, where you temporarily replace a dependency (an API call, a database write, a file operation) with a controlled fake:
from unittest.mock import patch
import requests
def get_user(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
def test_get_user():
with patch("requests.get") as mock_get:
mock_get.return_value.json.return_value = {"id": 1, "name": "Test User"}
result = get_user(1)
assert result["name"] == "Test User"
unittest.mock.patch handles the patch-and-restore cycle safely — it automatically undoes the change when the with block exits, even if the test raises an exception, which is exactly the discipline you want when mutating shared global state.
2. Fixing or Working Around Third-Party Bugs
If a library has a bug and you can’t wait for an upstream fix, monkey patching lets you override the specific broken method without forking the whole package:
import some_library
def fixed_method(self, *args, **kwargs):
# corrected implementation
return self._value * 2
some_library.SomeClass.buggy_method = fixed_method
This is legitimate, but it’s also fragile — it depends on the library’s internal structure staying stable across versions, and it’s invisible to anyone reading some_library‘s own source.
3. Adding Compatibility Shims
Monkey patching is a common technique for backporting or polyfilling functionality — adding a method that exists in a newer Python/library version to an environment running an older one:
if not hasattr(str, "removeprefix"): # Python < 3.9 compatibility
def removeprefix(self, prefix):
return self[len(prefix):] if self.startswith(prefix) else self[:]
# Note: built-in types like str can't actually be patched this way in CPython;
# this pattern is typically applied to custom classes or wrapper types instead.
(Worth noting explicitly: CPython’s built-in types like str, int, and list are implemented in C and are immutable at the type level for performance and safety reasons — you cannot monkey patch str directly. Patching works on Python-defined classes, modules, and instances, not C-level built-ins.)
The Risks
Global, Invisible Side Effects
The core danger of monkey patching is that it changes behavior globally and silently. If you patch requests.get somewhere deep in a test helper and forget to restore it, every subsequent test in the same process can be affected — a classic source of “why does this test fail only when run after that other test” bugs.
Debugging Becomes Archaeology
When behavior doesn’t match the source code you’re reading, and the actual cause is a patch applied somewhere else entirely (maybe in a completely different file, imported for its side effects), tracking it down is genuinely painful. I’ve lost real hours to this exact scenario.
Fragility Across Versions
Patches that reach into private/internal attributes of a library are coupled to that library’s implementation details, not its public API. A minor version bump can silently break your patch — or worse, not break it loudly, just make it stop having the intended effect.
Doing It Safely
If you do need to monkey patch outside of tests, a few practices make it much less dangerous:
import contextlib
@contextlib.contextmanager
def patched_method(cls, method_name, new_func):
original = getattr(cls, method_name)
setattr(cls, method_name, new_func)
try:
yield
finally:
setattr(cls, method_name, original)
class Service:
def call(self):
return "real"
with patched_method(Service, "call", lambda self: "patched"):
print(Service().call()) # patched
print(Service().call()) # real
Scoping the patch to a context manager (or using unittest.mock.patch directly, which does the same thing) ensures the original behavior is always restored, even on exceptions, and makes the scope of the modification explicit and visible in the code rather than a permanent, hidden global mutation.
Monkey Patching vs Subclassing vs Composition
Before reaching for a monkey patch, it’s worth asking whether subclassing or composition would achieve the same goal more safely:
# Subclassing instead of patching
class FixedSomeClass(some_library.SomeClass):
def buggy_method(self, *args, **kwargs):
return self._value * 2
Subclassing keeps the change local and explicit — only code that uses FixedSomeClass sees the new behavior, unlike a monkey patch that changes SomeClass for every piece of code in the process. Monkey patching is the right tool specifically when you can’t control instantiation (the object is created deep inside a third-party library you’re calling into) or when you’re deliberately isolating behavior for a test.
Common Mistakes
- Patching without restoring, especially in ad-hoc scripts or notebooks, leaving global state mutated for the rest of the session.
- Patching too broadly — replacing an entire class or module attribute when only one specific behavior needed to change.
- Not documenting why the patch exists. A monkey patch with no comment explaining what bug or version issue it works around is a trap for the next person (often future you).
- Trying to patch immutable built-in types directly, which raises
TypeError: can't set attributes of built-in/extension type.
FAQs
Q: Is monkey patching considered Pythonic? It’s a well-established, sanctioned technique in specific contexts (testing above all), but it’s not something to reach for by default. Idiomatic Python favors explicit subclassing or composition when either is a real option.
Q: Can monkey patching affect performance? Generally the runtime cost of the patched method itself is what matters (same as any function call); the mechanism of patching adds no meaningful overhead. The real cost is maintenance and debugging complexity, not CPU cycles.
Q: Does monkey patching work the same way for instances as for classes? You can patch a single instance’s attribute too (instance.method = new_func.__get__(instance)), which affects only that one object rather than the whole class — useful when you want to override behavior for a single object rather than everything.
Summary
Monkey patching takes advantage of Python’s fundamentally dynamic object model — classes and modules are mutable namespaces you can modify after the fact. It’s an essential, well-supported technique for testing (unittest.mock.patch) and a legitimate, if riskier, tool for working around bugs or adding compatibility shims. Used without discipline, it produces global, invisible, hard-to-debug side effects; used with scoping (context managers, mock.patch, clear documentation) it’s a precise, powerful tool rather than a landmine.
