Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

Function annotations are arbitrary Python expressions associated with various parts of a function, specifically parameters and return values. They are evaluated at function definition time and stored as metadata within the function object, but they carry no inherent semantic meaning and are not enforced by the Python runtime.

Syntax

Annotations are defined using colons : for parameters and the arrow -> for return values.
def function_name(param1: annotation1, param2: annotation2) -> return_annotation:
    pass
When a parameter includes a default value, the annotation precedes the assignment operator =.
def function_name(param: annotation = default_value) -> return_annotation:
    pass
For variable-length arguments (*args and **kwargs), the annotation is placed immediately after the parameter name.
def function_name(*args: annotation, **kwargs: annotation) -> return_annotation:
    pass

Retrieving Annotations

Once the Python interpreter executes the def statement, the annotations are evaluated and stored in the function’s __annotations__ dunder attribute. This attribute is a standard Python dictionary where the keys are the parameter names (as strings) and the values are the evaluated annotation expressions. The return annotation, if present, is stored under the reserved key 'return'.
def calculate_velocity(distance: float, time: float = 1.0) -> float:
    return distance / time


# Accessing the annotations dictionary directly
print(calculate_velocity.__annotations__)
Output:
{'distance': <class 'float'>, 'time': <class 'float'>, 'return': <class 'float'>}
Since Python 3.10, the officially recommended best practice for retrieving annotations is using the inspect.get_annotations() function. This method provides a more robust interface than accessing the __annotations__ dictionary directly by handling edge cases like uninitialized attributes. However, by default, inspect.get_annotations() does not resolve stringized annotations (such as those generated when using from __future__ import annotations). It will return them exactly as they are stored: as strings. To evaluate stringized annotations into their corresponding Python objects, developers must explicitly pass eval_str=True to the function, or use typing.get_type_hints().
from __future__ import annotations
import inspect
import typing

def calculate_velocity(distance: float, time: float = 1.0) -> float:
    return distance / time


# Default behavior: returns stringized annotations as strings
print(inspect.get_annotations(calculate_velocity))
Output:
{'distance': 'float', 'time': 'float', 'return': 'float'}

# Resolving stringized annotations using eval_str=True
print(inspect.get_annotations(calculate_velocity, eval_str=True))


# Alternatively, resolving using typing.get_type_hints()
print(typing.get_type_hints(calculate_velocity))
Output:
{'distance': <class 'float'>, 'time': <class 'float'>, 'return': <class 'float'>}
{'distance': <class 'float'>, 'time': <class 'float'>, 'return': <class 'float'>}

Technical Characteristics

  • No Runtime Enforcement: The Python interpreter completely ignores annotations during function execution. Passing an argument of a different type than the annotation specifies will not raise a TypeError or affect the runtime behavior in any way.
  • Arbitrary Expressions: While commonly used for type hinting (classes or types), the Python language specification allows annotations to be any valid Python expression. This includes strings, integers, lists, dictionaries, or function calls.

# Annotations using arbitrary expressions rather than strict types
def complex_annotation(x: "The x coordinate", y: 10 + 20) -> {'status': bool}:
    pass

print(complex_annotation.__annotations__)
Output:
{'x': 'The x coordinate', 'y': 30, 'return': {'status': <class 'bool'>}}
  • Evaluation Timing and Forward References: By default, annotations are evaluated at function definition time. Any variables or expressions used within the annotation must be defined in the current scope before the def block is executed. If an annotation references a name that has not yet been defined (e.g., a method returning an instance of its own class), Python will raise a NameError. To resolve this, developers must use forward references by wrapping the annotation in a string literal:
class Node:
    def add_child(self, child: "Node") -> None:
        pass
Alternatively, PEP 563 introduced deferred evaluation of annotations. By adding the from __future__ import annotations directive at the top of a module, all annotations within that module are automatically stored as strings and are not evaluated at definition time. This prevents NameError exceptions for undefined names, reduces module initialization time, and requires the explicit resolution techniques (eval_str=True or typing.get_type_hints()) demonstrated in the retrieval section.
from __future__ import annotations

class Node:
    def add_child(self, child: Node) -> None:
        pass
Master Python with Deep Grasping Methodology!Learn More