How PropertyMock Intercepts Attribute Reads in Python

This article explains the internal mechanics behind Python’s unittest.mock.PropertyMock and how it intercepts attribute reads on mocked objects. While standard Mock objects return callable sub-mocks upon attribute access, PropertyMock uses Python's descriptor protocol to execute custom logic on access without requiring explicit function call syntax. By defining a custom __get__ method and attaching itself to a class definition, PropertyMock mimics dynamic attributes, properties, and getters seamlessly during testing.

The Role of Python’s Descriptor Protocol

To understand PropertyMock, you must first understand Python descriptors. A descriptor is an object attribute with "binding behavior," meaning its attribute access is overridden by methods in the descriptor protocol: __get__(), __set__(), and __delete__().

Built-in Python @property decorators are descriptors. When you access instance.my_attribute, Python inspects the class of instance. If it finds an object in the class namespace that defines a __get__() method, Python calls that method instead of retrieving a standard value from the instance dictionary:

# Behind the scenes:
type(instance).__dict__['my_attribute'].__get__(instance, type(instance))

PropertyMock is a subclass of Mock that implements this exact descriptor protocol.

How PropertyMock Implements __get__

Under the hood, PropertyMock overrides the __get__ method to intercept read requests. When an attribute represented by a PropertyMock is accessed:

  1. Access Trigger: The code attempts to read an attribute on an instance (e.g., obj.status).
  2. Descriptor Delegation: Because Python locates the PropertyMock on the class level, it invokes PropertyMock.__get__(self, obj, obj_type).
  3. Execution as a Callable: Inside __get__, PropertyMock calls itself: return self().
  4. Value Resolution: Calling self() triggers standard Mock evaluation rules. It increments the mock's call count, records call arguments, checks for assigned return_value or side_effect configurations, and returns the result to the caller.

Because __get__ invokes the mock automatically, the caller receives the return value directly without having to add parentheses () to invoke the property.

The Class vs. Instance Binding Requirement

Because descriptors rely on class-level lookup, Python only invokes __get__ automatically when the descriptor exists on the target object's class, not on the object's instance dictionary.

If you attach a PropertyMock directly to an instance:

from unittest.mock import Mock, PropertyMock

mock_instance = Mock()
mock_instance.status = PropertyMock(return_value="active")
print(mock_instance.status)

The output will be the <PropertyMock ...> instance itself, rather than the string "active". Python's attribute lookup rules bypass __get__ when an attribute is stored directly in an instance's __dict__.

To successfully intercept reads, PropertyMock must be assigned to the class:

from unittest.mock import Mock, PropertyMock

# Attach to the class of the mock
mock_instance = Mock()
type(mock_instance).status = PropertyMock(return_value="active")

# Accessing the attribute triggers __get__()
print(mock_instance.status)  # Outputs: 'active'

When using helper utilities like unittest.mock.patch.object, the patcher handles this automatically under the hood by binding the PropertyMock to the target object's class for the duration of the test:

from unittest.mock import patch

with patch.object(TargetClass, 'status', new_callable=PropertyMock) as mock_status:
    mock_status.return_value = "active"
    instance = TargetClass()
    assert instance.status == "active"
    mock_status.assert_called_once()

Intercepting Writes

In addition to intercepting reads, PropertyMock implements the descriptor's __set__ method. When an assignment occurs (e.g., obj.status = "idle"), PropertyMock.__set__(self, obj, val) executes, internally calling self(val). This captures the assignment as a function call with arguments, allowing assertions such as mock_status.assert_called_with("idle").