Imagine tracking a score for every student in a class. You could give each one its own variable — score_a = 90, score_b = 85, score_c = 78 — but the approach falls apart fast. By the tenth student you have lost track of the names, and you cannot loop over them, because separate variables have no collective identity.
A list fixes this. It stores an ordered sequence of values under a single name, so a dozen or a thousand items live in one place and can be processed by one loop. Lists are also mutable: you can change, extend, and shrink them after they exist. That mutability is their superpower, and — as you'll see near the end of this lesson — the source of their most common bug.
Building and indexing
You build a list with square brackets, separating the items with commas:
scores = [90, 85, 78]
Each slot has an index that counts from zero. You read or overwrite a slot through that index — the first item is at 0, the second at 1, and so on:
scores[0] = 95 # replace the first item
print(scores[0]) # 95
Indexing is how you reach in and touch a single element without disturbing the rest of the row. Ask for an index that does not exist, though, and Python raises an IndexError rather than silently handing you garbage.
flowchart LR L["scores = [90, 85, 78]"] --> C0["0 → 90"] L --> C1["1 → 85"] L --> C2["2 → 78"] style L fill:#3776ab,color:#fff
Length, append, and looping
Three tools cover most day-to-day list work. len(scores) tells you how many items the list holds. scores.append(99) adds a new item to the end and returns None — it changes the list in place rather than handing you a new one. And x in scores asks whether a value is present anywhere in the list, returning True or False.
To act on every element you loop with for. Python hands you each item, one at a time, in order:
for s in scores:
print("score:", s)
The loop variable s takes each value in turn; the body runs once per item. If you also need the position, enumerate(scores) hands you index and value together.
scores = [90, 85, 78]
print(scores[0])
print(len(scores))
scores.append(100)
print(scores)
for s in scores:
print("score:", s)
Useful list methods
Beyond append, a handful of methods cover the rest of what lists do. insert(i, x) drops x at index i, shifting everything after it to the right. remove(x) deletes the first item equal to x. pop() pulls out and returns the last item (handy for treating a list like a stack). And sort() orders the list in place:
queue = ['b', 'a', 'c']
queue.sort()
print(queue) # ['a', 'b', 'c']
Notice that these mutating methods all return None. That is deliberate — they change the list itself, so there is no new list to hand back. Writing queue = queue.sort() is a classic mistake that leaves you with None.
Slicing out a piece
Indexes also let you carve out a section of a list with a slice. Write two indexes separated by a colon and Python returns a new list from the first index up to, but not including, the second:
letters = ['a', 'b', 'c', 'd', 'e']
print(letters[1:4]) # ['b', 'c', 'd']
Leave a side out and it defaults to an edge: letters[:2] takes the first two, letters[3:] takes everything from index three onward. Negative indexes count from the back, so letters[-1] is the last item and letters[-2] the second-to-last. A slice never raises on an out-of-range index; it simply stops where the data stops.
letters = ['a', 'b', 'c', 'd', 'e']
print(letters[1:4]) # ['b', 'c', 'd']
print(letters[:2]) # ['a', 'b']
print(letters[-1]) # 'e'
print('c' in letters) # True
print(letters[1:4] is not letters) # True - a new list object
Two names, one list
There is a subtlety that catches almost every Python learner once. Assigning a list to a new name does not copy it — both names end up pointing at the very same list object in memory.
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] -- a changed too!
Changing b changed a, because there was never a second list. The line b = a only stuck a second label onto the existing list. This is not a quirk of lists specifically; it is how Python treats every mutable container, so it is worth slowing down to understand.
flowchart LR A["a = [1, 2, 3]"] --> OBJ["one shared list object"] B["b = a"] --> OBJ C["c = a.copy()"] --> OBJ2["separate list object"] style OBJ fill:#b45309,color:#fff style OBJ2 fill:#3776ab,color:#fff
Write swap_ends(items) that returns a new list with the first and last items exchanged. The original list must not change. Lists of length 0 or 1 come back unchanged.
def swap_ends(items):
# build and return a new list; do not mutate the input
pass
Copy first with list(items) so the original is safe.
Tuple-unpack the swap: out[0], out[-1] = out[-1], out[0].
def swap_ends(items):
if len(items) < 2:
return list(items)
out = list(items)
out[0], out[-1] = out[-1], out[0]
return out
Write count_above(numbers, limit) that returns how many numbers in the list are greater than limit.
def count_above(numbers, limit):
count = 0
# loop over numbers and count those above limit
return count
Loop with
for n in numbers:.Increment
countonly whenn > limit.
def count_above(numbers, limit):
count = 0
for n in numbers:
if n > limit:
count += 1
return count
Fix double_all so it returns a new list where each number is doubled. (Right now it appends n unchanged.)
def double_all(numbers):
result = []
for n in numbers:
result.append(n) # bug: not doubled
return result
result.append(n)keeps the value unchanged.Append
n * 2instead.
def double_all(numbers):
result = []
for n in numbers:
result.append(n * 2)
return result
Write running_totals(numbers) that returns a new list where each position holds the total of numbers from the start up to and including that position. So running_totals([1, 2, 3, 4]) returns [1, 3, 6, 10].
def running_totals(numbers):
result = []
# keep a running sum; append the new total after each step
return result
Carry a
totalvariable that starts at 0 and grows by eachn.Append the running total, not the raw number, on each pass.
def running_totals(numbers):
result = []
total = 0
for n in numbers:
total += n
result.append(total)
return result
Making lists from other things
You will often build a list from something that is not a list yet. The list() constructor turns any sequence into a fresh list — list('abc') gives ['a', 'b', 'c'], and list(range(5)) gives [0, 1, 2, 3, 4]. Strings also split into lists on a delimiter you choose: 'a,b,c'.split(',') yields ['a', 'b', 'c'].
These three — the constructor, range, and split — cover most of the lists you build from raw input. Notice they all return brand-new list objects, so they are safe to mutate later without surprising the original data. The reverse move, ''.join(list_of_strings), glues a list back into one string.
Recap
- A list is an ordered, mutable sequence; reach items by index, counting from
0. len()counts items,.append()adds to the end in place, andintests membership.insert,remove,pop, andsortmutate the list and all returnNone.- A slice
a[i:j]returns a new sublist; negative indexes count from the end. b = ashares one list;b = a.copy()makes a separate one.
Next you'll meet list comprehensions — a compact way to build a list like the ones you just looped over, often in a single readable line.