Unit 2 · Data & Control Flow

Lesson · Unit 2 · 10 min read

Python lists, explained for people who'll actually use them.

Lists are the workhorse data structure in Python. If you're going to write a hundred lines of code, you're going to touch a list. Here's what they are, how to read and change them, and the three mistakes every beginner makes.

Section · 01

What a list actually is

A list is an ordered collection of values, held in a single variable, that you can change after creation. That’s it. Three traits, in that order: ordered, collection, changeable.

You write a list with square brackets and commas between the values:

groceries = ["bread", "eggs", "olive oil", "coffee"]
high_scores = [98, 87, 94, 71, 100]
mixed = ["York", 2025, True, 3.14]

Notice the last one — Python lists don’t care if you mix types. A string, an int, a bool, and a float can sit in the same list. That’s different from arrays in Java or C, where every element has to be the same type.

Section · 02

Reading: indexes and slices

You read a value out of a list with list_name[index]. Indexes start at zero, not one. This is the single most-common source of off-by-one bugs in Python.

groceries = ["bread", "eggs", "olive oil", "coffee"]

groceries[0]    # "bread"     (first)
groceries[1]    # "eggs"
groceries[3]    # "coffee"    (last, since there are 4 items)
groceries[4]    # IndexError — there is no index 4

Negative indexes count from the end, which is useful when you don’t know how long the list is:

groceries[-1]   # "coffee"     (last)
groceries[-2]   # "olive oil"  (second to last)

You can also grab a chunk of the list with slicing. The syntax is list_name[start:stop], and stop is exclusive — Python gives you everything from start up to but not including stop:

groceries[1:3]   # ["eggs", "olive oil"]    (indexes 1 and 2)
groceries[:2]    # ["bread", "eggs"]        (start defaults to 0)
groceries[2:]    # ["olive oil", "coffee"]  (stop defaults to end)
groceries[:]     # a full copy of the list

That last one — list[:]— is the simplest way to get a shallow copy. Skip it and you’re passing references around, which is mistake #3 below.

Section · 03

Changing: lists are mutable

Lists are mutable. You can change them after they exist. That’s the whole point — if data won’t change, use a tuple. Five things you’ll do constantly:

groceries = ["bread", "eggs", "olive oil"]

# 1. Replace by index
groceries[0] = "sourdough"
# ["sourdough", "eggs", "olive oil"]

# 2. Append to the end
groceries.append("coffee")
# ["sourdough", "eggs", "olive oil", "coffee"]

# 3. Insert at a specific index
groceries.insert(1, "butter")
# ["sourdough", "butter", "eggs", "olive oil", "coffee"]

# 4. Remove a known value
groceries.remove("eggs")
# ["sourdough", "butter", "olive oil", "coffee"]

# 5. Remove by index (and get the value back)
last = groceries.pop()
# last = "coffee"
# groceries = ["sourdough", "butter", "olive oil"]

append adds one item to the end. If you want to merge another list into this one, use extend instead — that’s mistake #1 below.

Section · 04

Three mistakes everyone makes

1. Using `append` when you meant `extend`

nums = [1, 2, 3]
nums.append([4, 5])      # [1, 2, 3, [4, 5]]   — nested!
nums.extend([4, 5])      # [1, 2, 3, 4, 5]     — flat

append(x) adds x as a single element. If x is a list, you get a list inside a list. extend(x) unpacks x and adds each item individually.

2. Assigning the result of a mutating method

# WRONG — .sort() returns None, not the sorted list
nums = [3, 1, 4, 1, 5]
nums = nums.sort()       # nums is now None

# RIGHT — sort mutates in place
nums = [3, 1, 4, 1, 5]
nums.sort()              # nums is now [1, 1, 3, 4, 5]

# OR use sorted(), which returns a new list
nums = [3, 1, 4, 1, 5]
ordered = sorted(nums)   # ordered = [1, 1, 3, 4, 5], nums unchanged

Methods that change the list in place — .sort(), .append(), .reverse(), .remove() — return None. If you assign the result to a variable, that variable becomes None and you lose your list. Burn this one into your brain.

3. Copying with `=` instead of slicing

original = [1, 2, 3]
copy = original              # NOT a copy — same list, two names
copy.append(4)
# original is now [1, 2, 3, 4] — you mutated the original

# Actually copy with slicing or .copy()
copy = original[:]           # shallow copy
# or
copy = original.copy()       # same thing, clearer name

= in Python binds names to objects. It does not copy. Two names pointing at the same list will both see every change. For nested lists you also need to know about copy.deepcopy(), but you can save that for the day you actually hit the bug.

Curriculum source

Lesson content is original to YorkSims. Topic structure aligns with Python for Everybody by Dr. Charles R. Severance (py4e.com), licensed under Creative Commons Attribution 3.0 Unported.