python / cpython

The Python programming language
https://www.python.org
Other
63.27k stars 30.29k forks source link

ModuleType.__annotations__ and type.__annotations__ result in AttributeError #123242

Closed ndjensen closed 1 month ago

ndjensen commented 2 months ago

Bug report

Bug description:

If you call dir() on ModuleType or type, it shows __annotations__ as one of the attributes. However, when you attempt to access that attribute, for ModuleType you get AttributeError: type object 'module' has no attribute '__annotations__' and for type you get AttributeError: type object 'type' has no attribute '__annotations__'.

>>> from types import ModuleType
>>> dir(ModuleType)
['__annotations__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> ModuleType.__annotations__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'module' has no attribute '__annotations__'
>>> 
>>> dir(type)
['__abstractmethods__', '__annotations__', '__base__', '__bases__', '__basicsize__', '__call__', '__class__', '__delattr__', '__dict__', '__dictoffset__', '__dir__', '__doc__', '__eq__', '__flags__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__instancecheck__', '__itemsize__', '__le__', '__lt__', '__module__', '__mro__', '__name__', '__ne__', '__new__', '__or__', '__prepare__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__setattr__', '__sizeof__', '__str__', '__subclasscheck__', '__subclasses__', '__subclasshook__', '__text_signature__', '__weakrefoffset__', 'mro']
>>> type.__annotations__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'type' has no attribute '__annotations__'

I found this bug because I have a method that iterates over the attributes returned from dir and gets each attribute. After upgrading from Python 3.8 to 3.11, this caused an interesting failure. I would expect that attributes returned by dir() would be accessible and not result in an AttributeError.

CPython versions tested on:

3.11, 3.13

Operating systems tested on:

Linux

Linked PRs

sobolevn commented 2 months ago

Docs say:

With an argument, attempt to return a list of valid attributes for that object. https://docs.python.org/3/library/functions.html#dir

So, this seems like a bug to me. __annotations__ is not a valid attribute for this object. It should not be returned.

I can see __abstractmethods__ there as well, which also raises:

>>> type.__abstractmethods__
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    type.__abstractmethods__
AttributeError: __abstractmethods__

And __annotate__ for 3.14+

picnixz commented 2 months ago

Fun fact:

>>> type.__annotations__
Traceback (most recent call last):
  File "<python-input-62>", line 1, in <module>
    type.__annotations__
AttributeError: type object 'type' has no attribute '__annotations__'. Did you mean: '__annotate__'?
>>> type.__annotate__
Traceback (most recent call last):
  File "<python-input-63>", line 1, in <module>
    type.__annotate__
AttributeError: type object 'type' has no attribute '__annotate__'. Did you mean: '__annotations__'?
picnixz commented 2 months ago

@sobolevn Do you want to work on it? or can I take it perhaps?

sobolevn commented 2 months ago

@picnixz sure, go ahead!

picnixz commented 2 months ago

Ok, I see what happens:

static PyObject *
type_get_annotations(PyTypeObject *type, void *context)
{
    if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
        PyErr_Format(PyExc_AttributeError, "type object '%s' has no attribute '__annotations__'", type->tp_name);
        return NULL;
    }
    ...
}

So the attribute does exist. It's just that it immediately raises an error. The reason why it's being listed in __dir__ is that it's actually set in the __dict__. So the question is: is it really a bug or not? it results in an AttributeError, yes, but maybe we could return an empty dict in the case of type? or maybe change the error message? (and for non-heap types, we would just return empty dicts everytime, but still disallow to set __annotations__).

We could outright filter the output of type___dir___impl by rejecting __annotations__ and __annotate__ if we are dealing with a heap type, but only if a parent does not already have __annotations__.

cc @JelleZijlstra

JelleZijlstra commented 2 months ago

I don't think the issue reported here is severe enough to justify changing this behavior. As Nikita noted, type.__abstractmethods__ behaves similarly.

picnixz commented 2 months ago

Well.. maybe amend the docs a little? or at least the error message? (though I don't have an better error message for that). Otherwise, we should close the issue. For beginners and for introspection tools, it could be undesirable (in Sphinx, I think we do getattr(obj, name, sentinel) even if we iterate over the names returned by dir but I should check that's indeed the case, otherwise we could have surprises).