Type Hints & mypy

Difficulty: Intermediate

Question

How do type hints work in Python? Why use them?

Answer

Type hints (PEP 484, Python 3.5+) add optional type annotations to Python code. They are NOT enforced at runtime - they are used by static type checkers (mypy, pyright), IDEs, and documentation.

Benefits: - Catch type errors before runtime - Better IDE autocompletion and refactoring - Self-documenting code - Easier to maintain large codebases

Python 3.10+ simplifies syntax: use X | Y instead of Union[X, Y], use list[int] instead of List[int].

Code examples

Basic Type Hints

# Variable annotations
name: str = "Alice"
age: int = 30
scores: list[float] = [95.5, 87.0, 92.3]
config: dict[str, int] = {'timeout': 30, 'retries': 3}

# Function annotations
def greet(name: str, excited: bool = False) -> str:
    if excited:
        return f"Hello, {name}!!!"
    return f"Hello, {name}"

# Optional (can be None)
def find_user(user_id: int) -> dict | None:  # Python 3.10+
    if user_id > 0:
        return {'id': user_id, 'name': 'Alice'}
    return None

# Union types (pre-3.10: Union[int, str])
def process(value: int | str) -> str:
    return str(value)

# Collections
def average(numbers: list[float]) -> float:
    return sum(numbers) / len(numbers)

def word_count(text: str) -> dict[str, int]:
    words = text.lower().split()
    return {w: words.count(w) for w in set(words)}

print(greet("Alice", excited=True))
print(average([1.0, 2.0, 3.0]))
print(word_count("the cat sat on the mat"))

Type hints make function signatures self-documenting. Python 3.10+ allows X | Y syntax. These are not enforced at runtime - they're for tools and humans.

Advanced Types: TypeVar, Generic, TypedDict

from typing import TypeVar, Generic, TypedDict, Callable, Literal

# TypeVar: generic type variable
T = TypeVar('T')

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

print(first([1, 2, 3]))       # int inferred
print(first(['a', 'b', 'c'])) # str inferred

# Generic classes
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()
    
    def peek(self) -> T:
        return self._items[-1]

stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop())  # 2

# TypedDict: typed dictionary
class UserDict(TypedDict):
    name: str
    age: int
    email: str

user: UserDict = {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
print(user['name'])

# Callable type
def apply_fn(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

print(apply_fn(lambda x, y: x + y, 3, 4))  # 7

# Literal type
def set_mode(mode: Literal['read', 'write', 'append']) -> str:
    return f"Mode set to: {mode}"

TypeVar enables generic functions/classes. TypedDict types dictionary keys. Callable types function signatures. Literal restricts to specific values.

Type Narrowing and Runtime Checking

from typing import TypeGuard, assert_type

# isinstance for type narrowing
def process(value: int | str | list[int]) -> str:
    if isinstance(value, int):
        # mypy knows value is int here
        return f"Integer: {value * 2}"
    elif isinstance(value, str):
        # mypy knows value is str here
        return f"String: {value.upper()}"
    else:
        # mypy knows value is list[int] here
        return f"List sum: {sum(value)}"

print(process(5))           # 'Integer: 10'
print(process('hello'))     # 'String: HELLO'
print(process([1, 2, 3]))   # 'List sum: 6'

# TypeGuard for custom type narrowing
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process_strings(items: list[object]) -> str:
    if is_string_list(items):
        # mypy now knows items is list[str]
        return ', '.join(items)
    return 'Not all strings'

print(process_strings(['a', 'b', 'c']))  # 'a, b, c'
print(process_strings([1, 'b', 3]))      # 'Not all strings'

# Runtime type checking with beartype or typeguard
# from beartype import beartype
# @beartype
# def add(a: int, b: int) -> int:
#     return a + b
# add('x', 'y')  # Raises BeartypeCallHintParamViolation

isinstance() narrows types for mypy. TypeGuard enables custom type predicates. For runtime enforcement, use beartype or typeguard libraries.

Key points

Concepts covered

Type Hints, mypy, typing module, Generic Types, TypeVar, Protocol