Skip to main content

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:

  • width and height are annotated with float.
  • 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'>
warning

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'>}
note

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:

  1. Use func.__annotations__ for quick, simple dictionary access.
  2. Use inspect.signature(func) when you need to handle default values and parameters together.
  3. Use typing.get_type_hints(func) for the most robust parsing, especially to handle forward references and string-based type hints correctly.