Overview of Monkey Patching in Python
In Python’s dynamic world, few features are as intriguing, or controversial, as monkey patching. It’s a technique that allows you to tweak classes, modules, or methods at runtime without modifying the original source code.
Sounds powerful? It is.
Sounds risky? Also true.
This post provides a comprehensive guide to monkey patching, covering what it is, why it exists, when to use it, when to avoid it, and how to do it safely in modern Python projects, including Django.
What is Monkey Patching?
Monkey patching means changing or extending code during runtime. You might replace a method, override a function, or add attributes to classes on the fly. This happens without modifying the original source, hence the term “patching.”
The phrase “monkey patch” originated in the Ruby community, but Python’s flexible nature has made it widely adopted here too.
Under the Hood
Python treats everything as an object, and functions/classes are first-class citizens. That means you can reassign methods or attributes as easily as variables.
Example:
class Calculator:
def add(self, a, b):
return a + b
# Normal behavior
c = Calculator()
print(c.add(2, 3)) # 5
# Monkey patch the add method
def new_add(self, a, b):
return f"{a} plus {b} equals {a + b}"
Calculator.add = new_add
print(c.add(2, 3)) # 2 plus 3 equals 5
You’ve just monkey-patched Calculator.add at runtime.
Why Use Monkey Patching?
1. Testing and Mocking
When writing unit tests, you may need to isolate external dependencies like databases, APIs, or time-based operations. Monkey patching lets you inject fake responses without altering the main logic.
Example:
import datetime
def get_today():
return datetime.date.today()
In testing:
import my_module
my_module.datetime.date.today = lambda: datetime.date(2025, 1, 1)
2. Hotfixing Bugs in Dependencies
Let’s say you found a bug in a third-party library, but there’s no time to fork and patch it properly. You can monkey patch the problematic function locally while waiting for an official fix.
3. Extending Library Behavior
Sometimes, you need a slight tweak to a library function like logging, filtering, or feature toggling, but it doesn’t provide a hook or extension point. Monkey patching fills that gap.
How to Monkey Patch (The Right Way)
You can patch:
- Methods in classes
- Functions in modules
- Properties and constants
Example:
import math
# Override math.sqrt
def custom_sqrt(x):
print("Calling custom sqrt")
return x ** 0.5
math.sqrt = custom_sqrt
print(math.sqrt(9)) # Calling custom sqrt → 3.0
Monkey Patching in Testing (Better Approach)
Instead of hardcoding patches, use the built-in unittest.mock.patch() it’s safer, scoped, and auto-reversible.
from unittest.mock import patch
import my_module
def test_fetch_data():
with patch("my_module.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"status": "ok"}
assert my_module.fetch_data() == {"status": "ok"}
This ensures the patch is active only inside the with block or test function.
Monkey Patching in Django
Django is extensible, but not always easily customizable. Monkey patching lets you hook into internal behaviors without digging into the framework’s guts too much.
Example 1: Patch Django Admin queryset
from django.contrib.admin.options import ModelAdmin
def patched_queryset(self, request):
qs = super(ModelAdmin, self).get_queryset(request)
return qs.filter(is_active=True)
ModelAdmin.get_queryset = patched_queryset
Example 2: Patch signal dispatching
from django.db.models.signals import pre_save
def patch_dispatch(self, sender, **kwargs):
print("Patched signal called!")
pre_save.dispatch = patch_dispatch
Tip: Use these patches only in development or controlled environments, as they may disrupt internal functionality.
Real-World Use Cases
| Use Case | Description |
| Fixing bugs in dependencies | Patch buggy methods until a fix is released. |
| Mocking time or I/O operations | Replace calls to datetime.now() or open() for deterministic testing. |
| Customizing Django admin | Override built-in behaviors for specific admin views. |
| Changing behavior in CMS plugins | Patch misbehaving Wagtail/Django CMS components. |
| Dynamic feature toggling | Enable/disable features at runtime without deployment. |
Risks and Dangers
Monkey patching can become a slippery slope:
1. Hard to Understand
Other developers won’t expect methods to behave differently from their definitions. Surprise overrides lead to confusion and bugs.
2. Breaks on Updates
Your patch might depend on internal logic or private methods. A version update can silently break your patch.
3. Global Impact
Monkey patching affects all instances globally, not just the one you intended.
Best Practices
| Tip | Explanation |
| Use sparingly | Only patch when other solutions aren’t viable. |
| Limit to tests or dev | Avoid using monkey patches in production unless absolutely necessary. |
| Document it clearly | Add comments explaining what and why you patched. |
| Use context managers | Prefer unittest.mock.patch() to limit patching scope. |
| Patch at startup | If patching globally, do it in apps.py or main wsgi.py. |
| Monitor library changes | Regularly check if your patches are still necessary. |
Summary
Monkey patching is a double-edged sword. It empowers you to:
- Patch bugs
- Mock dependencies
- Customize behavior
- Build flexible tests
- But it also introduces:
- Fragile code
- Debugging nightmares
- Upgrade risks
Use it only when necessary, document it well, and always explore alternatives first.
Conclusion
Monkey patching is a powerful tool, best kept in a locked drawer. Use it sparingly, only when conventional approaches fail. When handled with care, it offers remarkable flexibility. But if misused, it can lead to elusive, hard-to-debug problems that will haunt your future self. Treat it with the respect it demands.
