Repository: python/cpython
Pull request: gh-99535: Add test for inheritance of annotations and update documentation
<!--
Thanks for your contribution!
Please read this comment in its entirety. It's quite important.

# Pull Request title

It should be in the following format:

```
gh-NNNNN: Summary of the changes made
```

Where: gh-NNNNN refers to the GitHub issue number.

Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.

# Backport Pull Request title

If this is a backport PR (PR made against branches other than `main`),
please ensure that the PR title is in the following format:

```
[X.Y] <title from the original PR> (GH-NNNN)
```

Where: [X.Y] is the branch name, e.g. [3.6].

GH-NNNN refers to the PR number from `main`.

-->
Closes #99535.
I've added a simple test for checking that annotations are not inherited by derived classes and update the documentation about __annotations__

<!-- gh-issue-number: gh-99535 -->
* Issue: gh-99535
<!-- /gh-issue-number -->


Problem statement:
`__annotations__` are not inherited in 3.10 while they are in 3.8
# Bug report

Python 3.8.13 inherits annotations from a subclass:

```bash
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:

```bash
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?

<!-- gh-linked-prs -->
### Linked PRs
* gh-99990
* gh-100507
* gh-100509
<!-- /gh-linked-prs -->

Hints from issue discussion:
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 `TypedDict`s and `NamedTuple`s inherit all annotations from parents. Classes are inconsistent to them.

Should we treat this as a bug? 🤔 
3.12 `main` branch:

```python
>>> 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:

```python
>>> 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__`:

```python
>>> 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`:

```python
>>> 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})
```
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.
2f2b69855d6524e15d12c15ddc0adce629e7de84 and  #88067
Yes, you are right. The change is intentional: https://github.com/python/cpython/issues/88067#issuecomment-1093911703
So, adding tests and docs about it should be the way to go? 🤔 
@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.

Commit message: Add note to get_type_hints

Patch to review:
diff --git a/Doc/howto/annotations.rst b/Doc/howto/annotations.rst
index 2bc2f2d4c839e2..c63814492c99e5 100644
--- a/Doc/howto/annotations.rst
+++ b/Doc/howto/annotations.rst
@@ -57,6 +57,10 @@ Accessing The Annotations Dict Of An Object In Python 3.10 And Newer
   newer is to call :func:`getattr` with three arguments,
   for example ``getattr(o, '__annotations__', None)``.
 
+  Starting from Python 3.10, accessing the annotations
+  of a class will not give anymore the ones of its base
+  classes.
+
 
 Accessing The Annotations Dict Of An Object In Python 3.9 And Older
 ===================================================================
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 94c9cb11f02d6d..66a4c648839fcf 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -2773,6 +2773,10 @@ Introspection helpers
    .. versionchanged:: 3.9
       Added ``include_extras`` parameter as part of :pep:`593`.
 
+   .. versionchanged:: 3.10
+      ``__annotations__`` of a class does not contain anymore the annotations
+      of its base classes.
+
    .. versionchanged:: 3.11
       Previously, ``Optional[t]`` was added for function and method annotations
       if a default value equal to ``None`` was set.
diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py
index 58f907eac09d53..b77362601f6f98 100644
--- a/Lib/test/test_grammar.py
+++ b/Lib/test/test_grammar.py
@@ -415,6 +415,27 @@ class Cbad2(C):
                 x: int
                 x.y: list = []
 
+    def test_annotations_inheritance(self):
+        # Check that annotations are not inherited by derived classes
+        class A:
+            attr: int
+        class B(A):
+            pass
+        class C(A):
+            attr: str
+        class D:
+            attr2: int
+        class E(A, D):
+            pass
+        class F(C, A):
+            pass
+        self.assertEqual(A.__annotations__, {"attr": int})
+        self.assertEqual(B.__annotations__, {})
+        self.assertEqual(C.__annotations__, {"attr" : str})
+        self.assertEqual(E.__annotations__, {})
+        self.assertEqual(F.__annotations__, {})
+
+
     def test_var_annot_metaclass_semantics(self):
         class CMeta(type):
             @classmethod


Code review comments:
diff_hunk: @@ -2773,6 +2773,10 @@ Introspection helpers
    .. versionchanged:: 3.9
       Added ``include_extras`` parameter as part of :pep:`593`.
 
+   .. versionchanged:: 3.10
+      ``__annotations__`` of a class does not contain anymore the annotations
path: Doc/library/typing.rst
text: @user1:
Feels weird to mention that here since there's no change to  `get_type_hints` itself.

@author:
I've added it after a suggestion from @user2. To be fair, the method is almost equivalent to `__annotations__` so also for me would make sense to signal the change in some way. 
What do you think about something like "The method does not return anymore the hints of the base classes of obj" instead?

@user2:
As a maintainer of several runtime libraries that work with annotaions, I would expect to see it here, because it is the main entrypoint for runtime annotations.

Yes, technically it was not changed, but `.__annotations__` was.

@user1:
Maybe the note here should say what changed in the observable behavior of `get_type_hints()` instead of talking about `__annotations__`.
```suggestion
      Calling ``get_type_hints()`` on a class no longer returns the annotations
```

@author:
@user1 that's a great compromise. I've modified the entry.

diff_hunk: @@ -57,6 +57,10 @@ Accessing The Annotations Dict Of An Object In Python 3.10 And Newer
   newer is to call :func:`getattr` with three arguments,
   for example ``getattr(o, '__annotations__', None)``.
 
+  Starting from Python 3.10, accessing the annotations
+  of a class will not give anymore the ones of its base
+  classes.
path: Doc/howto/annotations.rst
text: @user1:
I would say something like this:

Before Python 3.10, accessing ``__annotations__`` on a class that defines no annotations but that has a parent class with annotations would return the parent's ``__annotations__``. In Python 3.10 and newer, the child class's annotations will be an empty dict instead.

@author:
The section in which the paragraph is located is explicitely about Python 3.10+, but I like the idea of being more explicit.
What do you think about:

Starting from Python 3.10 and newer, classes do not inherit anymore the `__annotations__` of their parent classes, so, accessing a class that does not define `__annotations__` will always return an empty dict.

@user2:
I think @user1's version is clearer. Even though the section is already about 3.10+, it is still clearer (and not out of place) to first explain the old behavior and then contrast the new behavior.