Monkey Patching Done Right: Essential Rules for Safe Runtime Modifications
Monkey patching is a runtime component override. This is useful for mocking dependencies during testing or extending features from an external library.
Let's patch the print function:
import builtins
original_print = builtins.print
def monkey_print(*args, **kwargs):
message = ' '.join(map(str, args))
original_print(f'🐵{message}🐵', **kwargs)
# Applying the patch
builtins.print = monkey_print
print('This is a monkey patching') # 🐵This is a monkey patching🐵
Okay, this is a great tool for unit testing, extending a feature from that library, or fixing that midnight issue. But take it easy—using monkey patching is very dangerous if you don't follow these golden rules.
Be explicit
You should explicitly patch the feature by creating a new function to patch:
import builtins
original_print = builtins.print
def monkey_print(*args, **kwargs):
message = ' '.join(map(str, args))
original_print(f'🐵{message}🐵', **kwargs)
def apply_print_patch():
builtins.print = monkey_print
apply_print_patch()
print('This is a monkey patching') # 🐵This is a monkey patching🐵
Encapsulate business logic
Modularize your business logic by creating dedicated functions or classes, which naturally leads to more testable code:
import builtins
original_print = builtins.print
# Business logic
def add_monkey_to_message(msg: str) -> str:
return f'🐵{msg}🐵'
def monkey_print(*args, **kwargs):
message = add_monkey_to_message(' '.join(map(str, args)))
original_print(message, **kwargs)
def apply_print_patch():
builtins.print = monkey_print
apply_print_patch()
print('This is a monkey patching') # 🐵This is a monkey patching🐵
Use context
The patching should be specified in context and reverted when it is no longer used:
import builtins
from contextlib import contextmanager
# Business logic
def add_monkey_to_message(msg: str) -> str:
return f'🐵{msg}🐵'
original_print = builtins.print
def apply_monkey_patch():
def monkey_print(*args, **kwargs):
message = add_monkey_to_message(' '.join(map(str, args)))
original_print(message, **kwargs)
builtins.print = monkey_print
@contextmanager
def monkey_context():
apply_monkey_patch()
try:
yield
finally:
builtins.print = original_print
with monkey_context():
print('This is inside monkey patching') # 🐵This is inside monkey patching🐵
print('This is outside monkey patching') # This is outside monkey patching
Test it
If you're patching a component from an external library, you should test if this component exists before patching it, which is important before a version upgrade, ensuring the library will work:
import builtins
from contextlib import contextmanager
# Business logic
def add_monkey_to_message(msg: str) -> str:
return f'🐵{msg}🐵'
class PrintPatchException(Exception):
pass
original_print = builtins.print
def apply_monkey_patch():
def monkey_print(*args, **kwargs):
message = add_monkey_to_message(' '.join(map(str, args)))
original_print(message, **kwargs)
builtins.print = monkey_print
@contextmanager
def monkey_context():
if not hasattr(builtins, 'print'):
raise PrintPatchException()
apply_monkey_patch()
try:
yield
finally:
builtins.print = original_print
with monkey_context():
print('This is inside monkey patching') # 🐵This is inside monkey patching🐵
print('This is outside monkey patching') # This is outside monkey patching
Conclusion
Monkey patching is an awesome tool, but it can be dangerous if you don't apply proper guardrails to ensure correct behavior of your override features. The key is being explicit about your overrides and thoroughly testing them. By following these golden rules, you can harness the power of monkey patching while keeping your code safe and maintainable.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more