Using Type Information in Code

Python is dynamically typed at runtime but increasingly strongly typed in its tooling. Type hints are optional annotations on function parameters, return values, and variables that describe the kinds of values they hold. They are ignored by the interpreter, inspected by editors and type checkers, and combined with runtime tools to power validation libraries like pydantic and serialization tools like dataclasses.

Write a type hint with : Type on a parameter and -> Type on a return value. Modern Python lets you use the built-in containers directly: list[int], dict[str, float], tuple[str, int]. Union types: int | None or Optional[int]. Generic callable: Callable[[int, int], int]. Literal values: Literal["get", "post"].

Types are documentation you can check. Run mypy or pyright on your project and it reports mismatches (calling a function with a str where it expects int, forgetting a return, using None unchecked). With “strict” mode on, the checker enforces that every public function is annotated — a low-cost safety net that catches real bugs.

Types are also available at runtime through typing.get_type_hints and inspect.signature. Libraries like pydantic, dataclasses, attrs, sqlmodel, and fastapi read annotations to auto-generate validators, schemas, and documentation. Even if you don't run a type checker, hints power a growing ecosystem of tools.

Writing hints that help

Start with public function signatures. Then add hints to top-level variables where the type isn't obvious (cache: dict[str, User] = {}). Leave locals uannotated unless they're ambiguous — the checker infers them.

Prefer Iterable[X], Sequence[X], or Mapping[K, V] in parameters: those accept more types without losing safety. Use concrete list/dict in return types, where you do know the exact type.

Generics, Protocol, TypedDict

TypeVar and generic classes let you describe containers or functions that work with any type (def first(xs: list[T]) -> T). Protocol is Python's structural typing: any class with the right methods satisfies it, no inheritance required.

TypedDict types a dict with specific string keys — perfect for JSON payloads. NewType creates zero-cost type aliases (UserId = NewType("UserId", int)) that keep different flavors of int apart.

Typing toolbox.

ToolPurpose
typing
module
Primary typing utilities.
PEP 484
spec
Type hints foundation.
int | None
syntax
Union with | (PEP 604, 3.10+).
list[int]
syntax
Built-in generics (PEP 585, 3.9+).
TypedDict
class
Typed dict keys and values.
Protocol
class
Structural typing (duck-typed interfaces).
mypy
tool
Static type checker.
pyright
tool
Fast type checker from Microsoft.

Using Type Information in Code code example

The script defines typed functions, a Protocol, and uses typing.get_type_hints to inspect annotations at runtime.

# Lesson: Using Type Information in Code
from dataclasses import dataclass
from typing import Iterable, Protocol, TypedDict, get_type_hints


def total(values: Iterable[float]) -> float:
    return sum(values)


def top_three(scores: dict[str, int]) -> list[tuple[str, int]]:
    return sorted(scores.items(), key=lambda p: p[1], reverse=True)[:3]


class HasName(Protocol):
    name: str


@dataclass
class User:
    name: str
    age: int


def greet(entity: HasName) -> str:
    return f"hello {entity.name}"


class Address(TypedDict):
    city: str
    zip: str


def describe(addr: Address) -> str:
    return f"{addr['city']} ({addr['zip']})"


print("total:    ", total([1, 2, 3.5]))
print("top 3:    ", top_three({"ana": 91, "ben": 78, "cai": 85, "dev": 95}))
print("greet:    ", greet(User(name="ana", age=30)))
print("describe: ", describe({"city": "oslo", "zip": "0150"}))

# Runtime introspection
print("total hints:", get_type_hints(total))
print("Address hints:", get_type_hints(Address))

Four typing techniques:

1) `Iterable[float]` is looser than `list[float]` — any iterable works.
2) `Protocol` lets unrelated classes satisfy an interface by matching attributes.
3) `TypedDict` describes dicts with known string keys (great for JSON).
4) `get_type_hints(fn)` exposes the annotations at runtime.

Add a generic function with TypeVar.

from typing import TypeVar
T = TypeVar("T")

def first_or_default(xs: list[T], default: T) -> T:
    return xs[0] if xs else default

print(first_or_default([1, 2, 3], 0))
print(first_or_default([], "none"))

Annotations are introspectable.

def f(x: int, y: str) -> bool: return True
assert f.__annotations__ == {"x": int, "y": str, "return": bool}

Running prints:

total:     6.5
top 3:     [('dev', 95), ('ana', 91), ('cai', 85)]
greet:     hello ana
describe:  oslo (0150)
total hints: {'values': typing.Iterable[float], 'return': }
Address hints: {'city': , 'zip': }