Chapter 5 · Real programs
A project of your own
Step 1 of 18 · Concept
A tool for a small shop
By the end of this lesson you have finished a real tool of several files.
The tool keeps a shop's stock. It reads the items from a CSV file, answers commands, and prints a report.
Every piece comes from an earlier lesson. You will:
- write the item, a dataclass,
- load the items from the file, and skip bad lines,
- print a report, then add a command of your own.
Step 2 of 18 · Concept
One folder, four modules
The project is one folder. The package app holds three modules (lesson 8), one job each:
| file | its job |
|---|---|
app/models.py | the class Item |
app/store.py | reads the CSV file into Item objects |
app/report.py | turns the items into a table |
main.py | reads the commands and calls the rest |
Step 3 of 18 · Quiz
Which module reads the file?
Here are the project's files. Which one opens data/items.csv?
main.py
app/
__init__.py
models.py
store.py
report.py
data/
items.csv
Step 4 of 18 · Demo
Run the finished tool
This is the finished tool, with its commands list, report and quit given in advance as its input. Each >
shares its line with what comes next (lesson 4).
Run it, then open each file in the file tree. Line 4 of data/items.csv is wrong on purpose.
Step 5 of 18 · Concept
The model: a dataclass
An item has a name, a quantity and a price. @dataclass writes its __init__ and __repr__ (lesson 19):
@dataclass
class Item:
name: str
qty: int
price: float
It needs two methods of its own:
| method | gives | for 12 pens at 1.5 |
|---|---|---|
total() | the stock's value, qty times price | 18.0 |
__str__() | the line print shows (lesson 15) | pen: 12 at 1.50 |
Step 6 of 18 · Exercise
Step 1: the Item
In app/models.py, finish the two methods of Item:
total()returnsqtytimesprice.__str__()returns the name,:, the quantity,atand the price with 2 decimals.
main.py tries them. It must print:
pen: 12 at 1.50
ruler: 8 at 1.20
18.00
9.60
A method reads the object's fields through self: self.qty, self.price.
total returns self.qty * self.price. __str__ returns an f-string, the price with the spec :.2f.
return f"{self.name}: {self.qty} at {self.price:.2f}"
Step 7 of 18 · Concept
The store reads the CSV
load_items(path) opens the file and reads it with csv.DictReader (lesson 20). Each row is a dictionary of text,
so the numbers need converting:
qty = int(row["qty"])
price = float(row["price"])
items.append(Item(row["name"], qty, price))
It returns two things, as a function can (lesson 11): the items, and messages about bad lines.
Step 8 of 18 · Exercise
Step 2: load the items
Finish load_items in app/store.py: open path, read its rows with csv.DictReader, and append an Item for
each row. This file has no bad lines yet, so errors stays empty. The program must print:
pen: 12 at 1.50
notebook: 5 at 3.25
eraser: 30 at 0.40
ruler: 8 at 1.20
items: 4
bad lines: 0
Read the file as lesson 20's grades did: with open(..., newline=""), then a loop over csv.DictReader(f).
In the loop, convert row["qty"] with int and row["price"] with float, then items.append(Item(...)).
for row in csv.DictReader(f):
Step 9 of 18 · Concept
Bad lines are reported, not fatal
Real files have mistakes. Line 4 of the shop's file is glue,x,2.00. Without a try, its ValueError stops the
whole program, and no item is loaded.
A better loader handles each row on its own:
- It tries to convert the row and append its item.
- On a
ValueErrorit appends a message such asline 4: not a numbertoerrors. - Then it carries on with the next row.
The header is line 1, so enumerate(rows, start=2) counts the rows as the file's lines (lesson 11).
Step 10 of 18 · Exercise
Step 3: skip bad lines
Run it first: the bad line stops it with a ValueError. Change load_items so that it reports the bad line and
loads the rest. The program must print:
line 4: not a number
pen: 12 at 1.50
notebook: 5 at 3.25
eraser: 30 at 0.40
ruler: 8 at 1.20
items: 4
bad lines: 1
The try goes inside the loop, around the three lines of one row. Around the whole loop, one bad row would end it.
Number the rows with enumerate(..., start=2). In except ValueError:, append an f-string with that number.
for n, row in enumerate(csv.DictReader(f), start=2):
Step 11 of 18 · Demo
The report: sorted and lined up
The report lists the items by name with sorted(items, key=lambda item: item.name) (lesson 13).
Lesson 9's width and decimals go together in one spec: {total:>9.2f} fills 9 characters, with 2 decimals.
Right-aligned numbers put their decimal points in one column.
Run it. The square brackets show where each field starts and ends.
Step 12 of 18 · Exercise
Step 4: the report
Finish report in app/report.py; the header line is there. Add a line per item, sorted by name. Give the name
12 characters, the quantity 4, and the total 9 with 2 decimals.
The program must print:
Item Qty Total
eraser 30 12.00
notebook 5 16.25
pen 12 18.00
ruler 8 9.60
lines is a list of text. "\n".join(lines) makes it one text, a line each. Append a line per item.
Loop over sorted(items, key=lambda item: item.name). Each line is one f-string with three fields.
f"{item.name:<12}{item.qty:>4}{item.total():>9.2f}"
Step 13 of 18 · Concept
main.py ties it together
if __name__ == "__main__":
items, errors = load_items("data/items.csv")
for message in errors:
print(message)
while True:
command = input("> ")
match command.split():
case ["list"]:
for item in items:
print(item)
case ["report"]:
print(report(items))
case ["quit"]:
break
case _:
print("unknown command:", command)
print("bye")
if __name__ == "__main__":runs the tool only whenmain.pyis the file Run names (lesson 8).- The loop asks for a command until
quitbreaks out of it. matchpicks the first case that fits the command's words (lesson 19);_fits anything.
Step 14 of 18 · Concept
The program's objects at run time
While the tool waits for its next command, its names live in the <module> frame (lesson 2). The drawing shows two of
them. They are written at the
module's level, under the if, not inside a function.
items points at one list. The list holds an arrow to each Item, in the file's order. The report sorts a new list
(lesson 13), so list still shows the file's order.
Step 15 of 18 · Check yourself
Check yourself: add a command
Add the command value to main.py. It prints Value: and the total of every item's total(), with 2
decimals. With the input value, then quit, the program must print:
line 4: not a number
> Value: 55.85
> bye
One more case, for the word value, before the case that fits anything. Lesson 13's sum adds up the totals. After
case _, Python stops with SyntaxError: wildcard makes remaining patterns unreachable.
Step 16 of 18 · Challenge · optional
Challenge: choose the file
Optional.
Run types python3 main.py data/autumn.csv: the file's name comes after main.py. Make the tool load the file
named there, not data/items.csv. With the input report, then quit, it must print:
> Item Qty Total
glue 6 12.00
pen 40 60.00
stapler 2 15.00
> bye
The words after python3 are the program's arguments. The module sys keeps them in the list sys.argv.
sys.argv[0] is main.py, and sys.argv[1] is the next word. Add import sys to main.py.
items, errors = load_items(sys.argv[1])
Step 17 of 18 · Concept
Carry on in the Playground
You can now write a Python program of several modules. It reads and writes files and keeps its data in lists, dictionaries and objects. It handles bad input with exceptions, and prints a report.
The Playground, linked from the course page, is an editor with the same Python. Ideas for a next project:
- a to-do list that keeps its tasks in a JSON file between runs,
- a quiz that reads its questions from a CSV file,
- a text game of rooms and items, each a class.
Step 18 of 18 · Recap
Recap
- A project splits its work into modules, one job each, in the package
app. - Data flows from a CSV file into
Itemobjects, and out as a report. - A
tryaround each row reports a bad line, and the rest still load. {total:>9.2f}gives a width and decimals together: the columns line up.main.pyreads commands withinput()and picks a case withmatch.
Next: the Playground. The course is complete: build a program of your own.
Review
A question from this lesson, in a new order each time. Answer from memory: 1 of 1 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
from app.report import report
from app.store import load_items
if __name__ == "__main__":
items, errors = load_items("data/items.csv")
for message in errors:
print(message)
while True:
command = input("> ")
match command.split():
case ["list"]:
for item in items:
print(item)
case ["report"]:
print(report(items))
case ["quit"]:
break
case _:
print("unknown command:", command)
print("bye")
from app.models import Item
pen = Item("pen", 12, 1.5)
ruler = Item("ruler", 8, 1.2)
print(pen)
print(ruler)
print(f"{pen.total():.2f}")
print(f"{ruler.total():.2f}")
from app.store import load_items
items, errors = load_items("data/items.csv")
for item in items:
print(item)
print("items:", len(items))
print("bad lines:", len(errors))
from app.store import load_items
items, errors = load_items("data/items.csv")
for message in errors:
print(message)
for item in items:
print(item)
print("items:", len(items))
print("bad lines:", len(errors))
name = "pen"
qty = 12
total = 18.0
print(f"[{name:<12}][{qty:>4}][{total:>9.2f}]")
print(f"[{'eraser':<12}][{30:>4}][{12.0:>9.2f}]")
from app.report import report
from app.store import load_items
items, errors = load_items("data/items.csv")
print(report(items))
from app.report import report
from app.store import load_items
if __name__ == "__main__":
items, errors = load_items("data/items.csv")
for message in errors:
print(message)
while True:
command = input("> ")
match command.split():
case ["list"]:
for item in items:
print(item)
case ["report"]:
print(report(items))
case ["quit"]:
break
case _:
print("unknown command:", command)
print("bye")
name,qty,price
stapler,2,7.50
pen,40,1.50
glue,6,2.00