Lesson 13 of 34 · 0 of 2 exercises solved

Type Hints & Modern Python

label what kind of value a or expects. Python ignores them at runtime, but tools like mypy use them to catch before you run the code, and they make your code easier to read.

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 (loads on first run)Ctrl+Enter runs
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))
Python (loads on first run)Ctrl+Enter runs

Common question

Do type hints make Python a statically typed language?

Type hints are annotations on , return values and variables that say what types you intend. Python records them and does nothing else with them. Their power comes from tools that read them ahead of time: checkers that flag a str passed where an int was promised, and editors that can complete attribute names because they know what kind of object a variable holds.

Annotate function signatures first, since they describe how to use the function. Use the built-in ([int], dict[str, float]), Optional or X | for values that may be missing, and dataclasses for records. Run a type checker in your editor or in CI, and treat what it finds the way you would treat a failing test.

Source: PEP 484: "Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention."

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • Once every parameter is annotated, Python will refuse to run code with the wrong types.

  • Since they aren't enforced, type hints are worthless.

  • A is a different kind of with special runtime type checking.

Practice

Annotate total_price fully: prices is a of floats, tax_rate is a float defaulting to 0.0, and it returns a float. Then implement it: the sum of the prices with the tax applied.

Exercise

Annotate and implement

3 checks
def total_price(prices, tax_rate=0.0):
    ...


print(total_price([10.0, 5.5], tax_rate=0.2))
 
Python (loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Turn Point into a dataclass with float fields x and y, and add a distance_to(other) . Dataclasses give you __init__, __repr__ and __eq__ for free.

Exercise

A dataclass with behaviour

3 checks
from dataclasses import dataclass
import math


class Point:
    x: float
    y: float

    def distance_to(self, other):
        ...


p, q = Point(0, 0), Point(3, 4)
print(p, q, p.distance_to(q))
 
Python (loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Check yourself

Quick check

What happens at runtime if you call this with a string: def double(n: int) -> int: return n * 2 → double("ab")?

Finished reading?2 exercises still open. You can come back to them later.

Tip: use the ← and → keys to move between lessons.