Type Hints & Static Typing

Python Advanced — Type Hints & Static Typing
Python Advanced
Course 3 · Chapter 5 · Type Hints & Static Typing

🏷️ Type Hints & Static Typing

Course 1, Chapter 2 introduced Python as dynamically typed — variables can hold any type, checked only at runtime. This chapter covers the layer Python added on top of that: optional type hints, the typing module's building blocks, mypy as an external static checker, Protocol for structural typing, and generics.

✍️ Basic Type Hints

def add(a: int, b: int) -> int: return a + b name: str = "Ada" age: int = 28

A type hint is written as name: Type for a variable or parameter, and -> Type after a function's parameter list for its return type. This looks similar to Kotlin's own type annotations, but with one crucial difference — see the warning below.

⚠ Type Hints Are NOT Enforced at Runtime
def add(a: int, b: int) -> int: return a + b print(add("hello", "world")) # "helloworld" — runs FINE, no error at all!

Unlike Go or Kotlin, where the compiler rejects a type mismatch before the program can even run, Python's type hints are pure documentation as far as the language itself is concerned — the interpreter never checks them. add("hello", "world") above happily runs and string-concatenates, despite the hints clearly saying both parameters should be int. Catching this kind of mismatch requires a separate tool — mypy, covered next.

🧱 typing Module Building Blocks

def get_names() -> list[str]: # a list of strings return ["Ada", "Grace"] def get_scores() -> dict[str, int]: # a dict of str keys, int values return {"Ada": 100} def find_user(user_id: int) -> str | None: # might return a str, or might return None if user_id == 1: return "Ada" return None

list[str], dict[str, int], and similar generic-collection hints use Course 1's own collection types (Chapter 6, Chapter 7) directly as the annotation. X | None (modern syntax) or Optional[X] (the older typing module form, equivalent) marks a value that might legitimately be missing — the same idea as the Option<T>-style types more strictly-typed languages use, but here purely advisory rather than compiler-enforced.

🔍 mypy

# bad_math.py def add(a: int, b: int) -> int: return a + b add("hello", "world")
# run separately, from the command line: mypy bad_math.py # output: # bad_math.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int" # bad_math.py:4: error: Argument 2 to "add" has incompatible type "str"; expected "int"

mypy (installed via pip, per Course 1, Chapter 9) reads a script's type hints and statically checks whether the code actually respects them — without ever running the code. This is what gives Python-with-hints something closer to Go's or Kotlin's compile-time safety net: run mypy as a separate step (often wired into CI, per the site's own CI/CD Pipelines course), catch mismatches before deployment, while the interpreter itself stays exactly as permissive as it's always been.

🦆 Protocol (Structural Typing)

from typing import Protocol class Honker(Protocol): def honk(self) -> str: ... class Car: # does NOT inherit from Honker at all def honk(self) -> str: return "Beep!" def make_it_honk(thing: Honker) -> None: print(thing.honk()) make_it_honk(Car()) # Beep! — works, even though Car never inherited from Honker

Course 3, Chapter 1's ABC is nominal typing — a class must explicitly inherit from the abstract base to count as satisfying it. Protocol is structural typing — any class with a matching honk() method satisfies Honker, whether or not it ever mentions Honker by name. This formalizes Python's long-standing "duck typing" culture (if it walks like a duck and honks like a car horn...) into something mypy can actually check statically.

🧬 Generics

from typing import TypeVar T = TypeVar("T") def first(items: list[T]) -> T: return items[0] first([1, 2, 3]) # mypy infers T = int here, so this returns int first(["a", "b"]) # mypy infers T = str here, so this returns str

TypeVar lets a function or class be generic over whatever type it's actually given — first() works correctly on a list[int] or a list[str] alike, and mypy tracks that the return type always matches whatever T resolved to for that specific call, the same generic pattern Go's and Kotlin's own generics (already covered elsewhere on the site) provide, just checked externally rather than by the language itself.

Type Enforcement: Go vs Kotlin vs Python

LanguageWhen types are checked
GoCompile time, mandatory — a type mismatch is a compile error, the program cannot even build.
KotlinCompile time, mandatory — same guarantee as Go, enforced by the compiler before any code runs.
PythonNever, by the language itself — hints are optional documentation; mypy can check them separately, entirely opt-in, and skipping it (or running mismatched code anyway) is always possible.

This is the single biggest mindset shift in this chapter: in Go or Kotlin, "it compiled" is itself a real type-safety guarantee. In Python, "it has type hints" guarantees nothing on its own — the guarantee only exists if mypy (or an equivalent) is actually run, and even then, only for the code paths it checks.

Type hints

name: Type, -> Type — documentation only, not enforced by the interpreter.

mypy

A separate static checker — run it to actually catch type mismatches before runtime.

Protocol

Structural typing — matches by shape (duck typing), no inheritance required, unlike ABC.

TypeVar / generics

A function/class that works across types, with the relationship tracked by mypy.

💻 Coding Challenges

Challenge 1: Annotate a Function

Take the fizzbuzz(n) function from Course 2, Chapter 8's testing challenge, and add full type hints: n should be typed as int, and the return type should reflect that it always returns a str.

Goal: Practice adding basic type hints to an existing, already-working function.

→ Solution

Challenge 2: A Protocol for Comparable Items

Define a Protocol called HasArea with a method area(self) -> float. Write a function describe(shape: HasArea) -> str that returns f"Area is {shape.area()}". Call it with an instance of the Square class from Course 2, Chapter 2 — it should work despite Square never mentioning HasArea.

Goal: Practice structural typing with Protocol and confirm it works without inheritance.

→ Solution

Challenge 3: A Generic last() Function

Using TypeVar, write a generic function last(items: list[T]) -> T that returns the last item of any list. Call it once with a list[int] and once with a list[str], printing both results.

Goal: Practice writing a generic function with TypeVar, mirroring this chapter's first() example.

→ Solution

🎯 What's Next

Next chapter: Packaging & Distributionpyproject.toml, setuptools/poetry, and publishing to PyPI.