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 * heightIn 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}"
anns = greet.__annotations__
print(f"Annotations Dictionary: {anns}")
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'>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
sig = inspect.signature(process_data)
for name, param in sig.parameters.items():
print(f"Parameter: {name}")
print(f" - Annotation: {param.annotation}")
print(f" - Default: {param.default}")
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: NoneMethod 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
print(f"Direct: {connect.__annotations__}")
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'>}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.