Python
typing module
type hints
list vs List
tuple usage

Using List/Tuple/etc. from typing vs directly referring type as list/tuple/etc

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Before Python 3.9, you had to import List, Dict, Tuple, Set, etc. from the typing module to use generic type hints (e.g., List[int]). Since Python 3.9, the built-in types list, dict, tuple, set support subscripting directly (e.g., list[int]), making the typing imports unnecessary for most cases. The typing versions still exist for backward compatibility but are considered legacy. This article explains when to use which, the migration path, and the remaining cases where typing is still needed.

Before Python 3.9

python
1from typing import List, Dict, Tuple, Set, Optional
2
3def process_items(items: List[int]) -> Dict[str, List[int]]:
4    result: Dict[str, List[int]] = {}
5    for item in items:
6        key = "even" if item % 2 == 0 else "odd"
7        result.setdefault(key, []).append(item)
8    return result
9
10def find_user(user_id: int) -> Optional[Tuple[str, int]]:
11    # Returns (name, age) or None
12    ...
13
14names: Set[str] = {"Alice", "Bob"}

List[int] is typing.List subscripted with int. Without the import, list[int] raises TypeError in Python 3.8 and earlier.

Python 3.9+: Built-in Generics

python
1# No imports needed for basic type hints
2def process_items(items: list[int]) -> dict[str, list[int]]:
3    result: dict[str, list[int]] = {}
4    for item in items:
5        key = "even" if item % 2 == 0 else "odd"
6        result.setdefault(key, []).append(item)
7    return result
8
9def find_user(user_id: int) -> tuple[str, int] | None:
10    ...
11
12names: set[str] = {"Alice", "Bob"}

Since Python 3.9, list[int], dict[str, int], tuple[str, ...], and set[int] work directly. Since Python 3.10, X | None replaces Optional[X].

Using from __future__ import annotations (Python 3.7+)

python
1from __future__ import annotations
2
3# This makes ALL annotations strings (PEP 563)
4# So list[int] works even on Python 3.7/3.8
5def greet(names: list[str]) -> dict[str, str]:
6    return {name: f"Hello, {name}" for name in names}

With from __future__ import annotations, annotations are not evaluated at runtime — they become strings. This lets you use the modern list[int] syntax on Python 3.7+.

Comparison Table

Type Hinttyping (pre-3.9)Built-in (3.9+)
List of intsList[int]list[int]
DictionaryDict[str, int]dict[str, int]
Tuple (fixed)Tuple[str, int]tuple[str, int]
Tuple (variable)Tuple[int, ...]tuple[int, ...]
SetSet[str]set[str]
Frozen setFrozenSet[int]frozenset[int]
OptionalOptional[int]`int \None` (3.10+)
UnionUnion[int, str]`int \str` (3.10+)
TypeType[MyClass]type[MyClass]

When typing Is Still Needed

Some constructs have no built-in equivalent:

python
1from typing import (
2    TypeVar, Generic, Protocol, Callable,
3    Literal, TypeAlias, TypeGuard, Annotated
4)
5
6# TypeVar for generics
7T = TypeVar("T")
8
9def first(items: list[T]) -> T:
10    return items[0]
11
12# Callable
13def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
14    return func(a, b)
15
16# Literal
17def set_mode(mode: Literal["read", "write"]) -> None:
18    ...
19
20# Protocol (structural typing)
21class Drawable(Protocol):
22    def draw(self) -> None: ...
23
24# Generic classes
25class Stack(Generic[T]):
26    def __init__(self) -> None:
27        self._items: list[T] = []
28    def push(self, item: T) -> None:
29        self._items.append(item)
30    def pop(self) -> T:
31        return self._items.pop()

Migration Example

python
1# Before (Python 3.8)
2from typing import List, Dict, Optional, Tuple, Set, Union
3
4def analyze(
5    data: List[Dict[str, Union[int, str]]],
6    threshold: Optional[float] = None,
7) -> Tuple[Set[str], int]:
8    ...
9
10# After (Python 3.10+)
11def analyze(
12    data: list[dict[str, int | str]],
13    threshold: float | None = None,
14) -> tuple[set[str], int]:
15    ...

The migration is purely syntactic — runtime behavior is identical.

Runtime vs Static Type Checking

python
1# Type hints are for static checkers (mypy, pyright), not runtime
2def add(a: int, b: int) -> int:
3    return a + b
4
5add("hello", "world")  # No runtime error — Python ignores type hints
6# mypy: error: Argument 1 to "add" has incompatible type "str"; expected "int"
7
8# Runtime checking with isinstance
9def add_safe(a: int, b: int) -> int:
10    if not isinstance(a, int) or not isinstance(b, int):
11        raise TypeError("Arguments must be integers")
12    return a + b

Common Pitfalls

  • Using list[int] on Python 3.8 without __future__ annotations: list[int] raises TypeError at runtime on Python 3.8 and earlier. Either import from typing or add from __future__ import annotations at the top of the file.
  • Mixing typing.List and list in the same codebase: Using List[int] in some files and list[int] in others is inconsistent. Pick one style based on your minimum Python version and apply it consistently.
  • Confusing Tuple[int, int] with Tuple[int, ...]: tuple[int, int] is a fixed-length tuple of exactly two ints. tuple[int, ...] is a variable-length tuple of ints. Omitting the ... means fixed length, which may not be what you intended.
  • Assuming type hints enforce types at runtime: Python does not check type hints during execution. def f(x: int): ... accepts any argument type. Use mypy or pyright for static checking, and isinstance() for runtime validation.
  • Importing deprecated typing aliases on Python 3.9+: typing.List, typing.Dict, etc. are deprecated since Python 3.9 and may be removed in future versions. Use the built-in types directly to avoid deprecation warnings in type checkers.

Summary

  • Use list[int], dict[str, int], tuple[str, ...] directly on Python 3.9+ — no typing imports needed
  • Use from __future__ import annotations to enable the modern syntax on Python 3.7-3.8
  • typing.List, typing.Dict, etc. are deprecated since 3.9 — use built-in types in new code
  • typing is still needed for TypeVar, Generic, Protocol, Callable, Literal, and other advanced constructs
  • Use X | None instead of Optional[X] on Python 3.10+ (or with __future__ annotations on 3.7+)
  • Type hints are for static analysis tools — Python does not enforce them at runtime

Course illustration
Course illustration

All Rights Reserved.