I'll Be Backend
Principles · Python

Object-oriented programming: turn a script into objects

Keep each piece of data with the rules that use it, so features stop copying code.

14 steps5 chapters~5 min at 1.25×#oop#objects#classes#parallel arrays
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the Python video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–8

Payoff

Steps 9–10

Python bonus

Steps 11–12

Recap

Steps 13–14

The code: before and after

Before

names = ["Ana", "Ben", "Cara"]
plans = ["basic", "pro", "pro"]
months = [3, 12, 2]
for name, plan, count in zip(names, plans, months):
    price = 30 if plan == "pro" else 10
    total = price * count
    print(f"{name} owes {total}")

After

@dataclass
class Subscription:
    name: str
    plan: str
    months: int

    def monthly_price(self) -> int:
        return 30 if self.plan == "pro" else 10

    def total(self) -> int:
        return self.monthly_price() * self.months


subs = [
    Subscription("Ana", "basic", 3),
    Subscription("Ben", "pro", 12),
    Subscription("Cara", "pro", 2),
]
for s in subs:
    print(f"{s.name} owes {s.total()}")
for s in subs:
    if s.total() > 100:
        print(f"Reminder: {s.name}")

Transcript, step by step

Problem

Step 1: A gym bills its members Before

This gym keeps its members in three lists, one for names, one for plans and one for months. The loop zips them together by position, picks a monthly price from the plan, and multiplies it by the months. Then it prints each bill, so Ana owes 30, Ben owes 360 and Cara owes 60.

Step 2: Add a reminder Code smell

The front desk now wants a reminder for every member who owes more than 100. The quick way is a second loop that copies the price and total lines, then prints a reminder for the big bills.

Step 3: Parallel lists, a rule in a loop Code smell

In a script, the data sits in parallel lists that belong together only by position, and the rule that turns them into a bill lives inside one loop, so nothing else can use it. You can see the price rule here and copied here, the total here and copied here, and one member spread across three lists here. When the pro plan goes up to 35, two lines must change, and the one you miss keeps the old 30. So Ben's bill would say 420, while his reminder still works from 360. And every new detail about a member, like a start date, is a fourth list that must stay in the same order as the other three.

Refactor

Step 4: Refactor first Back it out

Let's take the second loop out again. We'll refactor first, so that adding the reminder becomes a small change, and then we'll add it.

Step 5: One member, one object Replace Array with Object

First, a class called Subscription holds the three things we know about a member, its name, its plan and its months. Its init method is the constructor, and it stores those three values as fields on self. The three lists become one list of three Subscription objects. The loop now goes over that list with nothing to zip, and it reads s dot plan and s dot months. One member is now one object, so its name, plan and months can't drift apart.

Step 6: Name the bill rule Extract Function

The price and total lines move into a plain function called total, which takes one subscription. The loop prints total of s, and every bill comes out the same as before.

Step 7: Move total to its data Move Method

Total only ever read the subscription's own plan and months, so it moves into Subscription as a method. Its parameter becomes self, because the fields it needs belong to the object it's called on. The loop now asks s dot total. Now the data and the rule that uses it live in one place, and that is what an object is.

Step 8: Give the price a name Extract Method

The price rule gets its own method, monthly price, which returns 30 for pro and 10 for any other plan. Total becomes monthly price times months, so the rule now has a name you can read and call.

Payoff

Step 9: Add the reminder again Payoff

Now the reminder goes in as a second loop that asks each subscription for its total. Subscription didn't change, and neither did the bill loop. Ben owes 360, so he's the one member who gets a reminder.

Step 10: One home for each rule Payoff

The price rule is in one place now, the total is in one place, and each member is one object. A pro price of 35 is one edit in monthly price, and both the bill and the reminder pick it up. A new member detail, like a start date, is one new field on one class instead of a fourth list kept in order. Each member's name, plan and months now sit in one object with the rules that read them, and that is object-oriented programming in this code.

Python bonus

Step 11: Every field named three times Python bonus

The init method names each field three times, once as a parameter, once as the attribute on self and once as the value assigned. A dataclass declares each field once, with its type, and writes that init method for you.

Step 12: Subscription as a dataclass Python bonus

Subscription becomes a dataclass with name, plan and months listed once as typed fields. The decorator writes the same init method from those fields, so the three Subscription calls still work as they are. Monthly price and total didn't change, and the loops still read s dot name.

Recap

Step 13: Where we started, where we ended Before / after

On the left, three lists hold each member by position, and the loop works out the price and the total inline. On the right, one Subscription dataclass holds a member, and it owns the monthly price and total rules. The bill loop asks each subscription for its total, and it never sees the price rule.

Step 14: When to reach for it Takeaways

Reach for objects when a few values always travel together and several parts of the code compute from them. Skip them for a one-off script that runs once, because when nothing else uses its rules, a few lines top to bottom do the job. You'll spot the need by parallel lists matched by position, and by the same calculation copied into a second loop. Python's own date class works this way, because it keeps a date together with methods like iso format.