Skip to content

How to Access and Use Annotations in Python Functions

Learn how to define, retrieve, and process Python function annotations for type hints, validation, and metadata storage using __annotations__, inspect.signature(), and typing.get_type_hints().

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}"

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: 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

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:

  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.
Navigation