The Hook
You have a list of dicts and a loader that works. Then the on-call lead asks for srv-07.status() and you have to explain that the data and the logic that judges it live in two different places, so every caller has to remember the rules. A class puts them in one thing. A list of those things is the fleet.
Waypoint 1: The Blueprint and the Instance
class Record: is a blueprint. It does not contain data, it describes where data will go. __init__ fills in one particular object when it is created, and you never call it yourself: Record("R-001", 87.0, 100.0) runs it for you.
The first parameter is self, always. self is this particular object, and Python passes it in so you never write it at the call site. Record("R-001", 87.0, 100.0) is closer to Record.__init__(new_object, "R-001", 87.0, 100.0) than to a normal function call.
Two objects built from the same class share nothing. Change r1.value and r2.value is untouched, because each keeps its own copy of the attributes. Reaching that data is dot notation, no quotes and no square brackets, which is the visible difference from last week’s dict.
class Record:
def __init__(self, label, value, limit):
self.label = label
self.value = value
self.limit = limit
r1 = Record("R-001", 87.0, 100.0)
r2 = Record("R-002", 120.0, 100.0)
print(r1.label, r1.value)
print(r2.label, r2.value)
record_dict = {"label": "R-001", "value": 87.0}
print(record_dict["label"]) # string key
print(r1.label) # attribute w05-init-attributeWaypoint 2: Methods and the Key to Its Own Room
A method is a function that lives inside a class. Its first parameter is self, and that is how it reaches the object’s own attributes without being handed them again. self is whichever object made the call, so one method body serves the entire fleet.
Methods call each other through self. status() asking self.percent() instead of recalculating keeps the percentage rule in exactly one place.
And a method can change its own object’s state, which week 3’s functions could not. That is not a contradiction. The wall is still there, a method just holds the key to its own room: self is the object, not a copy of it. No return needed when the point was to update the object rather than report on it.
class Record:
def __init__(self, label, value, limit):
self.label = label
self.value = value
self.limit = limit
def percent(self):
return (self.value / self.limit) * 100
def status(self):
p = self.percent()
if p >= 100:
return "OVER LIMIT"
elif p >= 90:
return "WARNING"
return "OK"
r = Record("R-002", 95.0, 100.0)
print(r.status())
class Checker:
def __init__(self):
self.over_limit_count = 0
def check(self, value, limit):
if value > limit:
self.over_limit_count += 1
c = Checker()
c.check(120, 100)
c.check(87, 100)
print(c.over_limit_count) # 1, changed without returning anything Watch out: The Trap
- Forget
selfin__init__and every call raisesTypeError: __init__() takes 1 positional argument but 2 were given. - Write
value = valueinstead ofself.value = valueand the object stores nothing. The name on the left of the dot is the attribute. - Call
status()without the object and you get aNameError. Methods belong to an instance:r.status(). Record.labelis anAttributeError. Attributes live on the object, not the blueprint.- A method’s return updates nothing by itself. Either reassign the result or set
self.somethingand know which one you meant.
w05-method-statusWatch it stack
A stack is a list with one rule: last in, first out. push adds on top, pop takes the top one off, and both are methods because the list belongs to the object.
The scene below is that object in the room. stack_push(value) drops a block into the container and stack_pop() ejects the top one. The Stack methods move the same items in Python, so the two must move together or the picture disagrees with the data. Run the exercise and three blocks fall in; then the top one comes back out.
w05-stack-pushWaypoint 3: A List of Objects
A list of objects has the same shape as last week’s list of dicts, and one thing more: each item knows how to answer questions about itself. records[0].label is index in, then dot in. records["label"] is a TypeError, because a list is not a dict no matter how familiar the bracket looks.
Looping is identical to the dict version. The difference is what happens inside: record.status() instead of recalculating the percentage at the call site, which means the rule now travels with the data.
Loading is week 4’s loader with one line changed. Build a Record inside the loop instead of a dict.
class Record:
def __init__(self, label, value, limit):
self.label = label
self.value = value
self.limit = limit
def status(self):
percent = (self.value / self.limit) * 100
return "OVER LIMIT" if percent >= 100 else "OK"
records = [
Record("R-001", 87.0, 100.0),
Record("R-002", 120.0, 100.0),
]
print(type(records)) # list
print(type(records[0])) # Record
for record in records:
print(record.label, record.status())
def load_records(path):
"""Read label,value,limit lines and return a list of Records."""
records = []
with open(path) as f:
for line in f:
label, value, limit = line.strip().split(",")
records.append(Record(label, float(value), float(limit)))
return recordsWaypoint 4: Count Is a Query, Not a Side Effect
A running total outside the loop is the right default. When the count belongs to the fleet as a whole, give it a function of its own: count_over(records) asks each record for its status and counts the answers. Reading an object must not write to it.
The tempting shortcut is to increment a counter inside status(). Do not. A getter that changes state makes r.status() unsafe to call twice, and a report that prints the same record twice silently doubles the night’s incident count.
A class attribute is still useful for values every instance shares, like the warning threshold. It is not a place to accumulate per-call results.
class Record:
warning_at = 90 # shared configuration, not a running total
def __init__(self, label, value, limit):
self.label = label
self.value = value
self.limit = limit
def percent(self):
return (self.value / self.limit) * 100
def status(self):
percent = self.percent()
if percent >= 100:
return "OVER LIMIT"
if percent >= self.warning_at:
return "WARNING"
return "OK"
def count_over(records):
"""Return how many records are over their limit. Calls no setter."""
return sum(1 for record in records if record.status() == "OVER LIMIT")
fleet = [Record("R-001", 87.0, 100.0), Record("R-002", 120.0, 100.0)]
for r in fleet:
print(r.label, r.status())
print("over limit:", count_over(fleet))
print("still two after a second read:", count_over(fleet)) Watch out: The Trap
self.over_limit_count += 1inside a method reads the shared value, then writes a per-instance copy. The class attribute never moves.- Incrementing a counter inside a getter. Calling
status()twice on the same record counts the same incident twice and raises nothing. - Building a
Recordwith the arguments in the wrong order puts values on the wrong attributes and raises nothing. Match the__init__signature, or pass keywords. load_recordsthat forgetsreturngives youNone, and the crash happens three lines later in whatever tried to loop over it.records["label"]is aTypeError. A list of objects is not a dict.
The exercise below keeps one class attribute on purpose: there the counter belongs to the type, and check() is an action, not a getter. The rule is about where the write happens, not about banning shared state.
w05-class-counterThe Debrief
The fleet is now a list of objects, each one carrying its own numbers and the method that judges them, plus a query that counts the damage without changing it. srv-07.status() is a real sentence and it means something. Week 6 is where the export stops being a few hundred rows, and hand-rolled loops stop being the right tool.