Understanding Monkeypatch in Pytest

Pytest monkeypatch temporarily changes functions, objects, or environment values during tests. Learn when to use it and how to avoid fragile test code.

Written by Siddhi Rao Siddhi Rao
Reviewed by Sourabh G Sourabh G
Last updated: 5 August 2026 19 min read

Key Takeaways

  • Pytest monkeypatch temporarily replaces functions, attributes, dictionary values, environment variables, paths, or directories and restores the original state after each test.
  • Patch the name used by the module under test, not necessarily the dependency’s original definition, or the real call may still run.
  • Use monkeypatch to control APIs, configuration, time, errors, and file paths, but reconsider the design when one test needs several unrelated patches.

Pytest is one of the most widely used testing frameworks in the Python ecosystem. In the 2024 Python Developers Survey, conducted by the Python Software Foundation and JetBrains, 53% of respondents using unit-testing frameworks selected Pytest.

One feature that supports its practical testing model is the monkeypatch fixture. It lets you temporarily replace functions, attributes, environment variables, dictionary values, and other dependencies without changing production code. This helps you test failures, missing configuration, and controlled system states more reliably.

Whether you are using monkeypatch for the first time or reviewing an existing test suite, you need to understand what to patch, where to apply it, and when dependency injection may be the safer choice.

What is Monkeypatching?

Monkeypatching is a dynamic technique that allows developers and QA engineers to change the behavior of existing classes, objects, or modules at runtime, all without directly changing their source code.

This method offers significant flexibility and adaptability, enabling users to enhance the functionality of existing components, resolve issues, etc.

In the context of testing, it allows you to replace parts of your application with mock objects or functions to control their behavior during tests.

This is particularly beneficial when dealing with external APIs, database connections, or any functionality that might introduce variability or side effects into your tests.

What is Monkeypatching

Example:

Consider a simple function that calculates the sum of two numbers but includes a delay:

def calculate_sum(a: int | float, b: int | float) -> str:

delay()

return f"Sum of the 2 Numbers is `{a + b}`"


def delay():

time.sleep(5)  # Simulates a delay

In a test case without monkeypatching, this function would take a long time to execute due to the delay. By using monkeypatching, you can replace the delay function with a mock that does nothing:

def test_calculate_sum(monkeypatch):

def mock_delay():

     pass  # No delay
    

monkeypatch.setattr("module_name.delay", mock_delay)

result = calculate_sum(2, 2)

assert result == "Sum of the 2 Numbers is `4`"

Key Monkeypatch Methods in Pytest

The monkeypatch fixture provides separate methods for changing attributes, dictionaries, environment variables, import paths, and working directories. Pytest records each change and restores the original state after the requesting test or fixture finishes.

1. setattr() and delattr()

Use setattr() when your code calls a function or accesses an attribute that should behave differently during the test. This is common with external API clients, system time, file locations, and utility functions.

Patch the reference used by the module under test, not necessarily the location where the original function was defined. For example, if report.py imports getcwd directly, patch report.getcwd rather than os.getcwd.

# report.py

from os import getcwd




def get_report_path():

    return f"{getcwd()}/reports"
# test_report.py

import report




def test_get_report_path(monkeypatch):

    monkeypatch.setattr(report, "getcwd", lambda: "/tmp/test-app")



    assert report.get_report_path() == "/tmp/test-app/reports"

Use delattr() when you need to confirm how the application behaves when an attribute or method is unavailable. For example, you can remove a network request method to prevent tests from making real HTTP calls.

2. setitem() and delitem()

These methods modify dictionary-like objects. They are useful when an application stores defaults, feature flags, or runtime settings in a shared configuration dictionary.

# settings.py

DEFAULT_CONFIG = {

    "timeout": 10,

    "retries": 3,

}



def get_timeout():

    return DEFAULT_CONFIG["timeout"]
# test_settings.py

import pytest

import settings





def test_custom_timeout(monkeypatch):

    monkeypatch.setitem(settings.DEFAULT_CONFIG, "timeout", 2)



    assert settings.get_timeout() == 2




def test_missing_timeout(monkeypatch):

    monkeypatch.delitem(

        settings.DEFAULT_CONFIG,

        "timeout",

        raising=False,

    )



    with pytest.raises(KeyError):

        settings.get_timeout()

The raising argument controls whether Pytest raises an error when the target attribute, dictionary key, or environment variable does not exist. Set it to False when absence is an acceptable starting condition.

3. setenv() and delenv()

Use these methods to test code that reads credentials, deployment modes, service URLs, or feature settings from the environment.

import os

import pytest




def get_api_url():

    url = os.getenv("API_URL")



    if url is None:

        raise RuntimeError("API_URL is not configured")



    return url




def test_api_url(monkeypatch):

    monkeypatch.setenv("API_URL", "https://test.example.com")



    assert get_api_url() == "https://test.example.com"





def test_missing_api_url(monkeypatch):

    monkeypatch.delenv("API_URL", raising=False)




    with pytest.raises(RuntimeError):

        get_api_url()

setenv() also accepts a prepend argument. This is useful when adding a value to variables such as PATH without replacing the existing contents.

4. syspath_prepend(), chdir(), and context()

Use syspath_prepend() when a test must import modules from a temporary or generated directory. Use chdir() when behavior depends on the current working directory. Both changes are reversed after the test.

context() gives a patch a narrower lifetime than the full test. This is helpful when patching standard-library functions or other objects that could interfere with Pytest itself.

def test_limited_patch(monkeypatch):

    with monkeypatch.context() as patch:

        patch.setenv("APP_MODE", "testing")

        assert os.getenv("APP_MODE") == "testing"



    # The patch has already been reversed here.

You normally do not need to restore values manually. Pytest handles cleanup automatically, including when the test fails.

Here’s a table comparing the key Monkeypatch methods and when to use them.

MethodUse it to
setattr()Replace a function, method, class attribute, or module attribute
delattr()Temporarily remove an attribute or method
setitem()Add or replace a value in a dictionary
delitem()Remove a dictionary key
setenv()Set or replace an environment variable
delenv()Remove an environment variable
syspath_prepend()Add a directory to the beginning of sys.path
chdir()Change the working directory for a test
context()Limit a group of patches to a specific block

Common Use Cases of Pytest Monkeypatch

Monkeypatching is useful when a test depends on behavior that is slow, unpredictable, difficult to reproduce, or unsafe to trigger directly. The aim is not to replace every dependency. Patch the smallest boundary needed to create the condition you want to test.

1. Replacing External API Calls

A unit test should not depend on a live API. The service may be unavailable, return different data, enforce rate limits, or make the test noticeably slower. You can replace the HTTP call with a controlled response instead.

# weather.py

import requests





def get_temperature(city):

    response = requests.get(

        f"https://api.example.com/weather/{city}"

    )

    response.raise_for_status()

    return response.json()["temperature"]

The function sends an HTTP request, checks whether the response was successful, and returns the temperature value from the JSON body.

# test_weather.py

import weather





class MockResponse:

    def raise_for_status(self):

        pass



    def json(self):

        return {"temperature": 24}




def test_get_temperature(monkeypatch):

    monkeypatch.setattr(

        weather.requests,

        "get",

        lambda url: MockResponse(),

    )



    assert weather.get_temperature("Austin") == 24

monkeypatch.setattr() replaces requests.get inside the weather module with a function that returns MockResponse. No real network request is made.

The fake response implements the two methods used by the production code. raise_for_status() does nothing, which represents a successful request. json() returns a fixed payload. The assertion confirms that get_temperature() extracts the expected value from that payload.

The patch is applied to weather.requests.get because that is the reference used by the function under test.

2. Testing Environment-Based Configuration

Applications often read API keys, database URLs, deployment modes, and feature settings from environment variables. Tests should cover both configured and missing values without changing the environment permanently.

import os

import pytest





def get_database_url():

    url = os.getenv("DATABASE_URL")



    if not url:

        raise RuntimeError("DATABASE_URL is required")



    return url

This function returns the database URL when the variable is present. It raises an error when the value is missing or empty.

def test_database_url(monkeypatch):

    monkeypatch.setenv(

        "DATABASE_URL",

        "postgresql://localhost/test_db",

    )



    assert get_database_url() == "postgresql://localhost/test_db"




def test_missing_database_url(monkeypatch):

    monkeypatch.delenv("DATABASE_URL", raising=False)



    with pytest.raises(

        RuntimeError,

        match="DATABASE_URL is required",

    ):

        get_database_url()

The first test uses setenv() to create a known value. It verifies that the function reads and returns that value correctly.

The second test uses delenv() to ensure the variable is absent. raising=False prevents Pytest from failing if the variable was already missing before the test began. The test then confirms that the application raises the expected error.

Pytest restores the original environment after each test, so one test cannot leave a value that affects another.

3. Controlling Time-Dependent Behavior

Code that uses the real clock can produce tests that pass or fail depending on the date they are executed. This affects expiry checks, scheduled jobs, cache rules, subscription periods, and date-based pricing.

# subscription.py

from datetime import date



def is_expired(expiry_date):

    return date.today() > expiry_date

The function compares the current date with an expiry date. Testing it directly would make the result depend on the actual day.

# test_subscription.py

from datetime import date



import subscription




class FixedDate(date):

    @classmethod

    def today(cls):

        return cls(2026, 8, 5)




def test_expired_subscription(monkeypatch):

    monkeypatch.setattr(

        subscription,

        "date",

        FixedDate,

    )



    assert subscription.is_expired(

        date(2026, 8, 1)

    )

FixedDate inherits from Python’s date class but overrides today() to always return August 5, 2026. The patch replaces the date reference used inside subscription.py.

The assertion checks an expiry date of August 1, 2026. Since the controlled current date is August 5, the subscription should be considered expired.

The test patches subscription.date, not datetime.date, because subscription.py imported date directly into its own namespace.

4. Changing Configuration and Feature Flags

Some applications keep runtime settings in dictionaries. setitem() lets you change one value without replacing the entire configuration object.

# features.py

FEATURES = {

    "new_checkout": False,

}




def checkout_version():

    return "v2" if FEATURES["new_checkout"] else "v1"

The function chooses a checkout version based on the new_checkout feature flag. Its default value is False, so the application normally returns v1.

# test_features.py

import features




def test_new_checkout(monkeypatch):

    monkeypatch.setitem(

        features.FEATURES,

        "new_checkout",

        True,

    )



    assert features.checkout_version() == "v2"

The test changes only the new_checkout key to True. It then verifies that the function follows the enabled branch and returns v2.

After the test finishes, Pytest restores the original value. This matters because modifying a shared dictionary directly could cause later tests to run with the wrong configuration.

5. Triggering Error Conditions

Failure branches are often difficult to reproduce safely. A file operation may need to fail, an API may need to time out, or a dependency may need to return invalid data.

# exporter.py

def save_report(path, content):

    with open(path, "w") as file:

        file.write(content)

Under normal conditions, the function opens a file and writes the report content. To test a permission failure directly, you would need to manipulate operating-system permissions, which can behave differently across environments.

# test_exporter.py

import pytest




import exporter





def test_write_failure(monkeypatch):

    def raise_permission_error(*args, **kwargs):

        raise PermissionError("Write access denied")




    monkeypatch.setattr(

        exporter,

        "open",

        raise_permission_error,

    )




    with pytest.raises(

        PermissionError,

        match="Write access denied",

    ):

        exporter.save_report("report.txt", "data")

The replacement function accepts any arguments and immediately raises PermissionError. Patching exporter.open ensures that the failure occurs when save_report() tries to open the file.

pytest.raises() confirms both the exception type and its message. The test therefore proves that the expected failure reaches the caller without requiring a real permission change.

In production code, you may catch this exception and return a user-friendly error. The same patch can then verify that handling logic.

6. Isolating File Paths and Working Directories

Code that uses relative paths depends on the current working directory. A test may pass when run from the project root but fail when executed from an IDE, CI server, or another directory.

# config_loader.py

from pathlib import Path




def load_config():

    return Path("config.txt").read_text()

The function expects config.txt to exist in the current working directory.

# test_config_loader.py

import config_loader





def test_load_config(monkeypatch, tmp_path):

    config_file = tmp_path / "config.txt"

    config_file.write_text("test-mode")



    monkeypatch.chdir(tmp_path)



    assert config_loader.load_config() == "test-mode"

The tmp_path fixture creates an isolated temporary directory. The test writes a controlled configuration file into that directory and uses monkeypatch.chdir() to make it the current working directory.

When load_config() reads config.txt, it finds the temporary file rather than a file from the repository or the developer’s machine. The assertion confirms that the correct content was loaded.

This keeps the test independent of the directory from which Pytest was started and prevents test files from being written into the project.

Step-by-Step Guide on Using Pytest Monkeypatch

Follow these steps when adding monkeypatch to a test.

Step 1: Identify the Dependency

Find the external value or behavior that makes the test difficult to control.

This may include:

  • An API call
  • An environment variable
  • A system date or time value
  • A configuration setting
  • A file path
  • A function imported from another module

Patch only the dependency required for the test.

Step 2: Check Where the Dependency Is Used

Open the module under test and check how the dependency is imported.

Patch the name used by that module, not always the original package where the function or class was defined.

For example, if service.py imports get_user directly, patch service.get_user.

Step 3: Add the monkeypatch Fixture

Pass monkeypatch as an argument to the test function.

Pytest provides the fixture automatically when the test runs.

Step 4: Choose the Correct Method

Select the method based on what you need to change:

  • Use setattr() to replace a function, method, or attribute
  • Use delattr() to remove an attribute
  • Use setitem() or delitem() for dictionary values
  • Use setenv() or delenv() for environment variables
  • Use chdir() to change the working directory
  • Use syspath_prepend() to modify the import path

Step 5: Create the Replacement

Create a fixed value, function, or object that represents the condition you want to test.

The replacement only needs to support the attributes and methods used by the production code.

Step 6: Apply the Patch

Apply the patch before calling the function under test.

Make sure the patch target matches the lookup path identified in Step 2.

Step 7: Run the Code and Assert the Outcome

Call the production function normally and verify the expected result.

Depending on the scenario, assert:

  • The returned value
  • The raised exception
  • The updated state
  • The fallback behavior
  • The input passed to the patched dependency

Pytest restores the original value after the test finishes, so manual cleanup is usually not required.

Best Practices of Pytest Monkeypatching

Here are some of the best practices in Pytest Monkeypatching that can help you minimize risks and maintain a stable database:

1. Thorough Understanding of the Codebase

Before implementing monkeypatching, you should have a detailed understanding of the code with which you’re going to work. This includes:

  • Functionality: Know what each function does and how it interacts with other parts of your application.
  • Dependencies: Identify external dependencies that may affect the behaviour of the code you intend to test.
  • Side Effects: Be aware of any side effects that changes may introduce, particularly when mocking functions that interact with databases or external APIs.

2. Document and Communicate Your Monkey Patching Implementations

To have proper documentation for the code within your database is very important. It easily helps you to find the line of error or scope of improvements too.

  • Inline Comments: Use comments to explain why certain patches are applied, if they are different from the standard method.
  • Test Case Descriptions: Clearly describe what each test is verifying, including any monkeypatches used. This helps other developers understand the context quickly.
  • Change Logs: Maintain a change log for significant modifications made through monkeypatching. This is especially important in collaborative environments where multiple developers may work on the same code.

3. Ensure Comprehensive Testing

Monkeypatching should not compromise with any of the test results. This process will include:

  • Cover Edge Cases: Write tests for various scenarios, including edge cases that might not be immediately obvious.
  • Usage of Quality Assertions: Make sure that the assertions used in the code are robust and cover all the expected outcomes. It includes both positive and negative aspects.
  • Run Tests Regularly: Integrate your tests into a continuous integration (CI) pipeline to ensure they are run frequently, catching issues early.

4. Keep Yourself Updated with the Changes

As a user or developer, regularly check for the official updates or fixes that render your patches unnecessary. To stay ahead, you should include:

  • Library Updates: Regularly check for updates to libraries you depend on, as changes may impact your monkeypatched functions.
  • Review Release Notes from Official Sources: Always pay attention to the release notes for new features or old fixes that can help in simplifying your testing strategy.

Challenges of Pytest Monkeypatching

Monkeypatch makes difficult test conditions easier to reproduce, but it can also create tests that pass for the wrong reason. Most problems come from patching the wrong reference, replacing too much behavior, or coupling the test too closely to implementation details.

1. Patching the Wrong Target

A patch only works when it replaces the name that the code under test looks up at runtime. This is not always the module where the original function or class was defined.

For example, if a module imports a function directly, it creates a local reference to that function. Patching the original package may not affect the imported reference. The real dependency may still run, which can lead to network calls, file access, or unexpected test failures.

Check the import statement in the module under test and patch the reference used there.

2. Tests Becoming Coupled to Implementation Details

A monkeypatched test often knows which internal function is called and where it is imported. This can make the test fragile.

A refactor may move a helper function or change an import without changing the application’s behavior. Tests that patch the old path will fail even though the feature still works correctly.

Patch stable boundaries such as API clients, repositories, system time, or configuration providers. Avoid patching private helper functions unless their behavior is the specific subject of the test.

3. Replacements That Do Not Match Real Behavior

A replacement may return the correct value while ignoring other behavior that production code depends on.

For example, a fake HTTP response may provide json() but omit raise_for_status(). A mocked client may return a dictionary even though the real client returns an object. These differences can allow a test to pass while the production integration still fails.

The replacement does not need to recreate the full dependency, but it should match the interface used by the application. Include expected methods, return types, and exceptions.

4. Overuse of Patches

A test that applies several unrelated patches can become difficult to understand. It may also indicate that the function under test has too many responsibilities or creates dependencies internally.

When a test needs to patch time, configuration, file access, and an API client at once, it becomes harder to identify which condition caused a failure. The test may also stop representing a realistic execution path.

Keep each test focused on one behavior. If several patches appear repeatedly, consider moving shared setup into a fixture or passing dependencies into the production code directly.

5. Hidden Effects from Broad Fixtures

Monkeypatch can be used inside fixtures, including fixtures that run automatically. This is useful for blocking network access or setting a shared environment value, but broad fixtures can hide important test setup.

An engineer reading the test may not realize that a function, environment variable, or client has already been replaced elsewhere. This makes failures harder to trace.

Use autouse fixtures only for rules that should apply consistently across the selected test scope. Keep scenario-specific patches inside the test or an explicitly requested fixture.

6. Import-Time Configuration

Some modules read environment variables or create clients as soon as they are imported. Patching the environment after the import may have no effect because the value has already been stored.

This often causes confusion when setenv() appears correct but the application continues using the original configuration.

Where possible, read configuration when the function is called or pass it into the object that needs it. If import-time behavior cannot be changed, apply the patch before importing the module and reload it carefully.

7. Harder Failure Diagnosis

A patch changes the runtime environment of the test. When the replacement is incorrect, the resulting failure may appear inside the production function rather than near the patch itself.

Use clear replacement names and keep patch setup close to the assertion it supports. When a fake object has several methods or states, define it as a small class instead of a complex inline function.

Monkeypatch is most reliable when it controls one clear boundary and leaves the rest of the production behavior unchanged.

Conclusion

Pytest monkeypatch helps you isolate code from APIs, environment variables, system time, file paths, and other external dependencies. It gives each test a controlled state and restores the original values afterward. However, the test remains reliable only when you patch the correct reference and make the replacement match the behavior your application expects.

Start with the smallest dependency that blocks a reliable test. Patch the name used by the module under test, assert the visible result, and keep each test focused on one condition. When a test needs several patches or breaks after minor refactors, review the production design and consider dependency injection instead.

https://lp.jetbrains.com/python-developers-survey-2024/

Version History

  1. Aug 05, 2026 Current Version

    Updated selected sections to add practical guidance, explain key monkeypatch methods, expand common use cases, simplify the step-by-step process, and provide clearer coverage of common implementation challenges.

    Sourabh G
    Reviewed by Sourabh G Senior Software Engineer
Tags
Automation Testing Website Testing
Siddhi Rao
Siddhi Rao

Lead Customer Engineer

Siddhi Rao is a Lead Customer Engineer with 14+ years of experience in software testing, test automation, and quality engineering. She writes about automation testing, testing strategy, and practical QA workflows that help teams build reliable software and reduce release risk.

Patched Scenarios Failing in Browsers?
Verify patched scenarios across real browser environments.