Python  

Monkey Patching in Python: What It Is and How to Use It

Monkey patching in Python means changing a function, class, or module while the program is already running. It is useful in tests, quick hotfixes, and small extension points. It is also risky because it can change behavior far away from where the patch was written. Python supports this style because objects can have attributes replaced at runtime, and unittest.mock.patch() exists to do temporary replacement in tests. (Python documentation)

Abstract / Overview

Monkey patching is not magic. It is just a runtime reassignment.

python-monkey-patching

In Python, names point to objects. If you change what a name points to, later code can see the new behavior. That is why a line as simple as module.func = fake_func can change the output of a live program. Python’s own docs describe setattr() as the counterpart of getattr(), and it can assign either an existing attribute or a new one. The standard library also gives you patch(), patch.object(), and patch.dict() for safer, temporary patching in tests.

This matters because Python is used across many kinds of work. In JetBrains’ Python Developers Survey 2023, Python use was strongest in data analysis at 47%, machine learning at 42%, and web development at 40%. In Stack Overflow’s 2024 survey coverage, 76% of respondents said they use or plan to use AI tools, but only 43% trusted their accuracy, and 45% said AI tools struggle with complex tasks. That is a good reason to review monkey patches very carefully, especially if the code came from a generator or assistant.

The practical rule is simple: use monkey patching on purpose, keep the scope small, and always make it easy to undo.

Conceptual Background

Monkey patching works because Python is highly dynamic. Classes, instances, and modules expose attributes that can be replaced at runtime. In plain terms, you are swapping one callable or value for another after import time.

The most important rule is this: patch where the code looks the object up, not where the object was first defined. Python’s unittest.mock docs say the basic principle is to patch where an object is looked up. This is the source of many failed tests and confusing runtime bugs.

Imports are part of the story. Python docs warn that importlib.reload() is not foolproof. If code used from module import thing, That old reference can stay alive even after a reload. Existing class instances can also keep their old behavior. So a patch, reload, and re-import cycle is often less clean than people expect.

python-monkey-patching-flow

A second useful point is that Python also supports module attribute customization. The Python docs note that modules can define __getattr__() for custom attribute access, and the module __class__ attribute is writable. You do not need these features for normal monkey patching, but they show how flexible module behavior can be.

Step-by-Step Walkthrough

Patch a module function with direct assignment

This is the simplest form.

# payments.py
def charge(amount):
    return {"status": "real", "amount": amount}
# app.py
import payments

def fake_charge(amount):
    return {"status": "fake", "amount": amount, "id": "test-123"}

payments.charge = fake_charge

print(payments.charge(100))
# {'status': 'fake', 'amount': 100, 'id': 'test-123'}

This works, but it changes the module for every caller that uses payments.charge after the patch. That is why direct assignment is best for small scripts, debugging, or controlled internal tools.

Patch a class method

You can replace methods on the class so every new instance sees the new behavior.

class Worker:
    def run(self):
        return "real run"

def fake_run(self):
    return "fake run"

Worker.run = fake_run

print(Worker().run())
# fake run

This is a real monkey patch. You changed the class after it was created.

A common mistake is to patch a descriptor on the instance instead of the class. Python docs say patch and patch.object correctly patch and restore class methods, static methods, and properties, and they should be patched on the class rather than an instance.

Use patch() for test-only changes

For tests, this is usually the best tool.

# checkout.py
from payments import charge

def place_order(amount):
    result = charge(amount)
    return result["status"]
from unittest.mock import patch
from checkout import place_order

with patch("checkout.charge", return_value={"status": "ok"}):
    assert place_order(100) == "ok"

This is safer because the patch is temporary. It starts inside the with block and is restored when the block ends. The official docs describe patch() as a decorator or context manager that replaces an object during a test and restores it when the test ends.

Notice the target is "checkout.charge", not "payments.charge". That is because place_order() looks up charge inside checkout, after the from payments import charge line. Patch the lookup site.

Use patch.object() when you already have the object

This is clean when you want to change one known attribute.

from unittest.mock import patch

class Service:
    def healthcheck(self):
        return False

with patch.object(Service, "healthcheck", return_value=True):
    assert Service().healthcheck() is True

This reads well and keeps the patch local.

Use patch.dict() for settings and environment values

You do not always patch functions. Sometimes you patch the config.

import os
from unittest.mock import patch

def is_enabled():
    return os.environ.get("FEATURE_X") == "1"

with patch.dict(os.environ, {"FEATURE_X": "1"}):
    assert is_enabled() is True

This is very useful in tests because it changes a dictionary for a short time and then restores it. The Python docs list patch.dict() as one of the standard patch helpers in unittest.mock. (Python documentation)

Wrap the patch so your team can find it

Ad hoc patches spread fast. A better pattern is to put the patch behind a named function.

def install_test_payment_patch():
    import payments

    def fake_charge(amount):
        return {"status": "fake", "amount": amount}

    payments.charge = fake_charge

Now the patch has one obvious entry point. It is still a monkey patch, but at least it is visible.

Use Cases / Scenarios

Good use cases

Monkey patching makes sense in a few cases.

  • Fast test isolation when you need to replace network calls, file access, or time-based behavior

  • Short-term hotfixes in internal systems when a full deploy path is blocked

  • Controlled extension points in plugin-style tools

  • Local debugging when you want to inspect or log behavior without changing the source yet

Risky use cases

Monkey patching becomes a bad habit when it is used to hide a weak design.

  • Replacing core business logic in production with no audit trail

  • Patching third-party libraries in several files with different behavior

  • Changing the global state in a multi-threaded or long-running service

  • Fixing architecture problems that should really be solved with dependency injection, wrappers, or adapters

Practical rule for production

In production code, prefer these choices first.

  • A wrapper around the third-party library

  • A small adapter class

  • A feature flag

  • Dependency injection

  • A real upstream fix

Use a monkey patch only when the tradeoff is clear and documented.

If your team has old Python services full of hidden runtime patches, stop guessing. Bring in C# Corner Consulting to audit the risky spots, replace fragile patches with clear extension points, and set up safer tests before the next release.

Fixes

The patch does not work

The usual cause is the wrong target path.

Bad idea:

with patch("payments.charge"):
    ...

Good idea when the code under test did from payments import charge:

with patch("checkout.charge"):
    ...

Again, patch where the object is looked up.

The patch leaks into other tests

This happens when you assign directly and forget to restore.

Bad idea:

payments.charge = fake_charge

Better ideas:

  • Use with patch(...)

  • Use a decorator

  • Restore the original in the cleanup code

Python’s docs make clear that patch helpers are designed to restore the original object when the function or with block ends.

You get a TypeError about self

This usually means the replacement method signature does not match the original use.

Bad idea:

class Worker:
    def run(self):
        return "real"

def fake_run():
    return "fake"

Worker.run = fake_run

Good idea:

class Worker:
    def run(self):
        return "real"

def fake_run(self):
    return "fake"

Worker.run = fake_run

Keep the replacement callable shaped like the original.

Reload does not clean things up

Do not expect importlib.reload() to fix every patch problem. Python’s docs warn that reload is not fully reliable for all import styles, and old imported names or existing instances can keep their old behavior.

Properties or class methods behave strangely

Patch them on the class, not on one instance. That is the documented approach for descriptors such as properties, class methods, and static methods.

FAQs

1. Is monkey patching bad?

Not by itself. It is a sharp tool. It is great in tests and sometimes useful for emergency work. It becomes bad when it hides design problems or changes global behavior with no clear scope.

2. What is the difference between monkey patching and mocking?

Monkey patching is the act of changing behavior at runtime. Mocking is usually a testing technique that uses fake objects or fake return values. In Python, unittest.mock.patch() is a mocking tool that performs controlled monkey patching under the hood.

3. Why did my patch not affect the function call?

Because you probably patched the wrong namespace. If the code under test imported the name into its own module, patch that local reference, not the original definition site.

4. Can I monkey-patch third-party libraries?

Yes, you can. The real question is whether you should. In tests, often yes. In production, only with tight control, good docs, and a plan to remove the patch later.

5. Can I patch properties, static methods, and class methods?

Yes. The Python docs say patch and patch.object can patch and restore these descriptors correctly, and the patch should be applied to the class.

6. Should I use direct assignment or unittest.mock.patch()?

Use direct assignment only when you truly want a manual runtime change, and you control the full scope. Use patch() for tests because it is temporary, easier to read, and easier to undo.

7. Does reload() remove monkey patches?

Sometimes, but not in a fully safe way. Python’s docs say reload is not foolproof, especially when old names were imported directly or when old class instances already exist.

References

Conclusion

Monkey patching in Python is powerful because Python is flexible. That same flexibility is what makes monkey patching dangerous when it is hidden, global, or long-lived.

The safest path is simple.

  • Patch small things

  • Patch for a short time

  • Patch where the code looks up the name

  • Prefer unittest.mock.patch() in tests

  • Replace long-term patches with wrappers, adapters, or real fixes