PyPython · Lesson 13 of 14

Type Hints & Modern Python

Python type hints do not make Python a statically typed language — the interpreter still ignores them at runtime. But they let tools like mypy catch bugs before you run the code, and they make your code infinitely more readable.

Python
from typing import Optional, Union, Any, TypeVar, Generic
from collections.abc import Callable, Sequence, Iterator

# Basic type hints
def greet(name: str) -> str:
    return f"Hello, {name}!"

def add(a: int, b: int) -> int:
    return a + b

# Optional — value can be None
def find_user(user_id: int) -> Optional[str]:  # str | None in Python 3.10+
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

# Union — multiple possible types
def stringify(value: Union[int, float, str]) -> str:
    return str(value)

# Python 3.10+ syntax (cleaner)
def process(data: int | str | None) -> str:
    if data is None:
        return "none"
    return str(data)

# Container types
def sum_list(nums: list[int]) -> int:
    return sum(nums)

def merge(a: dict[str, int], b: dict[str, int]) -> dict[str, int]:
    return {**a, **b}

def first(items: Sequence[Any]) -> Any:
    return items[0]

# Callable types
def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)

print(apply(lambda x: x * 2, 5))  # 10
Python
from typing import TypeVar, Generic, Protocol
from dataclasses import dataclass

T = TypeVar('T')

# Generic class
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]

    def __len__(self) -> int:
        return len(self._items)


s: Stack[int] = Stack()
s.push(1)
s.push(2)
print(s.pop())  # 2

# Protocol — structural subtyping ("duck typing" with type safety)
class Drawable(Protocol):
    def draw(self) -> str: ...

@dataclass
class Circle:
    radius: float
    def draw(self) -> str:
        return f"Circle(r={self.radius})"

@dataclass
class Square:
    side: float
    def draw(self) -> str:
        return f"Square(s={self.side})"

def render(shape: Drawable) -> None:
    print(shape.draw())

render(Circle(5.0))    # works
render(Square(3.0))    # works — no explicit inheritance needed

# Type aliases
Vector = list[float]
Matrix = list[Vector]

def dot_product(a: Vector, b: Vector) -> float:
    return sum(x * y for x, y in zip(a, b))
◆ Note
Install mypy (pip install mypy) and run "mypy yourfile.py" to catch type errors before running. For new projects, use "mypy --strict" to enforce full type coverage. Type hints are especially valuable in larger codebases where you cannot hold the entire program in your head.