Collections Type Hints: Lists, Dicts, and Sets Guide
Collections like lists, dictionaries, and sets require special type syntax in Python. Instead of annotating just list, you specify what's inside the collection using angle brackets: List[int] for a list of integers, Dict[str, int] for a dictionary mapping strings to integers. This article teaches you to type all common Python collections accurately.
Why Collection Types Matter
Without collection types, mypy cannot verify what's inside:
# Without type hints
def process_items(items):
for item in items:
print(item.upper()) # mypy: error: "item" has unknown type
# With collection types
from typing import List
def process_items(items: List[str]) -> None:
for item in items:
print(item.upper()) # OK: mypy knows item is str
Collection types tell mypy (and other developers) both what the collection is and what types it contains, enabling better error detection and IDE autocompletion.
Typing Lists: List[T]
A List[int] is a list where every item is an integer:
from typing import List
# Correct: all items are integers
numbers: List[int] = [1, 2, 3, 4, 5]
# Correct: can reassign with other integers
numbers = [10, 20, 30]
# Incorrect: mixing types
mixed: List[int] = [1, "2", 3] # mypy: error: List item 1 has incompatible type "str"
# Incorrect: string item added
numbers.append("six") # mypy: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
For a list that can hold multiple types, use Union:
from typing import List, Union
mixed: List[Union[int, str]] = [1, "2", 3, "4"]
mixed.append("5") # OK
mixed.append(6) # OK
mixed.append(3.14) # mypy: error: Argument 1 has incompatible type "float"
In Python 3.10+, use the pipe syntax for brevity:
mixed: List[int | str] = [1, "2", 3, "4"]
Typing Dictionaries: Dict[K, V]
Dict[K, V] specifies both key and value types. K is the key type, V is the value type:
from typing import Dict
# Dict with string keys and integer values
scores: Dict[str, int] = {
"Alice": 95,
"Bob": 87,
"Charlie": 92,
}
# Correct access
print(scores["Alice"]) # 95
# Incorrect: integer key
scores[1] = 100 # mypy: error: Invalid index type "int" for "Dict"; expected type "str"
# Incorrect: string value
scores["Diana"] = "88" # mypy: error: Incompatible types in assignment (expression has type "str", expected "int")
Common patterns:
from typing import Dict, List, Optional
# Dict with list values
categories: Dict[str, List[str]] = {
"fruits": ["apple", "banana"],
"vegetables": ["carrot", "broccoli"],
}
# Dict with optional values
user_ages: Dict[str, Optional[int]] = {
"Alice": 30,
"Bob": None, # Age unknown
}
# Dict with flexible types (key: int, value: int or str)
config: Dict[int, int | str] = {
1: "debug",
2: 100,
}
Typing Sets and Frozensets
A Set[T] holds unique items of type T:
from typing import Set, FrozenSet
# Set of integers
primes: Set[int] = {2, 3, 5, 7, 11}
# Add a valid item
primes.add(13) # OK
# Add an invalid item
primes.add("13") # mypy: error: Argument 1 to "add" of "set" has incompatible type "str"; expected "int"
# Frozenset (immutable set)
immutable_ids: FrozenSet[int] = frozenset([100, 200, 300])
Set operations preserve types:
numbers: Set[int] = {1, 2, 3}
more_numbers: Set[int] = {3, 4, 5}
# Union of two sets preserves the type
union: Set[int] = numbers | more_numbers # {1, 2, 3, 4, 5}
# Intersection also preserves type
intersection: Set[int] = numbers & more_numbers # {3}
Typing Tuples: Tuple[T, ...]
Tuples are trickier because they can have fixed length and heterogeneous types:
from typing import Tuple
# Fixed-length tuple: exactly 2 elements, first is str, second is int
pair: Tuple[str, int] = ("Alice", 30)
# Incorrect: wrong number of elements
too_short: Tuple[str, int] = ("Alice",) # mypy: error: Incompatible types in assignment
# Incorrect: wrong types
wrong_types: Tuple[str, int] = (30, "Alice") # mypy: error: Incompatible types in assignment
# Variable-length tuple of integers: Tuple[int, ...]
numbers: Tuple[int, ...] = (1, 2, 3, 4, 5)
more_numbers: Tuple[int, ...] = (10, 20) # Different length is OK
# Mixed: (int, str, str, float)
mixed: Tuple[int, str, str, float] = (1, "hello", "world", 3.14)
Use Tuple[T, ...] when the tuple can be any length but all items are the same type. Use fixed Tuple[T1, T2, ...] when the tuple has a specific structure.
Typing Deques, Counters, and Other Collections
The typing module provides hints for less common collections:
from typing import Deque, Counter, DefaultDict
from collections import deque, Counter, defaultdict
# Deque of integers
queue: Deque[int] = deque([1, 2, 3])
queue.append(4) # OK
queue.append("5") # mypy: error: Argument 1 has incompatible type "str"
# Counter: Dict subclass that counts occurrences
word_counts: Counter[str] = Counter(["apple", "banana", "apple"])
# word_counts = {"apple": 2, "banana": 1}
# DefaultDict: Dict with a default factory
default_lists: DefaultDict[str, list] = defaultdict(list)
default_lists["key"].append(1) # OK, list() provides []
default_lists["other"].append("item") # OK
Handling Empty Collections
Empty collections need explicit annotation because mypy cannot infer their type:
# Wrong: mypy infers items as List[Unknown]
items = []
# Correct: explicit annotation
from typing import List
items: List[int] = []
# Also correct with Python 3.9+: use lowercase list
items: list[int] = []
# Correct: initialize with a value
items = [1, 2, 3] # mypy infers List[int]
Practical Example: A Grade Calculator
Here's a complete example combining multiple collection types:
from typing import Dict, List, Tuple, Optional
class GradeCalculator:
"""Calculate statistics on student grades."""
def __init__(self, student_grades: Dict[str, List[int]]) -> None:
"""
Args:
student_grades: Map of student name to their grades.
"""
self.grades = student_grades
def average(self, name: str) -> Optional[float]:
"""Return the average grade for a student, or None if not found."""
if name not in self.grades:
return None
grades = self.grades[name]
return sum(grades) / len(grades) if grades else None
def top_student(self) -> Optional[Tuple[str, float]]:
"""Return (name, average) of the student with the highest average."""
if not self.grades:
return None
best: Optional[Tuple[str, float]] = None
for name, grades in self.grades.items():
if grades:
avg = sum(grades) / len(grades)
if best is None or avg > best[1]:
best = (name, avg)
return best
# Usage
data: Dict[str, List[int]] = {
"Alice": [95, 92, 98],
"Bob": [87, 90, 85],
"Charlie": [88, 91, 89],
}
calc = GradeCalculator(data)
print(calc.average("Alice")) # 95.0
print(calc.top_student()) # ('Alice', 95.0)
Every variable is precisely typed: Dict[str, List[int]] for the input, Optional[float] for averages that might be None, Optional[Tuple[str, float]] for optional pairs.
Key Takeaways
- Use
List[T]to type a list of items of type T;Dict[K, V]for dictionaries;Set[T]for sets Tuple[T1, T2, ...]is for fixed-length heterogeneous tuples;Tuple[T, ...]for variable-length homogeneous tuples- Use
Union[T1, T2](orT1 | T2in Python 3.10+) when a collection can hold multiple types - Always annotate empty collections explicitly:
items: List[int] = [] - Collection types enable mypy to catch type errors and IDEs to provide autocompletion
- Import collection types from
typingfor Python < 3.9; use lowercaselist[T]directly in Python 3.9+
Frequently Asked Questions
What's the difference between list[int] and List[int]?
In Python 3.9+, you can use list[int] directly without importing. In Python < 3.9, you must import List from typing. Both are equivalent in modern Python; the lowercase versions are preferred.
Can I have a list of lists? How do I type it?
Yes: List[List[int]] is a list of lists of integers. Similarly, Dict[str, List[int]] is a dict mapping strings to lists of integers. Nesting works for any collection combination.
What's the type of an empty dictionary?
Use Dict[str, int] or dict[str, int] (with explicit key and value types). If you need a flexible dictionary, use Dict[str, Any] from typing.
Can I type a dictionary with multiple key types?
Yes, use Dict[Union[int, str], int] or Dict[int | str, int] (Python 3.10+) to allow both int and str keys.
Should I always use collection types, or is list without parameters OK?
Always use parameterized types when possible. Unparameterized list is equivalent to List[Any], which defeats the purpose of type checking. Use parameters: List[int], List[str], etc.
Further Reading
- Python
typingCollections Reference — Full list of available collection types and parameters - PEP 585: Type Hinting Generics in Collections — How lowercase list/dict/set typing works in Python 3.9+
- mypy Generic Types — Deep dive into generic type parameters
- Real Python: Collections and mypy — Practical examples and patterns