Chapter 9 — Built-in Data Structures: Containers for Tree, Fish, and Livestock Data

Series B2 — Python Language Foundations

NoteIn this chapter, you will:
  • Use a list for a list you can change
  • Take pieces of a list with slicing
  • Meet the tuple (a fixed list), dict (labelled data), and set (no duplicates)
  • Build a new list in one line with a comprehension
  • Store the data of several trees at once

All scripts for this chapter are in the kode/ch09/ folder. On the web version, you can also download them all at once (zip).

Up to Chapter 8, we kept every value in one variable: weight_g = 520. But field data is plentiful: hundreds of trees, dozens of ponds. Making hundreds of variables is clearly impossible. Python offers data structures: containers that hold many values at once. Picture a filing cabinet with four different kinds of drawers.

Container Shape Can it change? Good for
list [800, 1250, 640] yes ordered lists: the fish count of each pond
tuple (-6.6, 106.8) no fixed data: coordinates
dict {"species": "Teak"} yes labelled data: one tree
set {"catfish", "tilapia"} yes collections without duplicates: fish species

list: a list you can change

01_list.py
# Chapter 9 - list: a list you can change
fish_per_pond = [800, 1250, 640]

print(fish_per_pond)
print("First pond:", fish_per_pond[0])          # numbering starts at 0
print("Last pond:", fish_per_pond[-1])          # -1 = the very last one
print("Number of ponds:", len(fish_per_pond))
print("Total fish:", sum(fish_per_pond))
print("Most crowded pond:", max(fish_per_pond))
[800, 1250, 640]
First pond: 800
Last pond: 640
Number of ponds: 3
Total fish: 2690
Most crowded pond: 1250

Three important points:

  • A list is written in square brackets [ ], with items separated by commas.
  • The position numbers (indexes) start at 0: the first item is at [0]. Negative numbers count from the back: [-1] is the last one.
  • Useful built-in functions: len() (how many), sum() (total), max() (largest), min() (smallest).

A list’s contents can be changed after it is made:

02_list_change.py
# Chapter 9 - Changing what is in a list
fish_per_pond = [800, 1250, 640]

fish_per_pond.append(900)           # add at the end
fish_per_pond[0] = 820              # replace item number 0
fish_per_pond.remove(640)           # remove the value 640
print(fish_per_pond)

print(1250 in fish_per_pond)        # is it there?
print(500 in fish_per_pond)

fish_per_pond.sort()                # sort from small to large
print(fish_per_pond)
[820, 1250, 900]
True
False
[820, 900, 1250]
  • append(x) adds at the end; remove(x) removes the first matching value.
  • list[0] = 820 replaces item number 0.
  • x in list answers True or False: whether the value is there.
  • sort() sorts the list in place (the original list changes).

Slicing: taking a piece

The formula is list[start:stop:step]. Like range(), the stop number is not included.

A list has two numberings: from the front (starting at 0) and from the back (starting at -1). The slice [1:4] takes items 1, 2, 3.
03_slicing.py
# Chapter 9 - Slicing: taking a piece of a list [start:stop:step]
weight_g = [480, 500, 510, 520, 530, 540]

print(weight_g[1:4])     # items 1 to 3 (4 is not included)
print(weight_g[:3])      # the first three
print(weight_g[3:])      # from item 3 to the end
print(weight_g[::2])     # jump by 2
print(weight_g[::-1])    # reversed
[500, 510, 520]
[480, 500, 510]
[520, 530, 540]
[480, 510, 530]
[540, 530, 520, 510, 500, 480]

A blank part means “from the start” or “to the end”. The [::-1] trick reverses the whole order.

tuple: a locked list

A tuple resembles a list, but is written with round brackets and cannot be changed after it is made. It suits data that really shouldn’t change, such as location coordinates.

04_tuple.py
# Chapter 9 - tuple: a list that may not be changed (good for coordinates)
pond_location = (-6.6, 106.8)      # (latitude, longitude): sample numbers

latitude, longitude = pond_location    # unpacking a tuple
print(f"Latitude: {latitude}")
print(f"Longitude: {longitude}")
print(pond_location[0])
Latitude: -6.6
Longitude: 106.8
-6.6

The line latitude, longitude = pond_location is called unpacking: opening a tuple into several variables at once. Trying to change its contents fails:

08_tuple_wrong.py
# Chapter 9 - A mistake: trying to change what is inside a tuple
pond_location = (-6.6, 106.8)
pond_location[0] = -6.7
Traceback (most recent call last):
  File "08_tuple_wrong.py", line 3, in <module>
    pond_location[0] = -6.7
    ~~~~~~~~~~~~~^^^
TypeError: 'tuple' object does not support item assignment

The message is clear: “‘tuple’ object does not support item assignment”. That is the whole point of a tuple: protecting data from accidental change.

dict: labelled data

While a list is reached by number, a dict is reached by label (called a key). Perfect for describing one tree:

05_dict.py
# Chapter 9 - dict: labelled data (key -> value)
tree = {"species": "Teak", "diameter_cm": 32.5, "height_m": 18}

print(tree["species"])
tree["height_m"] = 19               # change a value
tree["healthy"] = True              # add a new label
print(tree)
print(tree.get("colour", "not recorded"))    # safe when the label is missing

for label, content in tree.items():
    print(f"{label}: {content}")
Teak
{'species': 'Teak', 'diameter_cm': 32.5, 'height_m': 19, 'healthy': True}
not recorded
species: Teak
diameter_cm: 32.5
height_m: 19
healthy: True
  • Written in curly brackets { }, each item as a pair "key": value.
  • tree["species"] fetches the value; tree["healthy"] = True adds or changes.
  • .get("colour", "not recorded") is safe: if the label is missing, it returns a fallback value.
  • .items() gives label-and-content pairs to use in a for.

If the label is missing and you use square brackets, Python complains:

09_missing_label.py
# Chapter 9 - A mistake: asking for a label that is not in the dict
tree = {"species": "Teak", "diameter_cm": 32.5}
print(tree["colour"])
Traceback (most recent call last):
  File "09_missing_label.py", line 3, in <module>
    print(tree["colour"])
          ~~~~^^^^^^^^^^
KeyError: 'colour'

KeyError means “key not found”, and Python names the key that failed.

set: a collection without duplicates

A set automatically drops duplicates and has no order. Useful for questions like “which fish species are there?”

06_set.py
# Chapter 9 - set: a collection with no duplicates and no order
pond_a_fish = {"catfish", "tilapia", "gourami", "catfish"}
pond_b_fish = {"tilapia", "pangasius"}

print("Species in pond A:", len(pond_a_fish))   # catfish is counted once
print(sorted(pond_a_fish))
print("Tilapia in pond A?", "tilapia" in pond_a_fish)
print("In both ponds:", sorted(pond_a_fish & pond_b_fish))
print("In either pond:", sorted(pond_a_fish | pond_b_fish))
Species in pond A: 3
['catfish', 'gourami', 'tilapia']
Tilapia in pond A? True
In both ponds: ['tilapia']
In either pond: ['catfish', 'gourami', 'pangasius', 'tilapia']

"catfish" was written twice but is counted only once. The & operator gives the intersection (in both ponds) and | gives the union. We use sorted() so the output is in order, because a set itself doesn’t promise any order.

Comprehension: a new list in one line

07_comprehension.py
# Chapter 9 - Comprehension: making a new list in one line
weight_g = [520, 480, 560, 495]

weight_kg = [g / 1000 for g in weight_g]
ready = [g for g in weight_g if g >= 500]

print(weight_kg)
print(ready)
[0.52, 0.48, 0.56, 0.495]
[520, 560]

Read [g / 1000 for g in weight_g] like a sentence: “for every g in weight_g, calculate g / 1000.” Adding an if at the end filters: [g for g in weight_g if g >= 500] takes only those ≥ 500.

TipTIP

A comprehension is shorthand for a for loop that fills a list. If it feels confusing, write it with an ordinary for first. Once you understand, then condense it.

Running Project B2: tree data

Now combine: a list of dicts. Each tree is one dict, and the plantation is a list of those dicts.

10_tree_data_project.py
# Chapter 9 - Running project B2: tree data as a list of dicts
tree_data = [
    {"species": "Teak", "diameter_cm": 32.5, "height_m": 18, "healthy": True},
    {"species": "Meranti", "diameter_cm": 45.2, "height_m": 27, "healthy": True},
    {"species": "Mahogany", "diameter_cm": 28.0, "height_m": 15, "healthy": False},
]

for tree in tree_data:
    status = "healthy" if tree["healthy"] else "needs checking"
    print(f"{tree['species']:8} diameter {tree['diameter_cm']:5.1f} cm, {status}")

diameters = [tree["diameter_cm"] for tree in tree_data]
print(f"Average diameter: {sum(diameters) / len(diameters):.2f} cm")

healthy_only = [tree["species"] for tree in tree_data if tree["healthy"]]
print("Healthy trees:", healthy_only)
Teak     diameter  32.5 cm, healthy
Meranti  diameter  45.2 cm, healthy
Mahogany diameter  28.0 cm, needs checking
Average diameter: 35.23 cm
Healthy trees: ['Teak', 'Meranti']

A few new things in the print line:

  • {tree['species']:8} means “show with a width of 8 characters”, so the columns line up. Single quotes are used inside because the f-string is already wrapped in double quotes.
  • {tree['diameter_cm']:5.1f} means width 5, one digit after the point.
  • "healthy" if tree["healthy"] else "needs checking" is a one-line if: the value is "healthy" when true, otherwise the other one.

Average diameter: (32.5 + 45.2 + 28.0) ÷ 3 = 35.23 cm. The Mahogany is not healthy, so it is left out of the “Healthy trees” list.

Exercises

  1. Make a list with the weights of five cows (any numbers). Print the heaviest, the lightest, and the average.
  2. From the list [480, 500, 510, 520, 530, 540], use slicing to take: the last three, and the first three reversed.
  3. Make a dict for one pond with the labels name, fish_count, avg_weight_g, and ready_to_harvest (the result of a comparison). Print everything with for and .items().
  4. Challenge: add one more tree to 10_tree_data_project.py, then write a list comprehension of the species whose height is more than 20 m.

Chapter Summary

  • list [...]: ordered, changeable; indexes start at 0, negative from the back.
  • Slicing [start:stop:step]; the stop number isn’t included.
  • tuple (...): unchangeable; dict {key: value}: labelled data; set {...}: no duplicates and no order.
  • len(), sum(), max(), min() work on lists.
  • Comprehensions build and filter lists in one line.
  • A list of dicts is the usual way to store many field records.

Next, in Chapter 10: we have data and ways to process it. Now we learn to wrap repeated work into functions, so it only needs writing once and can be used many times.