Back to Blog
Phase 1 Capstone: Building a Command-Line Inventory System

Phase 1 Capstone: Building a Command-Line Inventory System

November 13, 2026
Updated November 13, 2026
Reynov Christian

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.Path and with open(), the program will load saved inventory at startup and save changes to inventory.txt.
  • Modular Functions: Each action (loading, saving, adding, selling, viewing) will live in its own focused function with type hints.
  • Safe Input Handling: Using try and except 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" to int() raises a ValueError. Always use float() 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 run python inventory_app.py from 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 break load_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): Update display_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 separate sales_history.txt file 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

Comments

Loading comments…

Leave a Comment

0/5000