Home/Learn/Python A–Z/Type Hints — Optional, Union, Generics & mypy

Type Hints — Optional, Union, Generics & mypy

Intermediate
Modern Python

Type hints document intent and let tools (mypy, IDEs, FastAPI, Pydantic) catch bugs before runtime — Python stays dynamic, the annotations stay optional but professional.

Overview

Since Python 3.5, you can annotate parameters, returns and variables with types. The interpreter IGNORES them at runtime — they exist for humans, IDEs, static checkers (mypy/pyright), and frameworks that read them (FastAPI builds validation and docs from your annotations; Pydantic builds parsing). Modern syntax (3.10+) is clean: list[int] not List[int], int | None not Optional[int]. In hiring terms: typed Python is what production codebases look like — writing it fluently marks you as industry-ready.

The Syntax You Will Actually Use

Annotate function signatures first — highest value per keystroke. X | None must be handled before use (mypy enforces it), eliminating a whole class of NoneType errors.

Signatures, unions, narrowing, aliases
def average(marks: list[int]) -> float:
    return sum(marks) / len(marks)

def find_student(roll: int) -> dict[str, str] | None:   # may be absent
    db = {1: {"name": "Asha"}}
    return db.get(roll)

s = find_student(2)
# print(s["name"])          # mypy error: s might be None!
if s is not None:
    print(s["name"])        # narrowed — mypy happy

# Collections & defaults
def top_k(scores: dict[str, int], k: int = 3) -> list[tuple[str, int]]:
    return sorted(scores.items(), key=lambda kv: -kv[1])[:k]

# Variables & aliases
Matrix = list[list[int]]           # type alias
grid: Matrix = [[1, 2], [3, 4]]

from typing import Callable
def apply_twice(f: Callable[[int], int], x: int) -> int:
    return f(f(x))

# Run the checker:  pip install mypy && mypy app/

Generics & TypeVar — Preserve the Type Through

A TypeVar says "same type in as out". Generic containers parameterize classes. This is how library authors keep your IDE smart end-to-end.

TypeVar/Generic — types flow through your code
from typing import TypeVar, Generic

T = TypeVar("T")

def first(items: list[T]) -> T | None:     # returns element type!
    return items[0] if items else None

n = first([1, 2, 3])        # mypy knows: int | None
s = first(["a", "b"])       # mypy knows: str | None

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._data: list[T] = []
    def push(self, item: T) -> None:
        self._data.append(item)
    def pop(self) -> T:
        return self._data.pop()

st: Stack[int] = Stack()
st.push(10)
# st.push("hi")             # mypy error: expected int

# Why this matters beyond style — FastAPI reads your hints:
# @app.get("/users/{user_id}")
# def get_user(user_id: int) -> UserOut: ...
#   ^ validation, conversion AND OpenAPI docs, all from types

Key Points to Remember

  • 1Hints are ignored at runtime — enforced by mypy/pyright, exploited by FastAPI/Pydantic
  • 2Modern spelling: list[int], dict[str, int], int | None (3.10+)
  • 3X | None forces callers to handle absence — the end of surprise NoneType errors
  • 4TypeVar preserves types through functions; Generic parameterizes classes

Interview Questions

Sign in to ask Aria
1

Do type hints affect Python runtime behaviour? Who consumes them?

MediumThoughtworks
2

What does Optional[int] mean and how does mypy force you to handle it?

MediumAmazon
3

Why does FastAPI care about your type annotations?

MediumZomato

Ask Aria about Type Hints — Optional, Union, Generics & mypy

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…