The cinema now needs to handle multiple customers and produce a report. We will reuse the ideas from lessons 1–11, without introducing a framework to supervise twelve seats.
By the end: you can build an in-memory booking manager, test its rules, and explain where it does repeated work and where it avoids it. Allow 60–90 minutes. Try the specification first; open the solution when you need it.
Define the behaviour
The program starts with 12 seats and a price of 850 cents. It accepts three commands:
| Command | Behaviour |
|---|---|
book | Ask for a name and a whole-number quantity; accept or reject the request. |
report | Display booking count, sold tickets, revenue, and seats remaining. |
quit | End this run. |
Reject blank names, quantities of zero or less, and requests larger than the remaining capacity. Assign accepted bookings an ID such as B001. A rejected booking must not use an ID, change availability, or appear in the report. An unknown command should explain the supported commands and return to the prompt.
As in the first project, non-integer quantity text is outside this version’s input contract. int() will raise ValueError for banana. Exception handling is coming in lesson 18; we are not disguising a fragile conversion as complete validation.
Decide what to store
Use a list of booking dictionaries. Each record stores its ID, customer, quantity, and the price charged per ticket. Keeping the price in the record means a later change to the current price does not rewrite the earlier sale’s revenue.
Keep available_seats as a running number for fast availability checks. This duplicates information we could derive from bookings, so it creates an obligation: update it only when a booking is accepted. For this small sequential program, the rule is easy to inspect. It is not a concurrency-safe design for several real ticket desks.
We do not need a set or a dictionary indexed by ID yet. There is no repeated ID-lookup feature. Adding an index just because we learned about indexes would consume memory and create another structure to maintain.
Build in stages
- Write and call
booking_error()with valid and invalid values. It returns an error message or an empty string for success. - Write
format_euros()andsummarise(). Test them on a short hard-coded list before adding input. - Add the command loop and the
bookbranch. Check that rejected requests leave the list and seat count unchanged. - Connect
reportto the summary function. Then test a full interaction sequence.
Save the program as booking_manager.py.
Show the complete booking manager
def booking_error(name, quantity, available_seats):
if name == "":
return "Please enter a name."
if quantity <= 0:
return "Choose at least one ticket."
if quantity > available_seats:
return f"Only {available_seats} seats are available."
return ""
def format_euros(amount_cents):
euros = amount_cents // 100
cents = amount_cents % 100
return f"EUR {euros}.{cents:02d}"
def summarise(bookings):
tickets = 0
revenue_cents = 0
for booking in bookings:
tickets = tickets + booking["tickets"]
revenue_cents = revenue_cents + (
booking["tickets"] * booking["price_cents"]
)
return tickets, revenue_cents
capacity = 12
available_seats = capacity
ticket_price_cents = 850
bookings = []
while True:
command = input("book / report / quit: ").strip().lower()
if command == "quit":
break
elif command == "book":
name = input("Name: ").strip()
quantity = int(input("Tickets (whole number): "))
error = booking_error(name, quantity, available_seats)
if error != "":
print(error)
else:
booking_id = f"B{len(bookings) + 1:03d}"
bookings.append({
"id": booking_id,
"customer": name,
"tickets": quantity,
"price_cents": ticket_price_cents,
})
available_seats = available_seats - quantity
total_cents = quantity * ticket_price_cents
print(f"Accepted {booking_id}: {format_euros(total_cents)}")
elif command == "report":
tickets, revenue_cents = summarise(bookings)
print(f"Bookings: {len(bookings)}")
print(f"Tickets sold: {tickets}")
print(f"Revenue: {format_euros(revenue_cents)}")
print(f"Seats remaining: {available_seats}")
else:
print("Unknown command. Use book, report, or quit.")
print("Ticket desk closed.")
Read the unfamiliar-looking details
return tickets, revenue_cents returns a tuple. tickets, revenue_cents = summarise(bookings) unpacks it, as in lesson 10. The parentheses around the revenue expression let us split it across source lines without changing its meaning.
:03d pads the booking number to at least three digits; it does not impose a maximum of 999. Using the list length is sufficient here because accepted records are never deleted during a run. A cancellation feature or persistent storage would need a more robust ID policy.
booking_error() uses independent if statements safely because each failure returns immediately. Once the function returns, the later checks cannot run for that call. This differs from the independent printing checks in lesson four, which continued executing.
The functions receive their inputs explicitly. Neither summarise() nor booking_error() reads keyboard input or changes our stored bookings. This lets us test their decisions directly.
A complete manual test
Run report before any booking. All totals should be zero, with 12 seats remaining. Then enter this sequence:
| Command and answers | Expected outcome |
|---|---|
book, Mina, 3 | Accepted B001: EUR 25.50 |
book, Luca, 2 | Accepted B002: EUR 17.00 |
book, Ana, 8 | Rejected: only 7 seats remain |
book, spaces only, 1 | Rejected: enter a name |
book, Ana, 0 | Rejected: choose at least one ticket |
report | 2 bookings; 5 tickets; EUR 42.50; 7 seats remain |
book, Ana, 7 | Accepted B003: EUR 59.50 |
report | 3 bookings; 12 tickets; EUR 102.00; 0 seats remain |
quit | Ticket desk closed |
Also try an unknown command and a negative quantity. Check that available_seats + tickets_sold == capacity after each accepted booking. That relationship is an invariant: something that should remain true as the program changes state.
Performance, memory, and money
The summary scans the bookings once, calculates both totals together, and creates no second list of ticket values. It runs only when report is requested, not after every command. Availability uses the running counter rather than rescanning the history for every sale.
For a dozen seats, any savings are tiny. At larger scale, repeated full-history scans can become expensive. Maintaining every possible running metric is not automatically the answer: corrections and cancellations then have more state to update correctly. Decide from the actual access pattern and measure before making the design harder.
The booking list still grows with the number of records. A one-pass summary does not make the entire application use constant memory. Later, files, databases, and generators will let us process histories that do not fit comfortably in memory.
Faster code can shorten compute jobs, but lower cost is a separate outcome to verify. Extra memory, larger machines, storage, logging, and billing granularity can offset a runtime improvement. Throughout the course we will compare correctness first, then runtime, resource use, and cost where measurable.
Your turn: stop accidental overbooking
Temporarily move the seat update outside the accepted branch. Run the oversized-booking test and observe the broken invariant. Then put it back. This is an intentionally broken experiment, not an alternative solution.
For a useful extension, include the average tickets per booking in the report. Use len(bookings) and /, and handle the empty list before dividing.
Show the report extension
Add this inside the report branch after tickets has been calculated:
if len(bookings) == 0:
print("Average tickets: no bookings yet")
else:
average = tickets / len(bookings)
print(f"Average tickets: {average:.2f}")
:.2f formats a number to two decimal places for display. With bookings of 3 and 2, the average is 2.50. This is a measured average, not a money calculation; using a float is appropriate here.
Where we go next
This completes the first 12 lessons of the planned 110-lesson course. Our manager loses its records when it exits, accepts only integer-formatted quantities, and runs in one process. Those are the next reasons to learn scope, shared references, exceptions, files, and project structure—not reasons to start over with a giant framework.
Rebuild it from the specification before continuing. Explain why you chose each collection and when the summary does work. Return to the course roadmap.