Python subclass of native type with attr handler cannot properly store attributes
Port, board and/or hardware
esp32s3
MicroPython version
Micropyton version 1.27.0
When a Python class inherits from a native type that uses an attr handler to implement properties, setting attributes on instances fails to call the native property setters. Instead, the values are stored in the instance's members dictionary, shadowing the native properties.
Reproduction
Test script to reproduce MicroPython attr handler bug with Python subclasses.
Uses uctypes.struct which is a built-in MicroPython type with an attr handler.
No C compilation needed!
Bug: When a Python class inherits from a native type with an attr handler,
setting attributes stores them in the instance dict instead of calling the
native setter.
Expected: Setting struct fields should write to memory
Actual: Setting struct fields stores in instance dict and doesn't write to memory
import uctypes
# Define a simple struct layout: int32 at offset 0
STRUCT_LAYOUT = {
"value": uctypes.UINT32 | 0,
}
# Allocate memory for the struct
import array
mem_buffer = array.array('I', [0]) # One 32-bit integer initialized to 0
addr = uctypes.addressof(mem_buffer)
print("=== MicroPython Attr Handler Bug Reproduction ===")
print("Using uctypes.struct (built-in native type with attr handler)\n")
# Test 1: Native type works correctly
print("Test 1: Native uctypes.struct (baseline)")
print("-" * 50)
s1 = uctypes.struct(addr, STRUCT_LAYOUT, uctypes.NATIVE)
print(f"Initial memory value: {mem_buffer[0]}")
print(f"Read via struct: {s1.value}")
s1.value = 42
print(f"After setting s1.value = 42:")
print(f" Memory buffer: {mem_buffer[0]}")
print(f" Read via struct: {s1.value}")
if mem_buffer[0] == 42:
print("✓ Native type works correctly - value written to memory\n")
else:
print("✗ UNEXPECTED: Native type failed!\n")
# Reset memory
mem_buffer[0] = 0
# Test 2: Python subclass - this triggers the bug
print("Test 2: Python subclass of uctypes.struct (BUG)")
print("-" * 50)
class MyStruct(uctypes.struct):
"""Python subclass of native uctypes.struct"""
pass
s2 = MyStruct(addr, STRUCT_LAYOUT, uctypes.NATIVE)
print(f"Initial memory value: {mem_buffer[0]}")
print(f"Read via struct: {s2.value}")
# BUG: This should write to memory but stores in instance dict instead!
s2.value = 99
print(f"\nAfter setting s2.value = 99:")
print(f" Memory buffer: {mem_buffer[0]}")
print(f" Read via struct: {s2.value}")
# Check if value is in instance dict
if hasattr(s2, '__dict__'):
print(f" Instance __dict__: {s2.__dict__}")
if 'value' in s2.__dict__:
print("\n✗ BUG CONFIRMED!")
print(" 'value' is stored in instance __dict__!")
print(" The native attr handler was NOT called for STORE!")
print(" Memory was NOT updated!")
else:
print("\n✓ Native attr handler was called correctly")
else:
# Check if memory was actually written
if mem_buffer[0] == 99:
print("\n✓ Native attr handler was called correctly")
else:
print("\n✗ BUG CONFIRMED!")
print(f" Memory still contains: {mem_buffer[0]}")
print(" The native attr handler was NOT called!")
Expected behaviour
When storing an attribute on a Python instance of a native type with an attr handler:
The attr handler should be called with dest[0] = MP_OBJ_SENTINEL, dest[1] = value
Only if the attr handler returns without setting dest[0] should the value be stored in members dict
Impact:
Observed behaviour
This breaks property implementations for attribute setters for native types when used with Python subclasses.
Additional Information
Root Cause:
In objtype.c:
mp_obj_instance_load_attr() (~line 590) checks self->members first and returns immediately if found, never calling the native attr handler for subsequent reads
mp_obj_instance_store_attr() (~line 690) calls mp_obj_class_lookup() to find properties/descriptors, but mp_obj_class_lookup() only checks locals_dict and slots - it never invokes the native type's attr handler
For LOAD, there's special handling in mp_obj_class_lookup() (~line 215-220) that calls mp_load_method_maybe() on the native subobject, but there's no equivalent for STORE operations.
Suggested Fix:
In mp_obj_class_lookup(), when lookup->is_type == false and a native base type is found, check if it has an attr handler. If so, call it with the appropriate dest array for both LOAD and STORE operations before falling back to locals_dict lookup.
Note: most of the content was AI-Generated, but the Bug is real.
Code of Conduct
Yes, I agree
py/objtype: Ensures native attr handlers process subclass attribute stores
Summary
Problem Description
When a Python class inherits from a native type that uses an attr handler to implement properties, setting attributes on instances fails to call the native property setters. Instead, the values are stored in the instance's members dictionary, shadowing the native properties.
Root Cause
In objtype.c:
mp_obj_instance_store_attr()did not check if the native base type has an attr handler before storing attributes in the instance's member dictionary- For LOAD operations,
mp_obj_class_lookup()has special handling that callsmp_load_method_maybe()on the native subobject, but there was no equivalent for STORE operations
Solution
Modified mp_obj_instance_store_attr() to:
- Check for native base type early: Before any other processing, check if the instance's type has a native base class with an attr handler
- Try the native attr handler first: Call the native type's attr handler with
MP_OBJ_SENTINELto attempt the store/delete operation - Handle exceptions gracefully: If the attr handler raises
AttributeErrororKeyError(indicating the attribute isn't recognized), catch the exception and proceed with normal dict storage - Fall back to normal processing: If the attr handler doesn't handle the attribute, continue with property/descriptor checks and dict storage
Fixes #18592
Testing
This adds a test on based on uctypes in tests/extmod/uctypes_subclass_attrs.py.
Test Coverage:
-
Path 1: Native attr handler successfully processes attribute (Tests 1-2)
- Native type and subclass correctly delegate to attr handler
- Values written to memory, not stored in
__dict__ - Verifies the bug fix works correctly
-
Path 2: KeyError fallback to dict storage (Test 3)
- Unknown attribute triggers
KeyErrorin native handler - Exception caught, falls back to instance
__dict__storage - Python attributes coexist with native attributes
- Unknown attribute triggers
-
Path 3: TypeError re-raise (Test 4)
- Invalid type (
stringinstead ofUINT32) triggersTypeErrorin handler - Not
KeyError/AttributeError→ re-raised vianlr_jump() - Critical defensive programming path
- Invalid type (
-
Path 4: Test everything still works
Trade-offs and Alternatives
TBD