The Ultimate Guide To Using Pytest Monkeypatch with Real Code Examples

Picture this: you’re writing tests for a complex Python application, and suddenly, you hit a wall.

Your code more often than not, depends on a database, external APIs, environment variables, or intricate global settings that seem impossible to isolate or control.

You want to write fast, reliable unit tests, but every attempt to mock or patch pieces of the code leaves you in a tangled mess of complexity.

Tests are brittle, dependencies are hard or impossible to mock, and the test suite feels more like a maze than a safety net.

Sound familiar?

These struggles often stem from poor code design and deeply coupled dependencies.

While clean code principles can help mitigate these issues, the reality is that patching remains a useful tool in some scenarios.

But not all patching is created equal.

Enter Pytest’s monkeypatch fixture: a cleaner, safer way to mock and patch during testing.

In this article, you’ll learn why and how to use the monkeypatch fixture within Pytest to overcome common testing challenges.

From patching functions to modifying dictionaries and environment variables, we’ll explore how to use monkeypatch and guide you with practical examples to make informed decisions about when—and how—to monkeypatch effectively.

Let’s dive in.

Link To GitHub Repo

What is Patching?

Patching is a testing technique used to temporarily replace parts of your code—such as functions, objects, or modules—at runtime.

Its original purpose was to isolate the unit or class under test, allowing you to focus solely on its behavior and interactions.

However, patching has often been misconstrued as a way to isolate external dependencies like APIs, databases, or configuration settings.

While patching external dependencies can simplify test setups, it risks coupling tests too tightly to implementation details.

This goes against its intended use and can lead to brittle tests that break during refactors. Properly applied, patching supports clean code and focused unit testing.

What is Monkeypatching and Why Use It?

Monkeypatching is a technique used to modify code behavior at runtime, particularly useful in testing scenarios where certain dependencies or global settings make it challenging to isolate functionality.

The monkeypatch fixture in Pytest allows you to safely and temporarily alter attributes, environment variables, dictionary values, or even system paths for testing purposes.

This approach is useful for testing code that interacts with external systems like APIs, databases, or file systems, where direct access might be impractical or unreliable.

Monkeypatching ensures that you can mock or replace these dependencies without affecting other parts of your application.

Common Use Cases:

  • Patching Functions: Replace or modify the behavior of specific functions or methods during tests.
  • Environment Variables: Temporarily set or delete environment variables to simulate different configurations.
  • Dictionary Items: Modify dictionary values, such as application settings, for targeted test cases.
  • System Path Modifications: Adjust sys.path to control module imports.
  • External Dependencies: Mock network calls, authentication layers, or delays for better test isolation.

With monkeypatching, you can test your code in controlled environments, ensuring reliable and maintainable test cases while avoiding the pitfalls of direct dependency management.

Here are some key attributes of the monkeypatch fixture in Pytest.

Monkeypatch Attributes

  • setattr(obj, name, value): Set an attribute on an object for the duration of the test.
  • delattr(obj, name): Delete an attribute from an object.
  • setitem(mapping, name, value): Set a key-value pair in a dictionary.
  • delitem(mapping, name): Remove a key from a dictionary.
  • setenv(name, value): Set an environment variable.
  • delenv(name): Remove an environment variable.
  • syspath_prepend(path): Add a path to sys.path.
  • chdir(path): Change the current working directory.
  • context(): Apply patches in a controlled scope.

Each modification is automatically reverted after the test completes, ensuring a clean test environment for each run.

Now that we’ve reviewed the basics, let’s dive in with some practical examples.

Setting Up Your Environment for Testing

Let’s set up a simple Python environment to follow along and run the examples.

  1. Create a new directory for your project.
1
2
mkdir pytest-monkeypatch-example
cd pytest-monkeypatch-example

Alternatively, you can clone the GitHub repository for this article.

  1. Create a virtual environment.
1
2
python3 -m venv venv
source venv/bin/activate # For Linux/Mac, venv\Scripts\activate # For Windows
  1. Install Dependencies.
1
pip install -r requirements.txt

We’ll go through a few examples to demonstrate how to use monkeypatch in your tests.

Monkeypatching Functions

Example: Mocking Path.home to Test File Paths
Imagine you have a function that constructs the SSH directory path for the current user based on their home directory.

When testing, you don’t want the test to depend on the actual user’s home directory, as it could vary across systems. This is where monkeypatch could solve the problem.

Here’s how you can mock Path.home using monkeypatch to ensure consistent test behavior across any system:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# contents of test_module.py
from pathlib import Path


def get_ssh_path():
return Path.home() / ".ssh"


def test_get_ssh_path(monkeypatch):
def mockreturn():
return Path("/tmp")

monkeypatch.setattr(Path, "home", mockreturn)
assert get_ssh_path() == Path("/tmp/.ssh")

# Optional: reset the monkeypatch if needed for rest of the test
monkeypatch.delattr(Path, "home")

The test ensures the function get_ssh_path behaves as expected, regardless of the actual environment.

Monkeypatching Functions

Monkeypatching Returned Objects: Building Mock Classes

When testing functions that call external APIs, perhaps you wish to mock the returned objects to avoid actual API calls.

Mocking is especially useful when the API response contains methods or attributes your code depends on.

Let’s demonstrate this using the MeowFacts API, which provides random cat facts.

Example: Mocking the MeowFacts API Response
Suppose we have a function that retrieves a random cat fact using the requests library.

1
2
3
4
5
6
7
8
9
10
11
12
# contents of src/cat_facts.py
import requests


def get_cat_fact():
"""Fetches a random cat fact from the MeowFacts API."""
response = requests.get("https://meowfacts.herokuapp.com/")
return response.json()


if __name__ == "__main__":
print(get_cat_fact())

Running the module, you should see a random cat fact printed to the console.

Mocking with Monkeypatch

1
2
3
4
5
6
7
8
9
10
11
12
13
# contents of test_cat_facts.py
from src.cat_fact import get_cat_fact


class MockResponse:
@staticmethod
def json():
return {"data": ["Cats can jump up to six times their length."]}


def test_get_cat_fact(monkeypatch):
monkeypatch.setattr("requests.get", lambda x: MockResponse())
assert get_cat_fact() == {"data": ["Cats can jump up to six times their length."]}
  • monkeypatch.setattr replaces requests.get with mock_get.
  • mock_get returns an instance of MockResponse.
  • The MockResponse.json() method mimics the real API by returning a known response.

Reusing the Monkeypatch with a Fixture
If you need to mock the MeowFacts API across multiple tests, you can move the logic into a reusable fixture.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest
from src.cat_fact import get_cat_fact

@pytest.fixture
def mock_meowfacts_api(monkeypatch):
"""Mock requests.get to simulate the MeowFacts API."""
class MockResponse:
@staticmethod
def json():
return {"data": ["Cats sleep 70% of their lives."]}

def mock_get(*args, **kwargs):
return MockResponse()

monkeypatch.setattr("requests.get", mock_get)

def test_get_cat_fact(mock_meowfacts_api):
result = get_cat_fact()
assert result == {"data": ["Cats sleep 70% of their lives."]}

Monkeypatching Returned Objects

Global Monkeypatch: Use Cases and Dangers

Global monkeypatching allows you to override or disable functionality across all tests, providing a safety net to prevent unwanted operations.

A common use case is blocking external network calls during local or CI/CD tests, or mocking default settings in production-like configurations.

Example: Preventing HTTP Requests Globally
To block all HTTP requests using the requests library, you can define a global patch in a conftest.py file:

1
2
3
4
5
6
7
# contents of conftest.py
import pytest

@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
"""Disable all HTTP requests by removing requests.sessions.Session.request."""
monkeypatch.delattr("requests.sessions.Session.request")

This autouse=True fixture automatically applies to all tests, ensuring that no test makes actual HTTP calls.

Any attempt to use requests will result in an error, forcing tests to use mocks or fakes for network-dependent functionality.

Use Cases

  • Enforcing Isolation: Prevent tests from accidentally interacting with external systems.
  • Consistency: Ensure all tests rely on predefined responses or mocks, avoiding flaky tests due to external dependencies.
  • Security: Prevent sensitive data or operations from being unintentionally triggered during testing.

Dangers and Best Practices
While global monkeypatching is powerful, it comes with risks:

  • Patching Built-in Functions: Avoid patching core Python functions like open or compile, as this can disrupt Pytest internals or cause unexpected behavior.
  • Breaking Third-party Libraries: Patching libraries used by Pytest or other test utilities might destabilize your test suite.
  • Scope Control: Use monkeypatch.context() to limit the scope of a patch to a specific block of code

Global monkeypatching is best reserved for scenarios where consistent behavior across all tests is essential.

Use it cautiously, ensuring you don’t unintentionally interfere with core functionality or third-party tools.

Monkeypatching Environment Variables

More often than not, you may use environment variables to configure your application.

The monkeypatch fixture simplifies this by allowing you to safely set or delete environment variables for the duration of a test, ensuring a clean and isolated testing environment.

Common settings like API keys, database configurations, or feature flags can be easily mocked using monkeypatch.

Monkeypatching them helps you:

  • Simulate different runtime configurations.
  • Test error-handling for missing or misconfigured variables.
  • Ensure tests don’t accidentally affect the global environment.

Suppose we have a function that reads an environment variable and processes it:

1
2
3
4
5
6
7
8
import os

def get_app_mode():
"""Fetches the application mode from the APP_MODE environment variable."""
app_mode = os.getenv("APP_MODE")
if not app_mode:
raise OSError("APP_MODE environment variable is not set.")
return app_mode.lower()

Writing a couple of tests for this

1
2
3
4
5
6
7
8
9
10
11
12
def test_get_app_mode(monkeypatch):
"""Test behavior when APP_MODE is set."""
monkeypatch.setenv("APP_MODE", "Production")
assert get_app_mode() == "production"


def test_missing_app_mode(monkeypatch):
"""Test behavior when APP_MODE is not set."""
monkeypatch.delenv("APP_MODE", raising=False)

with pytest.raises(OSError, match="APP_MODE environment variable is not set."):
get_app_mode()

The monkeypatch.setenv and monkeypatch.delenv methods allow you to simulate different environment configurations, ensuring your code behaves as expected under various conditions.

Monkeypatching Environment Variables

Conclusion

In this article, you explored how Pytest’s monkeypatch fixture provides an elegant, powerful solution for overcoming challenges like mocking APIs, handling environment variables, and isolating dependencies during testing.

From patching functions and modifying dictionaries to overriding environment variables and building reusable mock classes, monkeypatch offers a versatile approach to creating reliable, isolated tests.

By understanding the basics of monkeypatching and its use cases, you can write cleaner, more maintainable tests that focus on behavior rather than implementation details.

I highly encourage you to explore the official Pytest documentation and experiment with different scenarios to deepen your understanding of monkeypatch.

If you have ideas for improvement or like for me to cover anything specific please Email me, I’d love to hear from you.

Till the next time… Cheers!

Additional Resources