Monkey patching should primarily be used for testing and quick bug fixes in third-party code where modification is not possible or practical. It should be viewed as a temporary and risky workaround, not a standard software development practice, due to its ability to dynamically modify code behavior at runtime. For your own code, clearer and safer alternatives such as inheritance, composition, and decorators are almost always the better choice.
1. The definitive use case: Testing and mocking
The safest and most common use for monkey patching is during testing. Testing often requires isolating the component you are testing from its external dependencies, such as APIs, databases, or file systems.
Replacing external calls
Instead of allowing your test to make a real, unpredictable, and slow network call to an external API, you can monkey patch the requests.get function to return a consistent, fake response.
Example:
# module_to_test.py
import requests
def get_cat_fact():
response = requests.get('https://meowfacts.herokuapp.com/')
return response.json()
# test_my_module.py
from unittest.mock import patch
from module_to_test import get_cat_fact
def test_get_cat_fact():
with patch('module_to_test.requests.get') as mock_get:
# Configure the mock object to return a specific response
mock_get.return_value.json.return_value = {"data": ["A cat fact."]}
fact = get_cat_fact()
assert fact == {"data": ["A cat fact."]}
Use code with caution.
In this example, unittest.mock.patch performs the monkey patching under the hood, but it's a more controlled and recommended approach for testing.
Simulating different scenarios
Monkey patching allows you to temporarily alter the environment for a test, such as changing system paths, environment variables, or other global settings.
Example using pytest's monkeypatch fixture:
import os
def check_env_variable():
return os.getenv("API_KEY") is not None
def test_env_is_set(monkeypatch):
monkeypatch.setenv("API_KEY", "my-secret-key")
assert check_env_variable()
def test_env_is_not_set(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False) # Ensure it's not set
assert not check_env_variable()
Use code with caution.
2. The temporary and risky use case: Fixing third-party library bugs
Sometimes you find a critical bug in a third-party library that needs an immediate fix, but you can't wait for the library maintainers to release a new version. In this limited scenario, monkey patching can provide a temporary hotfix.
**Example:**Imagine a bug in a library's get_user_email function that always returns the wrong domain.
# A hypothetical third-party library file you can't modify
def get_user_email(username):
return f"{username}@wrong-domain.com"
# Your application's startup file, where you apply the patch
from a_third_party_library import get_user_email
original_get_user_email = get_user_email
def fixed_get_user_email(username):
return f"{username}@your-domain.com"
get_user_email = fixed_get_user_email
# All subsequent calls to get_user_email will use your patched version
print(get_user_email("alice")) # Prints "[email protected]"
Use code with caution.
This is a high-risk approach. It should be used as a last resort and be removed as soon as the official library fix is available.
3. Edge cases and legacy systems
In some rare instances, monkey patching might be used for rapid prototyping, experimentation, or for maintaining compatibility with outdated legacy code. For example, a legacy library might not support a new protocol, and a monkey patch could temporarily bridge the gap. In these cases, the patch should be heavily documented and a plan for its removal should be made.
The many reasons to avoid monkey patching
While powerful, the risks of monkey patching almost always outweigh the benefits in production code.
- Breaks on updates: Library updates can change the internal APIs your patch relies on, causing it to fail or have unintended consequences.
- Reduced readability: Patches obscure the original code's behavior. A developer reading the source might not realize the code is being altered elsewhere, making debugging a nightmare.
- Unintended side effects: Because monkey patching has a global effect, a patch can unexpectedly alter the behavior of other parts of the application.
- Technical debt: Every monkey patch adds to your technical debt, especially if it's not well-documented and isolated. They can be difficult to remove and maintain over time.
- Hindered collaboration: Patches can create conflicts in large teams, as it's difficult to track who made which changes and why.
Alternatives to monkey patching
Before resorting to a monkey patch, consider these cleaner, safer alternatives:
- Subclassing (Inheritance): Create a new class that inherits from the original and override the method you need to change.
- Composition/Wrapper Classes: Create a wrapper class that contains an instance of the original class. This allows you to intercept and modify calls to its methods.
- Dependency Injection: Pass the dependency as an argument to a function or class. This allows you to easily swap the real dependency with a mock in testing without needing to patch it globally.
- Decorators: For functions, decorators offer a safe and transparent way to wrap functionality.
When to use vs. when to avoid: a summary table
| Aspect | When to use monkeypatch | When to avoid monkeypatch |
|---|---|---|
| Purpose | Isolated testing and mocking external dependencies. | Production code, adding features, or extending your own libraries. |
| Code ownership | Fixing bugs in a third-party library you cannot modify. | Modifying libraries or code you control. |
| Timeframe | Temporary hotfixes while waiting for an official patch. | Long-term changes and functionality. |
| Maintainability | For rapid prototyping or experimentation. | When code clarity and long-term maintainability are crucial. |
| Alternatives | When inheritance, composition, or decorators are not a viable option. | When cleaner alternatives exist. |