← Back to Course Index
Lesson 12 of 13

Type Hints

~20 min · annotations, generics, Optional, Union, TypedDict, Protocol, mypy

Ref
Primary Source
Official Docs — typing module

Complete type hint reference. For a guided intro, Real Python — Python Type Checking is the best walkthrough available.

1 — Type Hints are Optional (but you'll see them everywhere)

Python is dynamically typed — type hints are purely informational at runtime. They don't enforce anything by themselves. The value comes from:

TypeScript
function greet(name: string): string {
  return `Hello, ${name}!`;
}

function add(a: number, b: number): number {
  return a + b;
}

// Enforced at compile time
greet(42);  // TS Error!
Python — type hints
def greet(name: str) -> str:
    return f"Hello, {name}!"

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

# NOT enforced at runtime — no error here
greet(42)   # runs fine, but mypy would flag it

# Check with mypy:
# mypy script.py → error: Argument 1 to "greet" has incompatible type "int"

2 — Basic Annotations

# Variables
name: str = "Alice"
age: int = 30
price: float = 9.99
active: bool = True

# Without assignment (declare intent)
user_id: int   # declared but not yet assigned

# Functions
def multiply(x: int, y: int = 1) -> int:
    return x * y

# No return value
def log(msg: str) -> None:
    print(msg)

# Any type (opt out of checking)
from typing import Any
def process(data: Any) -> Any:
    return data

3 — Collection Types

TypeScript
// Arrays
const nums: number[] = [1, 2, 3];
const words: Array<string> = ["a"];

// Object / Record
const user: { name: string; age: number } = ...;
const map: Record<string, number> = {};

// Tuple
const pair: [string, number] = ["Alice", 30];

// Union
function id(x: string | number): string {...}

// Optional (string | undefined)
function find(x?: string): string | null {...}
Python 3.9+ (modern)
# Lists, dicts, sets, tuples — use lowercase directly
nums:  list[int]         = [1, 2, 3]
words: list[str]         = ["a"]
ages:  dict[str, int]    = {"Alice": 30}
ids:   set[int]          = {1, 2, 3}

# Tuple — specify each element type
point:  tuple[int, int]        = (3, 4)
triple: tuple[str, int, bool]  = ("Alice", 30, True)
# Variable-length homogeneous tuple
coords: tuple[float, ...]      = (1.0, 2.0, 3.0)

# Union (Python 3.10+ can use |)
def parse(x: str | int) -> str:
    return str(x)

# Optional — can be None
def find(name: str | None = None) -> str | None:
    ...
💡 Python version matters

Python 3.9+: use built-in list[int], dict[str, int] directly.
Python 3.8 and below: must import from typing: from typing import List, Dict, Optional, Union.
Python 3.10+: can use X | Y for union instead of Union[X, Y].

4 — TypedDict — Typed Dicts

When a function receives or returns a dict with known keys, TypedDict is your interface or object type in TypeScript.

TypeScript
interface User {
  name: string;
  age: number;
  email?: string;  // optional
}

function process(user: User): string {
  return user.name;
}
Python — TypedDict
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

class UserWithEmail(User, total=False):  # total=False → all optional
    email: str

def process(user: User) -> str:
    return user["name"]

# Or with Required/NotRequired (Python 3.11+)
from typing import Required, NotRequired
class Config(TypedDict):
    host: Required[str]
    port: NotRequired[int]

5 — Callable, TypeVar & Generics

# Callable — type for functions
from typing import Callable

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)
# fn takes two ints, returns an int

# Callable with any signature
def run(callback: Callable[..., None]) -> None:
    callback()

# TypeVar — generic functions (like  in TypeScript)
from typing import TypeVar
T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

first([1, 2, 3])     # inferred as int
first(["a", "b"])    # inferred as str

Generic classes

# Python 3.12+ syntax
class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []

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

    def pop(self) -> T:
        return self._items.pop()

# Python 3.11 and below
from typing import Generic, TypeVar
T = TypeVar("T")

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:
        return self._items.pop()

6 — Protocol — Structural Typing (duck typing + types)

Protocol is Python's answer to TypeScript's structural typing — define what an object must be able to do, not what it must inherit from.

TypeScript — structural typing
interface Drawable {
  draw(): void;
}

// Anything with a draw() method satisfies this
function render(shape: Drawable): void {
  shape.draw();
}
Python — Protocol
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

# Any class with a draw() method satisfies this
# — no inheritance required (structural, not nominal)
class Circle:
    def draw(self) -> None:
        print("Drawing circle")

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

render(Circle())   # ✅ — mypy is happy

7 — Literal, Final & type narrowing

# Literal — restrict to specific values (like const union in TS)
from typing import Literal

Direction = Literal["north", "south", "east", "west"]

def move(direction: Direction) -> None:
    ...

move("north")  # ✅
move("up")     # mypy error

# Final — constant (like const in TS)
from typing import Final
MAX_SIZE: Final = 100
MAX_SIZE = 200   # mypy error

# Type narrowing — mypy tracks isinstance checks
def process(val: int | str) -> str:
    if isinstance(val, int):
        return str(val * 2)   # mypy knows val is int here
    return val.upper()         # mypy knows val is str here

# assert — narrow type (like TypeScript's type assertion)
assert isinstance(user, Admin)   # mypy treats user as Admin after this

8 — Running mypy

# Install
pip install mypy

# Check a file
mypy script.py

# Check a whole project
mypy .

# Strict mode (like TypeScript's strict: true)
mypy --strict script.py

# pyproject.toml config
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
💡 Pyright / pylance (VS Code)

If you use VS Code, install the Pylance extension — it uses pyright for type checking inline in the editor. Much faster feedback loop than running mypy manually. Set "python.analysis.typeCheckingMode": "basic" in your settings to start.

🧠 Quiz

1. Do Python type hints enforce types at runtime?

2. In modern Python (3.9+), how do you annotate a list of strings?

3. What is Protocol used for?

4. Callable[[int, str], bool] describes a function that…

0/4

Questions answered correctly.