Day 12 / 25 Classes & Objects 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Programming Fundamentals/Day 12

Week 3 · Algorithms & Object-Oriented Python · Day 12

Classes & Objects

The problem OOP actually solves, shown by watching a dictionary-of-data mess turn into a class.

Study time
4 hours
Reading
Think Python, Ch. 15–17
Focus
class · __init__ · self

By the end of today you can

  1. Recognise the code smell that a class fixes — before reaching for one
  2. Write a class with __init__ and methods
  3. Say what self is, and why it is a parameter rather than magic
  4. Tell instance state from class state, and predict which one you are changing
  5. Give an object a useful __repr__, and know why it beats __str__ for debugging
  6. Compose objects out of other objects
  7. Say when a dictionary is still the better answer

Today's videos

Watch each video, then work the matching sections below. Watching alone will not do it.

12A
Why Classes Exist (120 min)
The smell that a class fixes · every function taking the same first argument · class, __init__ and what self really is · instance state versus class state · the shared-mutable-attribute bug · __repr__, and why you write it first.
12B
Objects Inside Objects (120 min)
Composing objects · a Library that holds Books and asks rather than reaches · when a dictionary is still the right answer · converting Mini-Project 2's contacts into a class, behaviour unchanged.

1The problem, before the solution

Day 9 ended on a warning. Here is what it was warning about.

before.py
# every book is a dict with the same keys
b1 = {"title": "Think Python", "author": "Downey", "pages": 292, "read": 0}
b2 = {"title": "Head First Java", "author": "Sierra", "pages": 720, "read": 0}


def progress(book):
    return book["read"] / book["pages"] * 100


def read_pages(book, n):
    book["read"] = min(book["read"] + n, book["pages"])


read_pages(b1, 100)
print(f"{progress(b1):.1f}%")
34.2%

This works. Now count the warning signs.

  1. Every function takes book as its first argument. Every one.
  2. The data and the functions that understand it live in different places.
  3. Nothing stops b1["pages"] = -5, or a missing read key, or a typo — b1["athor"] fails at runtime, possibly in production.
  4. To know what a book is, you have to read every function that touches one.
The tell

When several functions all take the same thing as their first argument, that thing wants to be an object. That sentence is the entire motivation for classes, and it is worth more than any analogy about blueprints and cookie cutters.

The same program as a class

2class, __init__, self

Three pieces of syntax, one of which confuses everybody.

book.py
class Book:
    """One book, and how far through it you are."""

    def __init__(self, title, author, pages):
        """Set up a new Book. Runs once, per object."""
        self.title = title
        self.author = author
        self.pages = pages
        self.read = 0

    def progress(self):
        """Percentage read, 0-100."""
        return self.read / self.pages * 100

    def read_pages(self, n):
        """Record n more pages read, capped at the total."""
        self.read = min(self.read + n, self.pages)

    def __repr__(self):
        return f"Book({self.title!r}, {self.read}/{self.pages})"

What self actually is

self is not a keyword and it is not magic. It is an ordinary parameter that receives the object the method was called on. These two lines do the same thing:

self_demo.py
b1.read_pages(100)         # what you write
Book.read_pages(b1, 100)   # what Python actually does
Which explains two error messages

Forget self in a method definition and calling it gives TypeError: progress() takes 0 positional arguments but 1 was given — because Python passed the object and your method had nowhere to put it. And self.title versus title inside a method is the difference between this object's title and a local variable.

__repr__ — do this every time

3Instance state versus class state

The bug that only appears with two objects.

An attribute set on self belongs to one object. An attribute set on the class body belongs to all of them. That distinction is fine until a mutable value gets involved.

fixed.py
class Shelf:
    def __init__(self):
        self.books = []      # a NEW list for each object

    def add(self, title):
        self.books.append(title)


a = Shelf()
b = Shelf()
a.add("Think Python")
print(a.books, b.books)
['Think Python'] []
Class attributeInstance attribute
WrittenIn the class bodyOn self, usually in __init__
Belongs toEvery objectOne object
Good forConstants, counters, defaultsEverything else
Dangerous whenThe value is mutable
legit.py
class Book:
    MAX_TITLE = 120          # a constant — a fine class attribute
    count = 0                # deliberately shared, and immutable

    def __init__(self, title):
        self.title = title[:Book.MAX_TITLE]
        Book.count += 1      # note: Book.count, not self.count


Book("a"); Book("b"); Book("c")
print(Book.count)
3
Why Book.count += 1 and not self.count += 1

self.count += 1 reads the class attribute, adds one, and then creates a new instance attribute that shadows it — so the shared counter never moves. Reading through self works; assigning through it does not. This is the LEGB lesson from Day 7, in object form.

4Objects inside objects

The way real programs are actually built.

Objects hold other objects. That is not an advanced technique — it is the normal case, and it is how you keep any one class small.

library.py
class Library:
    """A named collection of Book objects."""

    def __init__(self, name):
        self.name = name
        self.books = []          # a list of Book objects

    def add(self, book):
        self.books.append(book)

    def total_pages(self):
        return sum(b.pages for b in self.books)

    def finished(self):
        """The books that have been fully read."""
        return [b for b in self.books if b.read == b.pages]

    def __repr__(self):
        return f"Library({self.name!r}, {len(self.books)} books)"

Notice what Library does not do: it never reaches inside a Book to calculate progress itself. It asks. That separation is what lets you change how Book works without touching Library — and it is the idea Day 13 makes explicit.

When a dictionary is still the right answer

Classes are not automatically better. Use a dict when the keys are genuinely dynamic — data from a JSON API, a config file, counts of arbitrary words. Use a class when the shape is fixed, known in advance, and has behaviour attached. A class with no methods and no validation is usually just a dict with extra typing.

Today's work: take Mini-Project 2's contact dictionaries and turn them into a Contact class with a ContactBook holding them. The rule, as always: the program's behaviour must not change.

>_Python playground

A real Python interpreter running inside your browser. Nothing is installed, nothing is uploaded, nothing can break.

scratch.pypython not loaded
Values for input(), comma separated →
Output appears here. The first run takes a few seconds while Python loads.

Exercise set

Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.

All Warm-up Core Challenge Reset day

Day 13 — Encapsulation, Properties & Composition

Invariants, @property, and deciding between has-a and is-a before you commit.

Continue →