Course progress Course outline 24 of 24 lessons available
Python Foundations
Data and Collections
Building Reliable Programs
Modeling with Objects
Professional Python
Advanced Python
Use a blueprint when data has rules
A dictionary is often enough for a record. A class becomes useful when one idea has state, rules for keeping that state valid, and actions that belong with it.
- Class the shared blueprint
- Instance one object made from it
- State that object's own values
class StudySession:
def __init__(self, topic, minutes):
self.topic = topic
self.minutes = minutes
first = StudySession("Python", 40)
second = StudySession("Git", 25)
print(first.topic)
print(second.minutes)
Output is Python and 25. Calling the class creates an instance and runs __init__. Initialization must return None; it prepares the new object rather than returning a replacement.
self carries one object’s backpack
An instance method declares self first. When you call it through an instance, Python supplies that instance automatically:
class StudySession:
def __init__(self, topic, minutes):
self.topic = topic
self.minutes = minutes
def label(self):
return f"{self.topic}: {self.minutes} minutes"
session = StudySession("Python", 40)
print(session.label())
print(StudySession.label(session))
Both calls print Python: 40 minutes. The first uses a bound method; the second supplies the same instance explicitly.
-
Call an instance
session.label() -
self arrives
it is this
session -
Read its state
self.topic - Return a result keep display outside
An invariant is a rule every usable instance must keep. Validate before publishing state:
class StudySession:
def __init__(self, topic, minutes=0):
topic = topic.strip()
if not topic:
raise ValueError("topic must not be empty")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
self.topic = topic
self._minutes = minutes
def add_minutes(self, amount):
if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0:
raise ValueError("amount must be a positive integer")
self._minutes += amount
return self._minutes
The transition method checks first, then changes state. A leading underscore means “non-public implementation detail” by convention; it does not enforce privacy.
Same-looking objects can still be different
first = StudySession("Python", 30)
second = StudySession("Python", 30)
alias = first
print(first is second)
print(first is alias)
print(first == second)
For this ordinary class with no custom equality, output is False, True, False. is asks whether two names point to the same object. == asks for equality, but a normal user-defined class falls back to identity until it defines value equality. Use is for identity checks such as value is None, not as a replacement for ordinary value comparison.
Mutable per-instance data must be created on self:
class StudyPlan:
def __init__(self):
self.sessions = []
If sessions = [] were in the class body, every instance would find the same shared list. A class attribute is appropriate for a shared constant, not each object’s private notebook.
Compute views and protect the notebook
A property presents inexpensive computed information with attribute syntax. A class method offers another focused construction path.
- @property a read-only computed view
- @classmethod another way to construct
- Tuple snapshot inspect without appending
class StudyPlan:
def __init__(self, sessions):
self._sessions = list(sessions)
@property
def total_minutes(self):
return sum(session.minutes for session in self._sessions)
@property
def sessions(self):
return tuple(self._sessions)
@classmethod
def from_rows(cls, rows):
return cls(StudySession(row["topic"], row["minutes"]) for row in rows)
plan.total_minutes needs no parentheses and cannot become stale because it is recalculated. cls(...) preserves the class through which from_rows() was called, while normal constructors still enforce invariants. The tuple blocks append through the returned value and captures the current outer list, but it does not recursively freeze mutable objects inside.
Build a study-plan model
Save this complete standard-library project as study_plan.py:
class StudySession:
def __init__(self, topic, minutes=0):
topic = topic.strip()
if not topic:
raise ValueError("topic must not be empty")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
self.topic = topic
self._minutes = minutes
@property
def minutes(self):
return self._minutes
def add_minutes(self, amount):
if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0:
raise ValueError("amount must be a positive integer")
self._minutes += amount
return self._minutes
class StudyPlan:
def __init__(self, title):
title = title.strip()
if not title:
raise ValueError("title must not be empty")
self.title = title
self._sessions = []
def add_session(self, session):
if not isinstance(session, StudySession):
raise TypeError("session must be a StudySession")
self._sessions.append(session)
@property
def sessions(self):
return tuple(self._sessions)
@property
def total_minutes(self):
return sum(session.minutes for session in self._sessions)
@classmethod
def from_rows(cls, title, rows):
plan = cls(title)
for row in rows:
plan.add_session(StudySession(row["topic"], row["minutes"]))
return plan
def report(self):
lines = [self.title, f"Total: {self.total_minutes} minutes"]
lines.extend(session.topic for session in self._sessions)
return "\n".join(lines)
def main():
first = StudySession(" Python ", 30)
second = StudySession("Git", 25)
assert first.add_minutes(10) == 40
assert second.minutes == 25
plan = StudyPlan("August plan")
plan.add_session(first)
snapshot = plan.sessions
plan.add_session(second)
assert len(snapshot) == 1 and len(plan.sessions) == 2
rows = [{"topic": "Files", "minutes": 20}]
assert StudyPlan.from_rows("Extra", rows).total_minutes == 20
print("Tests passed.")
print(plan.report())
if __name__ == "__main__":
main()
Output:
Tests passed.
August plan
Total: 65 minutes
Python
Git
The two sessions stay independent, construction and updates preserve valid state, and callers never receive the plan’s mutable list.
Three tiny missions and the handoff
- Reading goal. Build
ReadingGoal(title, target_pages)withrecord(pages)and a read-onlycompletedproperty. Test two independent goals and the exact target. - Shared-list detective. Put
entries = []in a class body, prove two instances share it, then move it toself.entriesin__init__and prove independence. - Text door. Add
StudySession.from_text("Python:45"). Parse there, but let the normal constructor enforce every topic and minutes invariant.
Sharp corners: __init__ cannot return another value, direct mutation can bypass invariants, is is identity rather than value equality, and returning an internal list leaks a second mutation path.
- I can distinguish a class from an instance.
- I can explain
selfand initialize valid state. - I keep mutable per-instance data on
self. - I can distinguish identity from equal-looking state.
- I can use a property and a focused class method.
- I can return a safe outer snapshot of an internal list.
- My complete model passes its assertions and prints 65 minutes.
- I completed the three tiny missions.
Next, you will choose composition for flexible “has-a” relationships, inheritance for genuine substitutable “is-a” relationships, and polymorphism for shared behavior.