Python Type Hints

Tutorial 41 of 65 · pythondeck.com Python course

Annotations describe parameter and return types: def f(x: int) -> str:. They are optional at runtime but checked by tools like mypy, pyright or pyre. Use typing for generics; from 3.9+ you can write list[int], dict[str, int] directly.

Type hints annotate intent for static checkers (mypy, Pyright) and IDEs; they do not enforce types at runtime by default. They improve maintainability in larger codebases and document function contracts. Python 3.9+ prefers built-in generics (list[int]) over typing.List.

Gradual typing lets you add hints incrementally. typing.Protocol, TypedDict, unions (X | Y), and Optional model real-world APIs without sacrificing duck typing at runtime.

Function annotations: parameters and return types after definitions.

Generics: list[str], dict[str, int], Sequence[T].

Optional[T] and unions str | None for nullable values.

typing.Protocol for structural interfaces; TypedDict for dict shapes.

type statement (3.12+) for aliases; Literal, Final, ClassVar.

Running mypy or Pyright in CI alongside tests.

Runtime cost of hints is negligible—they are stored in __annotations__ and ignored by the interpreter unless you use typing.get_type_hints or pydantic. Forward references as strings ("Node") solve mutual recursion; from __future__ import annotations postpones evaluation.

Strict optional checking catches None mishandling early. Align hint strictness with team policy—full strictness helps libraries; apps may hint only public boundaries.

Pydantic and dataclasses use hints for validation and serialization—hints become more than documentation when those tools are in play.

Expecting hints to enforce types at runtime without a validator or checker.

Using Any everywhere, defeating the purpose of gradual typing.

Mutable class attributes hinted as list without ClassVar confusion in dataclasses.

Outdated typing.List in new 3.9+ code when list suffices.

Hints that lie (wrong return type) worse than no hints—erodes trust.

Add hints to public functions first; run mypy in CI with a sensible strictness level.

Use modern syntax (X | Y, builtin generics) on supported Python versions.

Prefer Protocols and TypedDicts for flexible structural types.

Keep hints accurate; update them when refactoring function contracts.

Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.

The program below demonstrates function. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Example: Function
# Run in the REPL or save as a .py file and execute with python.
def add(a: int, b: int) -> int:
    return a + b

print(add(2, 3))

This sample walks through containers / optional in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# Example: Containers / optional
# Run in the REPL or save as a .py file and execute with python.
from typing import Optional

def find(name: str, names: list[str]) -> Optional[int]:
    return names.index(name) if name in names else None

print(find("Ada", ["Grace", "Ada"]))

Here is a hands-on illustration of dataclass typed. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.

# Example: dataclass typed
# Run in the REPL or save as a .py file and execute with python.
from dataclasses import dataclass
@dataclass
class Item:
    name: str
    price: float
    tags: list[str]

print(Item("pen", 1.5, ["office", "writing"]))

The program below demonstrates typed function. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Annotations document intent; they are not enforced at runtime
def greet(name: str, excited: bool = False) -> str:  # hinted signature
    msg = f"Hello, {name}"  # build message
    return msg + "!" if excited else msg  # return str

print(greet("Ada", excited=True))  # Hello, Ada!
def total(values: list[float]) -> float:  # homogeneous list
    return sum(values)  # numeric aggregation

print(total([1.5, 2.5]))  # 4.0
from typing import Optional  # optional value
def find(items: list[int], target: int) -> Optional[int]:  # index or None
    return items.index(target) if target in items else None

This sample walks through typed dict in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# TypedDict and generics help static checkers (mypy/pyright)
from typing import TypedDict  # structured dict typing

class User(TypedDict):  # keys with value types
    name: str  # required key
    score: int  # required key

def save(user: User) -> None:  # accept typed mapping
    print(user["name"], user["score"])  # key access

save({"name": "Grace", "score": 97})  # valid dict
from typing import TypeVar, Sequence  # generic sequence
T = TypeVar("T")  # type variable
def first(seq: Sequence[T]) -> T:  # generic first element
    return seq[0]  # assumes non-empty
print(first([10, 20]))  # 10

« Python Comprehensions All tutorials Python Context Managers »