Object-Oriented Python I
- Define a class with __init__ and explain what self refers to in every method
- Distinguish attributes (state) from methods (behavior) and instances from the class
- Implement __repr__ and __eq__ and explain what Python does without them
- Use class attributes for shared data and explain how they differ from instance attributes
- Convert a dict-based design (the tracker's tasks) into a class-based one
| Spaced-rep warm-up: due cards (watch for Day 4 aliasing cards) | 10 min |
| Concept study: ELI5 + tech, with the oop-objects visualizer | 20 min |
| Guided: blueprint, repr/eq, class-attribute labs | 45 min |
| Practice: the flashcard class | 20 min |
| Project: refactor the tracker to Task objects | 15 min |
| Quiz + flashcards | 10 min |
Builds on: Day 3 โ Functions and scope ยท Day 4 โ Dicts and aliasing ยท Day 7 โ The task tracker (today's refactor target)
An architect draws ONE blueprint for a house design; a builder then builds twenty houses from it. Each house has the same rooms in the same layout โ but each has its own address, its own paint color, its own family inside. Change the paint on house #7 and house #12 is unaffected. A class is the blueprint: it defines what every instance will have (attributes โ the rooms) and what it can do (methods โ the built-in machinery: heating, plumbing). An instance is one actual house built from that blueprint, with its own values filled in.
You've already been living in this idea: every string is an instance of the str blueprint, which is why "hi".upper() works โ upper is machinery installed in every house that blueprint builds. Today you stop renting other people's blueprints and draw your own. The word self is just the machinery asking "which house am I in right now?" โ when you call task.complete(), self IS that particular task, so the method reads and changes *that house's* rooms and nobody else's.
Classes are how Python's ecosystem speaks. Every tool you'll touch is classes: pydantic models validating API requests (Day 41), PyTorch's nn.Module holding your neural network (Day 87), every SDK client (Day 106). If self and __init__ aren't automatic, you'll be fighting syntax while trying to learn transformers. Bundling state with behavior is also how real systems stay sane โ "the Task knows how to complete itself" beats "seventeen functions that all take a task dict and hope it has the right keys." Interviews check this directly: "model X as classes" is a standard screen, and __repr__/__eq__ questions expose who has actually built objects.
One drawing, many houses โ class vs instances
step 1 / 4A class is the architect's drawing: it lists what every Task will have. No task exists yet.
Guided practice
Draw the blueprint โ your first class
15 min- Create
week-02/task_class.pywith the starter code. Read it top to bottom, then run it. - Trace the construction: when Task(1, "buy rope") runs, what does Python do first, and what is self during __init__? Write the two-step answer as a comment (allocate fresh object; run __init__ with self = that object).
- Prove instances are independent houses: complete t1 and print both tasks' done flags. Then prove methods are functions in disguise: call Task.complete(t2) โ the explicit form of t2.complete() โ and verify it worked.
- Introduce the classic bug on purpose: in rename(), drop the self. prefix (text = new_text), rerun, and observe the method silently doing nothing. Restore it. You will hit this bug for real within a week; now you'll recognize the symptom (method runs, state unchanged).
- Add a method is_overdue(self, current_day) that returns True if the task has a due_day attribute set and current_day is past it. Default due_day to None in __init__ and handle it.
Civilize your objects โ __repr__ and __eq__
15 min- In the same file, print a raw task: print([t1, t2]). Behold the <object at 0x...> junk โ that's what you're fixing.
- Add the __repr__ from the starter below. Print the list again: readable. Rule to keep: every class you ever write gets a __repr__ within five minutes of existing.
- Now equality: construct t3 = Task(1, "buy rope"); t3.complete(). Compare t1 == t3 โ False?! Default == is identity. Add the __eq__ below and compare again.
- Test the foreign-type edge: t1 == "banana" must be False, not a crash โ confirm the isinstance guard handles it.
- Think ahead in a comment: your tracker saves JSON. Add to_dict(self) returning the task as a plain dict, and a from_dict classmethod-style helper (a plain function make_task(d) is fine today). Round-trip one task: Task -> dict -> Task and verify == says they match. You just built the bridge between objects and Day 5's persistence.
Shared rooms โ class attributes and the counter
15 min- Create
counter_lab.pywith the starter code. Predict the three printed counts before running. - Explain in a comment why count lives on the CLASS: it's state about all tasks, owned by the blueprint, not any one house.
- Lookup fall-through drill: print(t1.MAX_TEXT_LEN) โ found on the class via the instance. Now set t1.MAX_TEXT_LEN = 999 and print Task.MAX_TEXT_LEN and t2.MAX_TEXT_LEN โ the class is untouched; you created an instance attribute that SHADOWS it. Connect this to Day 3's LEGB shadowing in one sentence.
- The shared-mutable trap: run section 2 and explain the spooky result โ tags defined in the class body is ONE list, aliased by every instance (Day 4's lesson in a new costume). Fix it: move tags into __init__ as self.tags = []. Verify independence.
- Close the file with the rule: constants and counters on the class; every mutable, per-object value born in __init__.
On your own
The flashcard class
20 minModel the spaced-repetition card you use every morning. Build flashcard.py from scratch.
Goal: a Flashcard class with attributes front, back, streak (consecutive correct answers, starts 0), and due_in (days until next review, starts 0). Methods: review(correct) โ if correct, streak += 1 and due_in doubles from a base of 1 (1, 2, 4, 8...); if wrong, streak resets to 0 and due_in to 1. Plus __repr__, __eq__ (front/back define identity โ streak doesn't), and to_dict. Write a short demo: three cards, a mixed run of reviews, print the deck sorted by due_in (sorted with key=lambda c: c.due_in โ a Day 12 preview).
Constraints: no globals; the doubling logic must be in the method, not the demo; t == t2 for same front/back but different streaks must be True.
Hints (only if stuck): due_in after a correct answer can be max(1, due_in * 2). That IS the gist of the SM-2-lite scheduler this program runs on you.
The tracker gets a Task class
Refactor Day 7's tracker: replace task dicts with the Task class (id, text, done, optional due_day; complete(), rename(), __repr__, __eq__, to_dict(), plus make_task(d) for loading). The command loop and JSON files must keep working exactly as before โ load turns each stored dict into a Task, save turns each Task back into a dict. This dict-object-dict bridge is the everyday shape of real systems (database row -> object -> API response). Commit in at least two steps on top of yesterday's history: "Add Task class with repr/eq/serialization" then "Refactor tracker to use Task objects". Behavior-preserving refactors with clean commits are exactly what Day 15 turns into a discipline.
Common mistakes & misconceptions
- Forgetting self. when setting state in a method (text = new_text). It creates a local that vanishes at return โ the method "runs but nothing happens." If state didn't change, hunt for a missing self.
- Forgetting self in the method signature (def complete():). The call task.complete() then explodes with "takes 0 positional arguments but 1 was given" โ that mysterious 1 IS self being passed automatically.
- Calling __init__ a constructor that returns the object. It initializes an already-created object and must return None. Task(...) does allocate-then-init; return self from __init__ is an error.
- Putting a mutable default in the class body (tags = []). One list, shared by every instance โ Day 4 aliasing in disguise. Per-instance mutables are born in __init__.
- Skipping __repr__ because "print works eventually." Debugging a list of <object at 0x...> wastes minutes every day; the five-minute __repr__ pays for itself same-day.
- Writing classes for everything now that you have them. A function that takes data and returns data is often better (Day 10 makes this judgment call explicit). Classes earn their keep when state and behavior genuinely belong together.
Q1. In t = Task(1, "rope"); t.complete() โ what is self inside complete()?
Q2. Two Tasks hold identical data. Without __eq__, t1 == t2 isโฆ
Q3. class Task: tags = [] โ then t1.tags.append("x"). What does t2.tags show?
Go deeper โ curated resources
- How "hi".upper() finds upper โ Attribute lookup walks instance -> class -> base classes (the MRO). Everything in Python is an object with a class, including classes and functions themselves โ type(type) is type. Pull that thread once; it makes Day 10's inheritance obvious.
- All three guided labs run, including both deliberate bugs (missing self., shared tags) triggered and fixed
- Flashcard class passes its own demo including equality-ignores-streak
- Tracker refactor: behavior unchanged, round-trip verified, two clean commits
- Quiz โฅ 2/3 (redo the counter lab if you missed question 3)
โ Back: self.x = value is Day 2's binding aimed at an object instead of a namespace; the shared-mutable trap is Day 4's aliasing; methods are Day 3's functions with the instance passed first; and the refactor target is Day 7's tracker, preserved by Day 8's commits.
Forward โ: Day 10 adds inheritance, dataclasses (which write __init__/__repr__/__eq__ for you โ earn them today, enjoy them tomorrow), and the composition judgment. Day 11's iterator protocol is more dunder methods; pydantic (Day 41) and nn.Module (Day 87) are this exact material, industrialized.
Unlocks: D10 Object-Oriented Python II ยท D11 Iterators & Generators ยท D15 Clean Code & Type Hints ยท D26 Linked Lists