Python from zero to data engineering Lesson 12 / 110

Project: a booking manager that does only the work it needs

Combine loops, collections, and functions in a small application. Validate bookings, calculate a report in one pass, and explain the memory and runtime choices.

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:

CommandBehaviour
bookAsk for a name and a whole-number quantity; accept or reject the request.
reportDisplay booking count, sold tickets, revenue, and seats remaining.
quitEnd 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

  1. Write and call booking_error() with valid and invalid values. It returns an error message or an empty string for success.
  2. Write format_euros() and summarise(). Test them on a short hard-coded list before adding input.
  3. Add the command loop and the book branch. Check that rejected requests leave the list and seat count unchanged.
  4. Connect report to 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 answersExpected outcome
book, Mina, 3Accepted B001: EUR 25.50
book, Luca, 2Accepted B002: EUR 17.00
book, Ana, 8Rejected: only 7 seats remain
book, spaces only, 1Rejected: enter a name
book, Ana, 0Rejected: choose at least one ticket
report2 bookings; 5 tickets; EUR 42.50; 7 seats remain
book, Ana, 7Accepted B003: EUR 59.50
report3 bookings; 12 tickets; EUR 102.00; 0 seats remain
quitTicket 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.

Search