Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design) Module 1 – LLD rounds and a repeatable answer method

The machine-coding round playbook

Plan a 90-minute machine-coding round for low-level design (LLD): a model, an in-memory repository, a command dispatcher, tests, a demo and what to skip.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot

What you will learn

  • Plan a 90-minute build: skeleton, in-memory storage, driver, tests and demo
  • Structure code so that a new requirement touches one place
  • Decide what to skip when time runs out, and say so explicitly

Before you start

On this page

A machine-coding round asks for working, readable code in a fixed time, often 90 to 120 minutes, in your own editor. You get a written problem statement, often with a sample input and the exact output it must produce. When the time is up you run it, walk the interviewer through the code, and are usually asked for a small change. Formats and lengths differ between employers; this lesson describes the format in general terms, not any company’s process.

Unlike a design discussion, the program is the product. A design that is not finished usually scores less than a smaller design that runs, so the plan below gets something running early and keeps it running.

A plan for 90 minutes

The minutes are MySmartCoPilot’s recommendation; shift them to your own speed, but keep the order:

  1. Model and interfaces, about 10 minutes. Read the statement twice, list the entities, and write the class skeletons and the public methods of the service. This is the answer framework squeezed into one step; the API skeleton is your design document.
  2. The happy path end to end, about 30 minutes. The main commands work on the sample input, from reading the input to printing the output. Nothing clever yet.
  3. Validation and errors, about 15 minutes. Unknown commands, wrong arguments, a full lot, a duplicate, an unknown id: one clear answer each, and the program carries on.
  4. Tests, about 15 minutes. Unit tests for the main rules, and one golden-output test: the sample input must produce exactly the sample output.
  5. An extension seam and clean-up, about 10 minutes. Rename what is unclear, remove duplication, and check that the most likely follow-up would land in one place.
  6. The demo, in the last 10 minutes. Run the sample input and the tests, then say what you skipped.
Timer Set one timer per stage (10, 30, 15, 15, 10 and 10 minutes) when you practise, so you notice which stage runs over.

Structure the code so a change touches one place

The statement in this lesson is a classic: a parking lot with slots for bikes, cars and trucks; PARK <plate> <type> gives the lowest free slot of that type and a ticket; LEAVE <ticket> frees the slot; FREE <type> counts free slots; STATUS lists the parked vehicles. A clean solution has four layers, and each kind of follow-up has a home:

Command lines go to dispatcher.py, which calls the ParkingLot service, which keeps tickets through a repository interface.Command lines in, answer lines outPARK KA01AB1234 CARPARKED KA01AB1234 slot 3 ticket T1dispatcher.pyparses a line, checks the arguments,calls one handler, formats the answer(a new command: one handler here)service.py: ParkingLotslots, duplicates, tickets(a new rule: changes here)repository.pyTicketRepository interfaceInMemoryTicketRepositorytwo dictionaries(a database: another class)values in, values outadd, remove, findimplements

Four layers, and where each kind of change goes

Text description of the diagram

The diagram shows five boxes from top to bottom.

  1. Command lines in, answer lines out: the program reads lines such as PARK KA01AB1234 CAR and prints answers such as PARKED KA01AB1234 slot 3 ticket T1. A two-way arrow joins them to the dispatcher.
  2. dispatcher.py parses a line, checks the arguments, calls one handler and formats the answer. A new command is one new handler here.
  3. service.py, the ParkingLot service, holds the rules about slots, duplicate plates and tickets. The dispatcher passes it values and gets values back. A new rule changes this file.
  4. repository.py defines the TicketRepository interface (add, remove, find), which the service uses.
  5. InMemoryTicketRepository keeps the tickets in two dictionaries and implements the interface, shown by a dashed arrow. A database would be another class implementing the same interface.

Here is the whole solution, run on the sample input. The main file wires the parts together; the other tabs hold the dispatcher, the service, the repository and the value types:

A parking lot in four layers, run on the sample input

parking/main.py

"""Reads commands from standard input and prints one answer per command."""
import sys

from dispatcher import run
from models import VehicleType
from repository import InMemoryTicketRepository
from service import ParkingLot

LAYOUT = {VehicleType.BIKE: 2, VehicleType.CAR: 2, VehicleType.TRUCK: 1}


def main():
    lot = ParkingLot(LAYOUT, InMemoryTicketRepository())
    for answer in run(sys.stdin, lot):
        print(answer)


if __name__ == "__main__":
    main()

Input (standard input)

PARK KA01AB1234 CAR
PARK MH12XY0001 car
PARK DL3CAF0007 CAR
PARK KA01AB1234 BIKE
PARK KA05MN4821 BIKE
FREE CAR
LEAVE T1
LEAVE T1
PARK DL3CAF0007 CAR
PARK TN09ZZ4321 BUS
PARK
FLY KA01AB1234
STATUS

Output

PARKED KA01AB1234 slot 3 ticket T1
PARKED MH12XY0001 slot 4 ticket T2
ERROR no free CAR slot
ERROR KA01AB1234 is already parked
PARKED KA05MN4821 slot 1 ticket T3
FREE CAR 0
LEFT KA01AB1234 slot 3
ERROR unknown ticket T1
PARKED DL3CAF0007 slot 3 ticket T4
ERROR unknown vehicle type BUS
ERROR usage: PARK <plate> <type>
ERROR unknown command FLY
1 KA05MN4821 BIKE
3 DL3CAF0007 CAR
4 MH12XY0001 CAR

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py

parking/dispatcher.py

"""Turns command lines into calls on the ParkingLot, and its answers into output lines."""
import shlex

from models import VehicleType
from service import ParkingError

COMMANDS = {}


def command(name, *params):
    """Register the decorated function as the handler of NAME with these parameters."""

    def register(handler):
        COMMANDS[name] = (params, handler)
        return handler

    return register


def vehicle_type(text):
    try:
        return VehicleType(text.upper())
    except ValueError:
        raise ParkingError(f"unknown vehicle type {text}") from None


@command("PARK", "plate", "type")
def park(lot, plate, type_name):
    ticket = lot.park(plate, vehicle_type(type_name))
    return f"PARKED {plate} slot {ticket.slot} ticket {ticket.ticket_id}"


@command("LEAVE", "ticket")
def leave(lot, ticket_id):
    ticket = lot.leave(ticket_id)
    return f"LEFT {ticket.vehicle.plate} slot {ticket.slot}"


@command("FREE", "type")
def free(lot, type_name):
    kind = vehicle_type(type_name)
    return f"FREE {kind.value} {lot.free_slots(kind)}"


@command("STATUS")
def status(lot):
    rows = [f"{t.slot} {t.vehicle.plate} {t.vehicle.type.value}" for t in lot.parked()]
    return "\n".join(rows) or "EMPTY"


def run(lines, lot):
    """Yield one answer per command line. Bad input is answered with ERROR, never raised."""
    for line in lines:
        try:
            words = shlex.split(line)
        except ValueError as unreadable:  # an unclosed quote, for example
            yield f"ERROR {str(unreadable).lower()}"
            continue
        if not words:
            continue
        name, args = words[0].upper(), words[1:]
        if name not in COMMANDS:
            yield f"ERROR unknown command {words[0]}"
            continue
        params, handler = COMMANDS[name]
        if len(args) != len(params):
            yield "ERROR usage: " + " ".join([name, *(f"<{p}>" for p in params)])
            continue
        try:
            yield handler(lot, *args)
        except ParkingError as refusal:
            yield f"ERROR {refusal}"

parking/service.py

"""The rules of the lot: no parsing and no printing. Values in, values or a ParkingError out."""
from bisect import insort
from itertools import count

from models import Ticket, Vehicle


class ParkingError(Exception):
    """A request that the lot refuses; the message is meant for the user."""


class ParkingLot:
    def __init__(self, layout, tickets):
        """layout: {VehicleType: number of slots}, numbered from 1 in that order.

        tickets: a TicketRepository.
        """
        self._tickets = tickets
        self._free = {}
        first = 1
        for vehicle_type, size in layout.items():
            self._free[vehicle_type] = list(range(first, first + size))
            first += size
        self._ids = count(1)

    def park(self, plate, vehicle_type):
        if self._tickets.find_by_plate(plate) is not None:
            raise ParkingError(f"{plate} is already parked")
        free = self._free.get(vehicle_type)
        if not free:
            raise ParkingError(f"no free {vehicle_type.value} slot")
        ticket = Ticket(f"T{next(self._ids)}", Vehicle(plate, vehicle_type), free.pop(0))
        self._tickets.add(ticket)
        return ticket

    def leave(self, ticket_id):
        ticket = self._tickets.remove(ticket_id)
        if ticket is None:
            raise ParkingError(f"unknown ticket {ticket_id}")
        insort(self._free[ticket.vehicle.type], ticket.slot)
        return ticket

    def free_slots(self, vehicle_type):
        return len(self._free.get(vehicle_type, []))

    def parked(self):
        return self._tickets.all()

parking/repository.py

"""Where tickets are kept. The service sees only the interface, so storage can change."""
from typing import Protocol

from models import Ticket


class TicketRepository(Protocol):
    def add(self, ticket: Ticket) -> None: ...
    def remove(self, ticket_id: str) -> Ticket | None: ...
    def find_by_plate(self, plate: str) -> Ticket | None: ...
    def all(self) -> list[Ticket]: ...


class InMemoryTicketRepository:
    def __init__(self):
        self._by_id = {}
        self._by_plate = {}

    def add(self, ticket):
        self._by_id[ticket.ticket_id] = ticket
        self._by_plate[ticket.vehicle.plate] = ticket

    def remove(self, ticket_id):
        ticket = self._by_id.pop(ticket_id, None)
        if ticket is not None:
            del self._by_plate[ticket.vehicle.plate]
        return ticket

    def find_by_plate(self, plate):
        return self._by_plate.get(plate)

    def all(self):
        return sorted(self._by_id.values(), key=lambda t: t.slot)

parking/models.py

"""The domain's value types: written in the first ten minutes, rarely changed afterwards."""
from dataclasses import dataclass
from enum import Enum


class VehicleType(Enum):
    BIKE = "BIKE"
    CAR = "CAR"
    TRUCK = "TRUCK"


@dataclass(frozen=True)
class Vehicle:
    plate: str
    type: VehicleType


@dataclass(frozen=True)
class Ticket:
    ticket_id: str
    vehicle: Vehicle
    slot: int

What to notice in each layer:

  • dispatcher.py is the only code that knows about text. It splits a line with shlex.split(), which follows shell rules, so a quoted argument such as "KA 05 MN 4821" stays one word; a line it cannot split, such as one with an unclosed quote, gets an ERROR line of its own. It looks the command up in a table, checks the number of arguments and turns every ParkingError into an ERROR line, so a bad line never stops the program. Each handler is registered by the @command decorator, so a new command is one new function.
  • service.py holds the rules and nothing else. It takes values (a plate, a VehicleType) and returns values (a Ticket); it never parses or prints, so tests and other front ends can call it directly. It keeps a sorted list of free slots per vehicle type, so the lowest free slot is simply the first one in that list.
  • repository.py hides the storage. The service depends on the TicketRepository interface, a typing.Protocol; the in-memory version uses two dictionaries, one by ticket id and one by plate. A database or a file would be another class with the same four methods, and the service would not change.
  • models.py holds frozen value types written in the first ten minutes: VehicleType, Vehicle and Ticket.

In-memory storage is the usual choice for a machine-coding round. Putting it behind an interface costs a few lines and answers the common follow-up “what if this had to be saved?” before it is asked.

Tests: the rules and one golden output

Two kinds of test pay off in a timed round. Unit tests pin down the rules that are easy to break while you refactor (here also the dispatcher’s answer to a line it cannot split), and a golden-output test runs the sample input and compares the answer, character for character, with the expected output:

Unit tests for the rules, and the sample input as a golden test Python · parking/test_parking.py
import sys
import unittest

from dispatcher import run
from main import LAYOUT
from models import VehicleType
from repository import InMemoryTicketRepository
from service import ParkingError, ParkingLot

SAMPLE_OUTPUT = """\
PARKED KA01AB1234 slot 3 ticket T1
PARKED MH12XY0001 slot 4 ticket T2
ERROR no free CAR slot
ERROR KA01AB1234 is already parked
PARKED KA05MN4821 slot 1 ticket T3
FREE CAR 0
LEFT KA01AB1234 slot 3
ERROR unknown ticket T1
PARKED DL3CAF0007 slot 3 ticket T4
ERROR unknown vehicle type BUS
ERROR usage: PARK <plate> <type>
ERROR unknown command FLY
1 KA05MN4821 BIKE
3 DL3CAF0007 CAR
4 MH12XY0001 CAR"""


def new_lot():
    return ParkingLot(LAYOUT, InMemoryTicketRepository())


class LotRules(unittest.TestCase):
    def test_lowest_free_slot_of_the_type(self):
        """a car gets the lowest free car slot, and a freed slot is used again first"""
        lot = new_lot()
        first = lot.park("A1", VehicleType.CAR)
        lot.park("A2", VehicleType.CAR)
        lot.leave(first.ticket_id)
        self.assertEqual(lot.park("A3", VehicleType.CAR).slot, first.slot)

    def test_full_and_duplicate_are_refused(self):
        """a full lot and a plate that is already parked are refused"""
        lot = new_lot()
        lot.park("T1", VehicleType.TRUCK)
        with self.assertRaisesRegex(ParkingError, "no free TRUCK slot"):
            lot.park("T2", VehicleType.TRUCK)
        with self.assertRaisesRegex(ParkingError, "already parked"):
            lot.park("T1", VehicleType.CAR)


class CommandLines(unittest.TestCase):
    def test_a_line_it_cannot_split_is_an_error(self):
        """a line with an unclosed quote gets an ERROR, and the next line still runs"""
        lines = ['PARK "KA 01 AB 1234 CAR', 'PARK "KA 01 AB 1234" CAR']
        answers = list(run(lines, new_lot()))
        self.assertEqual(answers, ["ERROR no closing quotation", "PARKED KA 01 AB 1234 slot 3 ticket T1"])


class SampleInput(unittest.TestCase):
    def test_sample_input_gives_the_sample_output(self):
        """commands.txt gives exactly the expected output"""
        with open("commands.txt", encoding="utf-8") as sample:
            answers = list(run(sample, new_lot()))
        self.assertEqual("\n".join(answers), SAMPLE_OUTPUT)


if __name__ == "__main__":
    load = unittest.defaultTestLoader.loadTestsFromTestCase
    suite = unittest.TestSuite([load(LotRules), load(CommandLines), load(SampleInput)])
    unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)

Output

test_full_and_duplicate_are_refused (__main__.LotRules.test_full_and_duplicate_are_refused)
a full lot and a plate that is already parked are refused ... ok
test_lowest_free_slot_of_the_type (__main__.LotRules.test_lowest_free_slot_of_the_type)
a car gets the lowest free car slot, and a freed slot is used again first ... ok
test_a_line_it_cannot_split_is_an_error (__main__.CommandLines.test_a_line_it_cannot_split_is_an_error)
a line with an unclosed quote gets an ERROR, and the next line still runs ... ok
test_sample_input_gives_the_sample_output (__main__.SampleInput.test_sample_input_gives_the_sample_output)
commands.txt gives exactly the expected output ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

This output changes from run to run: unittest prints how long the tests took, which differs from run to run

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_parking.py

The golden test is the one to write first: it protects everything that already works while you add the rest. When it fails, put the expected and the actual output side by side and look for the first line that differs.

Text Diff Paste the expected output and your program's output to see the first line where they differ.

A follow-up that lands in one place

A typical last-minute request is “add a command that tells me where a vehicle is”. With a dispatch table, the change is one handler. This file adds WHERE <plate> without editing any of the files above:

A new command as one new handler Python · parking/follow_up.py
"""A last-minute follow-up, "add WHERE <plate>": one new handler, no other file changes."""
from dispatcher import command, run
from models import VehicleType
from repository import InMemoryTicketRepository
from service import ParkingError, ParkingLot


@command("WHERE", "plate")
def where(lot, plate):
    ticket = next((t for t in lot.parked() if t.vehicle.plate == plate), None)
    if ticket is None:
        raise ParkingError(f"{plate} is not parked")  # the dispatcher prints it as ERROR
    return f"{plate} is in slot {ticket.slot}"


lot = ParkingLot({VehicleType.CAR: 2}, InMemoryTicketRepository())
for answer in run(["PARK KA01AB1234 CAR", "WHERE KA01AB1234", "WHERE MH12XY0001"], lot):
    print(answer)

Output

PARKED KA01AB1234 slot 1 ticket T1
KA01AB1234 is in slot 1
ERROR MH12XY0001 is not parked

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 follow_up.py

A new rule, such as “a bike may take a free car slot when every bike slot is taken”, would change only ParkingLot, which is given the layout of the slots; a new input format would change only the dispatcher. If a follow-up forces you to edit three layers, say which boundary was in the wrong place; that is a design insight, not a failure.

The same dispatcher in JavaScript

The idea does not depend on Python. Here is a smaller version in JavaScript, with the lot, a Map from command name to handler, and the same error handling; only the lot’s own ParkingError becomes an ERROR line, and anything else is rethrown:

A command dispatcher in JavaScript JavaScript · dispatch_js/dispatcher.mjs
// The dispatcher idea in JavaScript: a table from command name to handler,
// and all parsing kept out of the lot.
class ParkingError extends Error {}

class ParkingLot {
  constructor(carSlots) {
    this.free = Array.from({ length: carSlots }, (_, i) => i + 1);
    this.slotOf = new Map();
  }

  park(plate) {
    if (this.slotOf.has(plate)) throw new ParkingError(`${plate} is already parked`);
    if (this.free.length === 0) throw new ParkingError("no free CAR slot");
    const slot = this.free.shift();
    this.slotOf.set(plate, slot);
    return slot;
  }

  leave(plate) {
    const slot = this.slotOf.get(plate);
    if (slot === undefined) throw new ParkingError(`${plate} is not parked`);
    this.slotOf.delete(plate);
    this.free.push(slot);
    this.free.sort((a, b) => a - b);
    return slot;
  }

  freeSlots() {
    return this.free.length;
  }
}

const COMMANDS = new Map([
  ["PARK", { params: ["plate"], handle: (lot, p) => `PARKED ${p} slot ${lot.park(p)}` }],
  ["LEAVE", { params: ["plate"], handle: (lot, p) => `LEFT ${p} slot ${lot.leave(p)}` }],
  ["FREE", { params: [], handle: (lot) => `FREE CAR ${lot.freeSlots()}` }],
]);

export function run(lines, lot) {
  const answers = [];
  for (const line of lines) {
    const [word = "", ...args] = line.trim().split(/\s+/);
    if (word === "") continue;
    const name = word.toUpperCase();
    const command = COMMANDS.get(name);
    if (command === undefined) {
      answers.push(`ERROR unknown command ${word}`);
    } else if (args.length !== command.params.length) {
      const usage = [name, ...command.params.map((p) => `<${p}>`)].join(" ");
      answers.push(`ERROR usage: ${usage}`);
    } else {
      try {
        answers.push(command.handle(lot, ...args));
      } catch (error) {
        if (!(error instanceof ParkingError)) throw error;
        answers.push(`ERROR ${error.message}`);
      }
    }
  }
  return answers;
}

const sample = [
  "PARK KA01AB1234",
  "PARK MH12XY0001",
  "PARK DL3CAF0007",
  "LEAVE KA01AB1234",
  "PARK DL3CAF0007",
  "FREE",
  "FLY away",
];
for (const answer of run(sample, new ParkingLot(2))) console.log(answer);

Output

PARKED KA01AB1234 slot 1
PARKED MH12XY0001 slot 2
ERROR no free CAR slot
LEFT KA01AB1234 slot 1
PARKED DL3CAF0007 slot 1
FREE CAR 0
ERROR unknown command FLY

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node dispatcher.mjs

Use the language you type fastest and whose standard library you know best, if the round lets you choose.

The same layout in other languages

Each language has a usual way to split a small program into files and run it on an input file. The commands below are the ones the languages’ own documentation gives; this page runs the Python and JavaScript versions, and a later module of this track writes one design in each of these languages.

  • Python: main.py, dispatcher.py, service.py, repository.py and models.py in one folder. Run python3 main.py < commands.txt.
  • Java 22 or later: Main.java and one file per top-level class (ParkingLot.java, Dispatcher.java …) in one folder. Run java Main.java < commands.txt; for each class the code refers to, the launcher finds the file of the same name (ParkingLot.java for ParkingLot) and compiles it in memory.
  • Go: a module with go.mod, main.go in package main, and a parking package in its own folder. Run go run . < commands.txt.
  • Rust: a Cargo package with Cargo.toml, src/main.rs declaring mod parking;, and src/parking.rs. Run cargo run < commands.txt.

What to skip when time runs out, and how to say it

You will rarely finish everything. Decide what to drop on purpose, and say it, instead of running out of time in the middle of something.

  • Skip without regret: persistence (the in-memory repository is enough), concurrency (say where a lock would go), input formats beyond the statement, pretty output, configuration files, logging, and tests beyond the main rules and the golden test.
  • Never skip: the exact output of the sample input, the rules the statement names, and the separation between parsing, rules and storage. Those are what the round is judged on.
  • Say it explicitly. Keep a short “not done” list at the top of main.py or in a README, and read it out in the demo: “Not done: fees at exit. I would keep the entry time on the Ticket, add a fee rule to ParkingLot and print the fee in the LEAVE handler.” A named gap with a plan reads as a decision; a silent gap reads as a mistake.

Key takeaways

  • Work in this order: model and interfaces, the happy path end to end, errors, tests, a seam and clean-up, the demo.
  • Keep parsing in a dispatcher, rules in a service and storage behind a repository interface; each kind of change then has one home.
  • A dispatch table makes a new command one handler; an in-memory repository behind an interface answers the storage question for free.
  • Write the golden-output test for the sample input first.
  • Skip on purpose, keep a “not done” list, and say it in the demo.

Exercise

Exercise · Medium · Python

Write the command dispatcher for a meeting-room service

room_commands.py contains a finished in-memory service, RoomBook, for booking meeting rooms by the hour. Do not change it. Write run(lines), the dispatcher: it creates a new RoomBook, answers each command line with exactly one output line, and returns the list of answers. Blank lines are skipped, and command names may be written in any case (book is BOOK). The five commands:

  • ADD_ROOM <name> <seats> answers ADDED <name>.
  • BOOK <room> <hour> <person> answers BOOKED <id>, with the id the service returns (B1, B2 …).
  • CANCEL <id> answers CANCELLED <id>.
  • FREE <hour> <seats> answers FREE followed by the names of the rooms that are free at that hour and have at least that many seats, separated by spaces, or FREE none.
  • SCHEDULE <room> answers SCHEDULE <room> followed by its bookings as <hour>:<person>, in hour order, or SCHEDULE <room> empty.

When a line cannot be carried out, the answer starts with ERROR and the program carries on:

  • an unknown command: ERROR unknown command <the word as typed>;
  • the wrong number of arguments: ERROR usage: and the command with its parameters, for example ERROR usage: BOOK <room> <hour> <person> (the parameter names are the ones in the list above);
  • an hour or a number of seats that is not a whole number: ERROR hour must be a whole number or ERROR seats must be a whole number;
  • a refusal of the service (a BookingError): ERROR <its message>, for example ERROR no room Kaveri.

Keep the parsing in run() and its helpers, and leave every rule about rooms and bookings to the service. The sample tests run command scripts and compare the answers exactly.

Starter code · room_commands.py

"""Meeting rooms. The service RoomBook is given; write run(), the command dispatcher."""


class BookingError(Exception):
    """A request that the rules refuse; the message is meant for the user."""


class RoomBook:
    """Given: an in-memory meeting-room service. Do not change it."""

    OPEN, CLOSE = 8, 18  # a booking starts on the hour, at 8, 9 … 17, and lasts one hour

    def __init__(self):
        self._rooms = {}  # name -> seats
        self._bookings = {}  # booking id -> (room, hour, person)
        self._next_id = 1

    def add_room(self, name, seats):
        if name in self._rooms:
            raise BookingError(f"room {name} exists")
        if seats < 1:
            raise BookingError("a room has at least 1 seat")
        self._rooms[name] = seats

    def book(self, room, hour, person):
        if room not in self._rooms:
            raise BookingError(f"no room {room}")
        if not self.OPEN <= hour < self.CLOSE:
            raise BookingError(f"rooms open from {self.OPEN} to {self.CLOSE}")
        if any(r == room and h == hour for r, h, _ in self._bookings.values()):
            raise BookingError(f"{room} is booked at {hour}")
        booking_id = f"B{self._next_id}"
        self._next_id += 1
        self._bookings[booking_id] = (room, hour, person)
        return booking_id

    def cancel(self, booking_id):
        if self._bookings.pop(booking_id, None) is None:
            raise BookingError(f"no booking {booking_id}")

    def free_rooms(self, hour, seats):
        taken = {r for r, h, _ in self._bookings.values() if h == hour}
        fits = [name for name, size in self._rooms.items() if size >= seats]
        return sorted(name for name in fits if name not in taken)

    def schedule(self, room):
        if room not in self._rooms:
            raise BookingError(f"no room {room}")
        return sorted((h, p) for r, h, p in self._bookings.values() if r == room)


def run(lines):
    """Answer each command line with one output line, on a new RoomBook (see the prompt)."""
    service = RoomBook()
    answers = []
    # Write the dispatcher here.
    return answers
The sample tests · test_room_commands.py
from room_commands import run


def test_happy_path():
    """each command gives its answer"""
    script = [
        "ADD_ROOM Indus 6",
        "ADD_ROOM Ganga 10",
        "BOOK Indus 10 asha",
        "FREE 10 4",
        "SCHEDULE Indus",
        "CANCEL B1",
        "SCHEDULE Indus",
    ]
    assert run(script) == [
        "ADDED Indus",
        "ADDED Ganga",
        "BOOKED B1",
        "FREE Ganga",
        "SCHEDULE Indus 10:asha",
        "CANCELLED B1",
        "SCHEDULE Indus empty",
    ]


def test_schedule_and_free_lists():
    """SCHEDULE lists bookings in hour order; FREE lists rooms by name, or none"""
    script = [
        "ADD_ROOM Indus 6",
        "ADD_ROOM Ganga 10",
        "BOOK Indus 15 ravi",
        "BOOK Indus 9 mei",
        "SCHEDULE Indus",
        "FREE 11 4",
        "FREE 9 50",
    ]
    assert run(script)[-3:] == ["SCHEDULE Indus 9:mei 15:ravi", "FREE Ganga Indus", "FREE none"]


def test_service_refusals():
    """the service's refusals are answered as ERROR <message>"""
    script = [
        "ADD_ROOM Indus 6",
        "BOOK Indus 10 asha",
        "BOOK Indus 10 ravi",
        "BOOK Kaveri 9 ravi",
        "BOOK Indus 18 ravi",
        "CANCEL B9",
        "ADD_ROOM Indus 4",
    ]
    assert run(script)[2:] == [
        "ERROR Indus is booked at 10",
        "ERROR no room Kaveri",
        "ERROR rooms open from 8 to 18",
        "ERROR no booking B9",
        "ERROR room Indus exists",
    ]


def test_unknown_command():
    """an unknown command is named in the error, as it was typed"""
    assert run(["RESERVE Indus 10"]) == ["ERROR unknown command RESERVE"]


def test_wrong_number_of_arguments():
    """a wrong number of arguments gives the usage of the command"""
    assert run(["BOOK Indus 10", "FREE", "CANCEL B1 B2"]) == [
        "ERROR usage: BOOK <room> <hour> <person>",
        "ERROR usage: FREE <hour> <seats>",
        "ERROR usage: CANCEL <id>",
    ]


def test_numbers_are_checked():
    """an hour or a number of seats that is not a whole number is refused"""
    script = ["ADD_ROOM Kaveri lots", "ADD_ROOM Kaveri 4", "BOOK Kaveri ten asha", "FREE 9 four"]
    assert run(script) == [
        "ERROR seats must be a whole number",
        "ADDED Kaveri",
        "ERROR hour must be a whole number",
        "ERROR seats must be a whole number",
    ]


def test_case_and_blank_lines():
    """command names may be in any case, and blank lines are skipped"""
    assert run(["add_room Indus 4", "", "   ", "Book Indus 9 mei"]) == ["ADDED Indus", "BOOKED B1"]


def test_each_run_starts_empty():
    """every call of run() starts with no rooms and no bookings"""
    run(["ADD_ROOM Indus 4"])
    assert run(["SCHEDULE Indus"]) == ["ERROR no room Indus"]
A hint

Write one small function per command that takes the service and the argument strings and returns the answer line, and a dictionary from each command name to its parameter names and its function. run() then only splits the line, looks the name up, compares the number of arguments with the parameter names, and catches BookingError. A helper that turns "10" into 10 and raises BookingError("hour must be a whole number") for "ten" keeps the handlers short.

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.

  1. Question 1 of 6 Put the parts of a 90-minute machine-coding round in the order the lesson suggests.

    Give each item its position, from 1 (first).

    Show the answer to question 1

    Answer:

    1. Model and interfaces
    2. The happy path, end to end
    3. Validation and errors
    4. Tests
    5. An extension seam and clean-up
    6. The demo

    A thin version that runs end to end comes first, because a program that runs the sample input is worth more than a perfect half. Errors and tests follow, then a seam for the likely follow-up, and the demo last.

  2. Question 2 of 6 Where should the line "PARK KA01AB1234 CAR" be split into words and checked?

    Choose one answer.

    Show the answer to question 2

    Answer: In the dispatcher, which then calls the service with values

    Parsing belongs at the edge of the program. The service takes values (a plate and a vehicle type) and knows nothing about text, so a new input format, or a test, can call it directly.

  3. Question 3 of 6 Why does the service talk to a TicketRepository interface instead of a dictionary?

    Choose one answer.

    Show the answer to question 3

    Answer: Storage can change (a file, a database) without any change to the service's rules

    The in-memory repository is the right storage for the round, and the interface costs a few lines. It keeps the rules independent of storage, which is exactly the kind of change a follow-up question likes to test.

  4. Question 4 of 6 It is minute 75 of 90. The sample input works except for one command, and there are no tests. What is the best plan?

    Choose one answer.

    Show the answer to question 4

    Answer: Add a golden-output test for the sample input, finish or drop the last command, and say in the demo what you skipped

    One test that compares the sample output exactly protects everything that already works. Saying what is unfinished, and how you would do it, turns a gap into a decision the interviewer can see.

  5. Question 5 of 6 follow_up.py adds a WHERE command with one new handler. What does it print?

    What does this program print? Choose one answer.

    """A last-minute follow-up, "add WHERE <plate>": one new handler, no other file changes."""
    from dispatcher import command, run
    from models import VehicleType
    from repository import InMemoryTicketRepository
    from service import ParkingError, ParkingLot
    
    
    @command("WHERE", "plate")
    def where(lot, plate):
        ticket = next((t for t in lot.parked() if t.vehicle.plate == plate), None)
        if ticket is None:
            raise ParkingError(f"{plate} is not parked")  # the dispatcher prints it as ERROR
        return f"{plate} is in slot {ticket.slot}"
    
    
    lot = ParkingLot({VehicleType.CAR: 2}, InMemoryTicketRepository())
    for answer in run(["PARK KA01AB1234 CAR", "WHERE KA01AB1234", "WHERE MH12XY0001"], lot):
        print(answer)
    Show the answer to question 5

    Answer: it prints

    PARKED KA01AB1234 slot 1 ticket T1
    KA01AB1234 is in slot 1
    ERROR MH12XY0001 is not parked

    The @command decorator adds WHERE to the dispatcher's table, so run() knows it without any other change. The first car is in slot 1. For a plate that is not parked, the handler raises ParkingError, and run() turns it into an ERROR line, as it does for every other command.

  6. Question 6 of 6 The round lets you choose the language. Which should you use?

    Choose one answer.

    Show the answer to question 6

    Answer: The one you type fastest and whose standard library you know best

    The round measures working, readable code in a fixed time. The language you are fluent in lets you spend the time on the design instead of on syntax and library lookups.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.