Frappe Monkey Patching Guide: Overriding/Change Core Code Without Touching it

NOTE If you have advice or a good resource about this topic, please share with us

A guide on how to override core Frappe code and third-party app (or another frappe custom_app) functions using monkey patching techniques.

Table of Contents

  1. What is Monkey Patching?
  2. When to Use Monkey Patching
  3. Frappe-Specific Override Methods
  4. Implementation Patterns
  5. Best Practices
  6. Common Use Cases
  7. Troubleshooting

What is Monkey Patching?

Monkey patching is a technique that allows you to dynamically modify or extend existing code at runtime without changing the original source files. In Python, this is possible because functions and methods are first-class objects that can be reassigned.

monkey patch = change core [method, function, class] without touching the core

Basic Concept:

# Original function in some_module.py
def original_function():
    return "original behavior"

# Your override in custom_app
def enhanced_function():
    return "enhanced behavior"

# Monkey patch - replace the original
import some_module
some_module.original_function = enhanced_function

When to Use Monkey Patching

Good Use Cases:

  • Bug Fixes: Fix issues in core Frappe or third-party apps without waiting for updates or without even touching the core code (i will keep remind you WITHOUT TOUCHING THE CORE)
  • Feature Enhancement: Add functionality to existing methods (enhance it)
  • Integration Requirements: Modify behavior for specific business needs
  • Temporary Workarounds: Quick fixes while waiting for proper solutions

Avoid When:

  • Simple Customizations: Use Frappe’s built-in customization features instead of Monkey Patching
  • DocType Modifications: Use Custom Fields, Custom Scripts, or DocType inheritance instead of Monkey Patching
  • UI Changes: Use Client Scripts or Custom Apps with proper UI extensions instead of Monkey Patching
  • Performance Critical: Monkey patching adds overhead so don’t over using it

Frappe-Specific Override Methods

Method 1: Hook-Based Overrides (Recommended)

Frappe provides several hooks that allow clean overrides:

Note: you can find these hooks in the official documentation.

Document Events

# In hooks.py
doc_events = {
    "User": {
        "before_save": "custom_app.overrides.user_overrides.before_save",
        "after_insert": "custom_app.overrides.user_overrides.after_insert"
    },
    "*": {
        "on_update": "custom_app.overrides.global_overrides.on_update"
    }
}

Method Overrides

# In hooks.py
override_whitelisted_methods = {
    "frappe.desk.doctype.event.event.get_events": "custom_app.overrides.event_overrides.get_events"
}

Function Overrides

# In hooks.py
import custom_app.overrides.file_utils as custom_file_utils
import frappe.utils.file as core_file_utils

core_file_utils.set_file_details = custom_file_utils.custom_set_file_details

DocType Class Overrides

# In hooks.py
override_doctype_class = {
    "User": "custom_app.overrides.CustomUser"
}

# In custom_app/overrides.py
from frappe.core.doctype.user.user import User

class CustomUser(User):
    def validate(self):
        super().validate()
        # Your custom validation logic

Method 2: Boot Session Overrides

For core function overrides that need to be applied system-wide:

# In hooks.py
boot_session = "custom_app.overrides.apply_core_overrides"

# In custom_app/overrides.py
def apply_core_overrides():
    """Apply monkey patches to core Frappe functions"""
    import frappe.utils
    
    # Store original function
    frappe.utils._original_get_url = frappe.utils.get_url
    
    # Replace with enhanced version
    frappe.utils.get_url = enhanced_get_url

def enhanced_get_url(*args, **kwargs):
    """Enhanced version of frappe.utils.get_url"""
    # Your custom logic here
    result = frappe.utils._original_get_url(*args, **kwargs)
    # Additional processing
    return result

Method 3: App Installation Hooks

For overrides that should be applied when your app is installed:

# In hooks.py
after_app_install = "custom_app.overrides.apply_overrides"
before_app_uninstall = "custom_app.overrides.restore_overrides"

# In custom_app/overrides.py
def apply_overrides():
    """Apply all monkey patches"""
    override_core_functions()
    override_third_party_functions()

def restore_overrides():
    """Restore original functions"""
    restore_core_functions()
    restore_third_party_functions()

Implementation Patterns

Pattern 1: Simple Function Replacement

import frappe.utils

def apply_overrides():
    # Store original for potential restoration
    if not hasattr(frappe.utils, '_original_cint'):
        frappe.utils._original_cint = frappe.utils.cint
    
    # Replace with enhanced version
    frappe.utils.cint = enhanced_cint

def enhanced_cint(value, default=0):
    """Enhanced version with better error handling"""
    try:
        return frappe.utils._original_cint(value, default)
    except Exception as e:
        frappe.logger().warning(f"cint conversion failed: {e}")
        return default

Pattern 2: Method Decoration

def apply_overrides():
    import frappe.model.document
    
    # Store original
    original_save = frappe.model.document.Document.save
    
    def enhanced_save(self, *args, **kwargs):
        """Enhanced save with additional logging"""
        frappe.logger().info(f"Saving document: {self.doctype} - {self.name}")
        return original_save(self, *args, **kwargs)
    
    # Apply override
    frappe.model.document.Document.save = enhanced_save

Pattern 3: Class Method Override

def apply_overrides():
    from frappe.core.doctype.user.user import User
    
    # Store original method
    User._original_validate = User.validate
    
    def enhanced_validate(self):
        """Enhanced validation with custom rules"""
        # Call original validation
        self._original_validate()
        
        # Add custom validation
        if self.email and not self.email.endswith('@company.com'):
            frappe.throw("Only company emails are allowed")
    
    # Apply override
    User.validate = enhanced_validate

Pattern 4: Module-Level Function Override

def apply_overrides():
    import frappe.auth
    
    # Store original
    frappe.auth._original_check_password = frappe.auth.check_password
    
    def enhanced_check_password(user, pwd, doctype='User', fieldname='password'):
        """Enhanced password check with additional security"""
        # Add custom security checks
        if is_suspicious_login(user):
            frappe.throw("Login temporarily blocked")
        
        # Call original function
        return frappe.auth._original_check_password(user, pwd, doctype, fieldname)
    
    # Apply override
    frappe.auth.check_password = enhanced_check_password

Best Practices

1. Always Backup Original Functions

# Good: Store original for restoration
if not hasattr(module, '_original_function'):
    module._original_function = module.function

# Bad: Direct replacement without backup
module.function = new_function

2. Use Descriptive Function Names

# Good: Clear naming convention
def enhanced_get_user_permissions(user):
    pass

# Bad: Unclear naming
def new_func(user):
    pass

3. Implement Proper Error Handling

def apply_overrides():
    try:
        import target_module
        target_module.function = enhanced_function
        frappe.logger().info("Successfully applied override")
    except ImportError:
        frappe.logger().warning("Target module not found")
    except Exception as e:
        frappe.logger().error(f"Failed to apply override: {e}")

4. Provide Restoration Functions

def restore_overrides():
    """Restore all original functions"""
    try:
        import target_module
        if hasattr(target_module, '_original_function'):
            target_module.function = target_module._original_function
            delattr(target_module, '_original_function')
    except Exception as e:
        frappe.logger().error(f"Failed to restore override: {e}")

5. Document Your Overrides

def enhanced_function(*args, **kwargs):
    """
    Enhanced version of original_module.function
    
    Changes:
    - Added input validation
    - Improved error handling
    - Added logging
    
    Args:
        *args: Original function arguments
        **kwargs: Original function keyword arguments
    
    Returns:
        Same as original function
    """
    pass

6. Use Conditional Overrides

def apply_overrides():
    # Only apply if certain conditions are met
    if frappe.conf.get('enable_custom_overrides'):
        apply_user_overrides()
    
    if frappe.get_installed_apps().get('third_party_app'):
        apply_third_party_overrides()

Common Use Cases

Use Case 1: Fixing Third-Party App Bugs

# In custom_app/overrides/third_party_fixes.py
def apply_bug_fixes():
    """Fix known bugs in third-party apps"""
    try:
        import third_party_app.utils
        
        # Store original
        third_party_app.utils._original_buggy_function = third_party_app.utils.buggy_function
        
        def fixed_function(*args, **kwargs):
            """Fixed version of buggy_function"""
            # Add proper validation
            if not args or not isinstance(args[0], str):
                return None
            
            return third_party_app.utils._original_buggy_function(*args, **kwargs)
        
        # Apply fix
        third_party_app.utils.buggy_function = fixed_function
        
    except ImportError:
        pass  # Third-party app not installed

Use Case 2: Adding Logging to Core Functions

def add_logging_overrides():
    """Add comprehensive logging to core functions"""
    import frappe.model.document
    
    original_insert = frappe.model.document.Document.insert
    
    def logged_insert(self, *args, **kwargs):
        """Document insert with logging"""
        start_time = time.time()
        
        try:
            result = original_insert(self, *args, **kwargs)
            duration = time.time() - start_time
            
            frappe.logger().info(f"Document inserted: {self.doctype} - {self.name} ({duration:.2f}s)")
            return result
            
        except Exception as e:
            frappe.logger().error(f"Document insert failed: {self.doctype} - {e}")
            raise
    
    frappe.model.document.Document.insert = logged_insert

Use Case 3: Enhancing Security

def apply_security_overrides():
    """Add additional security checks"""
    import frappe.handler
    
    original_handle = frappe.handler.handle
    
    def secure_handle():
        """Enhanced request handler with security checks"""
        # Add rate limiting
        if is_rate_limited(frappe.session.user):
            frappe.throw("Rate limit exceeded")
        
        # Add IP filtering
        if is_blocked_ip(frappe.local.request_ip):
            frappe.throw("Access denied")
        
        return original_handle()
    
    frappe.handler.handle = secure_handle

Troubleshooting

Common Issues and Solutions

Issue 1: Override Not Applied

# Problem: Override applied too late
# Solution: Use boot_session hook
boot_session = "custom_app.overrides.apply_overrides"

Issue 2: Circular Import

# Problem: Importing module causes circular dependency
# Solution: Import inside function
def apply_overrides():
    import target_module  # Import here, not at module level
    target_module.function = enhanced_function

Issue 3: Override Conflicts

# Problem: Multiple apps overriding same function
# Solution: Check for existing overrides
def apply_overrides():
    import target_module
    
    if hasattr(target_module.function, '__name__'):
        if 'enhanced' in target_module.function.__name__:
            frappe.logger().warning("Function already overridden")
            return
    
    target_module.function = enhanced_function

Issue 4: Testing Overrides

# Create test functions to verify overrides
def test_overrides():
    """Test that overrides are working correctly"""
    import target_module
    
    # Test that override is applied
    assert hasattr(target_module, '_original_function')
    assert target_module.function.__name__ == 'enhanced_function'
    
    # Test functionality
    result = target_module.function(test_input)
    assert result == expected_output

Advanced Patterns

Pattern 5: Decorator-Based Overrides

def create_override_decorator(original_func):
    """Create a decorator that enhances the original function"""
    def decorator(enhancement_func):
        def wrapper(*args, **kwargs):
            # Pre-processing
            result = enhancement_func(*args, **kwargs)
            if result is not None:
                return result

            # Call original if enhancement returns None
            return original_func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
import frappe.utils
original_cint = frappe.utils.cint

@create_override_decorator(original_cint)
def enhanced_cint(value, default=0):
    """Enhanced cint with special handling"""
    if isinstance(value, str) and value.startswith('SPECIAL_'):
        return int(value.replace('SPECIAL_', ''))
    return None  # Let original handle it

frappe.utils.cint = enhanced_cint

Pattern 6: Context Manager Overrides

from contextlib import contextmanager

@contextmanager
def temporary_override(module, function_name, new_function):
    """Temporarily override a function"""
    original = getattr(module, function_name)
    setattr(module, function_name, new_function)
    try:
        yield
    finally:
        setattr(module, function_name, original)

# Usage
with temporary_override(frappe.utils, 'cint', custom_cint):
    # Custom behavior active only in this block
    result = frappe.utils.cint("123")

Pattern 7: Conditional Override Chain

def create_override_chain():
    """Create a chain of conditional overrides"""
    import frappe.auth

    original_login = frappe.auth.login_manager.login

    def chained_login(self, *args, **kwargs):
        # Override 1: Custom authentication
        if frappe.conf.get('custom_auth_enabled'):
            if custom_authenticate(self.user):
                return original_login(self, *args, **kwargs)

        # Override 2: LDAP integration
        if frappe.conf.get('ldap_enabled'):
            if ldap_authenticate(self.user):
                return original_login(self, *args, **kwargs)

        # Override 3: Two-factor authentication
        if frappe.conf.get('2fa_enabled'):
            if verify_2fa(self.user):
                return original_login(self, *args, **kwargs)

        # Default behavior
        return original_login(self, *args, **kwargs)

    frappe.auth.login_manager.login = chained_login

Summary

Monkey patching in Frappe is a powerful technique that should be used carefuly. Always prefer Frappe’s built-in customization methods when possible, and use monkey patching only when necessary. Remember to:

  • Always backup original functions
  • Use proper error handling
  • Document your changes completely (next developer after you will definitly need it)
  • Provide restoration mechanisms to restore the old implementation if you uninstall the custom_override_app from your site
  • Test your overrides as much as you can specially in production

With these patterns and best practices, you can safely extend and modify Frappe’s behavior while maintaining system stability and upgrade compatibility.

3 Likes

It is my opinion that this post is, on the whole, bad advice. What you’re putting out there is going to encourage people to do things that will have untested, unknown and unintended consequences and is explicitly breaking out of a framework that already has a plethora of extension APIs. The DocType Class override should be preferred to all other techniques that would otherwise require monkey patching, of which there are very few (in Python). Critically, the Frappe environment differs between scheduler/background jobs and the main runtime since the framework does not expose an “on_app_load” hook, which honestly people would abuse. I am frustrated that you are advocating for working against the framework instead of leveraging its features. This post indicates your competence in at least prompting an LLM and maybe software development but not much experience in actually having used these techniques to solve problems.

One of the challenges of learning a new framework is that it doesn’t work the same way as the framework you used before that.

Pattern 1 has to be called or it will not will be consistently applied. Since the Frappe framework does not feature a “on_app_load” hook, this code will only ever execute if it lives in an app-level __init__.py file. If it is called in hooks.py it will not be available in the background jobs since frappe.get_hooks() isn’t automatically called when then scheduler calls a function, leading to a false assumption that the override is working.

Your Pattern 2 technique will not work and the code in enhanced_save will never execute except immediately after installing the app, which may give a false positive if you’re running a this in a CI. It’s also entirely avoidable by using the class override and/or the doc_events hook.

Your Pattern 3 technique is demonstrably worse than the implementation recommended in the Frappe documentation, which uses a well understood python class inheritance pattern.

Your article does not cover any customizations to the database(s) or to front end APIs which are typically the most immediately useful and are built in to the framework.

3 Likes

thanks alooot for your opinion :heart: :heart: :heart:

don’t worry i had mention the cases that we need to use monkey-patching and i did warn that it must be used in major needs

as example in my work we had a lot of business needs that force us to use monkey patch.
the problem was "i don’t know how to do monkey patch properly :joy: "
so i collected parts from my experience and parts from others experience and ofc uncle chatgpt

btw i hope you read this file to understand what i want to do exactly for this community:

Thanks again :pray:

You don’t define “major” and the examples you provide don’t tell a cohesive story as to why somebody would actually need to make these changes.

You have demonstrated that you still don’t know how to correctly monkey patch in ERPNext and for that matter, neither does ChatGPT. Neither of you especially understands why. You are telling people a lie while showing them broken and inoperable code. It is abundantly clear what you are offering this community.

at the beginning i am sorry to say that but try to learn critical thinking or communication first
again, i hope you read the file to understand what i am trying to do since i used this framework

thank you

monky patching is critical technique that anyone may use and will definitly use while working with frappe,
there is no resource explain it so at least we may share what we experienced.

You’re incorrect and are advocating for something that you have not demonstrated that you understand. I am not going to read any more of your work or provide more technical feedback for you since you are insisting that your original errors are unimportant.

The reason that there isn’t a resource for monkey patching in the Frappe framework is because it generally isn’t a great solution and the framework itself provides better ones. You’re not filling a knowledge gap, you are demonstrating that you haven’t actually solved any problems like the examples you show. It is not a critical technique in Frappe or ERPNext development.

If this is a marketing effort, I hope you find the customers you deserve.

1 Like

no, a lot of customers ask for nonsense customization that forces me to override core code in frappe, and I have already used this technique many times and gave the clients what they ask for (even if it’s nonsense but that is the business need)
Unfortunately in ERP specifically, we are dealing with customers like that

Since I started using this hidden gem framework, i wondered why no one use it, and the reason is because no one understand it and there is no proper documentation for it,
but what i know is sharing with the community will give me feedback or help another one face the same problem (even if it’s not the best solution but it’s a solution)

so, thanks, but you are not helping here

@mohamed-ameer, thanks for your earlier post on monkey patching ERPNext – it was really helpful and inspiring. :pray:

We’re trying to build a custom app to allow sales users to manually override item rates in Quotation, Sales Order, and Sales Invoice, but only if the rate is equal to or higher than the pricing rule rate (not lower).

Example:

  • Item: item10
  • Default rate from pricing rule: 105 (140 - 25%)
  • Salesperson sets 110 → :white_check_mark: OK
  • Salesperson tries 100 → :x: Blocked

We added a checkbox custom_manual_rate to detect manual edits and used monkey patching on apply_pricing_rule and get_pricing_rule_for_item. Code here:
:point_right: dlits_core/dlits_core/dlits_custom/override_pricing_rule.py at main · shihab0020/dlits_core · GitHub

Problem:

  • On the first save, manual rates are respected.
  • On the second edit/save, ERPNext reverts all item prices back to pricing rule rates (e.g., 105), ignoring our custom logic.

We want item-level control, not full-document override using “Ignore Pricing Rule”.

Would really appreciate your guidance on what might be going wrong or if there’s a better way to achieve this.

Thanks again for your work and support! :clap:

I used monkey patches extensively in my custom apps, as experienced frappe/erpnext consultant and developer, I do think for real world business case, monkey patches are inevitable. sharing monkey patches use cases and best practice is good to community in my opinion.