Difficulty: Intermediate
How do type hints work in Python? Why use them?
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].
# 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.
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.
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.
Type Hints, mypy, typing module, Generic Types, TypeVar, Protocol