PythonMiddleTechnical
Что такое monkey patching и когда это допустимо?
Monkey patching — динамическая замена атрибутов модуля или класса во время выполнения. Допустим в тестах (через unittest.mock.patch) и для срочных hotfix сторонних библиотек. В production-коде почти всегда лучше заменить наследованием или композицией.
Что такое monkey patching
Monkey patching — это динамическая замена атрибутов модуля, класса или экземпляра во время выполнения программы. В Python это возможно потому, что атрибуты хранятся в словарях (__dict__) и могут быть перезаписаны в любой момент.
import requests
# Оригинальная функция
original_get = requests.get
def patched_get(url: str, **kwargs):
print(f"[PATCH] GET {url}")
return original_get(url, **kwargs)
# Заменяем метод в модуле
requests.get = patched_get
# Теперь все вызовы requests.get() будут логироваться
requests.get("https://example.com") # [PATCH] GET https://example.com
Основные сценарии применения
1. Тестирование с unittest.mock.patch
Самое частое и правильное применение — изоляция внешних зависимостей в тестах:
from unittest.mock import patch, MagicMock
import pytest
# Модуль под тестом
# myapp/services.py
# def fetch_user(user_id: int) -> dict:
# response = requests.get(f"/api/users/{user_id}")
# return response.json()
def test_fetch_user_success():
mock_response = MagicMock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mock_response.status_code = 200
# patch заменяет requests.get только внутри блока with
with patch("myapp.services.requests.get", return_value=mock_response):
from myapp.services import fetch_user
result = fetch_user(1)
assert result == {"id": 1, "name": "Alice"}
# После выхода из контекста requests.get восстановлен
# Декоратор — более распространённый стиль
@patch("myapp.services.requests.get")
def test_fetch_user_404(mock_get):
mock_get.return_value.status_code = 404
mock_get.return_value.json.return_value = {"error": "not found"}
from myapp.services import fetch_user
result = fetch_user(999)
assert "error" in result
2. Hotfix сторонней библиотеки
Когда баг в зависимости критичен, а обновление невозможно или опасно:
import some_library.utils as _utils
_original = _utils.parse_date
def _fixed_parse_date(value: str):
# Обходим баг с timezone-aware строками
if value.endswith("Z"):
value = value[:-1] + "+00:00"
return _original(value)
_utils.parse_date = _fixed_parse_date
# ВАЖНО: комментарий с номером issue и плановой датой удаления
# TODO(2026-Q3): удалить после обновления some_library >= 2.5.0
# Ref: https://github.com/some_library/issues/123
3. Добавление метода к сторонним классам
from datetime import date
def _to_iso_week(self: date) -> str:
return f"{self.isocalendar().year}-W{self.isocalendar().week:02d}"
# Добавляем метод к встроенному классу через патч модуля
# (не к самому date — он написан на C и не изменяем напрямую)
date.to_iso_week = _to_iso_week # type: ignore[attr-defined]
print(date(2026, 1, 5).to_iso_week()) # '2026-W01'
Правильный способ: контекстный менеджер для локального патча
from contextlib import contextmanager
from typing import Any, Generator
import time
@contextmanager
def mock_time(ts: float) -> Generator[None, None, None]:
original = time.time
time.time = lambda: ts # type: ignore[assignment]
try:
yield
finally:
time.time = original
with mock_time(1_700_000_000.0):
print(time.time()) # 1700000000.0
print(time.time()) # реальное время восстановлено
Когда monkey patching недопустим
- Изменение поведения в production-коде без явного флага или конфигурации
- Патч глобального состояния в многопоточном коде без синхронизации
- Замена методов у типов, реализованных на C (
list.append,dict.__setitem__) — в большинстве случаев это либо не работает, либо ломает интерпретатор
Подводные камни
- Порядок импорта имеет значение — если другой модуль уже сделал
from requests import get, ваш патч наrequests.getего не затронет. Патчить нужно в том пространстве имён, где атрибут используется:patch("mymodule.requests.get"). - Отсутствие восстановления при ошибке — без
try/finallyили контекстного менеджера патч остаётся при исключении. Всегда используйтеunittest.mock.patchили явныйfinally. - Гонки в многопоточном коде — глобальный патч на уровне модуля виден всем потокам. Если тест патчит
time.sleepв одном потоке, другой поток тоже получит заглушку. - Трудная отладка — стек вызовов после патча указывает на подменённую функцию, а не оригинальную. Добавляйте явные комментарии и логирование в patch-функции.
- Несовместимость с type checkers — mypy и pyright не знают о динамических патчах. Требуются
# type: ignoreили стабы. - Побочные эффекты на другие тесты — если патч не изолирован через
patchилиmonkeypatch(pytest), он может «протечь» в соседние тесты. Всегда используйтеautouseфикстуры или scope-ограниченные патчи. - C-расширения ограничены — атрибуты типов, определённых в C (
int,str,list), защищены и не принимают произвольные атрибуты:AttributeError: cannot set '_x' attribute of immutable type 'int'. - Альтернативы предпочтительнее — в production-коде вместо патча лучше использовать dependency injection, Protocol/ABC для замены реализации, или паттерн Adapter для обёртки стороннего API.
Common mistakes
- Описывать monkey patching только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом Python и реальной эксплуатацией.
What the interviewer is testing
- Объясняет monkey patching через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.