Free GuideIntermediate
35 min read
#python#type hints#typing#static typing#mypy#annotations

Type Hints

Comprehensive guide on Type Hints.

Type Hints

Overview#

Python is dynamically typed — variables don't have a fixed type, and nothing is checked at runtime by default. Type hints, introduced in Python 3.5 (PEP 484), let you annotate variables, function parameters, and return values with their intended types. Python itself never enforces these hints at runtime; instead, they're used by editors for autocomplete, by static analysis tools like mypy to catch bugs before running the code, and by other developers reading the code to understand intent. This notebook covers basic annotations, the typing module's key tools, generics, Optional, Union, type aliases, Protocol, and how to actually check your code with mypy.

1. Basic Type Hints#

Python 3.12 (WASM)
def greet(name: str) -> str: return f"Hello, {name}!" age: int = 25 price: float = 19.99 is_active: bool = True tags: list = ["python", "coding"]

The syntax variable: type annotates a variable, and -> type after a function's parameter list annotates its return type. Note this is purely informational at runtime:

Python 3.12 (WASM)
def add(a: int, b: int) -> int: return a + b print(add("hello", "world")) # "helloworld" — Python does NOT enforce the hint!

Type hints don't change how the code runs; they're a form of documentation that tools can verify separately.

2. Hinting Collections#

Python 3.12 (WASM)
from typing import List, Dict, Tuple, Set names: List[str] = ["Alice", "Bob"] scores: Dict[str, int] = {"Alice": 90, "Bob": 85} coordinates: Tuple[float, float] = (12.5, 45.2) unique_ids: Set[int] = {1, 2, 3}

Modern syntax (Python 3.9+)#

Since Python 3.9, you can use the built-in collection types directly as generics, without importing from typing.

Python 3.12 (WASM)
names: list[str] = ["Alice", "Bob"] scores: dict[str, int] = {"Alice": 90, "Bob": 85} coordinates: tuple[float, float] = (12.5, 45.2) unique_ids: set[int] = {1, 2, 3}

This modern form is now preferred; the typing.List, typing.Dict, etc. aliases still exist for backward compatibility with older Python versions.

3. Optional and Union#

Optional[X] means "an X, or None" — extremely common for function parameters or return values that might not have a value.

Python 3.12 (WASM)
from typing import Optional def find_user(user_id: int) -> Optional[str]: users = {1: "Alice", 2: "Bob"} return users.get(user_id) # returns None if not found result = find_user(3) # type checkers know this could be str OR None

Union[X, Y] means the value could be either type.

Python 3.12 (WASM)
from typing import Union def process(value: Union[int, str]) -> str: return str(value) process(42) # OK process("hi") # OK

Modern syntax (Python 3.10+)#

Since Python 3.10, the | operator can express unions directly, without importing anything.

Python 3.12 (WASM)
def process(value: int | str) -> str: return str(value) def find_user(user_id: int) -> str | None: # equivalent to Optional[str] ...

4. Type Aliases#

For complex, repeated type annotations, a type alias improves readability.

Python 3.12 (WASM)
from typing import Dict, List # Old style: plain assignment UserId = int UserDatabase = Dict[UserId, str] def get_user(db: UserDatabase, user_id: UserId) -> str: return db[user_id] # Modern style (Python 3.12+): explicit "type" statement type UserDatabase = dict[int, str]

5. Functions as Values: Callable#

Python 3.12 (WASM)
from typing import Callable def apply_twice(func: Callable[[int], int], value: int) -> int: return func(func(value)) def add_one(n: int) -> int: return n + 1 print(apply_twice(add_one, 5)) # 7

Callable[[ArgType1, ArgType2], ReturnType] describes a function's signature — here, Callable[[int], int] means "a function taking one int and returning an int."

6. Generic Functions and Classes with TypeVar#

Sometimes a function's return type depends on its input type, in a way that plain Union can't express precisely. TypeVar creates a "type variable" that ties multiple hints together.

Python 3.12 (WASM)
from typing import TypeVar, List T = TypeVar("T") def first_item(items: List[T]) -> T: return items[0] x = first_item([1, 2, 3]) # type checker infers: int y = first_item(["a", "b", "c"]) # type checker infers: str

Generic classes#

Python 3.12 (WASM)
from typing import Generic, TypeVar T = TypeVar("T") class Box(Generic[T]): def __init__(self, content: T) -> None: self.content = content def get(self) -> T: return self.content int_box: Box[int] = Box(42) str_box: Box[str] = Box("hello")

Modern generic syntax (Python 3.12+)#

Python 3.12 (WASM)
class Box[T]: def __init__(self, content: T) -> None: self.content = content def get(self) -> T: return self.content

7. Any and object#

Any tells the type checker to skip checking entirely for that value — useful as an escape hatch, but it should be used sparingly since it disables type safety wherever it's applied.

Python 3.12 (WASM)
from typing import Any def process(data: Any) -> None: print(data.whatever_attribute) # no error from the type checker, even if wrong

object is different — it means "any type is acceptable here, but you can only do things that work on every object" (like str() or ==), which is far more restrictive and safer than Any.

Python 3.12 (WASM)
def describe(value: object) -> str: return str(value) # OK, str() works on any object def describe_bad(value: object) -> str: return value.upper() # type checker ERROR: 'object' has no 'upper' method

8. Protocol: Structural Typing#

Protocol lets you define an interface based on behavior (which methods an object has) rather than inheritance — matching Python's duck-typing philosophy while still getting static type checking.

Python 3.12 (WASM)
from typing import Protocol class Speaker(Protocol): def speak(self) -> str: ... class Dog: def speak(self) -> str: return "Woof!" class Robot: def speak(self) -> str: return "Beep boop" def make_it_speak(entity: Speaker) -> None: print(entity.speak()) make_it_speak(Dog()) # OK — Dog has a compatible speak() method make_it_speak(Robot()) # OK — Robot too, with NO shared inheritance needed

Neither Dog nor Robot explicitly inherits from Speaker — a type checker verifies compatibility purely based on the presence of a matching speak() method, which mirrors how duck typing works at runtime.

9. TypedDict: Typed Dictionaries#

For dictionaries with a fixed, known set of string keys (common with JSON data), TypedDict provides much more precise checking than a plain Dict[str, Any].

Python 3.12 (WASM)
from typing import TypedDict class Movie(TypedDict): title: str year: int rating: float movie: Movie = {"title": "Inception", "year": 2010, "rating": 8.8} # movie: Movie = {"title": "Inception"} # type checker ERROR: missing keys # movie["year"] = "2010" # type checker ERROR: wrong type

10. Literal Types#

Literal restricts a value to a specific, fixed set of literal values — useful for options/flags/modes.

Python 3.12 (WASM)
from typing import Literal def set_mode(mode: Literal["read", "write", "append"]) -> None: print(f"Mode set to {mode}") set_mode("read") # OK set_mode("delete") # type checker ERROR: not one of the allowed literals

11. Checking Your Code with mypy#

Type hints do nothing on their own at runtime. To actually catch type errors, run a static type checker like mypy against your code.

bash
pip install mypy --break-system-packages mypy my_script.py
Python 3.12 (WASM)
# script.py def add(a: int, b: int) -> int: return a + b result = add("1", "2") # mypy would report: incompatible type "str"
bash
$ mypy script.py script.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"

Running the script directly with python script.py would work without any error at all — the mismatch is only caught by explicitly running mypy.

Notes#

  • Type hints are checked by external tools (mypy, pyright), editors, and IDEs — Python's own interpreter ignores them completely at runtime.
  • Optional[X] is exactly equivalent to Union[X, None] — it exists purely for readability.
  • Prefer the modern syntax (list[str], int | None) over typing.List[str] / typing.Optional[int] for Python 3.9+/3.10+ codebases — it's shorter and needs no extra imports.
  • Any should be a last resort, not a default — overusing it defeats the purpose of adding type hints in the first place.
  • Protocol matches Python's existing duck-typing philosophy far better than requiring formal inheritance from an abstract base class.
  • Adding type hints incrementally to an existing codebase is completely valid — you don't need to annotate everything at once for hints to add value.

Best Practices Checklist#

  • Annotate function parameters and return types, especially for public/reusable functions
  • Prefer modern syntax (list[int], int | None) over typing.List[int] / typing.Optional[int] on Python 3.9+/3.10+
  • Use Optional[X] (or X | None) whenever a value might be None
  • Avoid overusing Any — it disables type checking wherever applied
  • Use Protocol for structural/duck-typed interfaces instead of forcing formal inheritance
  • Use TypedDict for dictionaries with a fixed, known set of keys (e.g. parsed JSON)
  • Use Literal to restrict a parameter to a specific set of allowed values
  • Run mypy (or a similar type checker) as part of your workflow — hints alone catch nothing without a checking tool
  • Add type hints incrementally to existing code rather than treating it as all-or-nothing
Knowledge Checkpoint

Python Type Hints & Static Typing Checkpoint

Q1.Does standard CPython enforce type annotations at runtime by default?
AYes, it raises a TypeError if an incorrect argument type is passed.
BNo, type hints are ignored at runtime by CPython and are primarily used by static type checkers (like mypy) and IDEs.
CYes, but only when running with Python 3.12+.
DYes, if the function is decorated with `@typing.enforce`.
Q2.In Python 3.10+, what is the modern shorthand syntax for `Union[str, None]` (or `Optional[str]`)?
A`str | None`
B`str || None`
C`str & None`
D`str ? None`
Q3.What is `TypeVar` used for in the `typing` module?
ATo cast variable types dynamically.
BTo define generic type parameters that preserve relationships across function arguments and return types.
CTo validate JSON payloads.
DTo measure variable memory consumption.
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.