2.1 · Lists, tuples, dictionaries & sets

Level 2 · Data structures & OOP

2.1Lists, tuples, dictionaries & sets

Goal: Choose the right data structure and handle it efficiently.
Estimated time: 16 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • Four containers, four uses: ordered mutable list, frozen ordered tuple, dict by keys, set of unique values — the right container for the right use.
  • The dictionary is the queen structure: a list of dicts = 90% of real-world data (API, CSV, JSON) — access by key, .get() with no KeyError.
  • Copying is not assigning: = shares, .copy() copies flat, deepcopy() copies everything — verify with a test on the nested part.

Running mini-case — Nabil and the sales in Algiers

Nabil, a junior data analyst in Algiers, receives a sales export: a list of rows with duplicates, customers to look up by code, frozen GPS coordinates and categories to deduplicate. His first version looks up customers by scanning a list — slow and fragile. He rebuilds: sales as a list of dictionaries (one key per field), the customer directory as a dict indexed by code with .get(), coordinates as immutable tuples, categories as a set. This same dataset returns in 2.2 (reports formatted with f-strings, JSON saving), 2.3 (Vente and Client objects) and 2.4 (missing file handled cleanly).

Running thread of level 2: this same Nabil dataset returns in the lesson, the diagram, the script and the 48h challenge — and continues in 2.2, 2.3 and 2.4.

1. Four containers, four temperaments

Real applications handle collections: a list of sales, a customer directory, categories. Python offers four fundamental structures. The list ([]) is ordered and mutable — the Swiss army knife: adding with append(), access by index starting at 0, last element with [-1]. The tuple (()) is ordered but immutable: perfect for frozen data (GPS coordinates, a frozen pair) and usable as a dictionary key. The dictionary ({}) maps keys to values — the address book: access by key, not by position. The set (set()) keeps only unique values, with no order: ideal for deduplicating and testing membership with in. Nabil applies it: sales in a list, coordinates in a tuple, customers in a dict, categories in a set.

2. The dictionary, queen structure

The dictionary deserves its status: client = {"nom": "Nabil", "wilaya": "Algiers"}, access with client["nom"], and above all client.get("tel", "?") which returns a default value with no KeyError. A list of dictionaries represents 90% of real-world data (API, CSV or JSON rows): each row is a dict, the whole is a list. You iterate with .keys() (keys), .values() (values) and .items() (pairs: for cle, valeur in d.items()). Looking up by key in a dict is direct; looking up by key while scanning a list is slow and fragile — Nabil's rule: as soon as you look up by identifier, index into a dict.

3. Copying without sharing (labels strike back)

In Python, b = a does not copy: both names point to the same object — modifying one modifies the other. b = a.copy() copies shallowly (nested objects stay shared), copy.deepcopy() copies everything, including the nested part. Rule: copy() for a flat structure, deepcopy() for nested data — and a small test to verify (id() or a trial modification). Nabil remembers: his "copy" of the monthly table kept modifying the original until he switched to deepcopy for the nested rows.

Common misinterpretation: "A dict and a list are the same thing: I can do everything with lists". No. A list looks up by POSITION (scanning, slow, fragile as soon as you look up by identifier); a dict looks up by KEY (direct, readable, safe with .get()). Scanning a customer list to find a code every time is the program that slows down and gets complicated as soon as data grows. Nabil's rule: a collection to walk through in order = list; access by identifier = dict indexed by that identifier.
Containerize well The right container for the right use List (order) Ordered,mutable — append, indexDict (keys) Access by key —.get(), .items()Set (unique) Deduplicate,test with in
The right container for the right use
Text version of the diagram (SEO / screen readers / translation): 1) Nabil stores his sales in a LIST (order, append, index from 0) and his GPS coordinates in a frozen TUPLE (immutable, usable as a key). 2) He indexes his customers in a DICT by code (direct access, .get() with a default, iteration with .items()) instead of scanning a list — the list of dicts covers APIs, CSV and JSON. 3) He deduplicates categories in a SET and tests membership with in. 4) He COPIES consciously: = shares, .copy() is flat, deepcopy() is for nested data, verified by a test. Lesson: the right container for the right use, and copy without sharing.

🔎 Concrete example — 🔎 Concrete example — Deduplicating and counting Nabil's wilayas

The set deduplicates, the comprehension counts, as on Nabil's sales:

ventes = ["Algiers", "Oran", "Algiers", "Blida", "Oran", "Algiers"]
uniques = set(ventes)
comptes = {w: ventes.count(w) for w in uniques}
print(comptes)  # {'Algiers': 3, 'Oran': 2, 'Blida': 1}

Reading: set() deduplicates, the dict comprehension counts each wilaya.

Lesson: tuple for immutable composite keys, dict for access by key.

Section vocabulary

List
Ordered and mutable collection, in brackets: [1, 2, 3]. Adding with append(), access by index from 0, [-1] for the last item.

Simple example: Nabil stores his sales in a list iterated with for.

What it is not: a key-based access structure.

Reflex: order to walk through = list.

Tuple
Ordered but IMMUTABLE (non-modifiable) collection, in parentheses: (lat, lon). Protects frozen data, usable as a dict key.

Simple example: Nabil's GPS coordinates are tuples.

What it is not: a list modified along the program.

Reflex: frozen data = tuple.

Dictionary (dict)
Set of key→value pairs, in braces: {"nom": "Nabil"}. Access by key, .get() with a default and no KeyError, iteration with .items().

Simple example: Nabil finds a customer by code via annuaire[code].

What it is not: a list scanned to look up by identifier.

Reflex: looking up by identifier = dict.

Set
Unordered collection of UNIQUE values: {1, 2, 3}. Ideal for deduplicating (set(liste)) and testing membership with in.

Simple example: Nabil deduplicates categories with set(categories).

What it is not: a list with duplicates for testing membership.

Reflex: unique values or membership = set.

Copy (assignment / copy / deepcopy)
= shares the same object; .copy() copies shallowly (nested part shared); copy.deepcopy() copies everything, including the nested part.

Simple example: Nabil duplicates his nested monthly table with deepcopy.

What it is not: an assignment mistaken for a copy.

Reflex: nested data to duplicate = deepcopy + test.

In practice — In practice — Choosing and handling the right structure

  1. Create a sales list, add (append), remove (remove), sort (sort) and access by index. Iterate over it with a for loop.
  2. Create a "client" dictionary (nom, email, wilaya). Access by key, add a key, use .get() with a default, iterate with .items().
  3. Create a coordinate tuple (immutable) and try to modify it: observe the error. Conclude on the value of immutability.
  4. Deduplicate a list with set() then count with a dict comprehension. Check the difference between = and .copy() on a list.
You choose the right structure (list, tuple, dict, set) and handle it: the basis of data processing in Python.

Script ready to say — justifying the dict to a colleague (30 s)

"For Nabil's sales, I keep the list for order, but customers are indexed in a dict by code: direct access, .get() with a default instead of crashing, and iteration with .items(). Scanning the list on every lookup was slow and fragile — now it is readable and instant."

Say it while showing a lookup by code before (loop) and after (dict).

Copy-paste template — indexed directory and counting

ventes = [
    {"client": "C01", "wilaya": "Algiers", "montant": 1200},
    {"client": "C02", "wilaya": "Oran", "montant": 800},
    {"client": "C01", "wilaya": "Algiers", "montant": 450},
]
annuaire = {"C01": {"nom": "Nabil", "wilaya": "Algiers"}, "C02": {"nom": "Lina", "wilaya": "Oran"}}
for v in ventes:
    client = annuaire.get(v["client"], {"nom": "unknown"})
    print(client["nom"], "->", v["montant"])
wilayas = [v["wilaya"] for v in ventes]
print({w: wilayas.count(w) for w in set(wilayas)})

Export to deduplicate

Nabil's sales arrive duplicated: set() on the identifiers, then a cleaned list of dicts.

Directory by code

Each customer has a unique code: dict indexed by code, .get() with a default for unknown codes.

Frozen coordinates

Delivery points are tuples (lat, lon): immutable, usable as dict keys.

Monthly table to duplicate

The nested summary is duplicated with deepcopy: the original from Algiers stays intact.

✓ Best practices

  • Index into a dict as soon as you look up by identifier (never scan a list).
  • Use .get() with a default and .items() to iterate cleanly.
  • Copy consciously: = shares, copy() is flat, deepcopy() is for nested data.

✗ Common pitfalls

  • Looking up by key while scanning a list (slow and fragile).
  • Using dict[cle] without .get() and suffering a KeyError.
  • Believing that = copies the structure (it shares the same object).

Key takeaways

  • List = ordered + mutable; tuple = ordered + frozen (immutable).
  • Dictionary = key→value pairs (the directory), ubiquitous (JSON, config); .get() avoids KeyError.
  • Set = unique values, perfect for deduplicating and testing with in.
  • Indices start at 0; [-1] means the last one; = shares, copy()/deepcopy() copy.

48h challenge — apply tomorrow

Take a real export (Nabil's or yours): store it as a list of dicts, index a directory as a dict by code with .get(), deduplicate a column with set(), and prove the = vs .copy() difference on a nested case.

Section exit checklist
  • I can choose the right structure (list, tuple, dict, set) with one example of each.
  • I can index into a dict, use .get() and iterate with .items().
  • I have verified the difference between assignment, copy() and deepcopy().
Self-assessment 1-5: do I choose the right container for the right use?do I copy without sharing by accident?

Frequently asked questions

When should I use a tuple rather than a list?

Use a tuple for data that must not change (GPS coordinates, a date, a frozen pair). Immutability protects against accidental modification, lets you use the tuple as a dictionary key, and is slightly faster. Anything that evolves (additions, removals, sorting) belongs to lists.

How do I iterate over a dictionary?

Use .items() to get the pairs: "for cle, valeur in mon_dict.items()". You can also iterate over keys only (.keys(), or directly for cle in mon_dict) or values (.values()). For safe access, prefer .get(cle, defaut) over dict[cle] when the key may be missing.

Check your understanding

What is the difference between a list and a tuple?

Additional resources (optional, to go further)

Level 2 examFresh, harder questions to test what you've learned