← index #18592PR #9997
Related · medium · value 0.527
QUERY · ISSUE

Python subclass of native type with attr handler cannot properly store attributes

openby cnadler86opened 2025-12-18updated 2025-12-31
bug

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

CANDIDATE · PULL REQUEST

py/runtime: Avoid crash on calling members of uninitialized native type.

closedby laurensvalkopened 2022-11-17updated 2024-07-25
py-core

When subclassing a native type, calling native members in __init__ before super().__init__() has been called could cause a crash. In this situation, self in mp_convert_member_lookup is the native_base_init_wrapper_obj. This check ensures that a TypeError is raised when this happens.

Also fix a typo in a related comment.


This is an attempt at fixing the following crash in Pybricks MicroPython

from pybricks.parameters import Port
from pybricks.pupdevices import Motor  # Builtin type

class NewMotor(Motor):

    def __init__(self, port):

        # Would crash. After this PR it raises a TypeError.
        print(self.angle())

        super().__init__(port)

        # Would work.
        print(self.angle())

motor = NewMotor(Port.A)

EDIT: This isn't quite the right solution as seen in the tests, so I'll have to have another look.

This variant appears to work without breaking the other tests, but still needs some cleaning up:

diff --git a/py/objtype.c b/py/objtype.c
index e0753aace..101a82beb 100644
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -91,7 +91,7 @@ STATIC mp_obj_t native_base_init_wrapper(size_t n_args, const mp_obj_t *args) {
     self->subobj[0] = native_base->make_new(native_base, n_args - 1, 0, args + 1);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(native_base_init_wrapper_obj, 1, MP_OBJ_FUN_ARGS_MAX, native_base_init_wrapper);
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(native_base_init_wrapper_obj, 1, MP_OBJ_FUN_ARGS_MAX, native_base_init_wrapper);
 
 #if !MICROPY_CPYTHON_COMPAT
 STATIC
diff --git a/py/runtime.c b/py/runtime.c
index 5a7474fba..62254af0d 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -1111,13 +1111,13 @@ void mp_convert_member_lookup(mp_obj_t self, const mp_obj_type_t *type, mp_obj_t
                 #endif
                 else {
                     // Return a (built-in) bound method, with self being this object.
-                    #if MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG
-                    // Ensures that self is not uninitialized object.
-                    dest[0] = mp_obj_new_checked_fun(type, member);
-                    #else 
                     dest[0] = member;
-                    #endif
                     dest[1] = self;
+                    extern const mp_obj_fun_builtin_var_t native_base_init_wrapper_obj;
+                    if (self == MP_OBJ_FROM_PTR(&native_base_init_wrapper_obj)) {
+                        // object not initialized yet
+                        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("call super().__init__() first"));
+                    }
                 }
             } else {
                 // Return a bound method, with self being this object.

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied