This rule raises an issue when test functions manually modify global state such as environment variables, object attributes, or module search paths without using test isolation fixtures or mocking utilities provided by the test framework.
In Python/pytest, this specifically refers to using the monkeypatch fixture instead of directly modifying sys.path or
other global state.
When tests manually modify global state, they risk polluting the test environment for subsequent tests. This happens because the modified state persists beyond the test that changed it, unless the test explicitly restores the original values.
Manual cleanup has several problems:
Test utilities that manage state modifications solve these problems by automatically undoing all modifications when the test completes, regardless of whether the test passes or fails. They provide a clean, declarative API that makes the intent clear and eliminates the need for manual cleanup code.
State pollution from missed cleanup propagates across the suite, so failures show up in unrelated tests and erode trust in green CI runs.
Declare the monkeypatch fixture as a test parameter and use its API instead of manual save/restore logic:
monkeypatch.setenv(name, value) and monkeypatch.delenv(name) for environment variablesmonkeypatch.setattr(target, name, value) and monkeypatch.delattr(target, name) for object attributes, module-level
functions, and bound methodsmonkeypatch.setitem(mapping, name, value) and monkeypatch.delitem(mapping, name) for dictionary-like objectsmonkeypatch.syspath_prepend(path) and monkeypatch.chdir(path) for import path and working-directory changesTo replace a function or method, pass the attribute name as a string to monkeypatch.setattr and supply a stub function or
lambda as the replacement:
monkeypatch.setattr(myapp.api, "fetch", stub_fetch) — replace a module-level functionmonkeypatch.setattr(client, "send", lambda payload: {"status": "ok"}) — replace a method on an object instanceUse a named stub function when the replacement is reused or needs more than one statement; use a lambda for simple one-line return
values.
Each method automatically undoes the modification after the test completes, even when the test fails.
For straightforward temporary changes to attributes, environment variables, or callables on existing modules and objects, prefer
monkeypatch.
unittest.mock.patch is a better fit when you need to mock entire modules, assert call arguments with
Mock.assert_called_with, configure complex side effects, or nest several patches with context managers. Review manually before replacing
working patch-based code with monkeypatch.
import os
def test_api_key():
old_key = os.environ.get('API_KEY')
os.environ['API_KEY'] = 'test_key' # Noncompliant
result = get_api_configuration()
assert result['key'] == 'test_key'
# Cleanup easily forgotten or skipped on exception
if old_key:
os.environ['API_KEY'] = old_key
else:
del os.environ['API_KEY']
def test_api_key(monkeypatch):
monkeypatch.setenv('API_KEY', 'test_key')
result = get_api_configuration()
assert result['key'] == 'test_key'
# Automatic cleanup - no manual code needed
import myapp.config
def test_feature_flag():
old_value = myapp.config.ENABLE_FEATURE
myapp.config.ENABLE_FEATURE = True # Noncompliant
assert myapp.is_feature_enabled()
myapp.config.ENABLE_FEATURE = old_value
def test_feature_flag(monkeypatch):
monkeypatch.setattr(myapp.config, 'ENABLE_FEATURE', True)
assert myapp.is_feature_enabled()
import myapp.client
def test_notify_user():
client = myapp.client.Client()
original_send = client.send
client.send = lambda message: {"sent": True} # Noncompliant
result = client.notify("Hello")
assert result["sent"]
client.send = original_send
def test_notify_user(monkeypatch):
client = myapp.client.Client()
monkeypatch.setattr(client, "send", lambda message: {"sent": True})
result = client.notify("Hello")
assert result["sent"]