Skip to content

[3.14] annotationlib - calling get_annotations on instances gets an unexpected error #132261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
DavidCEllis opened this issue Apr 8, 2025 · 6 comments · Fixed by #132345
Closed
Assignees
Labels
3.14 new features, bugs and security fixes stdlib Python modules in the Lib dir topic-typing type-bug An unexpected behavior, bug, or error

Comments

@DavidCEllis
Copy link
Contributor

DavidCEllis commented Apr 8, 2025

Bug report

Bug description:

In 3.13, inspect.get_annotations would fail if called on instances and give you a TypeError explaining as much.

In 3.14 if you call annotationlib.get_annotations(inst, format=Format.STRING) on an instance you currently get a more confusing error.

from annotationlib import get_annotations, Format

class Example:
    a: int

ex = Example()

print(get_annotations(ex, format=Format.STRING))
Traceback (most recent call last):
  File "<python-input-8>", line 9, in <module>
    print(get_annotations(ex, format=Format.STRING))
          ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/david/src/cpython/Lib/annotationlib.py", line 719, in get_annotations
    ann = _get_and_call_annotate(obj, format)
  File "/home/david/src/cpython/Lib/annotationlib.py", line 826, in _get_and_call_annotate
    ann = call_annotate_function(annotate, format, owner=obj)
  File "/home/david/src/cpython/Lib/annotationlib.py", line 513, in call_annotate_function
    return annotate(format)
TypeError: Example.__annotate__() takes 1 positional argument but 2 were given

As far as I can tell this appears to be because __annotate__ has turned into a bound method.

You currently only see this if you use Format.STRING due to #125618 masking the issue, as with both other formats if __annotations__ has been created on the class it is used instead of calling __annotate__ and if it doesn't you get an empty dict.

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

@DavidCEllis DavidCEllis added the type-bug An unexpected behavior, bug, or error label Apr 8, 2025
@picnixz picnixz added stdlib Python modules in the Lib dir topic-typing 3.14 new features, bugs and security fixes labels Apr 8, 2025
@picnixz
Copy link
Member

picnixz commented Apr 8, 2025

cc @JelleZijlstra

@JelleZijlstra JelleZijlstra self-assigned this Apr 8, 2025
@JelleZijlstra
Copy link
Member

That's a difficult one. It's because ex.__annotate__ looks up the name in the class namespace and succeeds. This might get even worse if we do #124157 and the class dict contains some other internal object for __annotate__ instead of a function.

One approach I can think of would be to make it so the __annotate__ function doesn't turn into a bound method, for example by setting some flag on the function so that its __get__ method raises, or by wrapping the function with another descriptor. However, that would come with other costs (implementation complexity, maybe extra memory usage).

@JelleZijlstra
Copy link
Member

Actually I thought of something: we can store the annotate function at some other key in the class dictionary than __annotate__. The type_get_annotate descriptor can still look for the internal name, but then we don't have to worry about accessing it by accident. I tried this locally and it appears to work. This might also serve as a better fix for some aspects of https://peps.python.org/pep-0749/#annotations-and-metaclasses .

Turns out I should have eaten breakfast before spending time on GitHub :)

@DavidCEllis
Copy link
Contributor Author

DavidCEllis commented Apr 8, 2025

I slightly worry about moving __annotate__ around as I'm going to need some way to gain access to it via metaclass __new__ in order to continue generating __slots__ for a dataclass-like without having to create and recreate a class and then deal with all of the bugs associated with that method. Currently I'm pulling it out of the class namespace in order to use call_annotate_function on it.

My suggestion for this bug would have just been to wrap it as a staticmethod if possible so that __annotate__(...) works on an instance the same way __annotations__ does (in 3.13 - __annotations__ doesn't really work nicely in 3.14 either).

Breakfast on the other hand is always a good idea. :)

@JelleZijlstra
Copy link
Member

I'm going to need some way to gain access to it via metaclass __new__

What if we made https://docs.python.org/3.14/library/annotationlib.html#annotationlib.get_annotate_function support a dictionary as its argument so you can use it in this context? I generally want to get rid of any case where users may be tempted to reach directly into the dictionary, because that means we lose the flexibility to change later exactly what we put in the dictionary.

wrap it as a staticmethod if possible

I thought of that too; not too excited about the complexity it introduces, and it means that if you access __annotate__ it's no longer necessarily a function object.

so that __annotate__(...) works on an instance the same way __annotations__ does (in 3.13 - __annotations__ doesn't really work nicely in 3.14 either).

I forgot that .__annotations__ works on instances in earlier versions; we are indeed breaking that. But inspect.get_annotations() didn't work on instances already, and there were a few other sharp edges with accessing .__annotations__ on an instance, so I think it's better to make this fully consistent. Annotations are an attribute of the class, not the instance, so I think it's actually better that you can access them only on the class.

@DavidCEllis
Copy link
Contributor Author

I'm going to need some way to gain access to it via metaclass __new__

What if we made https://docs.python.org/3.14/library/annotationlib.html#annotationlib.get_annotate_function support a dictionary as its argument so you can use it in this context? I generally want to get rid of any case where users may be tempted to reach directly into the dictionary, because that means we lose the flexibility to change later exactly what we put in the dictionary.

I'm not sure - I'll note I'm not the only one doing this as someone else was also asking what the best practice would be for getting and replacing __annotate__ for a metaclass on the discuss help forum. So there definitely needs to be a defined way to do this. I'd prefer to keep __annotate__ in the namespace as a function as that seems slightly less magical?1


I thought of that too; not too excited about the complexity it introduces, and it means that if you access __annotate__ it's no longer necessarily a function object.

Ah I hadn't noticed that as I'd forgotten to look in __dict__ and only looked at X.__annotate__ and x.__annotate__. Would be nice if it could just behave like a built-in function.

Footnotes

  1. And admittedly also because I wouldn't need to change the existing code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.14 new features, bugs and security fixes stdlib Python modules in the Lib dir topic-typing type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants