Learning Goals
3 minBy the end of this lesson you can:
- Set an instance attribute with
obj.attr = value. - Read an attribute with
obj.attr. - Confirm that two instances of the same class can hold different attribute values.
- Recognise
AttributeErrorand how to dodge it withhasattr/getattr.
Warm-Up · You've Done This Already
5 minYou used dotted attribute access through all of Level 2 without thinking:
text = "hello" print(text.upper()) # → HELLO import math print(math.pi) # → 3.141592... from datetime import date today = date.today() print(today.year) # → 2026
text.upper, math.pi, today.year are all attribute reads on objects. Today we add attributes to our own classes — same dot, same syntax.
Each instance carries its own data inside it. The data is called attributes. You set them and read them with the dot.
New Concept · Set and Read
14 minSet with assignment
class Hero: pass aisyah = Hero() aisyah.name = "Aisyah" aisyah.hp = 100 aisyah.inventory = ["sword", "potion"] print(aisyah.name) # → Aisyah print(aisyah.hp) # → 100 print(aisyah.inventory) # → ['sword', 'potion']
The dot on the left side of = creates or replaces the attribute. The dot on the right (in print) reads it. Three reads, three writes, every one of them on this specific Hero.
Each instance is independent
wei_jie = Hero() wei_jie.name = "Wei Jie" wei_jie.hp = 80 print(aisyah.hp) # → 100 (her HP is untouched) print(wei_jie.hp) # → 80 (his is different)
Two heroes, two HP values. The class is the blueprint, but the data lives on each instance.
The AttributeError
Read an attribute that was never set, and Python crashes:
hero = Hero() print(hero.name) # AttributeError: 'Hero' object has no attribute 'name'
Two ways to dodge it:
# 1 — check before reading if hasattr(hero, "name"): print(hero.name) # 2 — getattr with a default print(getattr(hero, "name", "(no name)"))
Same shape as PY-L2-05's dict.get(key, default) — most Python objects offer this same safe-lookup idea.
The class is a real object too
Attributes can also be set on the class itself — "class attributes", shared by every instance.
class Hero: species = "human" # class attribute — shared by all heroes a = Hero() b = Hero() print(a.species) # → human print(b.species) # → human
If you reassign on one instance (a.species = "elf"), that creates an instance-level shadow — only that one Hero is now an elf. Useful but easy to confuse with instance attributes; we'll keep mostly to instance attrs for now.
The shape so far
class Hero: ← the blueprint pass aisyah = Hero() ← one instance aisyah.name = "Aisyah" ← attribute on THIS instance aisyah.hp = 100 wei_jie = Hero() ← another instance, totally independent wei_jie.name = "Wei Jie" wei_jie.hp = 80
The clumsy bit · setting attrs every time
Notice how repetitive this is. Every new hero needs four lines of x.name = ... setup. There's a better way — a special __init__ method that does the setup automatically. That's PY-L3-04. For today: get the manual pattern in your fingers first.
Worked Example · The Hero Roster
12 minSave as roster.py:
# roster.py — three heroes with attributes class Hero: pass # Build three heroes aisyah = Hero() aisyah.name = "Aisyah" aisyah.hp = 100 aisyah.attack = 12 aisyah.inventory = ["sword", "potion"] wei_jie = Hero() wei_jie.name = "Wei Jie" wei_jie.hp = 85 wei_jie.attack = 18 wei_jie.inventory = ["bow", "potion", "potion"] priya = Hero() priya.name = "Priya" priya.hp = 70 priya.attack = 22 priya.inventory = ["staff", "scroll"] # Use them heroes = [aisyah, wei_jie, priya] for h in heroes: items = ", ".join(h.inventory) print(f"{h.name:<10} HP {h.hp:>3} ATK {h.attack:>2} carrying: {items}") # Combined stats total_hp = sum(h.hp for h in heroes) print(f"\nParty HP : {total_hp}") print(f"Strongest: {max(heroes, key=lambda h: h.attack).name}")
Output
Aisyah HP 100 ATK 12 carrying: sword, potion Wei Jie HP 85 ATK 18 carrying: bow, potion, potion Priya HP 70 ATK 22 carrying: staff, scroll Party HP : 255 Strongest: Priya
Read the diff
Three new patterns to spot. (1) Each hero has four attributes — three numbers, one list. Lists work as attributes just like anything else. (2) The print loop reads attributes with h.name, h.hp, etc. — same syntax you used in Level 2 for dict-of-dicts, but cleaner. (3) The final two lines use sum and max with attribute access — key=lambda h: h.attack tells max to compare by attack value, return the whole hero.
Same data, different shape. List of dicts: h["name"], h["hp"]. List of objects: h.name, h.hp. The object version is shorter to type — and lets us add behaviour later. Both are useful; we're shifting up the abstraction ladder.
Try It Yourself
13 minDefine a Book class. Make one instance with attributes title, author, year. Print each.
Hint
class Book: pass b = Book() b.title = "Charlotte's Web" b.author = "E.B. White" b.year = 1952 print(f"{b.title} ({b.year}) by {b.author}")
Make two Hero instances. Set the hp attribute on each. Change one. Confirm the other is unaffected.
Hint
class Hero: pass a = Hero(); a.hp = 100 b = Hero(); b.hp = 100 # Take 30 damage off only one a.hp -= 30 print(a.hp) # → 70 print(b.hp) # → 100 (untouched)
Compound assignment a.hp -= 30 works exactly like on a plain variable.
Write a function safe_print_age(person) that prints the person's age, or (unknown) if the age attribute isn't set.
Hint
class Person: pass def safe_print_age(person): age = getattr(person, "age", None) if age is None: print("Age: (unknown)") else: print(f"Age: {age}") a = Person(); a.age = 12 b = Person() # no age set safe_print_age(a) # → Age: 12 safe_print_age(b) # → Age: (unknown)
getattr(obj, "attr", default) is the safe-read idiom. Use it whenever an attribute might or might not be set.
Mini-Challenge · The Spaceship Fleet
8 minBuild fleet.py. Define a Spaceship class. Build five ships, each with these attributes:
name— stringfuel— int, 0-100crew— list of crew member namescaptain— string
Store all five in a list. Then:
- Print every ship as a tidy table row using f-string width specifiers.
- Print the captain of the ship with the most fuel.
- Print the total crew across the fleet.
- Print the average fuel level.
Show one possible solution
# fleet.py — five ships with attributes class Spaceship: pass def make_ship(name, fuel, crew, captain): s = Spaceship() s.name = name s.fuel = fuel s.crew = crew s.captain = captain return s fleet = [ make_ship("Hawk", 80, ["Aisyah", "Aman"], "Aisyah"), make_ship("Falcon", 95, ["Wei Jie", "Priya", "Iman"], "Wei Jie"), make_ship("Eagle", 40, ["Hafiz"], "Hafiz"), make_ship("Phoenix", 100, ["Aizat", "Cikgu"], "Cikgu"), make_ship("Vulture", 25, ["Iman", "Priya"], "Iman"), ] print(f"{'Ship':<10}{'Fuel':>6}{'Crew':>6} Captain") print("-" * 36) for s in fleet: print(f"{s.name:<10}{s.fuel:>5}%{len(s.crew):>6} {s.captain}") most_fueled = max(fleet, key=lambda s: s.fuel) print(f"\nMost fueled: {most_fueled.name} (captain: {most_fueled.captain})") print(f"Total crew : {sum(len(s.crew) for s in fleet)}") print(f"Avg fuel : {sum(s.fuel for s in fleet) / len(fleet):.1f}%")
Non-negotiables: a helper make_ship function to avoid the four-line setup repetition, attribute access for every read, and one comprehension that uses an attribute as the sort key. The make_ship helper is a sneak peek at __init__ — exactly the kind of repetition that screams "there must be a better way".
Recap
3 minAttributes are data attached to a specific instance. Set with obj.attr = value. Read with obj.attr. Each instance carries its own attributes; two heroes can have different HP values. AttributeError happens when you read a non-existent attribute; getattr(obj, name, default) is the safe alternative. The make_ship-style helper hints at why we need __init__ — coming in PY-L3-04.
Vocabulary Card
- attribute
- A named piece of data stored on an instance.
- obj.attr = x
- Set an attribute. Creates it if it didn't exist.
- obj.attr
- Read an attribute.
- AttributeError
- The crash you get when reading an attribute that's never been set.
- getattr(obj, name, default)
- Safe read. Returns the attribute if present, otherwise the default.
Homework
4 minSave my_team.py. Define a Player class. Build at least four players for your favourite sport — each with name, position, goals, matches_played.
Then print:
- Every player on their own row.
- The top scorer.
- Goals per match for each player —
goals / matches_played, formatted to 2 decimals. - The whole team's combined goals.
Sample · my_team.py
# my_team.py — football squad with attributes class Player: pass def make(name, position, goals, matches): p = Player() p.name = name p.position = position p.goals = goals p.matches_played = matches return p squad = [ make("Aisyah", "ST", 12, 20), make("Wei Jie", "CM", 4, 22), make("Priya", "ST", 18, 21), make("Iman", "GK", 0, 19), ] print(f"{'Name':<10}{'Pos':<6}{'Goals':>6}{'Matches':>9}{'g/m':>8}") print("-" * 39) for p in squad: ratio = p.goals / p.matches_played if p.matches_played else 0 print(f"{p.name:<10}{p.position:<6}{p.goals:>6}{p.matches_played:>9}{ratio:>8.2f}") top = max(squad, key=lambda p: p.goals) print(f"\nTop scorer: {top.name} ({top.goals} goals)") print(f"Team goals: {sum(p.goals for p in squad)}")
Non-negotiables: a Player class, at least 4 players via a builder helper, a table-style printout with width specifiers, and stats lines using max and sum on attribute keys.