Isolation & Contracts

Patching Class Attributes with patch.object

patch("myapp.billing.Gateway.charge") works until the module is renamed, the class moves, or someone mistypes a segment of the string — at which point it either fails with an import error or, worse, patches something that exists but is not what the code uses. patch.object(Gateway, "charge") takes the class itself, so the reference is checked by Python at the moment the test module is imported and by the IDE's rename refactoring whenever the class moves.

It is also the more precise tool. It patches exactly one attribute on exactly one object — a class, an instance, or a module — with no ambiguity about which binding is being replaced. That precision matters most for class-level state: constants, class attributes, and methods whose behaviour should change for every instance at once, or for only one.

The distinction between the two scopes is the part worth internalising, because choosing wrongly produces tests that either pass for the wrong reason or fail mysteriously. Code that constructs its own collaborators — a service that creates a Gateway() internally — can only be affected by patching the class, since the test never holds the instance. Code that receives its collaborators can be tested more precisely by patching the instance the test passes in, leaving every other instance in the process untouched. This guide covers both scopes, the timing rules that decide whether a patch is seen at all, and how patch.object relates to pytest's own monkeypatch.

Prerequisites

  • Python 3.8+; unittest.mock in the standard library.
  • The target-resolution rules from where to patch, which apply to patch.object as much as to patch.

Solution

Python
from unittest.mock import patch

from myapp.billing import Gateway, RetryPolicy


def test_charge_failure_is_reported():
    # Class scope: every Gateway instance, including ones made inside the code.
    with patch.object(Gateway, "charge", autospec=True,
                      side_effect=ConnectionError("down")) as charge:
        result = checkout(cart_total=4999)

    assert result.status == "payment_unavailable"
    charge.assert_called_once()


def test_single_retry_is_attempted():
    # A class-level constant, replaced for the duration of the block only.
    with patch.object(RetryPolicy, "MAX_RETRIES", 1):
        attempts = run_with_policy(RetryPolicy(), always_fails)

    assert attempts == 1


def test_only_this_gateway_is_slow(gateway, other_gateway):
    # Instance scope: other instances keep the real method.
    with patch.object(gateway, "timeout_seconds", 0.01):
        assert gateway.timeout_seconds == 0.01
        assert other_gateway.timeout_seconds == 5.0
Class-scope versus instance-scope patching Patching the charge method on the Gateway class replaces it for every instance, including two created inside the code under test. Patching an attribute on one specific gateway instance replaces it only on that instance, leaving a second instance with the original value. Where the patch lands decides who sees it patch.object(Gateway, "charge") instance a: patched instance b: patched methods are looked up on the class, so every instance sees the double — including ones the code creates patch.object(gateway, "timeout") gateway: patched other: original instance attribute shadows the class one for that object only precise, and fully restored
Class scope is right when the code creates its own instances; instance scope is right when the test already holds the object it wants to change.

Why this works

patch.object(target, name, new) does three things: it records getattr(target, name), it calls setattr(target, name, new), and on exit it restores the recorded value — or deletes the attribute if it did not previously exist on that object. Because Python looks up methods on the class, replacing a method on the class changes behaviour for every instance, including instances created inside the code under test that the test never sees. Replacing an attribute on a single instance adds an instance-level attribute that shadows the class one for that object alone.

The restore step is what makes the technique safe to use freely. Whether the block exits normally, through a failed assertion, or through an unexpected exception, the original attribute is put back, so one test's patch never leaks into the next. That guarantee holds only for the context-manager and decorator forms, which is why the manual start/stop API is best reserved for fixtures that pair them in a finally.

Passing the object rather than a string removes the whole category of patch-target mistakes. There is no module path to resolve, no import that might pick up a different copy, and no string for a refactoring tool to miss.

Edge cases and failure modes

  • Constants copied at import. TIMEOUT = Settings.TIMEOUT at module level, or a default argument def f(timeout=Settings.TIMEOUT), captures the value once. Patching Settings.TIMEOUT later has no effect on those copies. Patch where the value is read.
  • Patching a method on an instance with autospec. The autospecced double for a bound method does not expect self. On the class it does. Match the scope to the spec.
  • Properties. patch.object(Gateway, "timeout", 0.01) replaces the property descriptor with a plain value on the class, which works but loses the property's logic for every instance. Use new_callable=PropertyMock to keep it a property.
  • Class methods and static methods. Patching them on the class needs care, because the descriptor wraps the function. autospec=True handles it correctly.
  • Leaking patches. The non-context form, patcher.start(), must be paired with stop(). Use monkeypatch or the context manager so a failing test still restores.

When a value is read, not where it is defined

The single most common reason a patch.object appears to do nothing is that the code under test does not read the attribute at the moment the test expects. Python binds values at specific times, and a patch applied after the binding changes nothing the code will see.

Three binding times account for nearly every case. Module import: DEFAULT_TIMEOUT = Settings.TIMEOUT at the top of a module captures the value once, when the module is first imported, and every later reference uses that copy. Function definition: a default argument def fetch(timeout=Settings.TIMEOUT) is evaluated once, when the def statement runs. Object construction: self.timeout = Settings.TIMEOUT inside __init__ copies the value into each instance at the moment it is created, so instances built before the patch keep the old value.

Only code that reads Settings.TIMEOUT at call time — inside a function body, on each invocation — sees a patch applied during the test. The practical rule is to find the line where the value is actually read in the code path under test, and patch the object that line reads from. If that line reads a module-level copy, patch the copy; if it reads an instance attribute set at construction, patch the instance or construct it inside the patch.

When a class attribute's value is captured Four binding times. A module-level copy is captured at import. A default argument is captured at function definition. An instance attribute set in init is captured at construction. Only a read inside a function body at call time sees a patch applied during the test. A patch only affects reads that happen after it module import — TIMEOUT = Settings.TIMEOUT → patch too late function definition — def f(timeout=Settings.TIMEOUT) → patch too late construction — self.timeout = Settings.TIMEOUT → only instances built inside the patch call time — return Settings.TIMEOUT * 2 → sees the patch
When a patch has no effect, find the line that reads the value and check which of these four rows it belongs to.

This is also an argument for reading configuration at call time in production code wherever the cost allows. It makes the code patchable without cleverness, and it makes runtime reconfiguration possible for the same reason. Where reading at call time is too expensive, injecting the value through a constructor gives tests the same control without any patching at all.

patch.object versus monkeypatch.setattr

pytest's monkeypatch fixture offers monkeypatch.setattr(target, name, value), which does almost the same thing as patch.object and is often the more natural choice inside a pytest suite. The two differ in ways that make each better for particular jobs.

monkeypatch.setattr is scoped to the test automatically. Every change is undone at teardown without a context manager or decorator, so a test that patches five attributes reads as five plain lines rather than a nest of with blocks. It also accepts a dotted string as its first argument, but the object form is the one to prefer for the same reasons as with patch.object. What it does not do is create a mock: the value passed is installed as-is, so replacing a method with something that records calls means building the Mock explicitly.

patch.object creates the mock for you, supports autospec and spec_set, and returns the double for assertions. It is the better tool when the replacement is a mock whose calls the test will inspect, and when signature checking matters.

A common and effective combination uses both: monkeypatch.setattr for plain values — constants, feature flags, configuration — where no call recording is needed, and patch.object(..., autospec=True) for methods whose calls the test asserts on. The result reads cleanly — configuration changes as flat lines at the top of the test, method doubles as explicit context managers around the action — and each tool does the part of the job it is designed for, with neither stretched to cover the other's case.

patch.object and monkeypatch.setattr compared Two tools. monkeypatch.setattr is undone automatically at teardown and installs a plain value, suiting constants and flags. patch.object creates a mock, supports autospec and spec_set, and returns the double for assertions, suiting methods whose calls the test inspects. Values with one, recorded calls with the other monkeypatch.setattr undone at teardown automatically installs the value you pass constants, flags, config no nesting, one line each patch.object creates the mock for you autospec, spec_set, return value methods you assert on signature checking included
Using each for what it does best keeps tests short without giving up the signature checks that make method doubles trustworthy.

Frequently Asked Questions

What is the difference between patch and patch.object?patch takes a dotted string and resolves it by importing the module at patch time. patch.object takes the object itself and the attribute name, so there is no string to get wrong and no import-time lookup. Both restore the original when the context exits.

Does patching a method on the class affect existing instances? Yes. Methods are looked up on the class, so patching the class attribute changes behaviour for every instance, including ones created before the patch. Patching an instance attribute affects only that instance.

How do I patch a class-level constant?patch.object(MyClass, "MAX_RETRIES", 1) replaces the value for the duration of the context. Code that copied the constant into a local variable or a default argument at import time will not see the change; patch where the value is read.

← Back to Patching Strategies for Complex Codebases