Python Type Hints: Add Types, Generics, and the typing Module
Imagine ordering food at a restaurant. The menu says "Spicy Level: 1-5" — that's a type hint. It doesn't stop you from writing "eleven" on the order form, but it tells everyone what's expected. If you follow it, things go smoothly. If you ignore it, your meal might come out wrong.
Type hints in Python work the same way. They're optional annotations that document what types your functions expect and return. Python won't enforce them at runtime, but tools like mypy can check them before your code runs — catching bugs before they become problems.
How Do You Add Type Hints to Variables and Functions?
The syntax is simple. For variables, add a colon and the type after the name. For functions, annotate each parameter and use -> for the return type.
The annotations don't change how the code runs. greet(42) would still work at runtime — Python ignores type hints during execution. But a type checker would flag it as an error.
Add proper type hints to this function and its variables. The function takes a list of integers and returns their average as a float. Also add type hints to the two variables.
Make the function work correctly, then print the result for the list [10, 20, 30, 40].
What Are Optional and Union Types?
Sometimes a value can be one of several types. A function might return a string or None. A parameter might accept an int or a float. Python's typing module provides Optional and Union for these cases.
Optional[str] is shorthand for Union[str, None] — it means "a string or None." Use Optional when a value might be absent, and Union when a value can be one of several distinct types.
How Do You Type Lists, Dicts, and Tuples?
Plain list, dict, and tuple annotations don't tell you what's inside the collection. The typing module lets you specify element types for precise annotations.
Write a function get_config that takes a Dict[str, str] of config values and an Optional[str] default value. It should look up a key and return the value if found, or the default if not.
Type-hint everything: parameters, return type, and the config variable.
config = {'host': 'localhost', 'port': '8080'}
print(get_config(config, 'host')) # localhost
print(get_config(config, 'debug', 'off')) # off
print(get_config(config, 'missing')) # NoneHow Do You Simplify Complex Type Annotations?
When type annotations get long and repetitive, type aliases let you give them short, descriptive names. This keeps your code readable.
def process(data: Dict[str, List[Tuple[int, float]]]) -> Dict[str, float]:
...Measurements = Dict[str, List[Tuple[int, float]]]
Summary = Dict[str, float]
def process(data: Measurements) -> Summary:
...What does this code print? Remember, type hints don't affect runtime behavior.
def add(a: int, b: int) -> int:
return a + b
result = add('hello', ' world')
print(result)
print(type(result).__name__)Write two print() statements with the exact output.
How Do You Type Functions and Callbacks?
Since functions are objects in Python, you sometimes need to annotate a parameter that expects a function. The Callable type handles this.
Callable[[int], int] means "a function that takes one int argument and returns an int." The first part is a list of parameter types, and the second is the return type.
Write a function filter_by that takes a List[int] and a predicate function (Callable[[int], bool]) and returns a List[int] of items that pass the predicate.
Test it with an is_even function on [1, 2, 3, 4, 5, 6].
What Is the Difference Between Runtime and Static Type Checking?
This is the most important thing to understand about Python type hints: they exist in two separate worlds.
Static checking happens before your code runs. Tools like mypy, pyright, and your IDE analyze your code and find type errors without executing anything. This catches bugs like passing a string where an int is expected.
Runtime checking happens while your code runs. Python itself does NOT check type hints at runtime. If you want runtime enforcement, you need to add it yourself using isinstance() checks or libraries like Pydantic.
def add(a: int, b: int) -> int:
return a + b
# Works at runtime! No error.
print(add("hello", " world"))def add(a: int, b: int) -> int:
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError("Both arguments must be int")
return a + b
print(add(3, 4)) # Works
# add("hello", " world") # TypeError!Write a function safe_divide(a: float, b: float) -> str that:
1. Checks that both arguments are int or float using isinstance. If not, return 'Error: arguments must be numbers'.
2. Checks if b is zero. If so, return 'Error: division by zero'.
3. Otherwise, returns the result as a string with 2 decimal places.
print(safe_divide(10, 3)) # 3.33
print(safe_divide(10, 0)) # Error: division by zero
print(safe_divide("10", 3)) # Error: arguments must be numbersWrite a fully type-annotated function summarize_scores that takes a Dict[str, List[int]] mapping student names to their test scores, and returns a Dict[str, float] mapping each student to their average score.
scores = {
'Alice': [90, 85, 92],
'Bob': [78, 88, 84]
}
print(summarize_scores(scores))
# {'Alice': 89.0, 'Bob': 83.33}Round averages to 2 decimal places.
What Should You Remember About Type Hints?
Type hints make Python code clearer, more maintainable, and less bug-prone. They're one of the most impactful features added to modern Python.
Key takeaways:
name: str = 'Alice' and def greet(name: str) -> str:Optional[X] means X or None; Union[X, Y] means X or YList[int], Dict[str, int], Tuple[str, int] specify element typesCallable[[args], return] types function parametersisinstance() or Pydantic for runtime enforcement