
Phase 1 Capstone: Building a Command-Line Inventory System
Congratulations on reaching the end of Phase 1! Over the last nine lessons, you have learned the core building blocks of Python: variables, conditionals, loops, lists, dictionaries, functions, error handling, and file persistence with pathlib.
Learning concepts one by one is great, but real programming confidence comes when you combine them into a single, useful application. In this capstone project, you will build a Command-Line Inventory and Sales System for a small shop. You will be able to add products, view stock, record sales, and save all data to a text file so nothing is lost when you close the program.
1. Planning the Application Architecture
Before writing a single line of code, professional developers plan how their data and functions will fit together. Our inventory system needs four core features:
- Data Storage: Each product will be stored as a dictionary keyed by product name, tracking its price and stock quantity.
- File Persistence: Using
pathlib.Pathandwith open(), the program will load saved inventory at startup and save changes toinventory.txt. - Modular Functions: Each action (loading, saving, adding, selling, viewing) will live in its own focused function with type hints.
- Safe Input Handling: Using
tryandexcept ValueError, the program will never crash if a user types letters when a number is expected.
2. Setting Up File Persistence with Pathlib
Create a new file named inventory_app.py. First, import Path from pathlib and write two helper functions to load and save our inventory in a simple, readable format (name|price|quantity per line).
from pathlib import Path
DATA_FILE = Path("inventory.txt")
def load_inventory(file_path: Path) -> dict[str, dict[str, float | int]]:
"""Load inventory items from a text file into a dictionary."""
inventory: dict[str, dict[str, float | int]] = {}
if not file_path.exists():
return inventory
with open(file_path, "r", encoding="utf-8") as file:
for line in file:
cleaned = line.strip()
if not cleaned:
continue
name, price_str, qty_str = cleaned.split("|")
inventory[name] = {
"price": float(price_str),
"quantity": int(qty_str),
}
return inventory
def save_inventory(
file_path: Path,
inventory: dict[str, dict[str, float | int]],
) -> None:
"""Save all inventory items to a text file."""
with open(file_path, "w", encoding="utf-8") as file:
for name, details in inventory.items():
line = f"{name}|{details['price']}|{details['quantity']}\n"
file.write(line)
Notice how load_inventory checks file_path.exists() first. On the very first run, inventory.txt does not exist yet, so returning an empty dictionary prevents a FileNotFoundError.
3. Adding Safe Input Helpers
In a command-line app, users often make typos. Instead of repeating try and except blocks everywhere, write two reusable helper functions for reading non-negative integers and positive prices. Note that using float for prices is fine for learning, while real accounting software uses decimal.Decimal to avoid rounding errors.
def get_positive_int(prompt: str) -> int:
"""Prompt the user until they enter a valid non-negative integer."""
while True:
try:
value = int(input(prompt))
if value < 0:
print("Please enter a number that is 0 or greater.")
continue
return value
except ValueError:
print("Invalid input. Please enter a whole number.")
def get_positive_float(prompt: str) -> float:
"""Prompt the user until they enter a valid positive price."""
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Price must be greater than 0.")
continue
return value
except ValueError:
print("Invalid input. Please enter a valid number (for example, 12.50).")
4. Implementing Inventory and Sales Features
Now let’s build the three core actions: viewing the current stock, adding or restocking a product, and recording a sale. We also keep a list of sales records during the session so the user can view a sales summary.
def display_inventory(inventory: dict[str, dict[str, float | int]]) -> None:
"""Print all products, prices, and stock levels in a clean table."""
if not inventory:
print("\nYour inventory is currently empty.")
return
print("\n--- Current Inventory ---")
print(f"{'Product':<20} {'Price':>10} {'Stock':>8}")
print("-" * 40)
for name, details in inventory.items():
price = details["price"]
qty = details["quantity"]
print(f"{name:<20} ${price:>9.2f} {qty:>8}")
def add_product(inventory: dict[str, dict[str, float | int]]) -> None:
"""Add a new product or update stock for an existing one."""
name = input("Enter product name: ").strip().title()
if not name:
print("Product name cannot be empty.")
return
price = get_positive_float("Enter unit price: ")
quantity = get_positive_int("Enter initial stock quantity: ")
if name in inventory:
inventory[name]["price"] = price
inventory[name]["quantity"] = int(inventory[name]["quantity"]) + quantity
print(f"Updated '{name}'. New stock: {inventory[name]['quantity']}.")
else:
inventory[name] = {"price": price, "quantity": quantity}
print(f"Added '{name}' to inventory.")
def record_sale(
inventory: dict[str, dict[str, float | int]],
sales_log: list[dict[str, float | int | str]],
) -> None:
"""Process a product sale, update stock, and log the transaction."""
name = input("Enter product name to sell: ").strip().title()
if name not in inventory:
print(f"Product '{name}' was not found in inventory.")
return
available = int(inventory[name]["quantity"])
if available == 0:
print(f"'{name}' is currently out of stock.")
return
qty_to_sell = get_positive_int(f"Enter quantity to sell (available: {available}): ")
if qty_to_sell == 0:
print("Sale cancelled.")
return
if qty_to_sell > available:
print(f"Not enough stock. Only {available} available.")
return
unit_price = float(inventory[name]["price"])
total_cost = unit_price * qty_to_sell
inventory[name]["quantity"] = available - qty_to_sell
sales_log.append({
"product": name,
"quantity": qty_to_sell,
"total": total_cost,
})
print(f"Sold {qty_to_sell}x '{name}' for ${total_cost:.2f}.")
5. Practical Project: The Main Menu Loop
Finally, connect everything inside a main() function. It loads saved data at startup, shows a clear menu, saves after every change, and prints a session sales summary when you exit. At the bottom, the if __name__ == "__main__": line is a standard Python pattern that runs main() only when you execute this file directly (we will explore how that works in detail in Phase 2).
def display_sales_summary(sales_log: list[dict[str, float | int | str]]) -> None:
"""Display total revenue and items sold in the current session."""
if not sales_log:
print("\nNo sales recorded in this session.")
return
total_revenue = sum(float(sale["total"]) for sale in sales_log)
print("\n--- Session Sales Summary ---")
for sale in sales_log:
print(f"- {sale['quantity']}x {sale['product']}: ${sale['total']:.2f}")
print(f"Total Revenue: ${total_revenue:.2f}")
def main() -> None:
"""Run the Command-Line Inventory and Sales System."""
inventory = load_inventory(DATA_FILE)
sales_log: list[dict[str, float | int | str]] = []
print("Welcome to the Python Inventory & Sales System!")
while True:
print("\nMenu:")
print("1. View Inventory")
print("2. Add / Restock Product")
print("3. Record a Sale")
print("4. View Session Sales Summary")
print("5. Save and Exit")
choice = input("Choose an option (1-5): ").strip()
if choice == "1":
display_inventory(inventory)
elif choice == "2":
add_product(inventory)
save_inventory(DATA_FILE, inventory)
elif choice == "3":
record_sale(inventory, sales_log)
save_inventory(DATA_FILE, inventory)
elif choice == "4":
display_sales_summary(sales_log)
elif choice == "5":
save_inventory(DATA_FILE, inventory)
display_sales_summary(sales_log)
print("Inventory saved. Goodbye!")
break
else:
print("Invalid choice. Please select a number from 1 to 5.")
if __name__ == "__main__":
main()
Run the script with python inventory_app.py from your terminal, add two products, record a sale, exit, and run it again. Your updated stock levels will still be there.
Common Beginner Mistakes
- Using
int()on decimal prices: Passing"12.50"toint()raises aValueError. Always usefloat()when reading prices. - Forgetting to save after updates: If you only call
save_inventory()on exit and the user closes the terminal with Ctrl+C, recent changes are lost. Saving right after each add or sale is much safer. - Running the script from the wrong folder:
Path("inventory.txt")looks in your current working directory. Make sure you runpython inventory_app.pyfrom the folder where your script lives. - Using the delimiter inside a product name: Because we split lines on
|, typing a product name that contains|would breakload_inventory(). In Phase 2, you will learn CSV and JSON formats that handle special characters safely.
Try It Yourself
- Low-Stock Alert (
inventory_alerts.py): Updatedisplay_inventory()to print a[LOW STOCK]warning next to any product whose quantity is below 5. - Delete Product Option (
inventory_delete.py): Add menu option 6 to remove a discontinued product from the dictionary and save the updated file. - Persistent Sales Log (
sales_history.py): Save each completed sale to a separatesales_history.txtfile using append mode ("a") so sales records survive across sessions too.
Frequently Asked Questions
Why did we use a pipe (|) instead of a comma to separate values in inventory.txt?
Product names sometimes contain commas, which would confuse a simple .split(","). Using | reduces that risk for a plain text file, and in Phase 2 you will switch to the built-in csv and json modules.
What happens if inventory.txt is empty?
The if not cleaned: continue check skips blank lines, so an empty file simply returns an empty dictionary without crashing.
Why normalize product names with .strip().title()?
Without normalization, "coffee" and "Coffee " would be stored as two separate dictionary keys. Cleaning and capitalizing input keeps lookups consistent.
Can I type shell commands like python inventory_app.py inside the >>> prompt?
No. The >>> prompt is the interactive Python REPL, not your OS terminal. Type exit() first to return to your terminal before running a .py file.
Need to review file handling before diving into the capstone? Read the previous lesson: Working with Files in Python: Text Persistence with Pathlib. Next, we kick off Phase 2 with Working with CSV Files: Tabular Data in Python, where you will learn how to export your data into real spreadsheet-ready CSV reports.
Learning programming works best as a collaborative process, and your questions and insights help make these tutorials more effective for everyone. If you get stuck connecting the functions together, run into a file path issue, or build a creative upgrade for the inventory system, please share your experience in the comments. Your feedback helps create a learning community where we can all benefit from each other’s discoveries and solutions.
You Might Also Like

A Week After PHP2Shell: This Is What I Learned
A personal story about discovering vulnerable PHP on my own servers after the PHP2Shell ne...

Working with Files in Python: Text Persistence with Pathlib
Learn how to save and load data permanently in Python using the modern pathlib module, con...

Handling Errors Like a Professional Python Developer
Learn to read Python tracebacks and handle errors gracefully with try, except, and finally...

Writing Clean Python Functions: Code Organization Made Simple
Master Python functions, including def, parameters, return values, scope, and docstrings. ...
Comments
Loading comments…