PythonLists

Python Lists Learning Path

11 tutorials beginner / intermediate

Lists are the most-used data structure in Python. They are flexible, ordered, mutable, and the first tool you reach for when you need to collect and process data. But there is more depth here than most tutorials cover.

At the CPython level, a list is a C struct called PyListObject that holds an array of pointers to PyObject instances. The list does not store your data directly -- it stores references to wherever your data lives on the heap. This is why a single list can hold integers, strings, and other lists at the same time, and it is the root cause of the aliasing bugs that catch developers off guard.

CPython MEMORY INSIGHT When CPython needs to grow a list, it does not just add one slot. The internal list_resize() function over-allocates using the formula new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3, producing the growth sequence 0, 4, 8, 16, 24, 32, 40, 52, 64, 76... This amortizes the O(n) copy cost so that repeated append() calls run in O(1) amortized time. The list shrinks back only when its length falls below half its allocated capacity.

Tutorials marked with the cert badge include a final exam that awards a certificate of completion you can download and share.

Python's sort is equally worth understanding at a deeper level. Both list.sort() and sorted() use Timsort, a hybrid of merge sort and insertion sort written by Tim Peters in 2002 specifically for Python. Timsort finds already-sorted sub-sequences (runs) in real data and merges them, giving O(n) performance on nearly-sorted input and O(n log n) worst-case.

The Python documentation guarantees sort stability: equal-key records always retain their original relative order. This applies to both list.sort() and sorted(), and has been an explicit guarantee since Python 2.2.

Python Software Foundation, Sorting HOW TO, docs.python.org/3/howto/sorting.html

This path covers lists from first principles through advanced manipulation patterns, including the copy semantics and aliasing behaviors that cause the majority of hard-to-trace bugs in Python code.

List Operation Complexity at a Glance

Operation Syntax Time Complexity Notes
Index accesslist[i]O(1)Direct pointer lookup in ob_item array
Lengthlen(list)O(1)Reads ob_size field directly -- never loops
Appendlist.append(x)O(1) amortizedOccasional O(n) resize, amortized constant
Pop from endlist.pop()O(1)No shift required
Pop from middlelist.pop(i)O(n)Elements after i must shift left
Insertlist.insert(i, x)O(n)Elements after i must shift right
Delete by indexdel list[i]O(n)Elements after i must shift left
Membership testx in listO(n)Linear scan; use set for repeated lookups
Slicinglist[a:b]O(k)k = b - a; creates a new list
Concatenationlist + list2O(k)k = total elements in both lists
Sortlist.sort()O(n log n)Timsort; O(n) best case on sorted input
Reverselist.reverse()O(n)In-place; swaps pointers
Extendlist.extend(it)O(k)k = length of iterable being added
list[i] is O(1) -- direct pointer lookup. len(list) is O(1) -- reads the stored ob_size field without looping.
append() is O(1) amortized. An occasional O(n) resize happens but the cost spreads across many appends. extend(it) is O(k) where k is the length of the iterable.
pop() from the end is O(1). pop(i) from a position and insert(i, x) are both O(n) because all elements after the position must shift.
x in list is O(n) -- linear scan. For repeated membership tests use a set. Slicing list[a:b] is O(k) where k is the slice length.
list.sort() and sorted() use Timsort: O(n log n) worst case, O(n) best case on already-sorted data. Sort is guaranteed stable.

Python Lists from Zero to Mastery: The Definitive Guide

Complete coverage of list creation, indexing, slicing, methods, comprehensions, nested lists, sorting, copying, and performance. Includes CPython memory model details you will not find in most tutorials.

Beginner read()

append vs extend in Python

The difference between append and extend, when to use each, and the performance implications of both. Includes what actually happens at the C level when each method is called.

Beginner read()

Concatenating Lists in Python

Every way to combine lists: +, extend, unpacking, itertools.chain, and when each approach is the right choice. Covers memory implications of each method.

Beginner read()

How to Find List Indices in Python

Finding element positions with index(), enumerate, comprehensions, and handling missing elements gracefully. Covers the ValueError raised by index() and how to avoid it.

Beginner read()

Python List Conversion

Converting between lists and other types: tuples, sets, dicts, strings, and generators. Includes which conversions are O(1) and which require an O(n) copy.

Beginner read()

Sorting in Python

sort() vs sorted(), key functions, reverse sorting, stable sort behavior, and how Timsort exploits real-world partial ordering to outperform generic O(n log n) sorts.

Beginner read()

Learn What List Comprehensions are in Python

Syntax, conditions, nested loops, and common use cases for list comprehensions. Covers when a comprehension is faster than a for-loop append and when to switch back to a loop for readability. Includes interactive exercises.

Beginner read()
03

How to Work Through This Learning Path

This path is structured to build on itself. Work through the fundamentals in order before moving to the advanced patterns group. Each tutorial is self-contained but written to assume the concepts from earlier tutorials in the sequence.

step 01

Start with the Definitive Guide

Read Python Lists from Zero to Mastery first. It establishes the full mental model: how PyListObject works in memory, what ob_item and allocated mean, and why the list holds references rather than values. Every other tutorial in this path assumes this foundation.

step 02

Learn the core mutation methods

Work through append vs extend and Concatenating Lists together. These two tutorials cover how Python grows and merges lists, which directly determines memory usage and performance in real code. Pay attention to which operations are O(1) amortized and which are O(n).

step 03

Master indexing, slicing, and searching

Read How to Find List Indices. Negative indexing, step slices, and the difference between index() and enumerate() are used constantly and misunderstood more often than they should be. Also covers how to handle the ValueError that index() raises when an element is absent.

step 04

Understand sorting and conversion

Read Sorting in Python and Python List Conversion. Sorting with key functions, understanding sort stability, and knowing when to convert a list to a set or tuple are all standard daily-use skills. These also establish the groundwork for the advanced patterns.

step 05

Tackle the advanced patterns

Work through the four tutorials in the Advanced Patterns group. Start with Why Is My List Changing Unexpectedly and Shallow Copy vs Deep Copy since aliasing bugs affect every level of Python code. Follow with the flattening tutorials once the reference model is solid.

04

Frequently Asked Questions

A Python list is a built-in, ordered, mutable sequence that can hold elements of any type, including a mix of types, in a single collection. Lists are the most widely used data structure in Python and are defined using square brackets, for example: my_list = [1, 'hello', 3.14]. Because they are mutable, elements can be added, removed, or changed after creation.
append() adds a single object as a new element at the end of the list. If you pass it another list, that entire list becomes one nested element. extend() iterates over its argument and adds each item individually, expanding the list by the number of items in the iterable. For example: my_list.append([1,2]) results in one new element that is itself a list, while my_list.extend([1,2]) adds two separate elements.
Python's list.sort() and sorted() use an algorithm called Timsort, created by Tim Peters in 2002. Timsort is a hybrid of merge sort and insertion sort that identifies already-sorted sub-sequences (called runs) and merges them efficiently. It guarantees stable sort behavior, meaning elements with equal keys keep their original relative order. The worst-case time complexity is O(n log n), and the best case for already-sorted data is O(n).
A shallow copy creates a new list object but its elements are references to the same objects as the original. If the list contains mutable objects like nested lists, modifying those objects in the copy also changes the original. A deep copy, created with copy.deepcopy(), recursively copies every object so the copy is completely independent of the original. Use list.copy(), list[:], or list() for shallow copies, and copy.deepcopy() when you need full independence.
In CPython, a list is a C struct called PyListObject containing three core fields: the standard object header (PyObject_VAR_HEAD, which includes the reference count, type pointer, and ob_size), a pointer to an array of PyObject pointers called ob_item, and an integer called allocated that tracks the total number of slots reserved in memory. The list stores references to objects, not the objects themselves. When the list grows beyond its allocated slots, CPython calls list_resize(), which over-allocates memory using the growth sequence 0, 4, 8, 16, 24, 32, 40, 52... to amortize the cost of future appends.
A list comprehension is a concise syntax for building a new list by applying an expression to each item in an iterable, optionally filtered by a condition. The form is [expression for item in iterable if condition]. List comprehensions are generally faster than equivalent for-loop appends because the interpreter can optimize the iteration. Use them for simple, readable transformations. When the logic becomes complex, a regular for loop is usually more maintainable.
Yes. Python's sort is guaranteed to be stable. The Python documentation states that when multiple records have the same key, their original order is preserved. This guarantee holds for both list.sort() and sorted(). It has been guaranteed since Python 2.2 and is a deliberate design property of Timsort, not an implementation detail.
Indexing (list[i]) and len() are O(1). append() is O(1) amortized. insert() and pop() at an arbitrary position are O(n) because elements must shift. Searching with in is O(n). Sorting is O(n log n). Slicing list[a:b] is O(k) where k is the slice length. Concatenation with + is O(k) where k is the total length of both lists. Deletion from the end with pop() is O(1).
For one level of nesting, use a list comprehension: [item for sublist in nested for item in sublist]. For one level handled more efficiently, use list(itertools.chain.from_iterable(nested)). For deeply nested or irregular structures, a recursive function or a stack-based loop is required. NumPy's ndarray.flatten() is the best choice when working with numeric arrays.
This happens because Python lists store references to objects, not copies of them. When you assign a list to another variable with b = a, both variables point to the same list object in memory. Modifying b also modifies a. The same issue occurs with nested lists inside a shallow copy. To avoid this, use b = a.copy() or b = a[:] for a new top-level list, or copy.deepcopy(a) when nested mutable objects must also be independent.