Iterable Objects II — Class Notes¶
Try me¶
These notes summarize the companion notebook Iterable Objects II and add practice cards and takeaways for your exam prep.
Key concepts: Tuples • Dictionaries • Nested Iterables • Comprehensions Extra concepts: Packing/Unpacking (*, **) • zip()
Introduction¶
Motivation¶
When we think of programming, we often think of arrays or lists of data.
However, real-world data is often more complex, and structured.
In real applications (like the IoT challenge), we need the right data structures to model and manipulate data effectively.
We need more design choices, data structures built for protection, fast access, or ease of use.
Objectives¶
Pass the basics and understand advanced iterable objects in Python.
Focus on two foundational data structures: tuples and dictionaries.
Specific objectives:
Pull the key differences between lists, tuples, and dictionaries.
Understand powerful techniques (nesting, and comprehensions).
This will give you different design options to manage data in your programs.
Intro for non-programmers¶
Lists are like arrays: ordered collections of items.
Tuples are like lists, but you can’t change them (immutable). Think of them as constants. A layer of protection (prevents unwanted/accidental changes down the line - data integrity). Brings performance wins (pre-allocation - Python knows final size).
Dictionaries: like address books: you look up values by keys (names), not positions. Useful when you need meaningful labels (not just numbers). Easier access to data (closer to how we find info in the real world). Plus, they are also faster than arrays!
Nesting: putting lists/dicts inside each other to model complex data (like a spreadsheet or a matrix).
Comprehensions: concise way to create lists/dicts by transforming/filtering existing ones.
Agenda¶
Class overview:
Intro & agenda (10 min)
Recap: Lists (15 min)
Tuples (10 min)
Dictionaries (20 min)
Card break (7 min)
Nested iterables (15 min)
Comprehensions (15 min)
Card break (7 min)
wrap-up (6 min)
Homework: Check extra contents and mini-challenges
0) Brief recap: Lists¶
What: Ordered, indexable, mutable sequences.
Mutable: can change size and content.
Syntax:
lst = [1, 2, 3]Indexing/Slicing:
lst[i],lst[a:b:c]Useful functions and methods:
len(lst),lst.append(x)
[ ]:
row = ["Peter", "Paul", "Mary", "John", "Lucy"]
# Example 1: head
s = int(input("start index: "))
e = int(input("end index: "))
print("Selected names:", row[s:e])
[ ]:
# Example 2: rolling update
last_values = [2.3, 2.5, 2.7, 2.9]
i = 0
while True:
new_value = float(input("New value (empty to stop): "))
if not new_value:
break
last_values[i%len(last_values)] = new_value
i += 1
print("Rolling average:", sum(last_values)/len(last_values))
1) Tuples (immutable sequences)¶
What: Ordered, indexable sequences like lists, but immutable. Why: Use for fixed records.
Syntax & rules
Literal:
t = (1, 2, 3)(the comma makes the tuple;(x,)is a 1‑tuple)Indexing/Slicing: Same as lists →
t[i],t[a:b:c](read‑only)Useful functions and methods:
len(t),t.index(x)
[ ]:
# Example
unknowns = ("x", "y", "z")
print("First:", unknowns[0])
print("Tail:", unknowns[1:])
try: # This allows us to demonstrate immutability
unknowns[0] = "X" # immutability demo
except TypeError as e:
print("TypeError:", e.__class__.__name__)
2) Dictionaries (mapping values by keys)¶
What: Key → value store (unordered). Similar to lists, but indexed by keys instead of positions. Why: Fast lookups by key; model records, easier to program. Create:
empty dictionary
d = {},init with a key value pair
d = {"k": 1}, Update:Insert a new value
d["l"] = 2Remove:del d["k"],d.pop("k")Lookup:"k" in d(by key),v in d.values()(by value)
[ ]:
contacts = {}
contacts["Paco"] = 655555555
contacts["Pepe"] = 666555111
contacts["Pili"] = 677777555
print("All:", contacts)
# Literal init
contacts = {"Paco": 655555555, "Pepe": 666555111, "Pili": 677777555}
print("Keys:", list(contacts.keys()))
print("Has 'Paco'?", "Paco" in contacts)
print("Has number 655... as a VALUE?", 655555555 in contacts.values())
# Removal
d = contacts.copy() # d = contacts is a shallow copy! changes will affect both d and contacts
del d["Pepe"] # Remove
pili = d.pop("Pili") # Retrieve & remove. Nice if you need the value.
print("After removals:", d)
2.1) Iteration patterns¶
intests membership on keys:"k" in dTrueif key exists.Keys (default):
for k in d:orfor k in d.keys():Values:
for v in d.values():Pairs:
for k, v in d.items():
[ ]:
# Iteration
for name in contacts:
print("Key only:", name, "→", contacts[name])
for name, phone in contacts.items():
print("Pair:", name, phone)
2.2) Safe read:¶
d[key]raisesKeyErrorif key missing.Method
d.get(key)returnsNoneif key missing.Use
d.get(key, default)to avoidKeyErrorif key missing.
[ ]:
d = {"a": 1, "b": 2}
print("d['a']:", d["a"])
print("d.get('a'):", d.get("a"))
print("d.get('c'):", d.get("c")) # missing key
print("d.get('c', 42):", d.get("c", 42)) # missing key with default
Exercise Cards (Peer Instruction)¶
How to use: predict → discuss → run the check cell (where provided). Keep answers short (what + why).
t = ("A", "B", "C")
t[0] = "Z"
Q: Does this run without exceptions? If not, what is the error?
Given:
contacts = {"Paco": 655555555, "Pepe": 666555111}
True? Choose all that apply."Paco" in contacts • 655555555 in contacts • 655555555 in contacts.values()[ ]:
contacts = {"Paco": 655555555, "Pepe": 666555111}
print("A:", "Paco" in contacts)
print("B:", 655555555 in contacts)
print("C:", 655555555 in contacts.values())
d = {"x":1, "y":2}
for k, v in d.items():
print(f"square of {k} is {v**2}")
Q: What is printed?
[1]:
d = {"x":1, "y":2}
for k, v in d.items():
print(f"square of {k} is {v**2}")
square of x is 1
square of y is 4
3) Nested iterables¶
List of lists (matrix‑like):
[[1,2],[3,4]]List of dicts (records):
[{"name":"Pepe","age":19}, ...]Dicts with list values:
{"colors": ["Orange","Blue"]}
Indexing through levels: data[1]["name"], matrix[0][0]
[ ]:
# List of lists
grid = [[1, 2],
[3, 4]]
print("grid[0] ->", grid[0])
print("grid[0][0] ->", grid[0][0])
# List of dicts
students = [{"name": "Pepe", "age": 19},
{"name": "Ingrid", "age": 18}]
print("Second student's name:", students[1]["name"])
[ ]:
## Mini challenge
# Print the age of student Ingrid
[ ]:
# Mini challenge
## For each of the two nested lists below, find the value 1 using indexing and print it
# Mini challenges
a = [[0, 0], [1,0]]
b = [[[0, 0], [0, 0]], [[1, 0], [0, 0]]]
3.1) Iteration over nested structures¶
Access specific fields during iteration:
for student in students:
print(student["name"])
Iterate outer, then inner:
records = [{"name":"A","age":19},{"name":"B","age":18}]
for record in records:
for key, value in record.items():
...
[ ]:
records = [{"name":"A","age":19},{"name":"B","age":18}]
for record in records:
print(record["name"])
for record in records:
for key, value in record.items():
print(f"{key}: {value}")
[ ]:
# Mini challenge:
## Use a for loop to find the average temperature
arduino_log = [{"time": "2022 - 08 - 31 00: 15", "temp": 25.6, "humidity": 66},
{"time": "2022 - 08 - 31 00: 45", "temp": 25.9, "humidity": 67},
{"time": "2022 - 08 - 32 00: 00", "temp": 25.7, "humidity": 66},
{"time": "2022 - 08 - 32 00: 15", "temp": 25.6, "humidity": 65}]
4) Comprehensions¶
List comp: [expr for x in xs if cond] Dict comp: {key_expr: val_expr for ...}
Good for: small, readable transformations/filters. Prefer loops for complex logic.
[ ]:
keys = ('a', 'b', 'c', 'd')
values = (1, 2, 3, 4)
squares = [i*i for i in values]
even_squares = [i*i for i in values if i % 2 == 0]
new_dict = {keys[i]: squares[i] for i in range(len(keys))}
print("squares:", squares)
print("even_squares:", even_squares)
print("new_dict:", new_dict)
[ ]:
# Tiny "average grade" demo without input()
samples = [{"name":"A", "grade": 7.5},
{"name":"B", "grade": 9.0},
{"name":"C", "grade": 6.5}]
avg = sum(s["grade"] for s in samples)/len(samples)
print("Average grade:", round(avg, 2))
[ ]:
# Mini challenge
# Use comprehension to find the average humidity
arduino_log = [{"time": "2022 - 08 - 31 00: 15", "temp": 25.6, "humidity": 66},
{"time": "2022 - 08 - 31 00: 45", "temp": 25.9, "humidity": 67},
{"time": "2022 - 08 - 32 00: 00", "temp": 25.7, "humidity": 66},
{"time": "2022 - 08 - 32 00: 15", "temp": 25.6, "humidity": 65}]
4.2) Common patterns¶
Extract field list:
[r["name"] for r in records]Filter records:
[r for r in records if r["age"] >= 18]Build dict from list:
{r["name"]: r["age"] for r in records}Aggregate:
sum(r["age"] for r in records) / len(records)
[ ]:
records = [{"name":"Samantha Eve","age":19},{"name":"Mark Grayson","age":18},{"name":"Amber Bennet","age":17}]
# Minichallenge: Discuss in peers and understand the following comprehensions
names = [r["name"] for r in records]
adults = [r for r in records if r["age"] >= 18]
age_dict = {r["name"]: r["age"] for r in records}
avg_age = sum(r["age"] for r in records) / len(records)
print("Names:", names)
print("Adults:", adults)
print("Age dict:", age_dict)
print("Average age:", avg_age)
Exercise Cards (Peer Instruction)¶
How to use: predict → discuss → run the check cell (where provided). Keep answers short (what + why).
students = [{"name":"A","age":19},{"name":"B","age":18}]
Q: Predict: students[1]["age"] and students[0]["name"].
[ ]:
students = [{"name":"A","age":19},{"name":"B","age":18}]
print(students[1]["age"], students[0]["name"])
vals = [0,1,2,3,4]
out = [v*v for v in vals if v%2==0]
Q: What is out?
[ ]:
vals = [0,1,2,3,4]
out = [v*v for v in vals if v%2==0]
print(out)
Map letters to their squared index (0-based) for "abcd":
letters = "abcd"
# result should be: {'a':0, 'b':1, 'c':4, 'd':9}
Q: Predict or sketch the comp; then run the check.
[ ]:
letters = "abcd"
res = {ch: i*i for i, ch in enumerate(letters)}
print(res)
Extra (Not covered in exam)¶
5) Packing & Unpacking (*, **)¶
Tuple unpack:
a, b = (1, 2); extended:first, *mid, last = tCall-time unpack:
f(*args_tuple); keyword unpack:g(**kwargs_dict)Useful with functions like
range,print.
[ ]:
t = (1, 2, 3, 4)
a, b, c, d = t
print(a, b, c, d)
first, *g, last = t
print("first:", first, "| g:", g, "| last:", last)
# Call-time unpack
range_args = (1, 10, 3)
print("range(*range_args):", list(range(*range_args)))
args = ("first string", "second string")
key_args = {"sep": ", ", "end": "\n"}
print(*args, **key_args)
6) zip()¶
What: Combine iterables position-wise into tuples. Stops at the shortest input. Unzip: a, b = zip(*pairs) (works if not empty).
[5]:
x = (1, 2, 3)
y = (4, 5, 6, 7)
k = ("a", "b", "c")
pairs = list(zip(x, y))
pairs_dict = dict(zip(k, x)) # k: x mapping nice pattern for dicts
print("pairs:", pairs)
print("pairs_dict:", pairs_dict)
# Unzip (guard empty)
if pairs:
a, b = zip(*pairs)
print("unzipped a:", a, "| b:", b)
pairs: [(1, 4), (2, 5), (3, 6)]
pairs_dict: {'a': 1, 'b': 2, 'c': 3}
unzipped a: (1, 2, 3) | b: (4, 5, 6)
Extra Cards¶
Card 7 — Nested iterables¶
What is the output of:
data = {"colors": ["Red", "Green", "Blue"], "numbers": [1, 2, 3]}
print(data["colors"][1])
[ ]:
data = {"colors": ["Red", "Green", "Blue"], "numbers": [1, 2, 3]}
print(data["colors"][1])
Card 8 - Comprehension vs loop¶
Which of these snippets produces the same result?
# Snippet A
result_A = []
for i in range(5):
if i % 2 == 0:
result_A.append(i * i)
# Snippet B
result_B = [i * i for i in range(5)]
# Snippet C
result_C = [i * i for i in range(5) if i % 2 == 0]
# Snippet D
result_D = sum([i * i for i in range(5) if i % 2 == 0])
[7]:
# Snippet A
result_A = []
for i in range(5):
if i % 2 == 0:
result_A.append(i * i)
# Snippet B
result_B = [i * i for i in range(5)]
# Snippet C
result_C = [i * i for i in range(5) if i % 2 == 0]
# Snippet D
result_D = sum([i * i for i in range(5) if i % 2 == 0])
print(f"result_A: {result_A}")
print(f"result_B: {result_B}")
print(f"result_C: {result_C}")
print(f"result_D: {result_D}")
result_A: [0, 4, 16]
result_B: [0, 1, 4, 9, 16]
result_C: [0, 4, 16]
result_D: 20
Card 9 — Scalar product with comprehension¶
What is the output of:
a = [1, 2, 3]
b = [4, 5, 6]
c = sum(a[i] * b[i] for i in range(len(a)))
[ ]:
a = [1, 2, 3]
b = [4, 5, 6]
c = sum(a[i] * b[i] for i in range(len(a)))
Card 10 - Series with comprehension¶
Which of these snippets produces a geometric series with common ratio of \(\frac{1}{2}\), that is (\(\frac{1}{2} + \frac{1}{4} + \frac{1}{8} + ...\) up to n? ```python n = 5 # Snippet A s_A = sum(1 /2 for i in range(n)) # Snippet B s_B = sum(1 / (2 ** i) for i in range(1, n + 1)) # Snippet C s_C = sum(1 / (2 ** i) for i in range(n))
[8]:
n = 5
# Snippet A
s_A = sum(1 /2 for i in range(n))
# Snippet B
s_B = sum(1 / (2 ** i) for i in range(1, n + 1))
# Snippet C
s_C = sum(1 / (2 ** i) for i in range(n))
print(f"s_A: {s_A}")
print(f"s_B: {s_B}")
print(f"s_C: {s_C}")
s_A: 2.5
s_B: 0.96875
s_C: 1.9375
Mini Challenges (apply patterns)¶
Filter + transform: From
students(list of dicts withgrade), build a list of names of students withgrade >= 8, uppercased. (One line comprehension or a tiny loop.)
[ ]:
Dict from pairs: Given
keys = ["id","name","age"]androw = [101,"Ada",28], build{"id":101,"name":"Ada","age":28}usingzip.
[ ]:
Takeaways¶
Tuples are immutable: safe to share, great for fixed records and unpacking.
Dictionaries store by key; test membership on keys (
k in d) or on values (v in d.values()).Nested data is just layers: index your way down (
data[1]["x"][0]).Comprehensions are ideal for small, clear transformations; prefer loops if it gets dense.
*and**unpack positional/keyword arguments—handy withrange,print, and functions you write.``zip`` combines;
zip(*pairs)unzips; stops at the shortest input.Aim for minimal changes to fix intent (great exam habit).