⚡IntermediatePython in Practice

Type Hints and Dataclasses

Adding type annotations for clarity and safety, and using @dataclass to eliminate boilerplate.

Overview

Python is dynamically typed variables don't have declared types at runtime. But since Python 3.5, you can add **type hints** (annotations) to functions and variables. They don't affect runtime behaviour Python ignores them by default but they enable: - **Static analysis** with tools like mypy or Pyright that catch type errors before you run the code - **Editor autocompletion** and inline error detection - **Self-documenting code** the signature tells you exactly what goes in and what comes out The `typing` module provides complex types: `Optional[T]`, `Union[T, U]`, `List[T]`, `Dict[K, V]`, `Tuple[T, ...]`, `Callable`, and more. Python 3.10+ supports `X | Y` union syntax directly. **@dataclass** (Python 3.7+) auto-generates `__init__`, `__repr__`, and `__eq__` from class annotations eliminating the boilerplate of writing them manually. Add `frozen=True` for immutable dataclasses that also get `__hash__`.

Key Points

  • •Type hints: `def fn(x: int, y: str) -> bool:` annotations, not enforced at runtime
  • •`Optional[T]` is `Union[T, None]` the value can be T or None
  • •Python 3.10+ union shorthand: `int | str` instead of `Union[int, str]`
  • •`list[int]`, `dict[str, int]` (lowercase, Python 3.9+) no typing import needed
  • •@dataclass auto-generates __init__, __repr__, __eq__ from field annotations
  • •`field(default_factory=list)` for mutable defaults in dataclasses
  • •`@dataclass(frozen=True)` makes instances immutable and hashable

Examples

Function and variable type hints

from typing import Optional, Union

def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()

def find_user(user_id: int) -> Optional[str]:
    # Returns a name or None if not found
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

def process(value: Union[int, float]) -> float:
    return float(value) * 2.0

# Variable annotations
scores: list[int] = [85, 90, 78]
config: dict[str, str] = {"host": "localhost", "port": "5432"}

result: Optional[str] = find_user(1)
print(result)    # Alice
print(find_user(99))   # None

@dataclass eliminating boilerplate

from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float

    def distance_from_origin(self) -> float:
        return (self.x**2 + self.y**2) ** 0.5

# __init__, __repr__, __eq__ generated automatically
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
print(p1)              # Point(x=3.0, y=4.0)
print(p1 == p2)        # True
print(p1.distance_from_origin())  # 5.0

@dataclass
class Student:
    name: str
    grades: list[int] = field(default_factory=list)  # mutable default

    def average(self) -> float:
        return sum(self.grades) / len(self.grades) if self.grades else 0.0

s = Student("Alice")
s.grades.extend([90, 85, 92])
print(s)               # Student(name='Alice', grades=[90, 85, 92])
print(s.average())     # 89.0

Frozen dataclasses and advanced typing

from dataclasses import dataclass
from typing import Callable, TypeVar

@dataclass(frozen=True)   # immutable like a tuple with named fields
class Colour:
    r: int
    g: int
    b: int

    def to_hex(self) -> str:
        return f"#{self.r:02X}{self.g:02X}{self.b:02X}"

red = Colour(255, 0, 0)
print(red.to_hex())     # #FF0000
# red.r = 128          # FrozenInstanceError can't modify

# Callable type hint
T = TypeVar("T")

def apply_twice(fn: Callable[[T], T], value: T) -> T:
    return fn(fn(value))

print(apply_twice(lambda x: x * 2, 3))   # 12

Common Mistakes

  • Using mutable defaults in dataclass without field(default_factory=...) all instances would share the same list object
  • Expecting type hints to enforce types at runtime they're only checked by static analysers, not Python itself
  • Using the typing module's List/Dict/Tuple in Python 3.9+ prefer lowercase list[int], dict[str, int] which don't require an import

Note

Run `mypy yourfile.py` (install with pip) to catch type errors before running your code. Start by typing just the public API of your functions you get most of the safety benefit with minimal annotation effort.

Exercises

Write Python code to solve each exercise. Run it in your browser and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What happens if you pass a string to a function annotated `def fn(x: int) -> str:` at runtime?