# Lists & dictionaries

> Store collections of values in lists and dictionaries, loop over them, and model real data such as a shopping cart or a word count.

Learn to Code roadmap · The building blocks · 18 min read · https://levelcode.org/roadmaps/learn-to-code/lists-and-dicts

Real programs rarely deal with one value at a time. A shopping cart has many items, a class has many students, a text has many words. Python's two workhorse **collections** handle almost all of this: the **list**, an ordered sequence of values, and the **dictionary**, which looks values up by a key. This lesson teaches both, the everyday operations on each, and how to combine them to model real data.

## Lists: ordered collections

A list holds values in order, inside square brackets:

```python
fruits = ["apple", "banana", "cherry"]
scores = [88, 92, 75, 100]
empty = []

print(len(fruits))     # 3
print(fruits[0])       # apple   (indexes start at 0)
print(fruits[-1])      # cherry  (negative counts from the end)
print(scores[1:3])     # [92, 75]  slicing works like strings
print("banana" in fruits)   # True
```

Unlike strings, lists are **mutable**: you can change them in place.

```python
fruits[0] = "apricot"          # replace an item
fruits.append("date")          # add to the end
fruits.insert(1, "blueberry")  # insert at a position
fruits.remove("banana")        # remove the first matching value
last = fruits.pop()            # remove and return the last item
print(fruits)                  # ['apricot', 'blueberry', 'cherry']
print(last)                    # date
```

Asking for an index that doesn't exist is an error, not an empty answer:

```python
fruits[10]     # IndexError: list index out of range
```

## Looping over lists

A `for` loop visits each item in order, which is how you'll process lists nearly every time:

```python
prices = [4.99, 12.50, 3.25]
total = 0
for price in prices:
    total += price
print(f"Total: ${total:.2f}")    # Total: $20.74
```

When you need the position as well, use `enumerate`, which gives you pairs of (index, item):

```python
for position, fruit in enumerate(["apple", "banana", "cherry"], start=1):
    print(f"{position}. {fruit}")
```

```text
1. apple
2. banana
3. cherry
```

Python has built-ins for the common calculations, so you often don't need a loop at all:

```python
scores = [88, 92, 75, 100]
print(sum(scores), min(scores), max(scores))   # 355 75 100
print(sum(scores) / len(scores))               # 88.75
print(sorted(scores))                          # [75, 88, 92, 100]  a new sorted list
scores.sort(reverse=True)                      # sorts the list itself, largest first
print(scores)                                  # [100, 92, 88, 75]
```

`sorted(x)` returns a new list and leaves `x` alone; `x.sort()` changes `x` and returns `None`. Mixing them up (`scores = scores.sort()`) is a classic bug that leaves you with `None`.

### Building a new list from an old one

A very common job is "make a list of X from a list of Y". The loop version:

```python
names = ["ana", "ben", "cy"]
capitalised = []
for name in names:
    capitalised.append(name.capitalize())
```

Python has a compact form for exactly this, a **list comprehension**:

```python
capitalised = [name.capitalize() for name in names]       # ['Ana', 'Ben', 'Cy']
passed = [s for s in [88, 45, 92, 51] if s >= 50]         # [88, 92, 51]
```

Read it as "the list of `name.capitalize()` for each `name` in `names`". Use comprehensions for simple transformations and filters; switch back to a loop when it gets complicated.

## Dictionaries: looking things up by key

A list finds items by position. Often you want to find them by a **name** instead: a phone number by person, a price by product. A **dictionary** (`dict`) stores key–value pairs in curly braces:

```python
prices = {"apple": 0.50, "bread": 2.20, "milk": 1.10}

print(prices["bread"])      # 2.2
prices["eggs"] = 3.00       # add a new pair
prices["milk"] = 1.25       # change an existing value
del prices["apple"]         # remove a pair
print("milk" in prices)     # True  (checks keys, not values)
print(len(prices))          # 3
```

Looking up a key that doesn't exist raises a `KeyError`. When a key might be missing, use `.get`, which returns a default instead:

```python
print(prices.get("cheese"))        # None
print(prices.get("cheese", 0))     # 0
```

Keys must be unique (assigning to an existing key replaces its value) and are usually strings or numbers. Values can be anything, including lists and other dictionaries.

Loop over a dictionary's pairs with `.items()`:

```python
for item, price in prices.items():
    print(f"{item:<8} ${price:.2f}")
```

```text
bread    $2.20
milk     $1.25
eggs     $3.00
```

Dictionaries remember the order in which keys were added.

### The counting pattern

Counting how often things occur is one of the most common uses of a dictionary:

```python
votes = ["red", "blue", "red", "green", "red", "blue"]

counts = {}
for colour in votes:
    counts[colour] = counts.get(colour, 0) + 1

print(counts)    # {'red': 3, 'blue': 2, 'green': 1}
```

For each vote, look up the current count (0 if this is the first time), add one, and store it back.

## Tuples and sets, briefly

Two more collections you'll meet:

- A **tuple** is like a list that can't be changed, written with parentheses: `point = (3, 4)`. Use it for small fixed groups of values, such as coordinates or a function returning two things (`return total, count`). You can unpack it: `x, y = point`.
- A **set** holds unique values with no order: `{"red", "blue"}`. Adding a duplicate does nothing, and checking membership with `in` is very fast. `set(votes)` gives you the distinct values of a list.

## Modelling real data: combining collections

Real data is usually a list of records, and each record is a dictionary:

```python
cart = [
    {"name": "Notebook", "price": 3.50, "quantity": 4},
    {"name": "Pen", "price": 1.20, "quantity": 10},
    {"name": "Backpack", "price": 35.00, "quantity": 1},
]

total = 0
for item in cart:
    line_total = item["price"] * item["quantity"]
    total += line_total
    print(f"{item['name']:<10} {item['quantity']:>3} × ${item['price']:>6.2f} = ${line_total:>6.2f}")
print(f"{'Total':<29} ${total:>6.2f}")
```

```text
Notebook     4 × $  3.50 = $ 14.00
Pen         10 × $  1.20 = $ 12.00
Backpack     1 × $ 35.00 = $ 35.00
Total                         $ 61.00
```

This shape, a list of dictionaries, is exactly what you'll get from files, spreadsheets and web APIs later on, so it's worth getting comfortable with now. Inside an f-string that uses double quotes, use single quotes for dictionary keys, as above.

## One trap: two names, one list

Assigning a list to a second variable doesn't copy it. Both names refer to the **same** list:

```python
original = [1, 2, 3]
alias = original
alias.append(4)
print(original)     # [1, 2, 3, 4]  surprise!
```

To get an independent copy, ask for one:

```python
copy = original.copy()     # or list(original), or original[:]
copy.append(5)
print(original)            # [1, 2, 3, 4]  unchanged
```

The same applies when you pass a list into a function: the function receives the same list, so if it calls `.append` the caller sees the change. Usually that's what you want; just be aware of it.

> [!TIP]
> Choose the collection by how you'll *find* things. Need them in order, or by position? A list. Need to look one up by a name or ID? A dictionary. Only care whether something is present? A set.

## Practice

Write `wordcount.py` that analyses a piece of text:

1. Store a paragraph of text in a variable (paste anything you like, a few sentences long).
2. Split it into words with `text.lower().split()`, and remove punctuation from each word with `word.strip(".,!?;:\"'()")`. Skip any word that becomes empty.
3. Count how often each word appears, using a dictionary.
4. Print the total number of words, the number of distinct words, and the 5 most common words with their counts.

For step 4, this sorts a dictionary's pairs by count, highest first: `sorted(counts.items(), key=lambda pair: pair[1], reverse=True)`. The `key=` part tells `sorted` what to compare: here, the second element of each pair.

**Solution**

```python
# wordcount.py
text = """The quick brown fox jumps over the lazy dog. The dog sleeps,
and the fox runs away. A quick fox is a happy fox!"""

counts = {}
total = 0
for raw in text.lower().split():
    word = raw.strip(".,!?;:\"'()")
    if not word:
        continue
    total += 1
    counts[word] = counts.get(word, 0) + 1

print(f"Total words:    {total}")
print(f"Distinct words: {len(counts)}")
print("Most common:")

ranked = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
for word, count in ranked[:5]:
    print(f"  {word:<8} {count}")
```

```text
Total words:    24
Distinct words: 15
Most common:
  the      4
  fox      4
  quick    2
  dog      2
  a        2
```

`split()` with no argument splits on any whitespace, including line breaks. `if not word` skips strings that became empty after stripping (an empty string counts as false). `ranked[:5]` slices the first five pairs, and each pair unpacks into `word, count` in the loop. When two words have the same count, `sorted` keeps them in the order they first appeared, because Python's sort is stable.

## Key takeaways

- A list is an ordered, changeable sequence: index from 0, `append`, `insert`, `remove`, `pop`, and loop with `for` or `enumerate`.
- `sorted()` returns a new list; `.sort()` changes the list and returns `None`.
- A dictionary maps unique keys to values: look up with `d[key]` or `d.get(key, default)`, loop with `.items()`.
- `counts[k] = counts.get(k, 0) + 1` is the counting pattern; a list of dictionaries is the standard shape for records.
- Assigning a list to another name doesn't copy it; use `.copy()` when you need an independent list.

## Quiz

1. numbers = [5, 3, 8]; result = numbers.sort(). What is result?

   A. [3, 5, 8]
   B. [8, 5, 3]
   C. None
   D. An error

   **Answer: C.** .sort() sorts the list in place and returns None. Use sorted(numbers) if you want a new sorted list as the result.

2. stock = {"pens": 10}. Which expression returns 0 instead of raising an error for a missing key?

   A. stock["pencils"]
   B. stock.get("pencils", 0)
   C. stock.pencils
   D. stock["pencils", 0]

   **Answer: B.** get returns the default when the key is absent. Square-bracket lookup raises KeyError for missing keys.

3. a = [1, 2]; b = a; b.append(3); print(a). What is printed?

   A. [1, 2]
   B. [3]
   C. [1, 2, 3]
   D. An error

   **Answer: C.** b = a doesn't copy the list; both names refer to the same list, so the append is visible through a. Use a.copy() for an independent list.

4. You need to look up a student's grade quickly by their student ID. Which collection fits best?

   A. A dictionary with IDs as keys
   B. A list of grades
   C. A set of IDs
   D. A tuple of grades

   **Answer: A.** Dictionaries are built for finding a value by its key. A list would have to be searched item by item, and a set stores no associated values.
