← Home

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 + b

Optional β€” 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 result

Tests

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"
    )

Resources