Learning Goals
3 minBy the end of this lesson you can:
- Build a list of instances with a comprehension or a loop.
- Loop a list of instances and call methods on each.
- Filter, sort and find max/min using
key=lambda x: x.attr. - Sum, count and average an attribute across the list.
Warm-Up · List Of Dicts vs List of Instances
5 minSame data, two shapes. Compare:
# list of dicts (PY-L2-11) heroes = [ {"name": "Aisyah", "hp": 100}, {"name": "Wei Jie", "hp": 85}, ] for h in heroes: print(h["name"], h["hp"])
# list of instances (today) class Hero: def __init__(self, name, hp): self.name = name self.hp = hp heroes = [Hero("Aisyah", 100), Hero("Wei Jie", 85)] for h in heroes: print(h.name, h.hp)
Same result. The class version is a bit longer up-front (you have to define Hero) but the loop and every operation that follows is cleaner — dots instead of brackets, no quoted keys.
Every list-of-dicts pattern from Level 2 works on a list of instances. Filter by attribute, sort by attribute, max-by-attribute, group-by-attribute — same shapes, cleaner syntax.
New Concept · The Standard Operations
14 minBuild many at once
Use a list comprehension over data:
# From a list of tuples raw = [("Aisyah", 100), ("Wei Jie", 85), ("Priya", 70)] heroes = [Hero(name, hp) for name, hp in raw] # From parallel lists names = ["Aisyah", "Wei Jie", "Priya"] hps = [100, 85, 70] heroes = [Hero(n, hp) for n, hp in zip(names, hps)]
One line, three (or twelve, or a thousand) heroes. Avoid the typo trap of hand-typing each one.
Filter by attribute
# Heroes still alive alive = [h for h in heroes if h.hp > 0] # Heroes with low HP low_hp = [h for h in heroes if h.hp < 30]
The condition uses h.attr — same as h["attr"] in the dict version.
Sort by attribute
# Sort by HP ascending by_hp = sorted(heroes, key=lambda h: h.hp) # Highest HP first strongest_first = sorted(heroes, key=lambda h: h.hp, reverse=True)
Same sorted + key pattern from PY-L2-04. The key function pulls an attribute out for comparison.
Max / min by attribute
top = max(heroes, key=lambda h: h.attack) weakest = min(heroes, key=lambda h: h.hp) print(top.name, "is the strongest attacker")
Notice: returns the whole instance, not just the attribute value. Then we read .name off the winner.
Sum / count an attribute
total_hp = sum(h.hp for h in heroes) # generator expression party_size = len(heroes) avg_hp = total_hp / party_size # Count those matching a condition n_alive = sum(1 for h in heroes if h.is_alive())
The generator expression (h.hp for h in heroes) is like a list comprehension but doesn't build an intermediate list — efficient for big iterations.
Group by attribute
by_class = {} # name → list of heroes for h in heroes: by_class.setdefault(h.class_name, []).append(h) for class_name, group in by_class.items(): print(f"{class_name}: {len(group)} hero(es)")
Same group-by recipe from PY-L2-12. setdefault creates the empty list if it's not there yet, otherwise gives you the existing one.
Call a method on each
for h in heroes: h.take_damage(10) # every hero takes 10 damage
Loop and invoke. This is the action half of OOP — same method, many instances, different effects on each.
Worked Example · The Party Roster
12 minSave as party.py:
# party.py — operations on a list of Hero instances class Hero: def __init__(self, name, hp=100, attack=10, role="fighter"): self.name = name self.hp = hp self.max_hp = hp self.attack = attack self.role = role def is_alive(self): return self.hp > 0 def take_damage(self, amount): self.hp = max(0, self.hp - amount) def report(self): return f"{self.name:<10} {self.role:<8} HP {self.hp:>3}/{self.max_hp:<3} ATK {self.attack:>2}" # 5 heroes built from tuples specs = [ ("Aisyah", 100, 12, "fighter"), ("Wei Jie", 85, 18, "ranger"), ("Priya", 70, 25, "mage"), ("Iman", 120, 8, "tank"), ("Aizat", 60, 14, "rogue"), ] party = [Hero(*spec) for spec in specs] # 1. List everyone print("=== Roster ===") for h in party: print(" " + h.report()) # 2. Filter — only mages mages = [h for h in party if h.role == "mage"] print(f"\nMages in party: {len(mages)}") # 3. Sort — by attack, descending ranked = sorted(party, key=lambda h: h.attack, reverse=True) print("\n=== By ATK (best first) ===") for h in ranked: print(f" {h.name:<10} ATK {h.attack}") # 4. Max — strongest hitter strongest = max(party, key=lambda h: h.attack) print(f"\nStrongest: {strongest.name} (ATK {strongest.attack})") # 5. Sum / Avg total_hp = sum(h.hp for h in party) print(f"Party HP : {total_hp}") print(f"Avg HP : {total_hp / len(party):.1f}") # 6. Call a method on each — a meteor strike print("\n=== Meteor strike (everyone takes 40) ===") for h in party: h.take_damage(40) print(" " + h.report()) # 7. Filter — survivors survivors = [h for h in party if h.is_alive()] print(f"\nSurvivors: {len(survivors)} of {len(party)}") # 8. Group by role by_role = {} for h in party: by_role.setdefault(h.role, []).append(h.name) print(f"\nBy role: {by_role}")
Read the diff
Eight different operations, all in 70 lines. Hero(*spec) unpacks each tuple into the constructor — same trick as PY-L2-04 unpacking applied to function calls. The eight ops match exactly the patterns from PY-L2-11's list-of-dicts lesson — but with dotted attribute access, no quoted keys.
Try It Yourself
13 minDefine Book(title, author, pages). Build a list of 5 books. Print the one with the most pages.
Hint
class Book: def __init__(self, title, author, pages): self.title = title; self.author = author; self.pages = pages shelf = [ Book("Charlotte's Web", "White", 192), Book("The Hobbit", "Tolkien", 310), Book("Diary of a Wimpy", "Kinney", 221), Book("Tom Gates", "Pichon", 256), Book("Wings of Fire", "Sutherland", 304), ] longest = max(shelf, key=lambda b: b.pages) print(f"{longest.title}: {longest.pages} pages")
From your book shelf, print just the books with >= 250 pages, sorted by author alphabetically.
Hint
big = [b for b in shelf if b.pages >= 250] big.sort(key=lambda b: b.author) for b in big: print(f" {b.author}: {b.title}")
Two operations chained. .sort mutates in place; if you want a new list, use sorted(big, key=...) instead.
Build a dict mapping author → list of book titles. Print each author with their books.
Hint
by_author = {} for b in shelf: by_author.setdefault(b.author, []).append(b.title) for author, titles in by_author.items(): print(f"{author}:") for t in titles: print(f" - {t}")
Same group-by-dict pattern from PY-L2-12, now reading b.author instead of b["author"].
Mini-Challenge · Hero Tournament
8 minBuild tournament.py. Simulate a tournament of 8 heroes.
- Build 8 Hero instances with random HP (50-100) and attack (5-25). Use
random.randint. - Pair them up (1v2, 3v4, etc.). In each pair, the higher-attack hero wins.
- The 4 winners advance to round 2. Same rule.
- The 2 finalists fight. One champion emerges.
- Print every round's results plus the final champion.
Show one possible solution
# tournament.py — single-elim by attack rating import random class Hero: def __init__(self, name, hp, attack): self.name = name; self.hp = hp; self.attack = attack def report(self): return f"{self.name} (HP {self.hp}, ATK {self.attack})" names = ["Aisyah", "Wei Jie", "Priya", "Iman", "Aizat", "Hafiz", "Aliya", "Daniel"] heroes = [Hero(n, random.randint(50, 100), random.randint(5, 25)) for n in names] print("=== Round 1 ===") round_no = 1 while len(heroes) > 1: winners = [] for i in range(0, len(heroes), 2): a, b = heroes[i], heroes[i + 1] winner = a if a.attack > b.attack else b print(f" {a.report()} vs {b.report()} → {winner.name}") winners.append(winner) heroes = winners round_no += 1 if len(heroes) > 1: print(f"\n=== Round {round_no} ===") print(f"\n🏆 Champion: {heroes[0].report()}")
Non-negotiables: 8 random heroes built via comprehension, a round-by-round loop that halves the list each iteration, and a final champion print. Notice heroes = winners at the end of each round — the list shrinks to half.
Recap
3 minA list of instances behaves exactly like a list of dicts — but with dotted access. Build with a comprehension. Filter with [x for x in lst if condition]. Sort with sorted(lst, key=lambda x: x.attr). Max/min the same way. Sum and count with generator expressions. Group with setdefault. Call a method on every instance with a simple loop. Same eight patterns, two flavours; today's flavour is cleaner.
Vocabulary Card
- list of instances
- The OOP equivalent of a list of dicts. Same operations, dotted access.
- key=lambda x: x.attr
- Tell sorted/max/min which attribute to compare by.
- Hero(*spec)
- Unpack a tuple into positional arguments. Cleaner than typing each argument by hand.
- generator expression
- Like a list comprehension but in round brackets:
(x.hp for x in heroes). Doesn't build an intermediate list.
Homework
4 minBuild music_library.py. Define Song(title, artist, plays, duration_sec). Build at least 8 songs. Report:
- Total play count.
- Total listening time (sum of
duration_sec, displayed as minutes:seconds). - Most-played song.
- Songs by your favourite artist.
- Songs grouped by artist — dict, count per artist.
- Sort the list by plays descending and print a top-5 leaderboard.
Sample · music_library.py
# music_library.py — operations on a list of Song instances class Song: def __init__(self, title, artist, plays, duration_sec): self.title = title self.artist = artist self.plays = plays self.duration = duration_sec library = [ Song("Levitating", "Dua Lipa", 124, 203), Song("New Rules", "Dua Lipa", 89, 209), Song("Bad Guy", "Eilish", 156, 194), Song("Lovely", "Eilish", 73, 200), Song("Dynamite", "BTS", 210, 199), Song("Butter", "BTS", 180, 164), Song("Bohemian Rhap","Queen", 52, 354), Song("Cupid", "FIFTY FIFTY",195, 174), ] total_plays = sum(s.plays for s in library) total_sec = sum(s.duration for s in library) print(f"Total plays : {total_plays:,}") print(f"Listening time: {total_sec // 60}m {total_sec % 60}s") top = max(library, key=lambda s: s.plays) print(f"Most played : {top.title} — {top.plays} plays") # By favourite artist print("\nBTS songs:") for s in [x for x in library if x.artist == "BTS"]: print(f" {s.title} ({s.plays} plays)") # Group by artist by_artist = {} for s in library: by_artist[s.artist] = by_artist.get(s.artist, 0) + 1 print(f"\nSongs per artist: {by_artist}") # Top-5 leaderboard print("\nLeaderboard:") for i, s in enumerate(sorted(library, key=lambda x: x.plays, reverse=True)[:5], 1): print(f" {i}. {s.title} — {s.plays} plays")
Non-negotiables: a Song class with __init__, 8 songs built via constructor, and the six requested reports. :, in the f-string is the thousands separator from PY-L2-16.