Python annotations: What Is Stored Inside?

In Python, the __annotations__ attribute is a dictionary that records type hints and metadata explicitly declared on functions, classes, and modules. This article details the exact data stored within the __annotations__ dictionary for both functions and classes, how keys and values are represented, and how runtime evaluation affects its contents.

Function Annotations

When defined on a function, the __annotations__ dictionary maps parameter names to their declared type annotations, along with a dedicated key for the return type.

For example, consider the following function:

def calculate_tax(subtotal: float, discount: float = 0.0, *items: str) -> float:
    return subtotal - discount

Accessing calculate_tax.__annotations__ yields:

{
    'subtotal': <class 'float'>,
    'discount': <class 'float'>,
    'items': <class 'str'>,
    'return': <class 'float'>
}

Class Annotations

When defined on a class, __annotations__ stores variable annotations declared within the class body.

For example, in the following class:

class User:
    username: str
    user_id: int
    is_active: bool = True

    def __init__(self, role: str):
        self.role: str = role

Accessing User.__annotations__ contains only the class-body attributes:

{
    'username': <class 'str'>,
    'user_id': <class 'int'>,
    'is_active': <class 'bool'>
}

The self.role declaration inside __init__ is an instance-level attribute assignment and is not recorded in User.__annotations__.

Evaluated Objects vs. Strings

By default, the values inside __annotations__ are references to the actual Python objects (such as <class 'int'>, typing.List, or custom classes). However, two exceptions alter this behavior:

  1. Forward References: If a type annotation is wrapped in quotes (e.g., 'User'), the value stored in the dictionary remains a raw string literal.
  2. Deferred Evaluation: When from __future__ import annotations is enabled (PEP 563), all annotations are automatically converted into strings at definition time rather than evaluated objects. This avoids circular import issues and reduces runtime overhead.

To safely resolve stringified annotations back to their underlying types at runtime, Python 3.10 and later provides inspect.get_annotations(obj), which standardizes accessing and evaluating the contents of __annotations__.