Sitemap
Press enter or click to view image in full size

From Zero to Working App: Build a Food Ordering System with Python

--

Learning Python through isolated exercises gets you comfortable with the syntax. But there’s a different kind of learning that happens when you build something end-to-end. At this moment, a dictionary stops being an abstract concept and becomes your restaurant menu, and a loop stops being a drill and becomes the engine that keeps your app running.

In this tutorial, we’ll build a complete food ordering app from scratch. Customers will be able to browse a menu, add items to a cart, adjust quantities, and check out. Everything we use comes from core Python: dictionaries, functions, loops, and user input. No libraries needed.

The Architecture Before the Code

Before writing anything, it helps to think about what the application needs. A food ordering app has three layers:

Data: What’s on the menu? What’s in the cart? What actions can a user take?

Logic: Functions that handle each action — displaying the menu, adding items, removing items, viewing and modifying the cart, and checking out.

Flow: A main loop that ties everything together, takes user input, and calls the right function.

We’ll build these layers in order.

Part 1: Global Variables

Global variables are values that need to be accessible throughout the entire application. We define all of them upfront.

RESTAURANT_NAME = "Hungry Hare"

By convention, global constants are written in ALL_CAPS. This signals to anyone reading your code that the value is meant to be stable.

Our menu uses a nested dictionary structure:

menu = {
"sku1": {"name": "Hamburger", "price": 6.51},
"sku2": {"name": "Cheeseburger", "price": 7.75},
"sku3": {"name": "Milkshake", "price": 5.99},
"sku4": {"name": "Fries", "price": 2.39},
"sku5": {"name": "Sub", "price": 5.87},
"sku6": {"name": "Ice Cream", "price": 1.55},
"sku7": {"name": "Fountain Drink", "price": 3.45},
"sku8": {"name": "Cookie", "price": 3.15},
"sku9": {"name": "Brownie", "price": 2.46},
"sku10": {"name": "Sauce", "price": 0.75}
}

Each item is identified by a SKU rather than its name. If the price of a hamburger changes, you update one dictionary value. If the item gets renamed, you update the name without touching anything else. Referencing items by a stable ID is a pattern you’ll see in real systems.

The actions dictionary maps numbers to the choices your user will see:

app_actions = {
"1": "Add a new menu item to cart",
"2": "Remove an item from the cart",
"3": "Modify a cart item's quantity",
"4": "View cart",
"5": "Checkout",
"6": "Exit"
}

Finally, the sales tax rate and an empty cart:

SALES_TAX_RATE = 0.07
cart = {}

The cart starts empty and gets populated as the user orders. It stores SKU keys with integer quantities as values — {"sku1": 2, "sku4": 1} means two hamburgers and one order of fries.

Separating your data from your logic is one of the most valuable habits in programming. When your menu lives in one dictionary and your cart in another, you can update either without hunting through your function code to find where prices or quantities are stored.

Part 2: Displaying the Menu

def display_menu():
"""Displays all menu item SKUs, names, and prices."""
print("\n****Menu****\n")
for sku in menu:
parsed_sku = sku[3:]
item = menu[sku]['name']
price = menu[sku]['price']
print("(" + parsed_sku + ") " + item + ": $" + str(price))
print("\n")

sku[3:] slices off the "sku" prefix so the user sees "(1) Hamburger: $6.51" instead of "(sku1) Hamburger: $6.51". Price is converted to a string with str() before concatenation since you can't use + to combine a string and a float directly.

Test it immediately after writing it:

display_menu()
****Menu****
(1) Hamburger: $6.51
(2) Cheeseburger: $7.75
(3) Milkshake: $5.99
(4) Fries: $2.39
...

Testing each function the moment you finish writing it is a habit worth building early. Catching a bug in a 10-line function is much easier than catching it after you’ve written 100 more lines that depend on it.

Part 3: Adding Items to the Cart

def add_to_cart(sku, quantity=1):
"""
Add an item and its quantity to the cart.

:param string sku: The input SKU number being ordered.
:param int quantity: The input quantity being ordered.
"""
if sku in menu:
if sku in cart:
cart[sku] += quantity
else:
cart[sku] = quantity
print("Added", quantity, "of", menu[sku]['name'], "to the cart.")
else:
print("I'm sorry. The menu number", sku, "that you entered is not on the menu.")

Two design decisions are worth noting. First, quantity=1 is a default parameter — if a customer adds an item without specifying how many, the function assumes one. This reduces the number of inputs required for the most common case.

Get Dataquest’s stories in your inbox

Join Medium for free to get updates from this writer.

Second, the function checks whether the SKU already exists in the cart. If it does, it increments the count. If it doesn’t, it creates a new entry. This prevents a second order of hamburgers from overwriting the first.

add_to_cart("sku1")       # Adds 1 hamburger
add_to_cart("sku1", 10) # Adds 10 more hamburgers
print(cart)
Added 1 of Hamburger to the cart.
Added 10 of Hamburger to the cart.
{'sku1': 11}

Part 4: Removing Items from the Cart

def remove_from_cart(sku):
"""
Remove an item from the cart.

:param string sku: The input SKU number to remove from the cart.
"""
if sku in cart:
removed_val = cart.pop(sku)
print("Removed", removed_val, "of", menu[sku]['name'], "from the cart.")
else:
print(f"I'm sorry. The item with SKU {sku} is not currently in the cart.")

dict.pop(key) removes the key and returns its value in one operation. The returned value lets us tell the user what was removed. The else branch handles the case where the user tries to remove something that isn't in the cart, giving a helpful message rather than crashing.

Part 5: Modifying Cart Quantities

def modify_cart(sku, quantity):
"""
Modify an item's quantity in the cart.

:param string sku: The input SKU number being modified.
:param int quantity: The input new quantity to use for the SKU.
"""
if sku in cart:
if quantity > 0:
cart[sku] = quantity
print("Modified", menu[sku]['name'], "quantity to", quantity, "in the cart.")
else:
remove_from_cart(sku)
else:
print(f"I'm sorry.", menu[sku]['name'], "is not currently in the cart.")

If the user sets a quantity to zero or below, there’s no reason to keep the item in the cart — so modify_cart calls remove_from_cart rather than duplicating that logic. When two functions need to do the same thing, one calls the other. You'll see this pattern throughout real codebases.

Part 6: Viewing Cart Contents

def view_cart():
"""Display the menu item names and quantities inside the cart."""
print("\n****Cart Contents****\n")
subtotal = 0
for sku in cart:
if sku in menu:
quantity = cart[sku]
subtotal += menu[sku]["price"] * quantity
print(quantity, " x ", menu[sku]["name"])
tax = subtotal * SALES_TAX_RATE
total = subtotal + tax
print("Total: $", round(total, 2))
print("\n")

The function loops through the cart, looks up each item’s price from the menu, and accumulates a subtotal. Tax and total are calculated outside the loop since they only need to be computed once after all items are accounted for. Rounding to 2 decimal places keeps the dollar amount clean.

Part 7: Checkout

def checkout():
"""Display the subtotal information for the user to checkout."""
print("\n****Checkout****\n")
view_cart()
print("Thank you for your order! Goodbye!")
print("\n")

Checkout is deliberately simple because view_cart() already does the work. This is reuse at its most direct — one function calls another that already knows how to do exactly what's needed.

Part 8: Getting User Input

def get_sku_and_quantity(sku_prompt, quantity_prompt=None):
"""
Get input from the user.

:param string sku_prompt: Prompt displayed before SKU entry.
:param string quantity_prompt: Prompt displayed before quantity entry.
Defaults to None when quantity input is not needed.

:returns: The full sku# value and optionally the quantity.
"""
item_sku = input(sku_prompt)
item_sku = "sku" + item_sku
if quantity_prompt:
quantity = input(quantity_prompt)
if not quantity.isdigit():
quantity = 1
quantity = int(quantity)
return item_sku, quantity
else:
return item_sku

A few things worth understanding here. The input() function always returns a string — when a user types "4", Python stores it as "4", not the integer 4. That's why we call int(quantity) before returning it.

item_sku = "sku" + item_sku prepends "sku" to whatever the user types, so they only have to type "4" to refer to Fries and the function assembles the full key "sku4" that matches our dictionary.

quantity_prompt=None as a default makes the function flexible. Some actions only need a SKU. Others need both a SKU and a quantity. The same function handles both cases by checking whether quantity_prompt was provided.

return item_sku, quantity returns two values as a tuple. The caller unpacks both in one line: ordered_sku, quantity = get_sku_and_quantity(...).

Part 9: The Main Order Loop

def order_loop():
"""Loop ordering actions until checkout or exit."""
print("Welcome to the " + RESTAURANT_NAME + "!")
ordering = True
while ordering:
print("\n****Ordering Actions****\n")
for number in app_actions:
description = app_actions[number]
print("(" + number + ")", description)
response = input("Please enter the number of the action you want to take: ")
if response == "1":
display_menu()
sku_prompt = "Please enter the SKU number for the menu item you want to order: "
quantity_prompt = "Please enter the quantity you want to order [default is 1]: "
ordered_sku, quantity = get_sku_and_quantity(sku_prompt, quantity_prompt)
add_to_cart(ordered_sku, quantity)
elif response == "2":
display_menu()
sku_prompt = "Please enter the SKU number for the menu item you want to remove: "
item_sku = get_sku_and_quantity(sku_prompt)
remove_from_cart(item_sku)
elif response == "3":
display_menu()
sku_prompt = "Please enter the SKU number for the menu item you want to modify: "
quantity_prompt = "Please enter the quantity you want to change to [default is 1]: "
item_sku, quantity = get_sku_and_quantity(sku_prompt, quantity_prompt)
modify_cart(item_sku, quantity)
elif response == "4":
view_cart()
elif response == "5":
checkout()
ordering = False
elif response == "6":
print("Goodbye!")
ordering = False
else:
print("You have entered an invalid action number. Please try again.")

The ordering = True flag controls the while loop. As long as it's True, the loop keeps running and presenting the action menu. When the user checks out or exits, ordering is set to False and the loop ends.

Every action branch calls a function we already wrote. The loop itself is almost entirely function calls — which is exactly the point. The individual functions handle the details; the loop handles the flow. Making sure every loop has a clear exit condition is one of the first habits to build as a programmer.

Run the app with:

order_loop()
Welcome to the Hungry Hare!

****Ordering Actions****
(1) Add a new menu item to cart
(2) Remove an item from the cart
(3) Modify a cart item's quantity
(4) View cart
(5) Checkout
(6) Exit

Please enter the number of the action you want to take:

What We Built

Starting with a blank file, we built a complete interactive food ordering app using nothing but Python fundamentals. The full architecture breaks down like this:

Global data layer: menu, cart, app_actions, RESTAURANT_NAME, SALES_TAX_RATE

Function layer: display_menu(), add_to_cart(), remove_from_cart(), modify_cart(), view_cart(), checkout(), get_sku_and_quantity()

Application loop: order_loop() — the entry point that ties every function together

Each function handles one thing. Each layer depends on the one below it. This structure makes the app straightforward to test, extend, and explain to someone else.

Where to Take It Next

There’s plenty of room to make this app your own, and extending it is good practice in its own right:

  • Add an inventory command. A quick view_cart() call added as action "7" lets users check their cart without navigating the full menu.
  • Add a clear cart option. A function that resets cart = {} and prints a confirmation message is a small but satisfying addition.
  • Reduce repetition. display_menu() gets called at the top of each order action. That repeated call could be factored out.
  • Build a front end. This app is the backend — the logic that powers the experience. Streamlit or Gradio let you wrap Python apps in a simple web UI without needing to know HTML or JavaScript.

This article is a condensed version of a full project tutorial originally published on Dataquest. For the complete walkthrough, including the guided project in your browser and the full solution notebook, visit the Dataquest blog.

--

--

Dataquest
Dataquest

Written by Dataquest

We are a team of passionate educators teaching data science.