Skip to main content

Python Annotations Rare Use Cases

· 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Python annotations, introduced in PEP 3107 for function parameters and return values, were initially generic metadata slots. While their primary use has become type hinting (PEP 484), expert developers leverage them for advanced and niche applications that go far beyond simple type declarations.

These use cases often involve frameworks or metaprogramming to make annotations act as declarative configuration or runtime execution instructions.

Use Case 1: Declarative Dependency Injection (FastAPI/Starlette)

This is the most widespread "expert" use of annotations. Frameworks like FastAPI and Starlette use the annotation slot (param: annotation) to declare the source and logic for resolving a function parameter at runtime. The annotation is used as a configuration object, not just a type hint.

Annotation TypePurposeExample
Dependency InjectionInstruct the framework to run a dependency function and inject its result.def route(user: Depends(get_current_user)):
Parameter SourceInstruct the framework where to find the parameter value in the HTTP request.def route(user_id: Path(ge=1)):
from fastapi import Path, Depends
from typing import Annotated

# 1. Annotation defines the logic (Dependency function)
def get_db_session():
# Placeholder for database session management
yield {"db_connection": "active"}

# 2. Annotation defines the source (Path parameter, with validation)
def read_item(
item_id: Annotated[int, Path(title="Item ID", gt=0)],
db: Annotated[dict, Depends(get_db_session)]
):
# FastAPI reads the annotation to execute Depends and Path validation
# before calling read_item.
return {"item_id": item_id, "db_status": db.get("db_connection")}

Annotation: The Path() and Depends() objects are the actual annotations; the int and dict are the embedded type hints.


Use Case 2: Runtime Contract Enforcement (Typeguard/Pydantic)

As type hints are advisory, annotations can be used with a decorator to force runtime validation. This turns a simple metadata hint into a mandatory check, guaranteeing type safety at runtime.

ToolMechanismEnforcement
TypeguardDecorator inspects __annotations__ at runtime.Raises TypeError if an argument or return value does not match the hint.
PydanticCreates a BaseModel that wraps attributes.Raises ValidationError if input data fails the hint/validator checks.
from typeguard import typechecked

@typechecked
def process_data(data: list[int]) -> list[str]:
"""Requires a list of integers and returns a list of strings."""
return [str(i * 2) for i in data]

try:
# This call passes a list of strings, violating the hint
process_data(["a", "b"])
except TypeError as e:
# Typeguard caught the violation at the function entry
print(f"Runtime error caught: {e}")

Use Case 3: Library Metaprogramming and Schema Generation

Annotations are widely used to generate schema and documentation automatically, often employing reflection (inspecting the code object at runtime).

ToolMechanismOutput
SQLAlchemyAnnotations can hint at ORM relationships or column types.Used to build the ORM object model and database schema structure.
Marshmallow/PydanticUsed to generate JSON Schemas.Generates documentation (OpenAPI/Swagger) by translating hints like str and list[int] into JSON Schema types (type: string, type: array, items: {type: integer}).
from pydantic import BaseModel, Field

class Item(BaseModel):
# Field uses the annotation slot to attach metadata (description, validation)
name: str = Field(description="Name of the item.", min_length=3)

# When Item.model_json_schema() is called, Pydantic reads the 'name: str'
# and the Field() metadata to produce a structured schema:

# JSON Schema Output Snippet:
# "name": {
# "type": "string",
# "description": "Name of the item.",
# "minLength": 3
# }

Use Case 4: Custom Configuration (Rare/Expert)

In highly customized codebases, developers use a simple dataclass or marker object as an annotation to configure a function registry or a code generation step.

  • Scenario: You have a system that registers background tasks and need to tell the registry which queue the task belongs to.
from dataclasses import dataclass
import inspect

# 1. Custom Marker Annotation
@dataclass
class QueueConfig:
queue_name: str
priority: int

# Global registry to store functions and their config
TASK_REGISTRY = {}

# 2. Function that registers tasks based on their annotation
def register_task(func):
# Inspect the function's return annotation
return_hint = inspect.signature(func).return_annotation

# Check if the annotation is our custom marker
if isinstance(return_hint, QueueConfig):
config = return_hint
TASK_REGISTRY[config.queue_name] = {
'func': func.__name__,
'priority': config.priority
}
return func

# 3. Applying the annotation for configuration
@register_task
def send_notification(user_id):
# The return annotation is the configuration object
return QueueConfig(queue_name='low_priority', priority=1)

# print(TASK_REGISTRY)
# Output: {'low_priority': {'func': 'send_notification', 'priority': 1}}

Annotation: In this rare case, the annotation is used entirely as a declaration of configuration data that is processed by the @register_task decorator at import time, completely ignoring the function's runtime behavior.


Sources and Further Reading

  1. Python Documentation - PEP 3107 (Function Annotations)
  2. Python Documentation - Inspecting Signatures (inspect.signature)
  3. FastAPI Documentation - Dependency Injection
  4. Pydantic Documentation - Schema Generation
  5. Typeguard Documentation - Runtime Checking