Back to Blog
Python

Python List Type: Behavior, Annotations, and Performance

python list type: Understand Python's list type: runtime behavior, type annotations, common operations, performance tradeoffs, and compatibility considerations for mod...

Pythonlisttype annotationssequencesperformancetyping
Illustration of a Python list as an ordered mutable sequence of mixed-type elements with type annotation brackets.

Python's list is a mutable, ordered sequence type that can hold elements of any type. When developers search for the python list type, they usually need two things at once: how lists behave at runtime and how to describe them in type annotations. Both matter in modern Python code, so this article covers the runtime semantics, the annotation syntax, and the performance tradeoffs that affect real programs.

The List Type at Runtime

A list literal is written with square brackets. The list object stores references to its elements, which means a single list can hold integers, strings, and custom objects in the same collection.

items = [1, 2, 3] mixed = [1, "two", 3.0] empty = []

Internally, CPython implements lists as dynamic arrays of object pointers. The array is overallocated so that appending does not reallocate on every call. Index access reads a pointer from the array, making items[i] a constant-time operation. The list grows and shrinks as elements are added or removed, and the same list object can be mutated in place.

Because lists hold references rather than values, copying a list does not copy the elements. A shallow copy shares the same element objects, which matters when those elements are themselves mutable.

Declaring the List Type in Annotations

Type hints describe the expected shape of data without changing runtime behavior. Since Python 3.9, the built-in list type can be parameterized directly:

def process_items(items: list[int]) -> list[str]: return [str(item) for item in items]

The annotation list[int] states that the argument is a list whose elements are integers. The return annotation declares a list of strings. Tools like mypy, pyright, and pyflakes use these annotations to catch type mismatches before the code runs.

Before Python 3.9, the same annotation required typing.List:

from typing import List def process_items(items: List[int]) -> List[str]: ...

The lowercase list in an annotation is a type, not a function call. The runtime list() constructor still creates a new list from an iterable, and the two uses are distinct.

Common Operations and Their Behavior

Lists support a set of operations that cover most collection needs: indexing, slicing, appending, extending, inserting, removing, and membership testing.

items = [1, 2, 3] items.append(4) # [1, 2, 3, 4] items.extend([5, 6]) # [1, 2, 3, 4, 5, 6] items.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6] items.remove(3) # removes the first 3 popped = items.pop() # removes and returns the last element

append adds one element at the end. extend adds each element of an iterable. insert places an element at a given index and shifts the remaining elements right. remove deletes the first matching value and raises ValueError if the value is absent. pop removes and returns an element, defaulting to the last position.

Slicing returns a new list:

subset = items[1:3] reversed_items = items[::-1]

A slice always produces a shallow copy. Modifying the slice does not affect the original list, but mutating an object inside the slice affects the same object referenced by the original.

Performance Characteristics of Lists

The performance profile of a list follows directly from its dynamic-array implementation. Indexing is O(1). Appending at the end is amortized O(1) because the overallocated array absorbs most appends without reallocation. Inserting or removing near the beginning is O(n) because every subsequent element shifts.

Membership testing with in is O(n) because lists are not hash-based. A set provides O(1) membership testing but requires the elements to be hashable and costs O(n) to build.

if target in items: ... if target in set(items): ...

The right choice depends on how many lookups happen. A single membership check on a small list is fine. If the same list is searched repeatedly, converting it to a set once and reusing the set avoids repeated linear scans.

Memory usage also follows from the reference-based design. The list array stores pointers, so the memory footprint is proportional to the number of elements, plus the overallocation slack. Each element object is allocated separately, which is why a list of small integers uses more memory than a dense array from the array module.

The List Type vs Tuple and Other Sequences

A tuple is an immutable, fixed-size sequence. A list is mutable and resizable. The distinction affects both correctness and intent.

Propertylisttuple
MutableYesNo
HashableNoYes, when elements are hashable
ResizableYesNo
Typical useDynamic collectionsFixed records

A tuple can serve as a dictionary key because it is hashable. A list cannot, because its contents could change after insertion. When a function returns a fixed pair of values, a tuple communicates that the structure is stable. When data accumulates over time, a list is the natural choice.

For homogeneous numeric data, the array module stores values compactly without per-element object overhead. For large numeric datasets, array.array or a NumPy array may be more appropriate than a list, at the cost of restricting element types.

Where the List Type Commonly Causes Confusion

A mutable default argument is a classic list-related bug:

def add_item(item, container=[]): container.append(item) return container

The default list is created once when the function is defined, not on each call. Every call that omits container shares the same list, so results accumulate across calls. The standard fix is to use None as the default and create a fresh list inside the function:

def add_item(item, container=None): if container is None: container = [] container.append(item) return container

Another common confusion is assignment versus copying:

new_list = old_list # same object copy_list = old_list.copy() # shallow copy

new_list = old_list creates a second name for the same list. Mutating either name changes the same underlying object. old_list.copy() or list(old_list) creates a new list with the same element references, so appending to the copy does not affect the original.

Type Annotations for Nested Lists

Nested collections appear in real code as matrices, grids, and grouped data. The annotation mirrors the nesting:

def transpose(matrix: list[list[int]]) -> list[list[int]]: return [list(row) for row in zip(*matrix)]

list[list[int]] means a list whose elements are themselves lists of integers. The nesting depth is explicit in the annotation. A list of dictionaries is written list[dict[str, int]] when each dictionary maps strings to integers.

Type checkers validate the element types at each level. A value like [[1, "two"], [3]] would be rejected for list[list[int]] because the inner list contains a string. The annotation also documents the intended structure for readers, which matters more as the nesting grows.

Compatibility Considerations

The built-in generic syntax list[int] requires Python 3.9 or newer. Code that must run on Python 3.8 uses typing.List instead. The two forms are equivalent for type checking, but only the built-in form works at runtime on Python 3.9+.

The from __future__ import annotations import changes how annotations are evaluated. Annotations become strings stored lazily rather than evaluated at function definition time. This can delay type errors until a tool like mypy inspects the code, and it affects runtime libraries such as pydantic that read annotations to build validation schemas. When a library depends on evaluating annotations at import time, the future import can change its behavior.

For codebases that support multiple Python versions, the typing.List form remains the safest choice. For new code targeting Python 3.9 and later, the built-in list[int] syntax is cleaner and requires no import.

python list type: Practical Usage and Code Examples | RYUSLOG DEV