Python · Beginner
Python lists and tuples: slicing, sorting, comprehensions
Why it matters
A Python list comprehension turns a four-line loop into one readable line, and sorting with a key function turns a pile of records into a leaderboard. You will do both every week in any data, backend or automation job. Interviewers also use lists to test whether you understand what Python variables really are: "what does a = [[0]*3]*3; a[0][0] = 1 print?" catches a surprising number of candidates.
This lesson covers lists and tuples properly: how they hold references, how to copy them, how to sort by several keys, when a comprehension helps and when it hurts, and which operations are slow.
The concept
A list holds references, and assignment never copies
A Python variable is a name attached to an object. b = a attaches a second name to the same list:
import copy
a = [3750, 3800]
b = a # b is another name for the same list
b.append(3250)
print(a, a is b)
c = a[:] # shallow copy (same as list(a) or a.copy())
c.append(9999)
print(a, c)
nested = [["Adelie", 3750], ["Gentoo", 5000]]
shallow = list(nested)
deep = copy.deepcopy(nested)
shallow[0][1] = 0 # changes the inner list, which both share
print(nested)
print(deep)
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid)[3750, 3800, 3250] True
[3750, 3800, 3250] [3750, 3800, 3250, 9999]
[['Adelie', 0], ['Gentoo', 5000]]
[['Adelie', 3750], ['Gentoo', 5000]]
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]b = a: one list, two names. Changing it through either name changes "both".a[:],list(a)anda.copy()make a shallow copy: a new outer list whose items are the same objects. For a list of numbers or strings that is all you need.For a list of lists (or of dictionaries), a shallow copy still shares the inner lists.
copy.deepcopycopies all the way down.[[0] * 3] * 3repeats a reference to one inner list three times, so changing one row changes all three. Build grids with a comprehension so each row is created separately.
Adding items: append, extend, + and insert
masses = [3750, 3800]
masses.append([3250, 3450])
print(masses)
masses = [3750, 3800]
masses.extend([3250, 3450])
print(masses)
combined = masses + [3650]
print(combined, masses)
print(masses.sort(), masses)
print(sorted([3, 1, 2], reverse=True))[3750, 3800, [3250, 3450]]
[3750, 3800, 3250, 3450]
[3750, 3800, 3250, 3450, 3650] [3750, 3800, 3250, 3450]
None [3250, 3450, 3750, 3800]
[3, 2, 1]| operation | effect | cost |
|---|---|---|
append(x) | adds x as one item (even if x is a list) | O(1) amortised |
extend(items) | adds each item of items | O(k) for k items |
a + b | makes a new list; neither original changes | O(len(a) + len(b)) |
insert(0, x) | shifts every existing item one place right | O(n) |
pop() / pop(0) | remove last / first | O(1) / O(n) |
Inserting at the front of a long list is slow because every item moves. When you need a queue (add at one end, remove at the other), use collections.deque:
import timeit
from collections import deque
n = 50_000
def front_inserts_list():
items = []
for i in range(n):
items.insert(0, i)
def front_inserts_deque():
items = deque()
for i in range(n):
items.appendleft(i)
ids = list(range(100_000))
ids_set = set(ids)
lookups = range(99_000, 100_000) # 1,000 lookups near the end of the list
print(f"insert(0) on list : {timeit.timeit(front_inserts_list, number=1):.3f} s")
print(f"appendleft on deque : {timeit.timeit(front_inserts_deque, number=1):.3f} s")
print(f"1,000 'in' on list : {timeit.timeit(lambda: [x in ids for x in lookups], number=1):.3f} s")
print(f"1,000 'in' on set : {timeit.timeit(lambda: [x in ids_set for x in lookups], number=1):.5f} s")insert(0) on list : 0.275 s
appendleft on deque : 0.002 s
1,000 'in' on list : 0.504 s
1,000 'in' on set : 0.00004 sThe second pair matters even more in practice. x in some_list checks items one by one (O(n)). If you test membership many times, convert the list to a set once; a set lookup is O(1) on average. Here that turned half a second into 40 microseconds.
sort() versus sorted()
items.sort()sorts the list in place and returnsNone. Writingitems = items.sort()throws your list away and leaves you withNone.sorted(items)returns a new sorted list and works on any iterable (tuples, sets, dictionary keys, a file's lines).Both take
key=(a function that returns what to sort by) andreverse=True.Python's sort is stable: items that compare equal keep their original order. That lets you sort in several passes, least important key first.
To sort by several keys at once, return a tuple from the key function. Tuples compare item by item, so (species, -mass) sorts by species A to Z, then by mass heaviest first (negating a number reverses just that part). operator.itemgetter(4) is a fast, readable shortcut for lambda r: r[4].
List comprehensions
A list comprehension builds a list from another iterable in one expression:
[float(r["body_mass_g"]) for r in rows if r["body_mass_g"] != "NA"]
# ^ what to keep ^ where from ^ optional filterThe
ifat the end filters (fewer items). A conditional expression at the front transforms (same number of items):["heavy" if m > 4500 else "light" for m in masses].Nested comprehensions read like nested loops, left to right:
[x for row in matrix for x in row]flattens a list of lists.{...}with one value makes a set comprehension,{k: v ...}a dict comprehension, and(...)a generator expression (lazy, covered in the generators lesson).
Use a plain loop when the body has side effects (printing, writing files), needs try/except, or the comprehension needs more than one if and two for clauses. Readability beats cleverness; reviewers will ask you to split a three-line comprehension.
Tuples
A tuple is an immutable sequence. Use it for a fixed record where position has meaning (a row, a coordinate, a dictionary key), and a list for a collection of similar things that grows or shrinks.
record = ("Gentoo", "Biscoe", 6300)
species, island, mass = record
print(species, island, mass)
first, *rest = [6300, 6050, 6000, 5950]
print(first, rest)
*_, last = [6300, 6050, 6000, 5950]
print(last)
not_a_tuple = ("Adelie")
a_tuple = ("Adelie",)
print(type(not_a_tuple), type(a_tuple), len(a_tuple))
a, b = 1, 2
a, b = b, a
print(a, b)
try:
record[2] = 6000
except TypeError as e:
print("TypeError:", e)Gentoo Biscoe 6300
6300 [6050, 6000, 5950]
5950
<class 'str'> <class 'tuple'> 1
2 1
TypeError: 'tuple' object does not support item assignmentUnpacking assigns each item to a name; the counts must match or you get
ValueError.Star unpacking collects "the rest" into a list.
*_is the convention for "I do not need these".The comma makes a tuple, not the brackets.
("Adelie")is just a string in brackets;("Adelie",)is a one-element tuple. This bites when you pass a single value to an API that expects a tuple.a, b = b, aswaps without a temporary variable (the right side is built as a tuple first).Because tuples cannot change, a tuple of strings and numbers is hashable and can be a dictionary key or a set member. A list cannot.
Never remove items from a list you are looping over
masses = [3750, None, None, 3250, None, 3450]
for m in masses:
if m is None:
masses.remove(m)
print(masses)
masses = [3750, None, None, 3250, None, 3450]
masses = [m for m in masses if m is not None]
print(masses)[3750, 3250, None, 3450]
[3750, 3250, 3450]The loop keeps an index. Removing an item shifts everything left, so the loop skips the item that moved into the removed one's place, and one None survives with no error. Build a new list (a comprehension with a filter is perfect) or loop over a copy: for m in masses[:].
Worked example: penguin leaderboards
Using the Palmer Penguins file (CC0, download penguins.csv), build three leaderboards with sorted and key functions. Each row becomes a tuple (species, island, sex, mass, flipper) so positions are fixed.
import csv
from operator import itemgetter
def to_float(text):
return None if text in ("", "NA") else float(text)
with open("penguins.csv", newline="", encoding="utf-8") as f:
rows = [
(r["species"], r["island"], r["sex"], to_float(r["body_mass_g"]), to_float(r["flipper_length_mm"]))
for r in csv.DictReader(f)
]
measured = [r for r in rows if r[3] is not None] # drop the 2 unmeasured penguins
print(len(rows), "rows,", len(measured), "measured")
# 1. Longest flippers overall: sort by flipper length, longest first.
print("\nLongest flippers")
for species, island, sex, mass, flipper in sorted(measured, key=itemgetter(4), reverse=True)[:5]:
print(f" {flipper:5.0f} mm {species:<9} {island:<9} {sex}")
# 2. Heaviest 3 per species: sort by species A-Z, then mass heaviest first.
# A tuple key sorts by the first item, then the second; negate the mass to reverse just that part.
by_species_then_mass = sorted(measured, key=lambda r: (r[0], -r[3]))
print("\nHeaviest three per species")
shown = {}
for species, island, sex, mass, flipper in by_species_then_mass:
if shown.get(species, 0) < 3:
print(f" {species:<9} {mass:6.0f} g {island:<9} {sex}")
shown[species] = shown.get(species, 0) + 1
# 3. Stability: sort by mass, then by species. Equal species keep their mass order.
two_step = sorted(sorted(measured, key=itemgetter(3)), key=itemgetter(0))
print("\nLightest Adelie after a two-step stable sort:", two_step[0][:4])344 rows, 342 measured
Longest flippers
231 mm Gentoo Biscoe male
230 mm Gentoo Biscoe male
230 mm Gentoo Biscoe male
230 mm Gentoo Biscoe male
230 mm Gentoo Biscoe male
Heaviest three per species
Adelie 4775 g Biscoe male
Adelie 4725 g Biscoe male
Adelie 4700 g Torgersen male
Chinstrap 4800 g Dream male
Chinstrap 4550 g Dream male
Chinstrap 4500 g Dream male
Gentoo 6300 g Biscoe male
Gentoo 6050 g Biscoe male
Gentoo 6000 g Biscoe maleLightest Adelie after a two-step stable sort: ('Adelie', 'Biscoe', 'female', 2850.0)What the output tells you:
Ties are real. Seven penguins share 230 mm, and only four of them made the list. A "top 5" that silently cuts off at an arbitrary tied row is a classic reporting bug; if ties matter, say so or include all tied rows.
The tuple key did two sorts in one pass. Species ascending, mass descending, no second loop needed.
Stable sorting means passes compose. Sorting by mass and then by species leaves each species internally ordered by mass, which is why the first row is the lightest Adelie.
Rewriting loops as comprehensions, and timing them
Is a comprehension faster? Measure instead of guessing. The script below multiplies the rows by 100 (34,400 rows) so timings are visible, writes three tasks as a loop and as a comprehension, checks they give the same answer, and times each with timeit.repeat:
import csv
import timeit
with open("penguins.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
rows = rows * 100 # 34,400 rows so the timings are measurable
def loop_masses():
out = []
for r in rows:
if r["body_mass_g"] != "NA":
out.append(float(r["body_mass_g"]))
return out
def comp_masses():
return [float(r["body_mass_g"]) for r in rows if r["body_mass_g"] != "NA"]
def loop_kg_labels():
out = []
for r in rows:
out.append("heavy" if r["body_mass_g"] != "NA" and float(r["body_mass_g"]) > 4500 else "light")
return out
def comp_kg_labels():
return ["heavy" if r["body_mass_g"] != "NA" and float(r["body_mass_g"]) > 4500 else "light" for r in rows]
def loop_islands():
out = []
for r in rows:
if r["island"] not in out:
out.append(r["island"])
return out
def comp_islands():
return sorted({r["island"] for r in rows}) # a set comprehension, then sort
assert loop_masses() == comp_masses()
assert loop_kg_labels() == comp_kg_labels()
assert sorted(loop_islands()) == comp_islands()
for name, fn in [("masses loop", loop_masses), ("masses comprehension", comp_masses),
("labels loop", loop_kg_labels), ("labels comprehension", comp_kg_labels),
("islands loop + list", loop_islands), ("islands set comp", comp_islands)]:
best = min(timeit.repeat(fn, number=10, repeat=5)) / 10
print(f"{name:<22} {best * 1000:7.2f} ms")
# Nested comprehension: flatten a list of lists (read the for clauses left to right like nested loops)
matrix = [[1, 2, 3], [4, 5, 6]]
print([x for row in matrix for x in row])
print([[row[i] for row in matrix] for i in range(3)]) # transposemasses loop 4.79 ms
masses comprehension 4.62 ms
labels loop 5.57 ms
labels comprehension 5.27 ms
islands loop + list 1.83 ms
islands set comp 1.06 ms
[1, 2, 3, 4, 5, 6]
[[1, 4], [2, 5], [3, 6]]On Python 3.13 the comprehensions are only 3 to 6% faster than the equivalent loops; most of the time goes into float() and dictionary lookups, not the loop itself. The biggest gain came from changing the data structure (a set instead of not in list), not the syntax. Choose comprehensions because they are clearer for simple "transform and filter" jobs, and choose data structures for speed.
Common mistakes
items = items.sort().sort()returnsNone. Useitems.sort()alone, oritems = sorted(items).Thinking
b = acopies. It does not. Usea.copy(),a[:]orcopy.deepcopyfor nested data.[[0] * n] * mfor a grid. All rows are the same list. Use[[0] * n for _ in range(m)].appendwhen you meantextend. You get a list inside your list.extend(or+=) adds the items.Removing from a list while looping over it. Items are skipped silently. Filter into a new list.
x in big_listinside a loop. That is O(n) per check and O(n²) overall. Build a set once.Using
list.insert(0, x)orpop(0)as a queue. Usecollections.deque.Forgetting the comma in a one-element tuple.
("x")is a string.Cramming logic into a comprehension. If it needs two
ifs, twofors and a function call, write the loop.
Interview angle
What is the difference between a list and a tuple?
A list is mutable (append, remove, sort in place) and is used for a variable-length collection of similar items. A tuple is immutable and is used for a fixed record where position has meaning. Because a tuple of hashable items is hashable, it can be a dictionary key or set member; a list cannot. Tuples are slightly smaller in memory. Mention unpacking (name, mass = row) as the everyday way tuples appear.
What does a = [[0]*3]*3; a[0][0] = 1; print(a) print, and why?
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]. Multiplying the outer list repeats a reference to the same inner list three times, so there is only one row object. Fix: [[0] * 3 for _ in range(3)] creates a new row each time.
sort() vs sorted(), and how do you sort by two fields, one descending?
sort() mutates the list and returns None; sorted() returns a new list from any iterable. For two fields, use a tuple key: sorted(rows, key=lambda r: (r.species, -r.mass)). When the descending field is not numeric, sort twice, least important key first, relying on stability: rows.sort(key=by_name, reverse=True); rows.sort(key=by_species).
Is a list comprehension faster than a for loop?
Slightly, in most versions, because the append is done without a method lookup each time. In Python 3.12+ comprehensions are inlined and the gap is small (a few percent in our measurement). Pick them for clarity; for real speed, change the algorithm or the data structure.
Practice task
Use penguins.csv and only the standard library.
Build a list of
(species, island, sex, bill_length, bill_depth)tuples with a comprehension, skipping rows where either bill measurement isNA. Print how many rows you kept (it should be 342).Print the five penguins with the shortest bills, breaking ties by the deepest bill first.
Using a single
sortedcall with a tuple key, list all penguins by island A to Z, then sex (femalebeforemale,NAlast), then mass heaviest first. Print the first 3 rows of each island.Rewrite this loop as a comprehension and check both produce the same list:
python ratios = [] for r in rows: if r[3] is not None and r[4] is not None: ratios.append(round(r[3] / r[4], 2))Write a function
top_n_with_ties(rows, key, n)that returns the topnrows plus any further rows tied with the n-th.
Done when: each step prints its result, step 4's assert passes, and top_n_with_ties(measured, itemgetter(4), 2) returns eight rows (231 mm, then all seven penguins at 230 mm).
Show a solution for steps 3 and 5
SEX_ORDER = {"female": 0, "male": 1}
ordered = sorted(measured, key=lambda r: (r[1], SEX_ORDER.get(r[2], 2), -r[3]))
def top_n_with_ties(rows, key, n):
ranked = sorted(rows, key=key, reverse=True)
if len(ranked) <= n:
return ranked
cutoff = key(ranked[n - 1])
return [r for r in ranked if key(r) >= cutoff]SEX_ORDER.get(r[2], 2) maps any unexpected value (like NA) to 2 so it sorts last. For top_n_with_ties, everything at or above the n-th row's value is included.
Further reading
Sorting techniques: key functions, stability and multi-pass sorts, from the official HOWTO.
More on lists and list comprehensions: the tutorial chapter on lists, tuples and comprehensions.
TimeComplexity on the Python wiki: the cost of every list, deque, set and dict operation.
copy module: shallow vs deep copies and how to customise them.