Skip to content

Monkey Patching#

Monkey patching means changing or extending behavior at runtime by replacing attributes on modules, classes, or instances — after the original code was defined. Tests use it to stub dependencies; libraries occasionally patch third-party bugs. It is powerful, fragile, and almost never belongs in production business logic.

How to use this page

Read after Modules and Packages and Unit Testing. Prefer Decorators or dependency injection when you control the source. Pair with Inspect Module for Introspection to verify patches.

At a glance
Track Python Advanced → Meta Programming
Sections 11 major topics
Outline Use the right-hand TOC to jump

Topics: What monkey patching is · Levels of patching · The canonical test use case · unittest.mock.patch patterns · Patching class methods and properties · Environment and import-time patching · Risks and why production code avoids patching · Thread safety and async · … (+3 more)

  1. What monkey patching is
  2. Levels of patching
  3. The canonical test use case
  4. unittest.mock.patch patterns
  5. Patching class methods and properties
  6. Environment and import-time patching
  7. Risks and why production code avoids patching
  8. Thread safety and async
  9. Detecting and debugging patches
  10. Legitimate production examples (rare)
  11. Manual patch with guaranteed restore

What monkey patching is#

import math

_original_sqrt = math.sqrt

def safe_sqrt(x: float) -> float:
    if x < 0:
        return 0.0
    return _original_sqrt(x)

math.sqrt = safe_sqrt
math.sqrt(4)    # 2.0
math.sqrt(-1)   # 0.0 — patched behavior

You mutated the namespace binding math.sqrt. Every subsequent math.sqrt(...) anywhere in the process sees the patch — including code that already imported sqrt:

from math import sqrt
sqrt(9)  # Still patched — same function object in module dict

Levels of patching#

Level Example Scope
Module attribute module.func = stub All importers of that module object
Class attribute Cls.method = new_method All instances (existing and future)
Instance attribute obj.method = new_method Single instance only
Builtin builtins.len = ... Entire interpreter — never in prod
class Greeter:
    def greet(self) -> str:
        return "hello"

g = Greeter()

def loud_greet(self) -> str:
    return "HELLO"

Greeter.greet = loud_greet      # class-level — affects g and new instances
g2 = Greeter()
g2.greet()  # 'HELLO'

g3 = Greeter()
g3.greet = lambda self: "hi"    # instance-level — only g3

The canonical test use case#

import datetime

def is_weekend() -> bool:
    return datetime.datetime.today().weekday() >= 5

# test:
def test_is_weekend_monday(monkeypatch):
    class FakeDate(datetime.datetime):
        @classmethod
        def today(cls):
            return cls(2026, 7, 6)  # Monday

    monkeypatch.setattr(datetime, "datetime", FakeDate)
    assert is_weekend() is False

pytest.MonkeyPatch (or unittest.mock.patch) saves originals and restores after the test — always patch with restoration.

from unittest.mock import patch

@patch("mymodule.datetime.datetime")
def test_with_mock(mock_dt):
    mock_dt.today.return_value.weekday.return_value = 5
    ...

Patch where the name is used, not where it is defined

If mymodule.py does from datetime import datetime, patch mymodule.datetime, not datetime.datetime in the stdlib.


unittest.mock.patch patterns#

from unittest.mock import patch, MagicMock

# context manager — auto-restore on exit
with patch("requests.get") as mock_get:
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = {"ok": True}
    # call code under test

# decorator
@patch("myapp.send_email")
def test_signup(mock_send):
    mock_send.return_value = True
    ...
Tool Restores automatically?
pytest.monkeypatch Yes, after test
unittest.mock.patch Yes, on __exit__ / test end
Manual assignment No — your responsibility

Patching class methods and properties#

class API:
    def fetch(self) -> dict:
        return {"real": True}

def fake_fetch(self) -> dict:
    return {"stub": True}

API.fetch = fake_fetch

Properties are trickier — you replace the descriptor on the class:

class Config:
    @property
    def timeout(self) -> int:
        return 30

Config.timeout = property(lambda self: 5)

Existing instances and descriptors

Replacing a method on the class affects attribute lookup for instances that don't shadow the name. Instance __dict__ entries override class patches.


Environment and import-time patching#

import os

os.environ["DATABASE_URL"] = "sqlite:///:memory:"
# Now import module that reads env at import time

Order matters: patch before the import if the module reads globals at import time. See Modules and Packages for import semantics.


Risks and why production code avoids patching#

Security and stability

Monkey patching is global mutable state. A patch in one library can break unrelated code in the same process. Malicious or careless patches to builtins, os, or subprocess are equivalent to code injection.

Risk Consequence
Global side effects Heisenbugs across threads/async tasks
Upgrade fragility Internal names change between versions
Hidden behavior Readers can't trust source code alone
No type-checker support Mypy sees original signatures
Async / threaded races Mid-test patch leaks to parallel tests

Prefer:

  • Dependency injection (pass collaborators as arguments)
  • Interfaces / protocols + test doubles
  • Official extension points (hooks, plugins)
  • Subclassing when the library supports it

Thread safety and async#

Patches are process-wide. In multi-threaded servers, a test patch without restoration can leak into request handlers. Use:

  • pytest with isolated processes (pytest-xdist careful configuration)
  • unittest.mock.patch as context manager in scoped code
  • Never patch in module-level test fixtures without teardown

Detecting and debugging patches#

import math
import inspect

print(math.sqrt)
print(inspect.getfile(math.sqrt))  # May point to unexpected file if patched

# Compare with known original
assert math.sqrt is math.__dict__.get("sqrt")  # Weak check

Inspect Module for Introspection helps audit what code is actually bound at runtime.


Legitimate production examples (rare)#

Context Example
Test suites Stub I/O, time, randomness
Gevent/eventlet Monkey-patches socket for cooperative I/O
Hotfix in locked deployment Temporary patch until upstream fix ships

Even gevent documents that patching is invasive — treat as framework infrastructure, not application pattern.


Manual patch with guaranteed restore#

from contextlib import contextmanager

@contextmanager
def patch_attr(obj, name, value):
    original = getattr(obj, name)
    setattr(obj, name, value)
    try:
        yield
    finally:
        setattr(obj, name, original)


with patch_attr(math, "sqrt", safe_sqrt):
    math.sqrt(-1)
# math.sqrt restored here

Interview traps (quick reference)#

Trap What goes wrong Safe approach
from mod import func then patch mod.func Call site still uses old binding Patch name in namespace where used
No teardown Pollutes other tests / requests patch, monkeypatch, or try/finally
Patching production for features Unmaintainable globals DI, hooks, subclass
Patching builtins Breaks entire runtime Forbidden outside isolated experiments
Instance vs class patch confusion "Why didn't other instances change?" Know MRO / instance dict precedence

Mental model checklist#

  1. Why does from math import sqrt still see a patched math.sqrt?
  2. Where should you apply a patch when the module under test uses from x import y?
  3. What is the scope difference between class-level and instance-level patches?
  4. Why is monkey patching especially dangerous in multi-threaded apps?
  5. What are three alternatives to monkey patching in application design?

What's next#

Topic Page
Unit testing Unit Testing
Modules / imports Modules and Packages
Decorators (compile-time wrapping) Decorators
exec / eval (stronger danger) Code Generations using exec() and eval()
Security practices Managing Secrets