Skip to content

List[int] vs list[int] - Generic Types

Generic types can be specified using either typing module classes (List[int]) or built-in types (list[int], available in Python 3.9+).

Mental Model

Before Python 3.9, you had to import capitalized generics from typing (List[int], Dict[str, int]). From 3.9 onward, the built-in types themselves accept subscripts (list[int], dict[str, int]). The two forms mean exactly the same thing — prefer the lowercase built-in syntax in new code and use the typing imports only when supporting older Python versions.

Legacy Typing Module Syntax

Before Python 3.9, use types from the typing module for generic annotations.

```python from typing import List, Dict, Set, Tuple

Pre-3.9 style using typing module

numbers: List[int] = [1, 2, 3] mapping: Dict[str, int] = {"a": 1, "b": 2} unique: Set[str] = {"x", "y", "z"} pair: Tuple[int, str] = (1, "one")

print(numbers, mapping, unique, pair) ```

[1, 2, 3] {'a': 1, 'b': 2} {'x', 'y', 'z'} (1, 'one')

Modern Built-in Syntax (Python 3.9+)

Starting with Python 3.9, use built-in types directly for generics.

```python

Python 3.9+ style using built-in types

numbers: list[int] = [1, 2, 3] mapping: dict[str, int] = {"a": 1, "b": 2} unique: set[str] = {"x", "y", "z"} pair: tuple[int, str] = (1, "one")

print(numbers, mapping, unique, pair) ```

[1, 2, 3] {'a': 1, 'b': 2} {'x', 'y', 'z'} (1, 'one')

When to Use Each Style

Choose based on your Python version and codebase consistency.

```python

New code with Python 3.9+: prefer built-in syntax

def process(items: list[str]) -> dict[str, int]: return {item: len(item) for item in items}

result = process(["cat", "elephant", "dog"]) print(result)

Use typing module for complex types

from typing import Callable callback: Callable[[int], str] = str print(callback(42)) ```

{'cat': 3, 'elephant': 8, 'dog': 3} 42


Exercises

Exercise 1. Rewrite the following annotation using the modern list[int] syntax instead of List[int]:

python from typing import List, Dict, Tuple def process(data: List[int]) -> Dict[str, Tuple[int, ...]]: pass

Solution to Exercise 1

python def process(data: list[int]) -> dict[str, tuple[int, ...]]: pass


Exercise 2. Write a function first_element(items: list[str]) -> str that returns the first element. What Python version is required to use list[str] instead of List[str]?

Solution to Exercise 2

```python def first_element(items: list[str]) -> str: return items[0]

print(first_element(["a", "b", "c"])) # "a" ```

Python 3.9+ is required. In 3.8 and earlier, you must use from typing import List and write List[str].


Exercise 3. Predict whether the following code works in Python 3.9+ and in Python 3.8:

python def keys_and_values(d: dict[str, int]) -> tuple[list[str], list[int]]: return list(d.keys()), list(d.values())

Solution to Exercise 3

In Python 3.9+, this works perfectly. In Python 3.8, it raises a TypeError at runtime because built-in types like dict and tuple do not support subscript syntax for type hints. To make it work in 3.8, use from __future__ import annotations or import from typing.


Exercise 4. Explain the purpose of from __future__ import annotations and how it allows using modern syntax in older Python versions.

Solution to Exercise 4

from __future__ import annotations (PEP 563) makes all annotations lazy — they are stored as strings and not evaluated at runtime. This means you can use list[int], dict[str, int], etc., even in Python 3.7+, because the subscript expression is never actually executed. The annotations are only evaluated when a tool like mypy or typing.get_type_hints() processes them.