Skip to main content Brad's PyNotes

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