PEP 3107 Function Annotations
TL;DR
PEP 3107 introduced syntax for adding metadata to function parameters and return values, stored in __annotations__
- the foundation for modern type hints.
Interesting!
Function annotations were introduced in Python 3.0 without any built-in meaning - they were just a standardized way to attach metadata that third-party tools could interpret!
Basic Annotation Syntax
python code snippet start
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old"
# Annotations are stored in __annotations__
print(greet.__annotations__)
# {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}
python code snippet end
All Annotation Forms
python code snippet start
def complex_function(
required: str,
with_default: int = 42,
*args: tuple,
keyword_only: float = 3.14,
**kwargs: dict
) -> bool:
return True
print(complex_function.__annotations__)
# {'required': <class 'str'>, 'with_default': <class 'int'>,
# 'args': <class 'tuple'>, 'keyword_only': <class 'float'>,
# 'kwargs': <class 'dict'>, 'return': <class 'bool'>}
python code snippet end
Arbitrary Expressions
python code snippet start
# Annotations can be any expression, not just types
def annotated_function(
data: "input data",
count: 5 + 5,
items: list,
callback: callable
) -> max(1, 2, 3):
pass
print(annotated_function.__annotations__)
# {'data': 'input data', 'count': 10, 'items': <class 'list'>,
# 'callback': <built-in function callable>, 'return': 3}
python code snippet end
Early Type Hinting (Before typing module)
python code snippet start
# Before typing module, people used various conventions
def process_data(
numbers: "list of integers",
multiplier: "numeric value",
output_format: "str: 'json' or 'csv'"
) -> "formatted string":
"""Early type hinting with string descriptions."""
pass
# Some used actual types when possible
def calculate(
values: list,
operation: type(lambda: None), # Function type
default: object
) -> object:
pass
python code snippet end
Documentation and Metadata
python code snippet start
class ValidationError(Exception):
pass
def validate_user(
username: "Must be 3-20 characters",
email: "Valid email address",
age: "Integer between 13-120"
) -> "User object or raises ValidationError":
"""Annotations used for documentation."""
if not (3 <= len(username) <= 20):
raise ValidationError("Invalid username length")
return {"username": username, "email": email, "age": age}
# Access documentation through annotations
for param, description in validate_user.__annotations__.items():
if param != 'return':
print(f"{param}: {description}")
python code snippet end
Runtime Annotation Processing
python code snippet start
def annotation_inspector(func):
"""Decorator that inspects function annotations."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
# Inspect annotations
annotations = func.__annotations__
for name, annotation in annotations.items():
if name == 'return':
print(f"Returns: {annotation}")
else:
print(f"Parameter {name}: {annotation}")
return func(*args, **kwargs)
return wrapper
@annotation_inspector
def example(x: int, y: str = "default") -> bool:
return x > 0
example(5)
# Output:
# Calling example
# Parameter x: <class 'int'>
# Parameter y: <class 'str'>
# Returns: <class 'bool'>
python code snippet end
Early Validation Systems
python code snippet start
def type_check(func):
"""Simple type checking decorator using annotations."""
def wrapper(*args, **kwargs):
# Get parameter names
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
# Check annotations
for name, value in bound.arguments.items():
if name in func.__annotations__:
expected_type = func.__annotations__[name]
if isinstance(expected_type, type) and not isinstance(value, expected_type):
raise TypeError(f"{name} must be {expected_type.__name__}, got {type(value).__name__}")
return func(*args, **kwargs)
return wrapper
@type_check
def add_numbers(a: int, b: int) -> int:
return a + b
print(add_numbers(5, 3)) # Works: 8
try:
add_numbers("5", 3) # Fails: TypeError
except TypeError as e:
print(f"Error: {e}")
python code snippet end
Forward References
python code snippet start
# Before Python 3.7, forward references needed quotes
def process_node(node: "TreeNode") -> "TreeNode":
pass
class TreeNode:
def __init__(self, value):
self.value = value
def add_child(self, child: "TreeNode") -> "None":
pass
# Check the annotations
print(TreeNode.add_child.__annotations__)
# {'child': 'TreeNode', 'return': 'None'}
python code snippet end
Annotation Evolution
python code snippet start
# PEP 3107 era (basic annotations)
def old_style(data: list, formatter: callable) -> str:
pass
# Modern typing era (specific type information)
from typing import List, Callable
def new_style(data: List[str], formatter: Callable[[str], str]) -> str:
pass
# Python 3.9+ (built-in generics)
def newest_style(data: list[str], formatter: callable[[str], str]) -> str:
pass
python code snippet end
Custom Annotation Objects
python code snippet start
class ParameterSpec:
def __init__(self, description, valid_range=None):
self.description = description
self.valid_range = valid_range
def __repr__(self):
return f"ParameterSpec('{self.description}')"
def calculate_bmi(
weight: ParameterSpec("Weight in kg", (30, 300)),
height: ParameterSpec("Height in meters", (0.5, 2.5))
) -> ParameterSpec("BMI value"):
return weight / (height ** 2)
# Rich annotation metadata
for name, spec in calculate_bmi.__annotations__.items():
print(f"{name}: {spec}")
python code snippet end
Introspection Utilities
python code snippet start
def analyze_function(func):
"""Analyze function annotations and structure."""
print(f"Function: {func.__name__}")
if not hasattr(func, '__annotations__'):
print(" No annotations")
return
annotations = func.__annotations__
# Parameter annotations
params = [name for name in annotations if name != 'return']
if params:
print(" Parameters:")
for param in params:
print(f" {param}: {annotations[param]}")
# Return annotation
if 'return' in annotations:
print(f" Returns: {annotations['return']}")
print()
# Test with various functions
def unannotated(x, y):
pass
def partially_annotated(x: int, y):
pass
def fully_annotated(x: int, y: str) -> bool:
pass
for func in [unannotated, partially_annotated, fully_annotated]:
analyze_function(func)
python code snippet end
Historical Context
python code snippet start
# PEP 3107 deliberately avoided specifying semantics
def flexible_annotations(
# Could be types
number: int,
# Could be constraints
username: "length 3-20",
# Could be documentation
callback: "function called on completion",
# Could be validation rules
email: "valid email format"
) -> "success status":
"""Annotations were completely open-ended."""
pass
# This flexibility allowed the typing ecosystem to evolve
# from simple metadata to sophisticated type systems
python code snippet end
PEP 3107 provided the syntactic foundation that enabled Python’s entire type system ecosystem - from simple documentation to sophisticated static analysis tools! This annotation syntax evolved into modern type hints with the typing module and works seamlessly with class definitions . Essential foundation for exception handling patterns and function introspection .
Reference: PEP 3107 – Function Annotations