Weird behavior of attributes using PyTypeObject.tp_getset hooks

Hi, I am working on a prototype of deferred expression with CPython.

Background:

In my latest commit 3375c80, I have a builtin function expose that creates a DeferExprExposed object which provides access to otherwise hidden attributes on DeferExpr objects (link to code).

Observed Problem:

The attributes are provided through PyTypeObject.tp_getset hooks (link to code). However, I observed some really wired behavior with them:

  1. First attribute access fails, 2nd time OK (and so on):

    >>> x => 1 # Creates a DeferExpr object `x`
    >>> expose(x).callable # 1st try fails
    AttributeError: 'DeferExprExposed' object has no attribute 'callable'
    >>> expose(x).callable # 2nd try works
    <function <lambda> at 0x10395a560>
    

    Note that expose(x) returns a new DeferExprExposed object each time. It does not reuse the same exposed object. (i.e. two tries happens on two different objects).

  2. Accessing attribute on the same object fails on 1st try, but succeed otherwise:

    >>> x => 1 # Creates a DeferExpr object `x`
    >>> e = expose(x)
    >>> e.callable # 1st try fails
    AttributeError: 'DeferExprExposed' object has no attribute 'callable'
    >>> e.callable # 2nd try works
    <function <lambda> at 0x100df2a30>
    

This left me clueless on what’s going on…

Tracing down the code in cpython/Objects/object.c, I surprised to find out that there is no logic checking for tp_getset at all… Seems like it only cares about tp_getattr and tp_getattro.

I am now wondering how could the second access to the attribute ever work out for me… :exploding_head:

Opened an issue #126752 on github/cpython since this haven’t received any comments.

Self triaged: PyType_Ready() must be called before instantiating an object of that type.