Classes and Objects
Bring related state and behavior together without turning every function into a class.
By the end, you’ll be able to…
- Construct independent instances with __init__.
- Distinguish instance state from shared class attributes.
- Choose classes when state and behavior form a useful unit.
01An object combines state with operations
A dictionary is a useful record, but any caller can misspell a key or change a value without following a rule. A class gives a domain concept a named interface. For a study session, the stored state might be a topic and completed minutes; an operation adds a positive duration. The object is useful because it keeps the rule beside the state it protects.
A class is a definition; an instance is one object created from it. Calling the class invokes construction. __init__ initializes an already-created instance and normally returns None. The conventional first parameter self refers to the instance receiving the method call. Python supplies it when you call session.add_minutes(20).
02Instance attributes belong to each object
Assigning self.minutes stores state on that instance. Each StudySession therefore starts with its own counter. A name assigned directly in the class body is a class attribute, shared through attribute lookup. Class constants such as a unit label are often appropriate; a shared mutable list of every instance’s notes usually is not.
The problem becomes visible when one object appends to a class-level list and another unexpectedly sees the new element. Put per-object mutable containers inside __init__, allocating a fresh container for each construction. Do not confuse two references to one instance with two independent instances: assigning other = session creates an alias, not a clone.
03Give methods a coherent responsibility
A method should operate on the concept represented by the object. A bank account can validate a withdrawal and update its balance. It need not also know how to render an HTML page, write a CSV export, or send a network request. Keeping unrelated responsibilities separate reduces the number of reasons the class must change.
Return values instead of printing from domain methods when callers may need the result in different interfaces. A remaining_minutes method can return an integer for a command-line display, a test, or a web response. Separating decisions from presentation makes the same object useful in more than one environment.
04Choose objects deliberately
Use a class when several operations share meaningful state and must preserve rules over time. A plain function is often clearer for a one-off transformation, and a dictionary or dataclass can suit a simple record. Creating a class with no state and one method may add ceremony without clarifying the problem.
Python attributes are accessible by default. A leading underscore communicates that a name is an implementation detail; it is not an access-control barrier. Start with a small public interface and use tests to describe supported behavior. Avoid promising every internal attribute to callers, because that makes later refactoring harder.
The ideas worth keeping.
- Initialize independent state on self.
- Remember that assignment creates aliases rather than copies.
- Use classes to express coherent stateful concepts and rules.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Build a bounded counter
Create Counter(limit) starting at zero. increment() must reject changes beyond the limit and return the new count. Demonstrate that rejected changes preserve state.
Explore a worked solution
class Counter: def __init__(self, limit): if limit < 0: raise ValueError("limit cannot be negative") self.limit = limit self.count = 0 def increment(self): if self.count >= self.limit: raise ValueError("limit reached") self.count += 1 return self.countcounter = Counter(1)assert counter.increment() == 1try: counter.increment()except ValueError: passassert counter.count == 1Validation occurs before mutation. The object groups the current count, its limit, and the only supported change.
Detect shared mutable state
Create a ReadingList with add(title). Construct two lists and verify adding to one does not change the other.
Explore a worked solution
class ReadingList: def __init__(self): self.titles = [] def add(self, title): if not title.strip(): raise ValueError("title cannot be blank") self.titles.append(title.strip())first = ReadingList()second = ReadingList()first.add(" Python Tutorial ")assert first.titles == ["Python Tutorial"]assert second.titles == []assert first.titles is not second.titlesAllocating the list inside __init__ gives each instance an independent container. A class-level titles = [] would share it.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Build a bounded counter
Create Counter(limit) starting at zero. increment() must reject changes beyond the limit and return the new count. Demonstrate that rejected changes preserve state.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.