Introduction
Since Python 3.9, dict[str, int] and typing.Dict[str, int] are functionally identical for type annotations — both describe a dictionary with string keys and integer values. Before Python 3.9, only typing.Dict supported subscripting (Dict[str, int]), while dict[str, int] was a TypeError. The modern recommendation is to use the built-in dict (lowercase) for type hints in Python 3.9+ and reserve typing.Dict for code that must support Python 3.8 or earlier. This change was introduced by PEP 585 to simplify type annotations by eliminating the need to import from typing.
The Historical Problem
1# Python 3.8 and earlier
2from typing import Dict, List, Tuple, Set
3
4def process(data: Dict[str, List[int]]) -> Dict[str, int]:
5 return {k: sum(v) for k, v in data.items()}
6
7# Using built-in dict was a TypeError:
8# def process(data: dict[str, list[int]]) -> dict[str, int]:
9# TypeError: 'type' object is not subscriptable
1# Python 3.9+
2# Built-in types support subscripting directly — no imports needed
3def process(data: dict[str, list[int]]) -> dict[str, int]:
4 return {k: sum(v) for k, v in data.items()}
5
6# typing.Dict still works but is redundant
7from typing import Dict
8def process(data: Dict[str, list[int]]) -> Dict[str, int]:
9 ... # Same thing, but requires an import
PEP 585 Built-in Generics (Python 3.9+)
1# Before (typing module required)
2from typing import Dict, List, Tuple, Set, FrozenSet, Type, Deque
3
4x: Dict[str, int]
5y: List[str]
6z: Tuple[int, str]
7s: Set[float]
8t: Type[MyClass]
9
10# After (Python 3.9+, use built-in types directly)
11x: dict[str, int]
12y: list[str]
13z: tuple[int, str]
14s: set[float]
15t: type[MyClass]
16
17# Also works for standard library types
18from collections import deque, defaultdict, OrderedDict, Counter
19
20a: deque[int]
21b: defaultdict[str, list[int]]
22c: OrderedDict[str, int]
23d: Counter[str]
| typing module | Built-in (3.9+) |
typing.Dict[K, V] | dict[K, V] |
typing.List[T] | list[T] |
typing.Tuple[T, ...] | tuple[T, ...] |
typing.Set[T] | set[T] |
typing.FrozenSet[T] | frozenset[T] |
typing.Type[T] | type[T] |
Using from __future__ import annotations (Python 3.7+)
1# Python 3.7 or 3.8 — use __future__ to enable built-in generic syntax
2from __future__ import annotations
3
4# Now this works even on Python 3.7/3.8
5def process(data: dict[str, list[int]]) -> dict[str, int]:
6 return {k: sum(v) for k, v in data.items()}
7
8# The annotations are stored as strings, not evaluated at runtime
9# So dict[str, int] is just the string "dict[str, int]"
10# Type checkers (mypy, pyright) understand it
With from __future__ import annotations, all annotations become strings. The dict[str, int] syntax is never evaluated at runtime, so it works even on Python versions that do not support built-in generics.
Runtime Behavior Differences
1# Python 3.9+
2from typing import Dict
3
4# Both create the same type hint
5hint1 = dict[str, int]
6hint2 = Dict[str, int]
7
8# But they are NOT identical objects
9print(hint1 == hint2) # True (equal)
10print(hint1 is hint2) # False (different objects)
11print(type(hint1)) # <class 'types.GenericAlias'>
12print(type(hint2)) # typing.Dict[str, int]
13
14# For isinstance checks, use the base type
15print(isinstance({"a": 1}, dict)) # True
16# isinstance({"a": 1}, dict[str, int]) # TypeError! Cannot use generic for isinstance
17# isinstance({"a": 1}, Dict[str, int]) # TypeError! Same issue
When to Use Which
1# RECOMMENDED: Python 3.9+ projects
2def get_config() -> dict[str, str]:
3 return {"host": "localhost", "port": "8080"}
4
5# RECOMMENDED: Python 3.7-3.8 projects with __future__
6from __future__ import annotations
7
8def get_config() -> dict[str, str]:
9 return {"host": "localhost", "port": "8080"}
10
11# REQUIRED: Python 3.7-3.8 WITHOUT __future__ (runtime annotations needed)
12from typing import Dict
13
14def get_config() -> Dict[str, str]:
15 return {"host": "localhost", "port": "8080"}
16
17# REQUIRED: When annotations must be evaluated at runtime
18# (Pydantic v1, FastAPI, dataclasses with field processing)
19from typing import Dict # May need typing.Dict for runtime evaluation on 3.8
Typing-Only Imports That Are No Longer Needed
1# Python 3.9+ — you can remove these typing imports:
2# from typing import Dict, List, Tuple, Set, FrozenSet, Type
3
4# Python 3.10+ — you can also remove:
5# from typing import Union → use X | Y
6# from typing import Optional → use X | None
7
8# Python 3.9+ example — clean, no typing imports
9def process_users(
10 users: list[dict[str, str]],
11 filters: set[str] | None = None, # 3.10+ union syntax
12) -> tuple[list[str], int]:
13 names = [u["name"] for u in users]
14 return names, len(names)
Common Pitfalls
Using dict[str, int] on Python 3.8 without __future__: This raises TypeError: 'type' object is not subscriptable at runtime. Either add from __future__ import annotations or use typing.Dict for Python 3.8 compatibility.
Using generic types with isinstance(): isinstance(obj, dict[str, int]) raises TypeError in all Python versions. Generic aliases cannot be used for runtime type checking. Use isinstance(obj, dict) without type parameters.
Mixing typing.Dict and dict inconsistently: Using Dict in some annotations and dict in others is not a bug but hurts readability. Pick one style per project and enforce it with a linter (ruff, flake8-pep585).
Forgetting that __future__ annotations are strings: With from __future__ import annotations, annotations are not evaluated at runtime. Code that accesses func.__annotations__ or uses typing.get_type_hints() may behave differently. Pydantic v1 and some decorators require real type objects, not strings.
Removing typing imports needed for runtime features: typing.TypeVar, typing.Generic, typing.Protocol, typing.Literal, and typing.Annotated have no built-in equivalents. Only container types (Dict, List, Tuple, Set) can be replaced with built-in generics.
Summary
Python 3.9+: use dict[K, V] instead of typing.Dict[K, V] — no import needed
Python 3.7-3.8: use from __future__ import annotations to enable dict[K, V] syntax
Python 3.6-3.8 without __future__: use typing.Dict[K, V]
dict[K, V] and Dict[K, V] are equal for type checking but are different runtime objects
Neither dict[K, V] nor Dict[K, V] can be used with isinstance() — use the plain dict type
The same pattern applies to list, tuple, set, frozenset, and type (PEP 585)