113. Typing Module
Type hints with Optional, List, Dict, Union, and Tuple
113. Typing Module
π·οΈ A codebase adds type hints for better IDE support and self-documentation. The developer used the wrong return type (dict instead of Optional[dict]), called Union like a function instead of subscripting it, and confused how to annotate collections.
π‘ Fun fact: The typing module was added in Python 3.5 (2015) via PEP 484, written by Guido van Rossum, Jukka Lehtosalo, and Εukasz Langa. Type hints are purely optional and are not enforced at runtime β Python remains a dynamically typed language. Tools like mypy, pyright, and IDE plugins read the annotations to catch type errors before you run your code. In Python 3.9+, you no longer need from typing import List, Dict β you can write list[int] and dict[str, int] directly using the built-in types.
β οΈ Watch out: Union(str, int) β using parentheses like a function call β raises TypeError: Cannot use a non-type as a parameter for a parameterized generic at import time, crashing before any code runs. Union is a generic alias, not a callable. Always use square brackets: Union[str, int]. The same applies to Optional, List, Dict, and Tuple β they all use brackets, not parentheses.
π€ Think about it: Type hints are just annotations β Python doesnβt enforce them at runtime. A function annotated -> int can still return a string and Python wonβt complain. Does this make type hints useless? What value do they provide if not enforced? When would you reach for a runtime validation library like pydantic instead of bare typing?
Learning objectives
- Use Optional[X] when a function can return X or None
- Use Union[X, Y] with square brackets (not parentheses) for multiple allowed types
- Annotate collections with List[X], Dict[K, V], Tuple[X, Y] to document element types
- Understand that type hints are documentation and tool hints, not runtime enforcement
- Inspect annotations with annotations or typing.get_type_hints()
Key concepts
- Optional[X] β value of type X or None
- Union[X, Y] β value is X or Y (square brackets!)
- List[X], Dict[K, V], Tuple[X, β¦] β typed collections
- -> ReturnType β function return annotation
- Python 3.9+ builtins β list[int], dict[str, int] work without importing
Try it
Concept detail
typing β Type Hints in Python
Type hints document what types functions expect and return. They donβt enforce types at runtime β they help IDEs and tools catch bugs early.
Basic Annotations
def greet(name: str) -> str:
return f"Hello, {name}"
def add(a: int, b: int) -> int:
return a + bOptional β value or None
from typing import Optional
def find_user(user_id: int) -> Optional[dict]:
# Can return a dict OR None
return users.get(user_id)Optional[X] is the same as Union[X, None].
Union β multiple types
from typing import Union
def process(value: Union[str, int]) -> str:
return str(value)
# Note: always square brackets, never Union(str, int)List, Dict, Tuple
from typing import List, Dict, Tuple
def summarize(scores: List[int]) -> Dict[str, float]:
return {"mean": sum(scores) / len(scores)}
def bbox(x: float, y: float) -> Tuple[float, float]:
return (x, y)In Python 3.9+, you can use the built-in list[int], dict[str, int], tuple[float, ...] directly.
Checking annotations
import inspect
hints = get_user.__annotations__
# {"user_id": int, "return": Optional[dict]}Solution
from typing import Optional, List, Dict, Union, Tuple
def get_user(user_id: int) -> Optional[dict]:
"""Look up a user by ID. Returns user dict or None if not found."""
users = {1: {"name": "Aryan", "age": 22}, 2: {"name": "Priya", "age": 20}}
return users.get(user_id) # FIX 2: Optional[dict] reflects that None is possible
def format_value(value: Union[str, int]) -> str:
# FIX 1: Union[str, int] with square brackets
return str(value)
def process_items(items: List[str]) -> List[str]:
"""Convert all items to uppercase strings."""
return [str(item).upper() for item in items]
def get_dimensions(width: float, height: float) -> Tuple[float, float, float]:
"""Return (width, height, area) as a tuple."""
return (width, height, width * height)
def merge_records(a: Dict[str, int], b: Dict[str, int]) -> Dict[str, int]:
"""Merge two dicts, summing values for duplicate keys."""
result = dict(a)
for key, val in b.items():
result[key] = result.get(key, 0) + val
return resultTests
import inspect
def test_format_value_string():
"""format_value must not crash at definition time (Union(str,int) does)"""
assert format_value("hello") == "hello"
def test_format_value_int():
assert format_value(42) == "42"
def test_get_user_found():
user = get_user(1)
assert user is not None
assert user["name"] == "Aryan"
def test_get_user_not_found_returns_none():
"""get_user must return None for missing users, not raise KeyError"""
result = get_user(999)
assert result is None, "get_user should return None for unknown user_id"
def test_process_items_converts_to_uppercase():
result = process_items(["apple", "banana"])
assert result == ["APPLE", "BANANA"]
def test_get_dimensions_returns_three_values():
result = get_dimensions(3.0, 4.0)
assert len(result) == 3
assert result[0] == 3.0
assert result[1] == 4.0
assert abs(result[2] - 12.0) < 0.001
def test_merge_records_sums_duplicates():
a = {"apples": 3, "bananas": 2}
b = {"bananas": 5, "cherries": 1}
result = merge_records(a, b)
assert result["bananas"] == 7
assert result["apples"] == 3
assert result["cherries"] == 1
def test_get_user_annotation_allows_none():
"""The return annotation should be Optional[dict], not just dict"""
hints = get_user.__annotations__
return_hint = str(hints.get("return", ""))
# Optional[dict] shows up as typing.Optional[dict]
assert "Optional" in return_hint or "None" in return_hint, (
"get_user should be annotated as Optional[dict] since it can return None"
)