Python List Type Hint: Using list[str] Correctly
python list type hint: Learn the correct syntax for Python list type hints, how list[str] works, common pitfalls, and tooling compatibility.
python list type hint requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you annotate a list in Python, the type hint syntax is list[str] on Python 3.9 and newer, or List[str] from the typing module on earlier versions. This article focuses on the modern list[str] form, how to use it in function signatures and variable annotations, and the mistakes that most often appear in real code.
The Basic Syntax for List Type Hints
The simplest form is list[str], which declares a list whose elements are all strings. For example:
names: list[str] = ["alice", "bob", "carol"]
The annotation list[str] is a generic alias for the built-in list type. It tells type checkers and human readers that every element in this list should be a string. If you try to assign a list containing an integer, a static type checker like mypy will report an error.
The same syntax works for other element types:
scores: list[int] = [95, 87, 91] records: list[float] = [1.5, 2.7, 3.9] flags: list[bool] = [True, False, True]
For more complex element types, you can nest the generic notation. A list of dictionaries, for instance, would be list[dict[str, int]].
Why list[str] Replaced typing.List
Before Python 3.9, the standard way to annotate a list was typing.List:
from typing import List names: List[str] = ["alice", "bob"]
Python 3.9 introduced the ability to use built-in collection types as generics, so list[str] became valid without importing anything. The typing.List form still works for backward compatibility, but it is no longer necessary in modern code.
The main advantage of list[str] is that it removes an import and aligns the annotation with the runtime type. list is what you actually use at runtime, and list[str] is what you write in annotations. This reduces cognitive overhead and makes the code cleaner.
If you are supporting Python 3.8 or earlier, you must use typing.List. In that case, list[str] will raise a TypeError at runtime because the built-in list does not support subscription on older versions.
Annotating Function Parameters and Return Types
List type hints are most valuable in function signatures. They document the expected input and output without requiring the reader to trace the implementation.
def get_names() -> list[str]: return ["alice", "bob"] def process_names(names: list[str]) -> None: for name in names: print(name.upper())
The return annotation -> list[str] states that the function returns a list of strings. The parameter annotation names: list[str] tells callers that only a list of strings is acceptable.
When a function modifies a list in place, the annotation should still reflect the element type. For example:
def add_name(names: list[str], name: str) -> None: names.append(name)
There is no need to annotate the return type as list[str] because the function returns None. The mutation is visible through the passed argument.
Nested Lists and Mixed Element Types
Lists can contain other lists, and the type hint should express that nesting. A matrix, for instance, is a list of lists of numbers:
matrix: list[list[int]] = [ [1, 2, 3], [4, 5, 6], ]
If you need a list that can hold multiple types, use Union or Any. For example, a list that contains either integers or strings:
from typing import Union values: list[Union[int, str]] = [1, "two", 3]
In Python 3.10 and later, you can use the | operator instead of Union:
values: list[int | str] = [1, "two", 3]
This is more concise and works with modern type checkers. However, if you need to support Python 3.9, stick with Union.
A common mistake is to annotate a list as list without the element type. That is equivalent to list[Any] and disables most of the value that type hints provide. Always specify the element type unless you genuinely have no information about the contents.
Common Mistakes with List Type Hints
One frequent error is using list[str] in a context where the runtime type does not match. For example, assigning a tuple to a variable annotated as list[str] will be flagged by a type checker, because a tuple is immutable and has a different type.
# Type checker error: tuple is not list names: list[str] = ("alice", "bob")
Another mistake is forgetting that list[str] is a mutable type. If you pass a list to a function that only needs to read it, consider using Sequence[str] from typing to allow both lists and tuples. This gives callers more flexibility without sacrificing type safety.
from typing import Sequence def print_names(names: Sequence[str]) -> None: for name in names: print(name)
Using Sequence instead of list in read-only parameters is a good practice because it accepts any sequence, including tuples and ranges.
Another common issue is using list[str] as a default argument value. This is safe because the annotation is not evaluated at runtime in a way that creates a shared mutable object. The default value itself must still be a list:
def add_default(names: list[str] = []) -> None: names.append("default")
This code is valid but has the classic mutable default argument problem: the default list is shared across calls. Type hints do not change that behavior. Use None and create a new list inside the function to avoid the issue.
Runtime Behavior and Performance Considerations
Type hints are not enforced at runtime by default. The list[str] annotation is stored in the function's __annotations__ attribute, but Python does not check that the elements are strings when you call the function. This means type hints have no direct performance impact on normal execution.
However, there is a subtle runtime cost when you use from __future__ import annotations. This future import makes all annotations strings, which delays their evaluation. That can be beneficial for performance because Python does not need to build the actual type objects at function definition time. The tradeoff is that tools that rely on runtime annotation inspection, such as some dependency injection frameworks, may need to call typing.get_type_hints() to resolve the strings.
For most applications, the performance difference is negligible. The real performance concern is not the annotation itself but the operations you perform on the list. Type hints help you write code that avoids unnecessary type checks or conversions, which can indirectly improve performance by making the intended data structure explicit.
One operational consideration is that type hints are only useful if you run a static type checker. Tools like mypy, pyright, and the built-in typing module's get_type_hints() can catch errors before runtime. Adding list[str] to your code gives these tools the information they need to detect mismatched element types early.
Compatibility and Tooling Support
list[str] requires Python 3.9 or later. If your project supports Python 3.8 or earlier, you must use typing.List or add from __future__ import annotations to defer annotation evaluation. The future import makes the annotation a string, so list[str] will not be evaluated at runtime, but type checkers will still understand it.
from __future__ import annotations def process_names(names: list[str]) -> None: pass ```n With the future import, the annotation is stored as the string `"list[str]"`, so it works on Python 3.8 even though `list[str]` is not a valid expression in that version. However, if you need to inspect the annotation at runtime with `typing.get_type_hints()`, the future import will cause it to be evaluated, and that evaluation will fail on Python 3.8. In that case, stick with `typing.List`. Most modern type checkers support `list[str]` fully. Mypy, Pyright, and Pyre all understand the built-in generic syntax. The `typing` module also provides `get_type_hints()` which can resolve `list[str]` correctly on Python 3.9 and later. When you use `list[str]` in a codebase that also uses older annotations, be consistent. Mixing `list[str]` and `typing.List` in the same project is valid, but it can confuse readers. Choose one style based on your minimum Python version and apply it consistently. A final edge case worth knowing: `list[str]` is not the same as `typing.List[str]` at runtime. They are distinct objects, but they are treated as equivalent by type checkers. If you use `typing.get_type_hints()` to inspect annotations, you may get either form depending on how the annotation was written. This rarely matters in practice, but it can affect code that compares annotation objects directly. For most projects, `list[str]` is the right choice. It is concise, requires no import, and works with all current type checkers. If you need to support older Python versions, `typing.List` remains a reliable fallback.