Monkey Patching in ERPNext (Frappe) — The Safe & Production-Ready Way
Sometimes in ERPNext, you need to change core behavior where no hook or override API exists.
A common example is overriding internal functions like make_packing_list.
This article explains the safest, production-ready way to do monkey patching in Frappe — without breaking:
-
Redis cache
-
bench clear-cache -
Scheduler
-
Multi-site setups
-
Future clones or upgrades
We’ll use make_packing_list as an example and add a simple msgprint to prove the override works.
Why NOT to monkey patch in hooks.py
Many developers try to monkey patch directly inside hooks.py.
This works temporarily, but causes serious issues later:
-
Redis serialization errors (
can't pickle module objects) -
Bench command failures
-
Random crashes during cache rebuild
-
Clone / restore instability
Rule:
hooks.pyis for declaring hooks — not for executing logic.
The Correct Pattern (Overview)
We will use three files:
yourapp/
├── __init__.py ← guarded loader
├── core_overrides.py ← monkey patches
└── overrides/
└── packed_item.py ← custom logic
Step 1: Create the override function
File:
yourapp/overrides/packed_item.py
import frappe
def make_packing_list(doc):
frappe.msgprint("Custom make_packing_list override executed")
For this article, we only show a
msgprintto keep the example simple.
Step 2: Create a central override loader
File:
yourapp/core_overrides.py
def apply():
import frappe
frappe.logger("yourapp").info("YourApp core overrides applied")
import erpnext.stock.doctype.packed_item.packed_item as original
from yourapp.overrides.packed_item import make_packing_list
original.make_packing_list = make_packing_list
Why this file exists
-
Keeps all monkey patches in one place
-
Easy to audit and debug
-
No side effects during app discovery
Step 3: Safely apply overrides from __init__.py
File:
yourapp/__init__.py
from __future__ import unicode_literals
__version__ = "0.0.1"
def _apply_overrides():
from yourapp.core_overrides import apply
apply()
try:
import frappe
# Apply overrides only when site context exists
if getattr(frappe.local, "site", None):
_apply_overrides()
except Exception:
# Never break bench commands
pass
Why this guard is CRITICAL
Frappe loads apps in multiple contexts:
| Context | Site Available |
|---|---|
| Web requests | |
| Scheduler | |
| Workers | |
bench clear-cache |
|
bench migrate |
Without the frappe.local.site guard:
-
Bench commands can crash
-
Redis cache may break
-
App discovery can fail
This guard makes the override bench-safe and production-safe.
Step 4: Restart and verify
bench restart
Now trigger any action that normally calls make_packing_list.
You should see:
Custom make_packing_list override executed
And in logs:
tail -f logs/frappe.log
YourApp core overrides applied
Final Result
-
Core function overridden safely -
No hooks abuse -
No Redis issues -
Bench commands work -
Clone & upgrade friendly
Best-Practice Summary
DO
-
Monkey patch in a dedicated file
-
Apply from
__init__.py -
Guard with
frappe.local.site -
Log when overrides load
DON’T
-
Monkey patch in
hooks.py -
Patch during hook loading
-
Patch without a site context check
Conclusion
Monkey patching is a last-resort tool in ERPNext — but when needed, this pattern is:
-
Clean
-
Predictable
-
Safe
-
Production-ready
If you must override core logic, this is the correct way to do it.