Low-Level Design (Object-Oriented Design) Module 2 – Designing objects for change
From a script to an object model
Find the change points in a billing script, then refactor it into MenuItem, Order and Bill classes in five steps, with a golden-output check after each one.
What you will learn
- Identify the change points in a procedural script that justify objects
- Refactor a script into classes in small steps without changing its output
- Compare how easily a script and an object model can be tested
Before you start
On this page
A script that works is not a problem. A script that keeps having to change is, because every change starts with a
hunt for all the lines it touches. In a low-level design (LLD) round, and in real code, the usual answer is an object
model: classes that each own some data and the rules that guard it. This lesson takes a 60-line canteen billing
script, finds where likely changes would land, and refactors it into MenuItem, Order and Bill classes in five
small steps, checking after every step that the program prints exactly what it printed before.
You need to be able to write a Python class with __init__ and methods; the
Object-Oriented Programming track teaches that syntax, and this track builds on it.
When a script is enough
A class keeps data and the code that works on that data in one place, which is how the Python tutorial introduces classes. That earns its keep when the data and the rules that guard it change together, and when you want to check one rule without running the whole program. A short script that runs a few times and is then thrown away rarely needs either: its whole life is one path through the code, and reading it from top to bottom is the simplest way to understand it.
Three signs that a script has outgrown that stage:
- Several functions read and change the same global variables.
- One change request means edits in several distant places, or two unrelated requests edit the same function.
- You want to test one rule, such as a discount, and the only way in is the whole program.
The script
An office canteen runs this script at its till. Each line of the input is a menu code and a quantity, and a blank line ends an order. The script prints a bill for every order, then the day’s takings:
"""Canteen billing, written as a quick script.
Reads orders from standard input, one item per line ("TEA 2") and a blank line
between orders, then prints a bill for each order and the day's takings.
Prices are whole numbers of paise (100 paise make one rupee).
"""
import sys
MENU = {
"TEA": ("Masala tea", 1500),
"COF": ("Filter coffee", 2000),
"SAM": ("Samosa", 1800),
"DOS": ("Masala dosa", 6000),
"THA": ("Veg thali", 12000),
}
bill_no = 0
day_total = 0
lines = []
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def print_bill():
global bill_no, day_total
bill_no += 1
print(f"Bill {bill_no}")
subtotal = 0
for name, price, qty in lines:
subtotal += price * qty
print(f" {name:<14}{qty:>3} x {money(price):>6}{money(price * qty):>9}")
print(f" {'Subtotal':<26}{money(subtotal):>9}")
discount = 0
if subtotal >= 50000:
discount = subtotal // 10
print(f" {'Discount 10%':<26}{'-' + money(discount):>9}")
tax = ((subtotal - discount) * 5 + 50) // 100
print(f" {'Tax 5%':<26}{money(tax):>9}")
total = subtotal - discount + tax
print(f" {'Total':<26}{money(total):>9}")
print()
day_total += total
lines.clear()
for row in sys.stdin:
row = row.strip()
if not row:
if lines:
print_bill()
continue
code, qty = row.split()
name, price = MENU[code]
lines.append((name, price, int(qty)))
if lines:
print_bill()
print(f"Day: {bill_no} bills, {money(day_total)} taken") Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 canteen_script.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Prices are whole numbers of paise, the hundredth part of a rupee, so every amount is an exact integer. A binary float
cannot hold most decimal fractions exactly (the Python tutorial’s chapter on floating-point arithmetic shows why), and a
bill has to add up to the last paisa. money() turns paise into text such as 103.95.
Now look at what print_bill() does: it numbers the bill, works out every amount, prints every line, adds the total
to the day’s takings and empties the list of lines. That is five jobs, tied together by three global variables
(bill_no, day_total and lines), and the loop at the bottom of the file changes lines too.
Find the change points
A change point is a place you would have to edit for a change that is likely to be asked for. Write down a few likely requests and find where each one lands:
| A likely request | Where it lands in the script |
|---|---|
| A new dish, or a new price | The MENU dictionary, and nothing else |
| Every quantity must be at least 1 | The loop at the bottom, beside lines.append(...) |
| 15% off from 400.00, instead of 10% from 500.00 | Three places in print_bill(): the if, subtotal // 10 and the label "Discount 10%" |
| The bill as JSON for a self-service kiosk | Every print() in print_bill(), mixed in with the calculations |
| Orders as CSV instead of lines of text | The loop at the bottom, which also fills the global lines |
| A test of the tax rounding on its own | Nowhere: the only way in is a whole order on standard input |
The first request already has a home: the menu is data, kept apart from the code, so it needs no class. The next four
land in print_bill() or in the loop, the two pieces of code that change the globals, and a request about money (the
discount) lands in the same function as a request about looks (the JSON). The last one has nowhere to land at all.
That is the signal that objects answer. Robert C. Martin gives his single responsibility principle this wording:
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
He traces the idea back to David Parnas, who argued in 1972 that a program should be divided by the design decisions that are likely to change, each one hidden inside a module of its own. The steps below do that on a small scale; a later module of this track covers the principle itself.
Record the golden output first
Martin Fowler defines a refactoring as
a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
For a program that reads input and prints text, its observable behaviour is what it prints, so the safety net is simple. Run the original on a few inputs and keep what it prints as the golden output. After every step, run the new version on the same inputs and compare: any difference means the step changed behaviour.
Choose the inputs so that every rule and every edge runs at least once:
- two orders: the sample above, a small order without a discount and a large one with it;
- no orders: an empty input, which still prints the day’s line;
- exactly 500.00: the boundary where the discount starts;
- unknown code:
XYZis not on the menu, so the script stops with aKeyError, and every step must stop the same way.
Five small steps
Each step changes one thing, and the comparison runs after each one. When it reports a difference, find the mistake or undo the step before you go on.
Step 1: stop changing global variables
Start with the global variables, because they are what makes a script hard to test. A common half-way stage is to cut the code into functions but leave the data in a global list. That looks fine until two pieces of code use the functions one after the other in the same program, as two tests in one test run do:
"""A half-done refactoring: functions exist, but the lines still live in a global."""
lines = [] # one list, shared by every caller in this program
def add(name, price, quantity):
lines.append((name, price * quantity))
def subtotal():
return sum(amount for name, amount in lines)
# Two independent checks, the way two tests would make them:
add("Masala tea", 1500, 2)
print("first order:", subtotal())
add("Samosa", 1800, 1)
print("second order:", subtotal()) # meant to be 1800 Output
first order: 3000 second order: 4800
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shared_lines.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The second order should come to 1800 paise, but the list still holds the first order’s tea. Python runs a module’s
top-level code only the first time the module is imported, so all the tests of one run share that one list, and a test
can pass on its own yet fail after another test. Step 1 therefore moves the loop into a main() function, which owns
the bill number, the day’s total and the lines, and print_bill() gets what it needs as arguments and returns the
bill’s total. MENU stays at module level: nothing writes to it, so sharing it does no harm. The
if __name__ == "__main__": test at the end calls main() only when the file runs as a program, so a test can import
the file without a bill being printed.
Steps 2 to 5: the input, then one class at a time
- Read the input separately.
read_orders(rows)turns lines of text into orders, each a list of(code, quantity)pairs. It takes anything that yields lines of text, such as a list or an open file, so a test can pass a list instead of standard input. - Make menu items values. A
MenuItemdataclass withfrozen=Truereplaces the(name, price)tuple. Code saysitem.priceinstead of remembering which position holds the price, assigning to a field raises an exception, and two items with the same fields are equal, as two equal numbers are.Menu.find(code)becomes the one place that looks an item up; it still raisesKeyErrorfor an unknown code, so the error does not change. - Give orders a class.
Order.add(item, quantity)is the one public way to add a line, andOrder.lineshands out a tuple that callers cannot change. EachOrderLineknows its own amount, so price times quantity is written once. - Let a bill work out its amounts.
Billcomputes the subtotal, discount, tax and total once, from an order, andprint_bill(bill)only formats them. The rules are named constants at the top of the file and the labels are built from them, so the printed “10%” can no longer drift away from the rule.
Each tab holds the whole program after that step, with what it printed for the two sample orders:
1 Local state · canteen/step1_local_state.py
"""Canteen billing, step 1: no global variable changes any more.
main() owns the bill number, the day's total and the current lines; print_bill()
is given what it needs and returns the bill's total instead of updating globals.
"""
import sys
MENU = {
"TEA": ("Masala tea", 1500),
"COF": ("Filter coffee", 2000),
"SAM": ("Samosa", 1800),
"DOS": ("Masala dosa", 6000),
"THA": ("Veg thali", 12000),
}
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def print_bill(bill_no, lines):
"""Prints one bill and returns its total."""
print(f"Bill {bill_no}")
subtotal = 0
for name, price, qty in lines:
subtotal += price * qty
print(f" {name:<14}{qty:>3} x {money(price):>6}{money(price * qty):>9}")
print(f" {'Subtotal':<26}{money(subtotal):>9}")
discount = 0
if subtotal >= 50000:
discount = subtotal // 10
print(f" {'Discount 10%':<26}{'-' + money(discount):>9}")
tax = ((subtotal - discount) * 5 + 50) // 100
print(f" {'Tax 5%':<26}{money(tax):>9}")
total = subtotal - discount + tax
print(f" {'Total':<26}{money(total):>9}")
print()
return total
def main():
bill_no = 0
day_total = 0
lines = []
for row in sys.stdin:
row = row.strip()
if not row:
if lines:
bill_no += 1
day_total += print_bill(bill_no, lines)
lines = []
continue
code, qty = row.split()
name, price = MENU[code]
lines.append((name, price, int(qty)))
if lines:
bill_no += 1
day_total += print_bill(bill_no, lines)
print(f"Day: {bill_no} bills, {money(day_total)} taken")
if __name__ == "__main__":
main() Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 step1_local_state.py
2 Input · canteen/step2_read_orders.py
"""Canteen billing, step 2: reading the input is a function of its own.
read_orders() turns lines of text into orders, so the billing code no longer
cares where an order comes from: standard input, a file or a test.
"""
import sys
MENU = {
"TEA": ("Masala tea", 1500),
"COF": ("Filter coffee", 2000),
"SAM": ("Samosa", 1800),
"DOS": ("Masala dosa", 6000),
"THA": ("Veg thali", 12000),
}
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def read_orders(rows):
"""Yields each order as a list of (code, quantity); a blank line ends an order."""
order = []
for row in rows:
row = row.strip()
if row:
code, qty = row.split()
order.append((code, int(qty)))
elif order:
yield order
order = []
if order:
yield order
def print_bill(bill_no, lines):
"""Prints one bill and returns its total."""
print(f"Bill {bill_no}")
subtotal = 0
for name, price, qty in lines:
subtotal += price * qty
print(f" {name:<14}{qty:>3} x {money(price):>6}{money(price * qty):>9}")
print(f" {'Subtotal':<26}{money(subtotal):>9}")
discount = 0
if subtotal >= 50000:
discount = subtotal // 10
print(f" {'Discount 10%':<26}{'-' + money(discount):>9}")
tax = ((subtotal - discount) * 5 + 50) // 100
print(f" {'Tax 5%':<26}{money(tax):>9}")
total = subtotal - discount + tax
print(f" {'Total':<26}{money(total):>9}")
print()
return total
def main():
bill_no = 0
day_total = 0
for order in read_orders(sys.stdin):
lines = [(*MENU[code], qty) for code, qty in order]
bill_no += 1
day_total += print_bill(bill_no, lines)
print(f"Day: {bill_no} bills, {money(day_total)} taken")
if __name__ == "__main__":
main() Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 step2_read_orders.py
3 MenuItem · canteen/step3_menu_item.py
"""Canteen billing, step 3: menu items are values, and the menu finds them.
A MenuItem replaces the (name, price) tuple, so code reads item.price instead of
remembering which position holds the price. Menu.find() is the one place that
looks an item up by its code.
"""
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class MenuItem:
code: str
name: str
price: int # paise
class Menu:
def __init__(self, items):
self._items = {item.code: item for item in items}
def find(self, code):
"""The item with this code (a KeyError for an unknown code, as before)."""
return self._items[code]
MENU = Menu([
MenuItem("TEA", "Masala tea", 1500),
MenuItem("COF", "Filter coffee", 2000),
MenuItem("SAM", "Samosa", 1800),
MenuItem("DOS", "Masala dosa", 6000),
MenuItem("THA", "Veg thali", 12000),
])
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def read_orders(rows):
"""Yields each order as a list of (code, quantity); a blank line ends an order."""
order = []
for row in rows:
row = row.strip()
if row:
code, qty = row.split()
order.append((code, int(qty)))
elif order:
yield order
order = []
if order:
yield order
def print_bill(bill_no, lines):
"""Prints one bill from (MenuItem, quantity) pairs and returns its total."""
print(f"Bill {bill_no}")
subtotal = 0
for item, qty in lines:
subtotal += item.price * qty
print(f" {item.name:<14}{qty:>3} x "
f"{money(item.price):>6}{money(item.price * qty):>9}")
print(f" {'Subtotal':<26}{money(subtotal):>9}")
discount = 0
if subtotal >= 50000:
discount = subtotal // 10
print(f" {'Discount 10%':<26}{'-' + money(discount):>9}")
tax = ((subtotal - discount) * 5 + 50) // 100
print(f" {'Tax 5%':<26}{money(tax):>9}")
total = subtotal - discount + tax
print(f" {'Total':<26}{money(total):>9}")
print()
return total
def main():
bill_no = 0
day_total = 0
for order in read_orders(sys.stdin):
lines = [(MENU.find(code), qty) for code, qty in order]
bill_no += 1
day_total += print_bill(bill_no, lines)
print(f"Day: {bill_no} bills, {money(day_total)} taken")
if __name__ == "__main__":
main() Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 step3_menu_item.py
4 Order · canteen/step4_order.py
"""Canteen billing, step 4: an Order holds its lines and adds them up.
Callers add items through Order.add() and read the lines back; they cannot
change the list behind the order's back. A line knows its own amount.
"""
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class MenuItem:
code: str
name: str
price: int # paise
class Menu:
def __init__(self, items):
self._items = {item.code: item for item in items}
def find(self, code):
"""The item with this code (a KeyError for an unknown code, as before)."""
return self._items[code]
@dataclass(frozen=True)
class OrderLine:
item: MenuItem
quantity: int
@property
def amount(self):
return self.item.price * self.quantity
class Order:
"""The lines of one order, in the order they were added."""
def __init__(self):
self._lines = []
def add(self, item, quantity):
self._lines.append(OrderLine(item, quantity))
@property
def lines(self):
return tuple(self._lines)
def subtotal(self):
return sum(line.amount for line in self._lines)
MENU = Menu([
MenuItem("TEA", "Masala tea", 1500),
MenuItem("COF", "Filter coffee", 2000),
MenuItem("SAM", "Samosa", 1800),
MenuItem("DOS", "Masala dosa", 6000),
MenuItem("THA", "Veg thali", 12000),
])
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def read_orders(rows):
"""Yields each order as a list of (code, quantity); a blank line ends an order."""
order = []
for row in rows:
row = row.strip()
if row:
code, qty = row.split()
order.append((code, int(qty)))
elif order:
yield order
order = []
if order:
yield order
def print_bill(bill_no, order):
"""Prints one bill and returns its total."""
print(f"Bill {bill_no}")
for line in order.lines:
print(f" {line.item.name:<14}{line.quantity:>3} x "
f"{money(line.item.price):>6}{money(line.amount):>9}")
subtotal = order.subtotal()
print(f" {'Subtotal':<26}{money(subtotal):>9}")
discount = 0
if subtotal >= 50000:
discount = subtotal // 10
print(f" {'Discount 10%':<26}{'-' + money(discount):>9}")
tax = ((subtotal - discount) * 5 + 50) // 100
print(f" {'Tax 5%':<26}{money(tax):>9}")
total = subtotal - discount + tax
print(f" {'Total':<26}{money(total):>9}")
print()
return total
def main():
bill_no = 0
day_total = 0
for codes in read_orders(sys.stdin):
order = Order()
for code, qty in codes:
order.add(MENU.find(code), qty)
bill_no += 1
day_total += print_bill(bill_no, order)
print(f"Day: {bill_no} bills, {money(day_total)} taken")
if __name__ == "__main__":
main() Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 step4_order.py
5 Bill · canteen/step5_bill.py
"""Canteen billing, step 5: a Bill works out every amount; print_bill() only formats.
The pricing rules are named once, at the top, and live in Bill. Changing how a
bill looks no longer risks changing what it costs, and the other way round.
"""
import sys
from dataclasses import dataclass
DISCOUNT_FROM = 50000 # paise: orders of 500.00 or more get the discount
DISCOUNT_PERCENT = 10
TAX_PERCENT = 5
@dataclass(frozen=True)
class MenuItem:
code: str
name: str
price: int # paise
class Menu:
def __init__(self, items):
self._items = {item.code: item for item in items}
def find(self, code):
"""The item with this code (a KeyError for an unknown code, as before)."""
return self._items[code]
@dataclass(frozen=True)
class OrderLine:
item: MenuItem
quantity: int
@property
def amount(self):
return self.item.price * self.quantity
class Order:
"""The lines of one order, in the order they were added."""
def __init__(self):
self._lines = []
def add(self, item, quantity):
self._lines.append(OrderLine(item, quantity))
@property
def lines(self):
return tuple(self._lines)
def subtotal(self):
return sum(line.amount for line in self._lines)
class Bill:
"""Every amount of one bill, worked out once from its order."""
def __init__(self, number, order):
self.number = number
self.lines = order.lines
self.subtotal = order.subtotal()
self.discount = 0
if self.subtotal >= DISCOUNT_FROM:
self.discount = self.subtotal * DISCOUNT_PERCENT // 100
# Half a paisa or more rounds up.
self.tax = ((self.subtotal - self.discount) * TAX_PERCENT + 50) // 100
self.total = self.subtotal - self.discount + self.tax
MENU = Menu([
MenuItem("TEA", "Masala tea", 1500),
MenuItem("COF", "Filter coffee", 2000),
MenuItem("SAM", "Samosa", 1800),
MenuItem("DOS", "Masala dosa", 6000),
MenuItem("THA", "Veg thali", 12000),
])
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def read_orders(rows):
"""Yields each order as a list of (code, quantity); a blank line ends an order."""
order = []
for row in rows:
row = row.strip()
if row:
code, qty = row.split()
order.append((code, int(qty)))
elif order:
yield order
order = []
if order:
yield order
def print_bill(bill):
"""Prints a bill: what it looks like is decided here, what it costs in Bill."""
print(f"Bill {bill.number}")
for line in bill.lines:
print(f" {line.item.name:<14}{line.quantity:>3} x "
f"{money(line.item.price):>6}{money(line.amount):>9}")
print(f" {'Subtotal':<26}{money(bill.subtotal):>9}")
if bill.discount:
print(f" {f'Discount {DISCOUNT_PERCENT}%':<26}{'-' + money(bill.discount):>9}")
print(f" {f'Tax {TAX_PERCENT}%':<26}{money(bill.tax):>9}")
print(f" {'Total':<26}{money(bill.total):>9}")
print()
def main():
bills = 0
day_total = 0
for codes in read_orders(sys.stdin):
order = Order()
for code, qty in codes:
order.add(MENU.find(code), qty)
bills += 1
bill = Bill(bills, order)
print_bill(bill)
day_total += bill.total
print(f"Day: {bills} bills, {money(day_total)} taken")
if __name__ == "__main__":
main() Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 step5_bill.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Not everything became a class. read_orders(), money() and print_bill() keep nothing between calls, so plain
functions suit them. Each class is there because it owns data that has rules: the menu’s items, an order’s lines, a
bill’s amounts. The diagram shows where the requests from the table land now.
Where each likely change lands after the refactoring
Text description of the diagram
The diagram has two columns. On the left are five change requests a canteen could make, drawn as pages; on the right are parts of the step 5 code. Each request has one arrow, to the one part it changes.
- A new dish or a new price changes the menu's data: the MenuItem values listed in MENU.
- A rule for quantities, such as "at least 1", goes in Order, which adds every line.
- A new discount or tax rule changes Bill, which works out every amount.
- A new layout, or JSON output for a kiosk, changes print_bill(), or adds a second function beside it, without touching the amounts.
- Orders in a new format, such as CSV, change only read_orders(), which parses the input.
In the original script only the first request had a place of its own, the MENU dictionary. The other four land in print_bill() or in the loop that reads standard input, and both of those change the script's global variables.
Version note
None of this needs a recent Python: frozen dataclasses have been in the standard library since Python 3.7, when the dataclasses module was added.
Check every step
check_steps.py is the comparison. It runs the original script on the four inputs to get the golden output, then runs
every step on the same inputs and reports the first line that differs. runpy.run_path() runs a file in a fresh
module namespace each time, and its run_name argument sets __name__ to "__main__", so the file runs as a
program; contextlib.redirect_stdout() collects what it prints, and sys.stdin is swapped for the input text while
it runs:
"""The safety net: run every step on the same inputs and compare with the script.
The original script's output for each input is the golden output. A step passes
when it prints exactly the same text and stops with the same error, if any.
"""
import contextlib
import io
import runpy
import sys
STEPS = [
"step1_local_state.py",
"step2_read_orders.py",
"step3_menu_item.py",
"step4_order.py",
"step5_bill.py",
"step4_merged.py",
]
def run(program, text):
"""What the program prints for this standard input, and the error it stops with."""
printed = io.StringIO()
saved, sys.stdin = sys.stdin, io.StringIO(text)
error = None
try:
with contextlib.redirect_stdout(printed):
runpy.run_path(program, run_name="__main__")
except Exception as exc:
error = f"{type(exc).__name__}: {exc}"
finally:
sys.stdin = saved
return printed.getvalue().splitlines(), error
def first_difference(expected, actual):
"""The first line number where two outputs differ, with both lines."""
for number, (want, got) in enumerate(zip(expected, actual), start=1):
if want != got:
return number, want, got
number = min(len(expected), len(actual)) + 1
want = expected[number - 1] if number <= len(expected) else "(end)"
got = actual[number - 1] if number <= len(actual) else "(end)"
return number, want, got
def main():
with open("orders.txt", encoding="utf-8") as f:
inputs = {
"two orders": f.read(),
"no orders": "",
"exactly 500.00": "THA 4\nCOF 1\n", # the discount starts here
"unknown code": "TEA 1\nXYZ 2\n",
}
golden = {name: run("canteen_script.py", text) for name, text in inputs.items()}
print("Golden output of canteen_script.py:")
for name, (lines, error) in golden.items():
end = f"stops with {error}" if error else f"ends with {lines[-1]!r}"
print(f" {name!r:<17}{end}")
for step in STEPS:
for name, text in inputs.items():
lines, error = run(step, text)
want_lines, want_error = golden[name]
if error != want_error:
print(f"{step:<21} differs for {name!r}: "
f"{error} instead of {want_error}")
break
if lines != want_lines:
number, want, got = first_difference(want_lines, lines)
print(f"{step:<21} differs for {name!r} at line {number}:")
print(f" expected {want!r}")
print(f" got {got!r}")
break
else:
print(f"{step:<21} same on all {len(inputs)} inputs")
if __name__ == "__main__":
main() Output
Golden output of canteen_script.py: 'two orders' ends with 'Day: 2 bills, 652.05 taken' 'no orders' ends with 'Day: 0 bills, 0.00 taken' 'exactly 500.00' ends with 'Day: 1 bills, 472.50 taken' 'unknown code' stops with KeyError: 'XYZ' step1_local_state.py same on all 4 inputs step2_read_orders.py same on all 4 inputs step3_menu_item.py same on all 4 inputs step4_order.py same on all 4 inputs step5_bill.py same on all 4 inputs step4_merged.py differs for 'two orders' at line 2: expected ' Masala tea 2 x 15.00 30.00' got ' Masala tea 3 x 15.00 45.00'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_steps.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The five steps print the same on all four inputs. The last entry in the report is a step that does not, and it is worth a closer look.
Text Diff When a step prints something else, paste the golden output and the new output side by side to see every changed line, not only the first.A common mistake: improving while you refactor
step4_merged.py takes step 4 and makes one small improvement, the kind that slips into a refactoring: its order
merges a repeated item into the line it already has.
"""Canteen billing, step 4 with a slip: the order merges repeated items.
This file reuses step 4 and changes one thing. Adding an item that is already in
the order adds to that line instead of making a new one. The totals stay the
same, but the printed bill does not, so this is not a refactoring.
"""
import sys
from step4_order import MENU, Order, OrderLine, money, print_bill, read_orders
class MergingOrder(Order):
def add(self, item, quantity):
for i, line in enumerate(self._lines):
if line.item == item: # "tidier": one line per item
self._lines[i] = OrderLine(item, line.quantity + quantity)
return
self._lines.append(OrderLine(item, quantity))
def main():
bill_no = 0
day_total = 0
for codes in read_orders(sys.stdin):
order = MergingOrder()
for code, qty in codes:
order.add(MENU.find(code), qty)
bill_no += 1
day_total += print_bill(bill_no, order)
print(f"Day: {bill_no} bills, {money(day_total)} taken")
if __name__ == "__main__":
main() Input (standard input)
TEA 2 SAM 3 TEA 1 THA 4 COF 2 DOS 1
Output
Bill 1 Masala tea 3 x 15.00 45.00 Samosa 3 x 18.00 54.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 step4_merged.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The totals are the same, but the bill is not: two tea lines became one, and the comparison stops at line 2. Merging may
well be what the canteen wants, but it changes behaviour, not structure. Finish the refactoring, then make that change
on its own, read the new output carefully and record it as the new golden output. The same goes for the small bug the
golden output has kept: for a single order the script prints Day: 1 bills. Note it, and fix it after the refactoring,
as a change of its own.
What the comparison cannot see
The golden output proves “same output” only for the inputs it was made from. A line with an unknown code and a bad quantity was not one of them:
"""An input that the golden set does not contain: an unknown code and a bad quantity."""
from check_steps import run
PROGRAMS = [
"canteen_script.py",
"step1_local_state.py",
"step2_read_orders.py",
"step5_bill.py",
]
for program in PROGRAMS:
lines, error = run(program, "XYZ two\n")
print(f"{program:<21} {error}") Output
canteen_script.py KeyError: 'XYZ' step1_local_state.py KeyError: 'XYZ' step2_read_orders.py ValueError: invalid literal for int() with base 10: 'two' step5_bill.py ValueError: invalid literal for int() with base 10: 'two'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 untried_input.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The script and step 1 look the code up first and stop with a KeyError. From step 2 on, read_orders() converts the
quantity before anything is looked up, so they stop with a ValueError. Both reject the line, so the difference does no
harm here, but nobody would have noticed it without trying that input. When you find such a case, add it to the inputs
and decide whether the difference matters.
Compare the testability
With the script, every test is a whole run: an order goes in on standard input and the test searches the printed text. With the object model, each rule can be checked on its own, in a line or two:
"""Tests of the object model from step 5: each rule is checked on its own,
without standard input, without printing and without a whole bill to compare."""
from step5_bill import Bill, Menu, MenuItem, Order
TEA = MenuItem("TEA", "Masala tea", 1500)
COFFEE = MenuItem("COF", "Filter coffee", 2000)
SAMOSA = MenuItem("SAM", "Samosa", 1800)
THALI = MenuItem("THA", "Veg thali", 12000)
def order_of(*lines):
order = Order()
for item, quantity in lines:
order.add(item, quantity)
return order
def test_no_discount_below_500():
assert Bill(1, order_of((THALI, 4), (TEA, 1))).discount == 0 # 495.00
def test_discount_from_exactly_500():
assert Bill(1, order_of((THALI, 4), (COFFEE, 1))).discount == 5000
def test_tax_rounds_half_a_paisa_up():
# 513.00 - 51.30 = 461.70, and 5% of that is 23.085
assert Bill(1, order_of((THALI, 4), (SAMOSA, 1), (TEA, 1))).tax == 2309
def test_orders_do_not_share_lines():
first = order_of((TEA, 2))
second = Order()
assert second.lines == ()
assert first.subtotal() == 3000
def test_items_with_the_same_values_are_equal():
assert MenuItem("TEA", "Masala tea", 1500) == TEA
def test_unknown_code_is_still_a_key_error():
try:
Menu([TEA]).find("XYZ")
except KeyError:
return
raise AssertionError("expected a KeyError")
if __name__ == "__main__":
for name, test in list(globals().items()):
if name.startswith("test_"):
test()
print("passed", name) Output
passed test_no_discount_below_500 passed test_discount_from_exactly_500 passed test_tax_rounds_half_a_paisa_up passed test_orders_do_not_share_lines passed test_items_with_the_same_values_are_equal passed test_unknown_code_is_still_a_key_error
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_bill.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The tax test shows the difference best. Every price on this menu is a whole number of rupees, so half a paisa of tax
appears only after the discount, and the test needs an order of more than 500.00. Against the script you would type
such an order, run the whole program and look for 23.09 in the bill; here it is one Bill and one comparison. The
tests are plain functions that use assert, and the loop at the bottom runs each one and prints its name.
| Criterion | The script | The objects |
|---|---|---|
| Tests call | The whole program, through standard input and printed text | Each class and function, with values made in the test |
| One rule | Build an order that triggers it, then compare the whole bill | Make a Bill and compare its discount or its tax |
| Shared state | Three globals, so each test runs the script afresh | None that changes: each Order and Bill is new |
| New layout | Breaks every golden output | Breaks only tests of printed text |
| When to choose | A short program, run and checked as a whole | Code that keeps changing, with rules to test one at a time |
Classes are not the only tool
A class is one way to give data a single home. A module of plain functions over data that never changes is another,
and it is often the right first step. The JavaScript version below uses both. The menu is a module of frozen data and
one function; Order and Bill are classes, because an order guards its lines while they are added and a bill works
out its amounts once and then freezes itself. It reads the same two orders and prints the same bills as the Python
steps:
canteen-js/main.mjs
// The canteen bills in JavaScript: the same orders and the same output as the
// Python steps. main.mjs only reads orders, builds bills and prints them.
import { findItem } from './menu.mjs';
import { Order } from './order.mjs';
import { Bill, money, renderBill } from './bill.mjs';
const ORDERS = `TEA 2
SAM 3
TEA 1
THA 4
COF 2
DOS 1
`;
// Each order as a list of [code, quantity]; a blank line ends an order.
function* readOrders(text) {
let order = [];
for (const row of text.split('\n').map((r) => r.trim())) {
if (row) {
const [code, quantity] = row.split(/\s+/);
order.push([code, Number(quantity)]);
} else if (order.length) {
yield order;
order = [];
}
}
if (order.length) yield order;
}
let bills = 0;
let dayTotal = 0;
for (const codes of readOrders(ORDERS)) {
const order = new Order();
for (const [code, quantity] of codes) order.add(findItem(code), quantity);
bills += 1;
const bill = new Bill(bills, order);
console.log(renderBill(bill));
dayTotal += bill.total;
}
console.log(`Day: ${bills} bills, ${money(dayTotal)} taken`); Output
Bill 1 Masala tea 2 x 15.00 30.00 Samosa 3 x 18.00 54.00 Masala tea 1 x 15.00 15.00 Subtotal 99.00 Tax 5% 4.95 Total 103.95 Bill 2 Veg thali 4 x 120.00 480.00 Filter coffee 2 x 20.00 40.00 Masala dosa 1 x 60.00 60.00 Subtotal 580.00 Discount 10% -58.00 Tax 5% 26.10 Total 548.10 Day: 2 bills, 652.05 taken
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.mjs
canteen-js/menu.mjs
// The menu as a module of data and functions: frozen objects that nothing can
// change, and one function that finds an item by its code. Prices are in paise.
const ITEMS = [
{ code: 'TEA', name: 'Masala tea', price: 1500 },
{ code: 'COF', name: 'Filter coffee', price: 2000 },
{ code: 'SAM', name: 'Samosa', price: 1800 },
{ code: 'DOS', name: 'Masala dosa', price: 6000 },
{ code: 'THA', name: 'Veg thali', price: 12000 },
];
export const MENU = Object.freeze(
Object.fromEntries(ITEMS.map((item) => [item.code, Object.freeze(item)])),
);
export function findItem(code) {
// Object.hasOwn, not MENU[code] alone: MENU.toString exists on every object.
if (!Object.hasOwn(MENU, code)) throw new Error(`no menu item with code ${code}`);
return MENU[code];
} canteen-js/order.mjs
// An order keeps its lines in a private field: callers add items and read a
// copy of the lines, but cannot change the order behind its back.
export class Order {
#lines = [];
add(item, quantity) {
this.#lines.push(Object.freeze({ item, quantity, amount: item.price * quantity }));
}
get lines() {
return [...this.#lines];
}
subtotal() {
return this.#lines.reduce((sum, line) => sum + line.amount, 0);
}
} canteen-js/bill.mjs
// A Bill works out every amount once; renderBill() only turns a bill into text.
export const DISCOUNT_FROM = 50000; // paise: orders of 500.00 or more get the discount
export const DISCOUNT_PERCENT = 10;
export const TAX_PERCENT = 5;
export class Bill {
constructor(number, order) {
this.number = number;
this.lines = Object.freeze(order.lines); // a copy: freezing it leaves the order alone
this.subtotal = order.subtotal();
this.discount = 0;
if (this.subtotal >= DISCOUNT_FROM) {
this.discount = Math.floor((this.subtotal * DISCOUNT_PERCENT) / 100);
}
// Half a paisa or more rounds up.
this.tax = Math.floor(((this.subtotal - this.discount) * TAX_PERCENT + 50) / 100);
this.total = this.subtotal - this.discount + this.tax;
Object.freeze(this);
}
}
export function money(paise) {
return `${Math.floor(paise / 100)}.${String(paise % 100).padStart(2, '0')}`;
}
export function renderBill(bill) {
const row = (label, amount) => ` ${label.padEnd(26)}${amount.padStart(9)}`;
const text = [`Bill ${bill.number}`];
for (const line of bill.lines) {
const name = line.item.name.padEnd(14);
const quantity = String(line.quantity).padStart(3);
const price = money(line.item.price).padStart(6);
text.push(` ${name}${quantity} x ${price}${money(line.amount).padStart(9)}`);
}
text.push(row('Subtotal', money(bill.subtotal)));
if (bill.discount) {
text.push(row(`Discount ${DISCOUNT_PERCENT}%`, `-${money(bill.discount)}`));
}
text.push(row(`Tax ${TAX_PERCENT}%`, money(bill.tax)));
text.push(row('Total', money(bill.total)));
return `${text.join('\n')}\n`;
} Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- A module that imports
MENUcannot give that name a new value: the language makes every imported name an immutable binding, so an assignment throws aTypeError. The object behind the name could still be changed, though, andObject.freeze()stops that. Freezing is shallow, somenu.mjsfreezes every item as well as the menu that holds them, and aBillfreezes its copy of the lines as well as itself. #linesis a private field: code outsideOrdercannot read or change it, and even naming it there is aSyntaxError. Python’s leading underscore in_linesis only a convention: it tells other programmers that the name is not part of the public interface, and Python does not enforce it.findItem()checks the code withObject.hasOwn(), because a plain lookup such asMENU["toString"]would find a method that every object inherits.
Version note
Private fields such as #lines, and Object.hasOwn(), are part of ECMAScript 2022. Node.js 24 and the QuickJS
engine that runs these files in the browser both support them.
Go has no class keyword at all. Its FAQ answers the question “Is Go an object-oriented language?” with “Yes and no”:
types have methods, but there is no type hierarchy. In Go the same design would be a package with an order type, its
methods and plain functions for the rest.
Key takeaways
- Objects pay off where data and the rules that guard it change together; a short script that runs a few times may not need them.
- Find the change points first: list likely requests and see where each one lands. Requests that land in many places, or unrelated requests that land in the same function, mark the code to restructure.
- Record a golden output for inputs that cover each rule, each edge and each error, and compare after every small step. It proves the same output only for those inputs.
- Remove global mutable state first: globals that change make tests depend on each other.
- Change structure or change behaviour, never both in one step. Improvements and bug fixes come after the refactoring, each with a new golden output.
- Plain functions and frozen data are often enough; give a class the data that has rules to guard.
Exercise
Exercise · Easy · Python
Refactor a library-fines script into classes
fines.py is a working procedural script: it prints a report of library fines from a global list of loans. Refactor it into at least three classes without changing what it prints. The sample tests use this fixed API:
Item(title, kind): a book, magazine or DVD, wherekindis"book","magazine"or"dvd".Loan(member, item, days_late)with a methodfine()that returns the fine in paise: nothing for up toGRACE_DAYSdays late, thenPER_DAY[kind]for every further day, and never more thanMAX_FINE.Ledger()withadd(loan)andtotal_for(member), the sum of that member's fines (0 for a member with no loans).main(), which prints the same report as the original script every time it is called.
Item and Loan are values: two made from the same arguments are equal, and assigning to one of their fields raises an AttributeError. Keep the constants PER_DAY, GRACE_DAYS and MAX_FINE and the function money(), and keep importing fines.py silent: only main() prints.
Two of the sample tests are the safety net from the lesson: the report is compared with the original output, and main() is called twice in one program. Run the sample tests on the starter code first: the report test passes, and the second call to main() already shows the problem with the global list.
Starter code · fines.py
"""Library fines, as a procedural script: refactor it into classes.
Fines are in paise (100 paise make one rupee). A loan is fined for each day it
is late after the grace days, and no loan is fined more than MAX_FINE.
"""
PER_DAY = {"book": 200, "magazine": 100, "dvd": 500} # paise for each day late
GRACE_DAYS = 2
MAX_FINE = 10000
loans = [] # (member, title, kind, days late)
def add_loan(member, title, kind, days_late):
loans.append((member, title, kind, days_late))
def fine(kind, days_late):
late = days_late - GRACE_DAYS
if late <= 0:
return 0
return min(late * PER_DAY[kind], MAX_FINE)
def money(paise):
return f"{paise // 100}.{paise % 100:02d}"
def print_report():
totals = {}
print(f"{'Member':<7}{'Title':<22}{'Late':>5}{'Fine':>8}")
for member, title, kind, days_late in loans:
amount = fine(kind, days_late)
totals[member] = totals.get(member, 0) + amount
print(f"{member:<7}{title:<22}{days_late:>5}{money(amount):>8}")
parts = [f"{member} {money(total)}" for member, total in sorted(totals.items())]
print("Totals:", ", ".join(parts))
def main():
add_loan("Asha", "Wings of Fire", "book", 5)
add_loan("Ravi", "Film Studies Monthly", "magazine", 1)
add_loan("Asha", "Planet Earth", "dvd", 40)
add_loan("Mei", "Science Weekly", "magazine", 3)
add_loan("Ravi", "The Hobbit", "book", 9)
print_report()
if __name__ == "__main__":
main() The sample tests · test_fines.py
import contextlib
import importlib
import io
import fines
GOLDEN = """\
Member Title Late Fine
Asha Wings of Fire 5 6.00
Ravi Film Studies Monthly 1 0.00
Asha Planet Earth 40 100.00
Mei Science Weekly 3 1.00
Ravi The Hobbit 9 14.00
Totals: Asha 106.00, Mei 1.00, Ravi 14.00
"""
def printed_by_main():
printed = io.StringIO()
with contextlib.redirect_stdout(printed):
fines.main()
return printed.getvalue()
def test_report_is_unchanged():
"""main() prints exactly what the original script printed"""
assert printed_by_main() == GOLDEN
def test_report_is_the_same_the_second_time():
"""main() prints the same report when it runs twice in one program"""
printed_by_main()
assert printed_by_main() == GOLDEN
def test_fine_counts_the_days_after_the_grace_days():
"""Loan.fine() charges each day late after the grace days"""
assert fines.Loan("Asha", fines.Item("Wings of Fire", "book"), 5).fine() == 600
assert fines.Loan("Mei", fines.Item("Science Weekly", "magazine"), 3).fine() == 100
def test_no_fine_within_the_grace_days():
"""a loan up to GRACE_DAYS late is not fined"""
for days in (0, 1, 2):
assert fines.Loan("Mei", fines.Item("The Hobbit", "book"), days).fine() == 0
def test_fine_is_capped():
"""no loan is fined more than MAX_FINE"""
dvd = fines.Item("Planet Earth", "dvd")
assert fines.Loan("Asha", dvd, 21).fine() == 9500
assert fines.Loan("Asha", dvd, 40).fine() == 10000
def test_ledger_totals_per_member():
"""Ledger.total_for() adds up one member's fines"""
ledger = fines.Ledger()
ledger.add(fines.Loan("Asha", fines.Item("Wings of Fire", "book"), 5))
ledger.add(fines.Loan("Asha", fines.Item("Planet Earth", "dvd"), 40))
ledger.add(fines.Loan("Mei", fines.Item("The Hobbit", "book"), 9))
assert ledger.total_for("Asha") == 10600
assert ledger.total_for("Mei") == 1400
assert ledger.total_for("Nobody") == 0
def test_ledgers_do_not_share_loans():
"""a new Ledger starts empty, whatever another Ledger holds"""
first = fines.Ledger()
first.add(fines.Loan("Asha", fines.Item("Wings of Fire", "book"), 5))
assert fines.Ledger().total_for("Asha") == 0
def test_items_and_loans_are_values():
"""Item and Loan compare by value, and their fields cannot be changed"""
book = fines.Item("The Hobbit", "book")
loan = fines.Loan("Mei", book, 9)
assert book == fines.Item("The Hobbit", "book")
assert loan == fines.Loan("Mei", fines.Item("The Hobbit", "book"), 9)
for value, field, new in ((book, "kind", "dvd"), (loan, "days_late", 0)):
try:
setattr(value, field, new)
except AttributeError: # what a frozen dataclass raises
continue
raise AssertionError(f"{type(value).__name__}.{field} could be changed")
def test_import_prints_nothing():
"""importing fines.py prints nothing: only main() prints the report"""
printed = io.StringIO()
with contextlib.redirect_stdout(printed):
importlib.reload(fines)
assert printed.getvalue() == "" A hint
Start with the values: an Item is a title and a kind, and a Loan is a member, an item and the days late that can work out its own fine from GRACE_DAYS, PER_DAY and MAX_FINE (a frozen dataclass suits both). Then give Ledger the list that is the global loans now, and let main() create a new Ledger each time it runs, so a second call starts empty.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Python Tutorial: Classes (Python Software Foundation)
- The Python Tutorial: Modules (Python Software Foundation)
- The Python Tutorial: Floating-Point Arithmetic, Issues and Limitations (Python Software Foundation)
- dataclasses, Data Classes (Python Software Foundation)
- runpy, Locating and executing Python modules (Python Software Foundation)
- contextlib.redirect_stdout (Python Software Foundation)
- The Single Responsibility Principle (Robert C. Martin (The Clean Code Blog))
- Definition Of Refactoring (Martin Fowler)
- ECMAScript 2022 Language Specification (ECMA-262, 13th edition) (Ecma International)
- Private elements (JavaScript reference) (MDN Web Docs (Mozilla))
- Object.freeze() (JavaScript reference) (MDN Web Docs (Mozilla))
- Object.hasOwn() (JavaScript reference) (MDN Web Docs (Mozilla))
- JavaScript modules (JavaScript guide) (MDN Web Docs (Mozilla))
- Go FAQ: Is Go an object-oriented language? (The Go Project)
- The Go Programming Language Specification: Keywords (The Go Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress