Monkey patching in Python means changing a function, class, or module while the program is already running. It is useful in tests, quick hotfixes, and small extension points. It is also risky because it can change behavior far away from where the patch was written. Python supports this style because objects can have attributes replaced at runtime, and unittest.mock.patch() exists to do temporary replacement in tests. (Python documentation)
Abstract / Overview
Monkey patching is not magic. It is just a runtime reassignment.

In Python, names point to objects. If you change what a name points to, later code can see the new behavior. That is why a line as simple as module.func = fake_func can change the output of a live program. Python’s own docs describe setattr() as the counterpart of getattr(), and it can assign either an existing attribute or a new one. The standard library also gives you patch(), patch.object(), and patch.dict() for safer, temporary patching in tests.
This matters because Python is used across many kinds of work. In JetBrains’ Python Developers Survey 2023, Python use was strongest in data analysis at 47%, machine learning at 42%, and web development at 40%. In Stack Overflow’s 2024 survey coverage, 76% of respondents said they use or plan to use AI tools, but only 43% trusted their accuracy, and 45% said AI tools struggle with complex tasks. That is a good reason to review monkey patches very carefully, especially if the code came from a generator or assistant.
The practical rule is simple: use monkey patching on purpose, keep the scope small, and always make it easy to undo.
Conceptual Background
Monkey patching works because Python is highly dynamic. Classes, instances, and modules expose attributes that can be replaced at runtime. In plain terms, you are swapping one callable or value for another after import time.
The most important rule is this: patch where the code looks the object up, not where the object was first defined. Python’s unittest.mock docs say the basic principle is to patch where an object is looked up. This is the source of many failed tests and confusing runtime bugs.
Imports are part of the story. Python docs warn that importlib.reload() is not foolproof. If code used from module import thing, That old reference can stay alive even after a reload. Existing class instances can also keep their old behavior. So a patch, reload, and re-import cycle is often less clean than people expect.

A second useful point is that Python also supports module attribute customization. The Python docs note that modules can define __getattr__() for custom attribute access, and the module __class__ attribute is writable. You do not need these features for normal monkey patching, but they show how flexible module behavior can be.
Step-by-Step Walkthrough
Patch a module function with direct assignment
This is the simplest form.
# payments.py
def charge(amount):
return {"status": "real", "amount": amount}
# app.py
import payments
def fake_charge(amount):
return {"status": "fake", "amount": amount, "id": "test-123"}
payments.charge = fake_charge
print(payments.charge(100))
# {'status': 'fake', 'amount': 100, 'id': 'test-123'}This works, but it changes the module for every caller that uses payments.charge after the patch. That is why direct assignment is best for small scripts, debugging, or controlled internal tools.
Patch a class method
You can replace methods on the class so every new instance sees the new behavior.
class Worker:
def run(self):
return "real run"
def fake_run(self):
return "fake run"
Worker.run = fake_run
print(Worker().run())
# fake runThis is a real monkey patch. You changed the class after it was created.
A common mistake is to patch a descriptor on the instance instead of the class. Python docs say patch and patch.object correctly patch and restore class methods, static methods, and properties, and they should be patched on the class rather than an instance.
Use patch() for test-only changes
For tests, this is usually the best tool.
# checkout.py
from payments import charge
def place_order(amount):
result = charge(amount)
return result["status"]from unittest.mock import patch
from checkout import place_order
with patch("checkout.charge", return_value={"status": "ok"}):
assert place_order(100) == "ok"This is safer because the patch is temporary. It starts inside the with block and is restored when the block ends. The official docs describe patch() as a decorator or context manager that replaces an object during a test and restores it when the test ends.
Notice the target is "checkout.charge", not "payments.charge". That is because place_order() looks up charge inside checkout, after the from payments import charge line. Patch the lookup site.
Use patch.object() when you already have the object
This is clean when you want to change one known attribute.
from unittest.mock import patch
class Service:
def healthcheck(self):
return False
with patch.object(Service, "healthcheck", return_value=True):
assert Service().healthcheck() is TrueThis reads well and keeps the patch local.
Use patch.dict() for settings and environment values
You do not always patch functions. Sometimes you patch the config.
import os
from unittest.mock import patch
def is_enabled():
return os.environ.get("FEATURE_X") == "1"
with patch.dict(os.environ, {"FEATURE_X": "1"}):
assert is_enabled() is TrueThis is very useful in tests because it changes a dictionary for a short time and then restores it. The Python docs list patch.dict() as one of the standard patch helpers in unittest.mock. (Python documentation)

Join the conversation! Your thoughts help the community grow.