Sitelet https://github.com/python/cpython/issues/99535
Skip to content

__annotations__ are not inherited in 3.10 while they are in 3.8 #99535

Description

@henrifroese

Bug report

Python 3.8.13 inherits annotations from a subclass:

Python 3.8.13 (default, Oct 19 2022, 17:54:22) 
[Clang 12.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...     x: int
... 
>>> class B(A):
...     pass
... 
>>> print(A.__annotations__)
{'x': <class 'int'>}
>>> print(B.__annotations__)
{'x': <class 'int'>}
>>> print(B.__dict__)
{'__module__': '__main__', '__doc__': None}

Python 3.10.8 does not:

Python 3.10.8 (main, Nov  4 2022, 08:45:18) [Clang 12.0.0 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...     x: int
... 
>>> class B(A):
...     pass
... 
>>> print(A.__annotations__)
{'x': <class 'int'>}
>>> print(B.__annotations__)
{}
>>> print(B.__dict__)
{'__module__': '__main__', '__doc__': None, '__annotations__': {}}

Can't find anything related to this in the changelogs, release notes, stackoverflow etc. Is this known/expected behaviour?

Linked PRs

Activity

  1. sobolevn commented on Nov 16, 2022

    @sobolevn
    Member

    I think we don't have tests for this 🤭

    At least, I am not able to find them.
    We have tests for TypedDict and NamedTuple that inherits annotations from parents.

    But, not for:

    • regular classes
    • dataclasses

    Right now TypedDicts and NamedTuples inherit all annotations from parents. Classes are inconsistent to them.

    Should we treat this as a bug? 🤔

  2. sobolevn commented on Nov 16, 2022

    @sobolevn
    Member

    3.12 main branch:

    >>> class A:
    ...   x: int
    ... 
    >>> class B(A):
    ...    ...
    ... 
    >>> A.__annotations__
    {'x': <class 'int'>}
    >>> B.__annotations__
    {}

    3.11 is the same.
    3.10 is the same
    3.9.9 is different:

    >>> class A:
    ...   x: int
    ... 
    >>> class B(A): ...
    ... 
    >>> A.__annotations__
    {'x': <class 'int'>}
    >>> B.__annotations__
    {'x': <class 'int'>}

    So, yes. I think it is a bug. And should be fixed / backported to 3.10 and 3.11

    Why?

    Looks like empty __annotations__ attribute is populated in __dict__:

    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__annotations__': {'x': <class 'int'>}, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
    >>> B.__dict__
    mappingproxy({'__module__': '__main__', '__doc__': None, '__annotations__': {}})

    Compare it to 3.9:

    >>> class A:
    ...   x: int
    ... 
    >>> class B:
    ...   ...
    ... 
    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__annotations__': {'x': <class 'int'>}, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
    >>> B.__dict__
    mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None})
  3. JelleZijlstra commented on Nov 16, 2022

    @JelleZijlstra
    Member

    I haven't checked the logs, but I suspect this is an intentional change made by @larryhastings as part of his efforts to improve __annotations__ handling. The 3.10+ behavior makes more sense to me: you Cls.__annotations__ should give you the annotations for the current class, not a base class.

  4. JelleZijlstra commented on Nov 16, 2022

    @JelleZijlstra
    Member
  5. sobolevn commented on Nov 16, 2022

    @sobolevn
    Member

    Yes, you are right. The change is intentional: #88067 (comment)

  6. sobolevn commented on Nov 16, 2022

    @sobolevn
    Member

    So, adding tests and docs about it should be the way to go? 🤔

  7. MonadChains commented on Dec 4, 2022

    @MonadChains
    Contributor

    @sobolevn I've added a small test to check the behavior of annotations w.r.t. inheritance. I've also updated the docs by adding a line to remark that annotations are not inherited anymore, but I'm not sure it is the right place for it.

  8. carljm commented on Dec 16, 2022

    @carljm
    Member

    One additional note:

    Right now TypedDicts and NamedTuples inherit all annotations from parents. Classes are inconsistent to them.

    I don't think this is quite accurate. NamedTuples behave the same as normal classes as far as the __annotations__ attribute is concerned:

    >>> from typing import NamedTuple
    >>> class A(NamedTuple):
    ...     x: int
    ...
    >>> A.__annotations__
    {'x': <class 'int'>}
    >>> class B(A):
    ...     pass
    ...
    >>> B.__annotations__
    {}
    >>> class C(A):
    ...     y: str
    ...
    >>> C.__annotations__
    {'y': <class 'str'>}

    i.e. __annotations__ are not inherited, the same as for normal classes. Annotations from base classes are still considered when deciding what fields the subclass should have, though, which is something specific to the use of annotations by NamedTuple. (The same is true for dataclasses.)

    TypedDicts do indeed "inherit" __annotations__, but this is a special case because of TypedDicts having very unusual inheritance behavior. When you inherit a TypedDict, all you get is its __annotations__; the base class itself does not even appear in your MRO! Anything subclassing TypedDict or any subclass of TypedDict ends up simply inheriting dict:

    >>> from typing import TypedDict
    >>> class A(TypedDict):
    ...     x: int
    ...
    >>> A.__annotations__
    {'x': <class 'int'>}
    >>> class B(A):
    ...     pass
    ...
    >>> B.__annotations__
    {'x': <class 'int'>}
    >>> B.mro()
    [<class '__main__.B'>, <class 'dict'>, <class 'object'>]
    >>> A.mro()
    [<class '__main__.A'>, <class 'dict'>, <class 'object'>]

    So in this special case "inheritance" of the __annotations__ attribute is necessary because the inheritance hierarchy itself is not preserved.

  9. added a commit that references this issue on Dec 24, 2022
  10. added a commit that references this issue on Dec 24, 2022
  11. added a commit that references this issue on Dec 24, 2022
  12. added a commit that references this issue on Dec 24, 2022
  13. added a commit that references this issue on Dec 27, 2022
  14. FrozenBob commented on Jan 1, 2023

    @FrozenBob
    Contributor

    The pull request introduced a documentation error for typing.get_type_hints. typing.get_type_hints recursively merges annotations from parent classes, which has always been intended behavior. The pull request mixed up this behavior with the __annotations__ inheritance change, and added an incorrect note to the typing.get_type_hints documentation claiming that typing.get_type_hints no longer does this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions