Module 2 · Core Data Structures ⏱ 17 min

Lists & Iteration

By the end of this lesson you will be able to:
  • Create a list and read or change its items by index
  • Grow a list with append and measure it with len
  • Loop over a list with for and process each item
  • Copy a list correctly and explain why two names can share one list

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
A list stores items in order, each at its own index.

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.

Read an item, count the list, grow it, then loop over it. Press Run.
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.

Slicing and membership. Notice slices return new lists.
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
b = a shares one object; a.copy() makes an independent second list.
Exercise

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
Exercise

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
Exercise

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
Exercise

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

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, and in tests membership.
  • insert, remove, pop, and sort mutate the list and all return None.
  • A slice a[i:j] returns a new sublist; negative indexes count from the end.
  • b = a shares 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.

Checkpoint quiz

What does scores.append(100) do?

Given nums = [10, 20, 30], what is nums[-1]?

After a = [1, 2]; b = a; b.append(3), what is a?

Go deeper — technical resources