Today's outcomes
- Distinguish a class from an instance.
- Read attributes and call methods.
- Explain how self identifies the receiving object.
Before you start: Complete Day 18 and its practical challenge.
Understand the concept
A class describes a kind of object
A class defines behavior and a namespace for a kind of object. An instance is one concrete object created from that class. Two study-session instances can share the same method definitions while holding different topics and minute counts.
Classes are useful when several operations belong with a coherent state. They are not required for every script. A pure function that calculates a mean from a list remains a good design. Introduce an object when it makes state ownership or an invariant clearer, rather than wrapping every number in a class.
The syntax class StudySession: creates the class, and StudySession() creates an instance using the class's construction behavior. Today we assign instance fields after creation so you can see the relationship directly. Day 20 moves that work into initialization, preventing half-prepared objects from reaching methods that require their fields.
Attributes belong to objects; methods operate through an instance
An attribute is accessed with a dot, such as session.minutes. A method is a function defined on the class and normally called through an instance. In session.describe(), Python supplies the receiving instance to the method's first parameter, conventionally named self.
self is not a keyword with automatic magic independent of the call; it is a parameter name. The convention makes methods easy to recognize. Within a method, self.minutes refers to a field on that receiving object. A bare minutes name would be a different local or global name, not automatically the instance's field.
Methods can observe or change state
A describe method can return a report without changing the object. A record method can update the object deliberately. Make that difference part of the interface. If record returns None, the caller should not assign its result back to the object expecting an updated instance.
State-changing methods should preserve the object's rules. A study goal cannot accept a negative practice duration under today's contract. Validate before updating so a rejected operation leaves state unchanged. Object-oriented syntax does not replace explicit validation or tests.
Worked example
class StudySession:
def describe(self):
return f"{self.topic}: {self.minutes} minutes"
def add_minutes(self, extra):
if type(extra) is not int or extra < 0:
raise ValueError("extra minutes must be a nonnegative integer")
self.minutes += extra
first = StudySession()
first.topic = "functions"
first.minutes = 30
second = StudySession()
second.topic = "lists"
second.minutes = 45
first.add_minutes(15)
assert first.minutes == 45
assert second.minutes == 45
assert first is not second
print(first.describe())
print(second.describe())
| Object | Topic | Initial minutes | After first.add_minutes(15) |
|---|---|---|---|
| first | functions | 30 | 45 |
| second | lists | 45 | 45 |
Both objects end with equal minute values but remain distinct instances. Equality of one attribute does not establish object identity. The method selects its state through self, so updating first does not automatically update second.
Calling describe before assigning topic or minutes would fail with AttributeError. That exposes the weakness of manual preparation. It is acceptable for today's transparent demonstration, but initialization tomorrow will give the class a reliable creation contract.
Guided practice
- Predict: which object's minutes change when you call second.add_minutes(10)? Trace self at the method's entry and the field after assignment.
- Modify: add an
is_full_session(target=60)method returning a Boolean without printing. Test 59, 60, and 61 and retain the inclusive threshold rule. - Reconstruct: represent the same session with a dictionary and two ordinary functions. Compare how the caller supplies state in the dictionary version versus the method version. Explain which design is clearer for this small task and why.
Avoid an assumption that using classes makes a program automatically reusable, correct, or secure. The contract, boundaries, and tests still determine those qualities.
Daily challenge
Create a StudyGoal class with methods record(minutes) and remaining(). Prepare each instance with a positive target and completed_minutes zero. record accepts a nonnegative actual integer, updates completed_minutes, and returns None. remaining returns the unfinished portion, bounded below by zero. Rejected negative or Boolean input must not change state.
| Action sequence for target 60 | Completed | Remaining |
|---|---|---|
| No recording | 0 | 60 |
| Record 30 | 30 | 30 |
| Record another 45 | 75 | 0 |
| Attempt -1 | Still 75 | Still 0 |
Over-target practice is allowed and retained in completed_minutes. The remaining amount becomes zero rather than negative. This is an explicit goal policy, different from the completed-day validation rule on Day 5.
Reference solution
Compare after attempting the challenge
class StudyGoal:
def record(self, minutes):
if type(minutes) is not int or minutes < 0:
raise ValueError("minutes must be a nonnegative integer")
self.completed_minutes += minutes
def remaining(self):
return max(0, self.target - self.completed_minutes)
goal = StudyGoal()
goal.target = 60
goal.completed_minutes = 0
assert goal.remaining() == 60
assert goal.record(30) is None
assert goal.remaining() == 30
goal.record(45)
assert goal.remaining() == 0 and goal.completed_minutes == 75
for invalid in [-1, True]:
try:
goal.record(invalid)
except ValueError:
pass
else:
raise AssertionError("Invalid recording should fail")
assert goal.completed_minutes == 75
print("Goal state:", goal.completed_minutes, goal.remaining())
The validation happens before the update, preserving the previous valid state. The observation method uses max to avoid a negative unfinished amount while retaining the actual accumulated practice.
Debug and explain
| Symptom | Investigate | Likely repair |
|---|---|---|
| AttributeError for a required field | Was the object fully prepared? | Initialize the field before method use |
| TypeError about method arguments | Does the definition include self? | Match method parameters to the call |
| Wrong object's state changes | Which instance receives the call? | Trace the receiver explicitly |
| Object name becomes None | Assigned result of a mutating method | Keep the instance; inspect method return contract |
Explain what the class defines, what each instance owns, and which methods change state. Then identify the creation weakness still present in the reference solution. That weakness gives tomorrow's initializer a concrete purpose.
Review and next step
Save the state trace and invalid-update checks. At the next review, create two independent goals and update only one. Day 20 replaces manual preparation with validated initialization and examines shared class attributes.
Sources and verification
Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.
Check your understanding
Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.
Review your practical work
Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.
Pass the knowledge check and finish the practical self-review to save completion.