How to Access and Use Annotations in Python Functions
Function annotations, introduced in Python 3, allow you to attach metadata to function parameters and return values. While most commonly used for type hinting, annotations can store any arbitrary Python object. Accessing this metadata programmatically is powerful for tasks like validation, documentation generation, and serialization.
This guide explains how to define, access, and process function annotations using Python's native tools and the typing module.
Understanding Function Annotations
Annotations are optional metadata attached to parameters and return values. They are defined using a colon : after the parameter name and an arrow -> after the parameter list.
def calculate_area(width: float, height: float) -> float:
"""Calculates the area of a rectangle."""
return width * height
In this example:
widthandheightare annotated withfloat.- The return value is annotated with
float. - Python does not enforce these types at runtime; they are stored as information.
Method 1: Accessing __annotations__ (Direct Access)
The simplest way to retrieve annotations is via the function's __annotations__ attribute. This attribute stores the annotations in a dictionary.
def greet(name: str, age: int = 25) -> str:
return f"Hello {name}, age {age}"
# ✅ Access the __annotations__ dictionary
anns = greet.__annotations__
print(f"Annotations Dictionary: {anns}")
# Access specific parameter annotation
print(f"Name type: {anns['name']}")
print(f"Return type: {anns['return']}")
Output:
Annotations Dictionary: {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}
Name type: <class 'str'>
Return type: <class 'str'>
Direct access via __annotations__ may return strings instead of actual classes if from __future__ import annotations is used (common in modern Python to handle forward references). Method 3 resolves this.
Method 2: Using inspect.signature() (Robust Access)
For more detailed introspection, such as separating annotations from default values, the inspect module is the standard tool.
import inspect
def process_data(items: list[int], verbose: bool = True) -> None:
pass
# ✅ Get the function signature
sig = inspect.signature(process_data)
# Iterate through parameters
for name, param in sig.parameters.items():
print(f"Parameter: {name}")
print(f" - Annotation: {param.annotation}")
print(f" - Default: {param.default}")
# Get return annotation
print(f"Return Annotation: {sig.return_annotation}")
Output:
Parameter: items
- Annotation: list[int]
- Default: <class 'inspect._empty'>
Parameter: verbose
- Annotation: <class 'bool'>
- Default: True
Return Annotation: None
Method 3: Using typing.get_type_hints() (Resolving References)
When using stringized annotations (e.g., "int" instead of int, or when using from __future__ import annotations), the previous methods might return strings. The typing.get_type_hints() function correctly evaluates these strings into actual Python types.
from typing import get_type_hints
def connect(host: "str", port: "int") -> "bool":
return True
# ⛔️ Direct access shows strings
print(f"Direct: {connect.__annotations__}")
# ✅ typing.get_type_hints resolves strings to types
hints = get_type_hints(connect)
print(f"Resolved: {hints}")
Output:
Direct: {'host': 'str', 'port': 'int', 'return': 'bool'}
Resolved: {'host': <class 'str'>, 'port': <class 'int'>, 'return': <class 'bool'>}
Use typing.get_type_hints() whenever you are building tools that rely on the actual type objects (like runtime type checkers or serializers), rather than just the raw annotation text.
Conclusion
To effectively work with function annotations in Python:
- Use
func.__annotations__for quick, simple dictionary access. - Use
inspect.signature(func)when you need to handle default values and parameters together. - Use
typing.get_type_hints(func)for the most robust parsing, especially to handle forward references and string-based type hints correctly.