TL;DR Quick Fix
On Python 3.8 or older, built-in types like list[int] and dict[str, int] can't be used as generic type hints directly. Two fast escapes: swap to typing module equivalents, or drop from __future__ import annotations at the top of your file.
# Breaks on Python < 3.9
def get_scores(names: list[str]) -> dict[str, int]:
return {name: 0 for name in names}
# Fix 1: typing module (works back to Python 3.5)
from typing import List, Dict
def get_scores(names: List[str]) -> Dict[str, int]:
return {name: 0 for name in names}
# Fix 2: lazy annotations (works back to Python 3.7)
from __future__ import annotations
def get_scores(names: list[str]) -> dict[str, int]:
return {name: 0 for name in names}
What Causes This Error
PEP 585, shipped in Python 3.9, added native generic support to built-in types. Before that, list and dict didn't support the subscript operator ([]) at runtime. Only the wrapper classes in the typing module β List, Dict, and friends β knew how to handle it.
By default, Python evaluates annotations at import time. The moment it hits list[int] on an older interpreter, it tries to subscript the raw list type β and blows up immediately. Common triggers:
- Function signatures:
def foo(x: list[int]) - Variable annotations:
scores: dict[str, float] = {} - Dataclass field definitions using PEP 585 syntax
- Pydantic or attrs models running on Python 3.8
Not sure which Python you're running? Check fast:
python --version
# Or inside your script:
import sys
print(sys.version_info) # e.g., sys.version_info(major=3, minor=8, micro=18, ...)
Fix Approaches
Option 1: Import from the typing module (safest for libraries)
Swap the bare built-in names for their uppercase counterparts from typing. Verbose, yes β but it works all the way back to Python 3.5 and plays nicely with every tool in the ecosystem.
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, Type
def process(items: List[int]) -> Tuple[int, ...]:
return tuple(items)
def lookup(mapping: Dict[str, List[str]]) -> Optional[str]:
return mapping.get("key", [None])[0]
Quick cheat-sheet for the types you'll reach for most often:
# Python < 3.9 β Python 3.9+
List[int] β list[int]
Dict[str, int] β dict[str, int]
Tuple[int, ...] β tuple[int, ...]
Set[str] β set[str]
FrozenSet[str] β frozenset[str]
Type[MyClass] β type[MyClass]
Optional[str] β str | None (3.10+)
Option 2: from future import annotations
One import, zero annotation changes. This line makes Python store all annotations as strings rather than evaluating them at runtime β so you can write PEP 585 syntax freely on Python 3.7+.
from __future__ import annotations
def merge(a: list[str], b: list[str]) -> dict[str, int]:
return {k: i for i, k in enumerate(a + b)}
For large codebases with hundreds of files, this is the quickest win β no hunt-and-replace across imports. One catch: libraries that introspect annotations at runtime (FastAPI's dependency injection, serializers like cattrs) will receive strings instead of real type objects, and may need extra handling.
Option 3: Upgrade to Python 3.9+
If you control the runtime, this is the cleanest long-term path. Python 3.9 made built-in generics first-class citizens β no imports, no workarounds.
# Python 3.9+ β no imports needed
def summarize(data: list[dict[str, int]]) -> list[int]:
return [sum(d.values()) for d in data]
Bonus: dataclass fields
Dataclasses are a sneaky trigger. Unlike function annotations, field annotations are evaluated at class definition time β so the error hits on import, not on function call.
# Fails on Python 3.8
from dataclasses import dataclass
@dataclass
class Report:
scores: dict[str, int] # TypeError right here, at import time
tags: list[str]
# Fix: one future import is all you need
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class Report:
scores: dict[str, int] # Fine now
tags: list[str]
Verifying the Fix
Run your module directly. A clean import means you're done:
python my_module.py
For a quicker check without running the whole script:
python -c "from my_module import get_scores; print(get_scores(['alice', 'bob']))"
Want to confirm the annotations are well-formed? Inspect them directly:
import inspect
import my_module
print(inspect.signature(my_module.get_scores))
# Output: (names: List[str]) -> Dict[str, int]

