Codetail

Article 5 of 15

Lists

Ordered collections. The workhorse data structure.

28 min read

Creating lists

A list is an ordered, mutable collection. You can put anything in a list: numbers, strings, booleans, other lists, even functions. Items don't have to be the same type.

Python
1# Literal syntax
2fruits = ["apple", "banana", "cherry"]
3nums = [1, 2, 3, 4, 5]
4empty = []
5
6# Mixed types are fine
7mixed = [1, "hello", True, None, 3.14]
8
9# Nested lists
10matrix = [
11 [1, 2, 3],
12 [4, 5, 6],
13 [7, 8, 9],
14]
15print(matrix[1][2]) # 6: row 1, column 2

list() constructor

Convert any iterable to a list.

Python
1list("hello") # ['h', 'e', 'l', 'l', 'o']
2list(range(5)) # [0, 1, 2, 3, 4]
3list((1, 2, 3)) # [1, 2, 3]: tuple to list
4list({3, 1, 2}) # [1, 2, 3]: set to list (order varies)
5
6# range() is common for generating sequences
7evens = list(range(0, 10, 2))
8print(evens) # [0, 2, 4, 6, 8]

Initializing with a default value

The * operator repeats a list. Use it to pre-fill a list with a known size. Works for immutables only. See the gotcha below.

Python
1zeros = [0] * 5
2print(zeros) # [0, 0, 0, 0, 0]
3
4flags = [False] * 3
5print(flags) # [False, False, False]

Gotcha: [[]] * 3 creates three references to the same inner list, not three separate lists. Modifying one modifies all. Use a comprehension instead: [[] for _ in range(3)].

Indexing and slicing

Lists are sequences, so everything from the Strings article applies: positive indices count from the left, negative from the right, and slicing extracts a sublist.

Python
1items = ["a", "b", "c", "d", "e"]
2# 0 1 2 3 4
3# -5 -4 -3 -2 -1
4
5print(items[0]) # "a": first
6print(items[-1]) # "e": last
7print(items[-2]) # "d": second from last
8
9print(items[1:3]) # ["b", "c"]: indices 1 and 2
10print(items[:2]) # ["a", "b"]: first two
11print(items[2:]) # ["c", "d", "e"]: from index 2 onward
12print(items[::-1]) # ["e", "d", "c", "b", "a"]: reversed

Slice assignment

Unlike strings, lists are mutable. You can assign directly to an index or a slice, which modifies the list in place.

Python
1items = [1, 2, 3, 4, 5]
2
3# Replace a single element
4items[0] = 10
5print(items) # [10, 2, 3, 4, 5]
6
7# Replace a slice: replacement can be a different length
8items[1:3] = [20, 30, 40]
9print(items) # [10, 20, 30, 40, 4, 5]
10
11# Delete a slice
12del items[1:3]
13print(items) # [10, 40, 4, 5]
14
15# Clear via slice
16items[:] = []
17print(items) # []

Nested lists

Python
1grid = [
2 [1, 2, 3],
3 [4, 5, 6],
4 [7, 8, 9],
5]
6
7# Access: row first, then column
8print(grid[0][0]) # 1: top-left
9print(grid[2][2]) # 9: bottom-right
10
11# Iterate rows
12for row in grid:
13 print(row)
14
15# Get a column (list comprehension)
16col_1 = [row[1] for row in grid]
17print(col_1) # [2, 5, 8]

Modifying lists

Lists are mutable: you can add, remove, and rearrange items after creation. Try the operations below, then read the explanations.

Interactive List
apple0
banana1
cherry2
date3
Try it, click an operation
Current state
['apple', 'banana', 'cherry', 'date']

Adding items

Python
1fruits = ["apple", "banana"]
2
3# append(): add one item to the end, O(1), fast
4fruits.append("cherry")
5print(fruits) # ["apple", "banana", "cherry"]
6
7# insert(index, value): add at a specific position, O(n), shifts items right
8fruits.insert(1, "avocado")
9print(fruits) # ["apple", "avocado", "banana", "cherry"]
10
11# extend(): add all items from another iterable
12fruits.extend(["date", "elderberry"])
13print(fruits) # ["apple", "avocado", "banana", "cherry", "date", "elderberry"]
14
15# + creates a NEW list, doesn't modify in place
16more = fruits + ["fig"]
17print(more is fruits) # False: different object

append vs extend: fruits.append(["fig", "grape"]) adds the list as a single nested element. fruits.extend(["fig", "grape"]) adds each item individually.

Removing items

Python
1fruits = ["apple", "banana", "cherry", "banana"]
2
3# remove(value): removes FIRST occurrence by value, raises ValueError if missing
4fruits.remove("banana")
5print(fruits) # ["apple", "cherry", "banana"]
6
7# pop(index): removes and returns item at index (default: last)
8last = fruits.pop()
9print(last) # "banana"
10print(fruits) # ["apple", "cherry"]
11
12first = fruits.pop(0)
13print(first) # "apple"
14
15# del: remove by index or slice, no return value
16items = [1, 2, 3, 4, 5]
17del items[1]
18print(items) # [1, 3, 4, 5]
19
20# clear(): remove everything
21items.clear()
22print(items) # []

Other operations

Python
1nums = [3, 1, 4, 1, 5, 9, 2]
2
3# copy(): shallow copy
4copy = nums.copy() # same as nums[:] or list(nums)
5
6# reverse(): in-place
7nums.reverse()
8print(nums) # [2, 9, 5, 1, 4, 1, 3]
9
10# sort(): in-place
11nums.sort()
12print(nums) # [1, 1, 2, 3, 4, 5, 9]
13
14# count(value): how many times value appears
15print(nums.count(1)) # 2
16
17# index(value): position of first occurrence
18print(nums.index(4)) # 4

List comprehensions

A list comprehension builds a new list from an iterable in a single expression. It is faster than a loop, more readable for simple transformations, and one of the most distinctly Pythonic features in the language.

The speed difference comes from how Python runs them. A regular loop calls .append() on every iteration. Each call is a Python-level method lookup and function invocation. A comprehension skips that entirely. Python handles the whole loop internally in C, using a single optimized bytecode instruction to add each item. The more items you process, the wider the gap.

Python
1# Loop version
2squares = []
3for x in range(6):
4 squares.append(x ** 2)
5
6# Comprehension version: same result, one line
7squares = [x ** 2 for x in range(6)]
8print(squares) # [0, 1, 4, 9, 16, 25]

Syntax: [expression for item in iterable if condition]. The if condition part is optional.

With a filter condition

Python
1nums = range(20)
2
3evens = [x for x in nums if x % 2 == 0]
4print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
5
6# Transform and filter at the same time
7even_squares = [x ** 2 for x in nums if x % 2 == 0]
8print(even_squares) # [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]
9
10# Filter a list of strings
11words = ["hello", "", "world", " ", "python"]
12non_empty = [w for w in words if w.strip()]
13print(non_empty) # ['hello', 'world', 'python']

Nested comprehensions

When a comprehension has multiple for clauses, the order trips people up. The rule is simple: write the for clauses in exactly the same order you would write the nested loops, top to bottom. Outer loop first, inner loop second.

Python
1# Flatten a 2D list
2matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3flat = [x for row in matrix for x in row]
4print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
5
6# Cartesian product
7colors = ["red", "green"]
8sizes = ["S", "M", "L"]
9variants = [(c, s) for c in colors for s in sizes]
10print(variants)
11# [('red','S'),('red','M'),('red','L'),('green','S'),('green','M'),('green','L')]

Formula

Translate stacked loops into a comprehension by reading top to bottom, left to right. The expression you want to collect goes at the front.

Nested loops

for row in matrix:   # outer
    for x in row:    # inner
        result.append(x)

Comprehension

[x                   # expression
  for row in matrix  # outer
  for x in row]      # inner

When not to use comprehensions

The readability advantage disappears when the logic gets complex. If you need multiple conditions, nested calls, or side effects, a plain loop communicates intent more clearly. Cleverness is not a virtue here.

Python
1# Too complex, use a regular loop
2result = [
3 process(item)
4 for item in get_items()
5 if item.is_valid()
6 if item.category in allowed_categories
7 if item.price < max_price
8]
9
10# Clear loop version
11result = []
12for item in get_items():
13 if not item.is_valid():
14 continue
15 if item.category not in allowed_categories:
16 continue
17 if item.price >= max_price:
18 continue
19 result.append(process(item))

If the comprehension does not fit on one readable line, write a loop. The goal is code that communicates clearly, not the fewest lines.

Sorting

Python gives you two ways to sort. The difference matters: sort() modifies the list in place and returns None. sorted() returns a new sorted list and leaves the original untouched.

Python
1nums = [3, 1, 4, 1, 5, 9, 2]
2
3# sorted(): returns new list, original unchanged
4ordered = sorted(nums)
5print(ordered) # [1, 1, 2, 3, 4, 5, 9]
6print(nums) # [3, 1, 4, 1, 5, 9, 2]: unchanged
7
8# sort(): in-place, returns None
9nums.sort()
10print(nums) # [1, 1, 2, 3, 4, 5, 9]
11
12# Reverse order
13print(sorted(nums, reverse=True)) # [9, 5, 4, 3, 2, 1, 1]

Sorting with a key

The key parameter takes a function applied to each item before comparison. The items themselves are not changed. Only the comparison uses the key.

Python
1words = ["banana", "fig", "apple", "date", "elderberry"]
2
3# Sort by length
4print(sorted(words, key=len))
5# ['fig', 'date', 'apple', 'banana', 'elderberry']
6
7# Sort alphabetically, case-insensitive
8names = ["alice", "Bob", "charlie", "Dave"]
9print(sorted(names, key=str.lower))
10# ['alice', 'Bob', 'charlie', 'Dave']
11
12# Sort a list of dicts by a field
13people = [
14 {"name": "Charlie", "age": 30},
15 {"name": "Alice", "age": 25},
16 {"name": "Bob", "age": 35},
17]
18by_age = sorted(people, key=lambda p: p["age"])
19print([p["name"] for p in by_age]) # ['Alice', 'Charlie', 'Bob']
20
21# Sort by multiple fields: age ascending, then name alphabetically
22by_age_then_name = sorted(people, key=lambda p: (p["age"], p["name"]))
23print([p["name"] for p in by_age_then_name])

Python's sort is stable: items that compare equal keep their original order. This matters when sorting by multiple criteria in sequence: sort by a secondary key first, then by the primary key, and ties in the primary key will preserve the secondary order.

Transforming lists

Transforming means applying the same operation to every item and collecting the results into a new list. The length never changes. Every item goes in, every item comes out changed.

In a comprehension, the expression before for is the transform. Whatever that expression evaluates to becomes the item in the new list.

Python
1nums = [1, 2, 3, 4, 5]
2
3# Double every number
4doubled = [x * 2 for x in nums]
5print(doubled) # [2, 4, 6, 8, 10]
6
7# Square every number
8squared = [x ** 2 for x in nums]
9print(squared) # [1, 4, 9, 16, 25]
10
11# Convert to strings
12as_strings = [str(x) for x in nums]
13print(as_strings) # ['1', '2', '3', '4', '5']
14
15# Works on any type: uppercase every word
16words = ["hello", "world", "python"]
17upper = [w.upper() for w in words]
18print(upper) # ['HELLO', 'WORLD', 'PYTHON']

Python's built-in map() does the same job. It takes a function and an iterable and applies the function to each item. Comprehensions are generally preferred because the expression is written inline, no lambda needed.

Python
1nums = [1, 2, 3, 4, 5]
2
3# These produce identical results
4doubled_comp = [x * 2 for x in nums]
5doubled_map = list(map(lambda x: x * 2, nums))
6
7print(doubled_comp) # [2, 4, 6, 8, 10]
8print(doubled_map) # [2, 4, 6, 8, 10]
9
10# map() shines when you already have a named function
11def fahrenheit(c):
12 return c * 9/5 + 32
13
14temps_c = [0, 20, 37, 100]
15temps_f = list(map(fahrenheit, temps_c))
16print(temps_f) # [32.0, 68.0, 98.6, 212.0]

Pick a transform below and watch every item change at once.

Map Explorer
nums = [1, 2, 3, 4, 5]
1
2
3
4
5
pick a transform below
1
2
3
4
5
Try it, pick a transform

Filtering lists

Filtering means keeping only the items that satisfy a condition and discarding the rest. The original list is never touched. You get back a new, shorter list.

The clearest way to filter in Python is a comprehension with an if clause. You read it left to right: give me each x from nums, but only if the condition is true.

Python
1nums = [1, 2, 3, 4, 5, 6, 7, 8]
2
3# Keep only even numbers
4evens = [x for x in nums if x % 2 == 0]
5print(evens) # [2, 4, 6, 8]
6
7# Keep numbers greater than 4
8big = [x for x in nums if x > 4]
9print(big) # [5, 6, 7, 8]
10
11# Works on strings too: keep non-empty values
12words = ["hello", "", "world", " ", "python"]
13non_empty = [w for w in words if w.strip()]
14print(non_empty) # ['hello', 'world', 'python']

Python also has a built-in filter() function that does the same thing. Comprehensions are preferred because they don't require a lambda and read more naturally.

Python
1nums = [1, 2, 3, 4, 5]
2
3# These produce identical results
4evens_comp = [x for x in nums if x % 2 == 0]
5evens_filter = list(filter(lambda x: x % 2 == 0, nums))
6
7print(evens_comp) # [2, 4]
8print(evens_filter) # [2, 4]

See it live. Pick a condition below and watch which items survive.

Filter Explorer
nums = [1, 2, 3, 4, 5, 6, 7, 8]
1
2
3
4
5
6
7
8
pick a condition below
1
2
3
4
5
6
7
8
Try it, pick a condition

Real-world patterns

Deduplicate while preserving order

Converting to a set removes duplicates but loses order. This pattern preserves it.

Python
1items = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
2
3# set(): fast but unordered
4unique_unordered = list(set(items))
5
6# dict.fromkeys(): preserves insertion order (Python 3.7+)
7unique_ordered = list(dict.fromkeys(items))
8print(unique_ordered) # [3, 1, 4, 5, 9, 2, 6]

Chunk a list into batches

Python
1def chunks(lst, size):
2 return [lst[i:i + size] for i in range(0, len(lst), size)]
3
4data = list(range(10))
5print(chunks(data, 3))
6# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Use a list as a stack

Lists make an efficient stack with append() and pop(). Both are O(1).

Python
1stack = []
2
3stack.append("first")
4stack.append("second")
5stack.append("third")
6
7print(stack.pop()) # "third": last in, first out
8print(stack.pop()) # "second"
9print(stack) # ["first"]
10
11# Practical: undo history
12history = []
13history.append(("type", "H"))
14history.append(("type", "i"))
15history.append(("delete",))
16
17last = history.pop() # undo last action

zip: iterate two lists in parallel

zip() pairs up items from two or more lists by position and lets you unpack them together in a loop. Index zero from each list, then index one, then index two. No manual indexing required.

Python
1names = ["Alice", "Bob", "Charlie"]
2scores = [92, 85, 78]
3
4for name, score in zip(names, scores):
5 print(f"{name}: {score}")

Shortest wins by default. When the lists are different lengths, zip() stops as soon as the shorter one runs out. The extra items in the longer list are silently ignored.

Python
1letters = ["a", "b", "c", "d"]
2nums = [1, 2]
3
4for letter, num in zip(letters, nums):
5 print(letter, num)
6# "c" and "d" are never reached

Longest with a fill value. If you need every item from every list, use zip_longest from itertools. It pads the shorter list with a fill value you choose instead of stopping early.

Python
1from itertools import zip_longest
2
3letters = ["a", "b", "c", "d"]
4nums = [1, 2]
5
6for letter, num in zip_longest(letters, nums, fillvalue=0):
7 print(letter, num)

Use zip() when mismatched lengths are a bug and you want to fail fast. Use zip_longest() when shorter lists are expected and a default value makes sense.

enumerate: index and value together

When you need both the position and the value while looping, enumerate() gives you both at once as a pair. It is the standard Python way to track position inside a loop without reaching for the index manually.

The common alternative, range(len(fruits)), works but forces you to index back into the list on every iteration. It is noisier, easier to get wrong, and signals to other Python developers that you are not yet thinking in Python idioms.

Python
1fruits = ["apple", "banana", "cherry"]
2
3# Avoid: manual index lookup on every iteration
4for i in range(len(fruits)):
5 print(i, fruits[i])
6
7# Prefer: enumerate gives you both directly
8for i, fruit in enumerate(fruits):
9 print(i, fruit)

The start parameter lets you begin counting from any number. Useful for user-facing output where 0-based numbering looks wrong.

Python
1fruits = ["apple", "banana", "cherry"]
2
3for i, fruit in enumerate(fruits, start=1):
4 print(f"{i}. {fruit}")

If you catch yourself writing range(len(...)) to get an index, stop and use enumerate() instead. It is almost always the right call.

Flatten and group

Flattening collapses a list of lists into a single list. The nested comprehension pattern works for one level deep. You iterate over each sublist, then over each item inside it. Outer loop first, inner loop second. The same rule as always.

Python
1nested = [[1, 2], [3, 4], [5, 6]]
2flat = [x for sublist in nested for x in sublist]
3print(flat) # [1, 2, 3, 4, 5, 6]

Grouping collects consecutive equal items together. itertools.groupby() works like SQL GROUP BY but only on runs of identical adjacent values. If you want to group all matching values regardless of position, sort the list first.

Python
1from itertools import groupby
2
3data = ["a", "a", "b", "b", "b", "a"]
4groups = [(key, list(group)) for key, group in groupby(data)]
5print(groups)
6# [('a', ['a', 'a']), ('b', ['b', 'b', 'b']), ('a', ['a'])]
7
8# Sort first to group all matching values together
9data_sorted = sorted(data)
10all_groups = [(key, list(group)) for key, group in groupby(data_sorted)]
11print(all_groups)
12# [('a', ['a', 'a', 'a']), ('b', ['b', 'b', 'b'])]