JEPA4Japan · tutorials

Classes and Objects

1,292 words 6 min read #Python

Combine state and behavior with instances, methods, and well-defined initialization.

Course progress Course outline 24 of 24 lessons available

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.

  1. Class the shared blueprint
  2. Instance one object made from it
  3. State that object's own values
The class describes a kind of thing; each instance owns its particular state.
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.

  1. Call an instance session.label()
  2. self arrives it is this session
  3. Read its state self.topic
  4. Return a result keep display outside
self identifies the instance involved in this method call.

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.

  1. @property a read-only computed view
  2. @classmethod another way to construct
  3. Tuple snapshot inspect without appending
Offer useful views and doors without leaking a mutable internal list.
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

  1. Reading goal. Build ReadingGoal(title, target_pages) with record(pages) and a read-only completed property. Test two independent goals and the exact target.
  2. Shared-list detective. Put entries = [] in a class body, prove two instances share it, then move it to self.entries in __init__ and prove independence.
  3. 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 self and 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.