This is a write-up on how we built a real stock-and-sales system for a Nairobi kiosk using only the Python basics: variables, conditionals, loops, functions, data structures, strings and files
The problem
A small kiosk in Nairobi tracks everything in a notebook: how many loaves of bread are left, what was sold today, how much money came in. Notebooks get lost, pages get smudged, and adding up a day's sales by hand is slow and error-prone.
Our group was asked to replace the notebook with a program. The owner needs to:
- See what is in stock.
- Restock items or add new ones.
- Sell products and record each sale.
- See a sales report.
- Search for a product.
- Keep everything saved between days.
The twist is that we could only use what we had learned in six sessions of Python: no classes, no try/except, no csv module. That constraint makes it a great end-to-end project, because every basic skill has to earn its place.
Project Scope
| Skill | How it shows up in Kiosk Manager |
|---|---|
| Basics |
input() prompts, correct data types, f-string output |
| Conditionals | Is there enough stock? Is the menu choice valid? |
| Loops | A while True menu; for loops over sales and stock |
| Functions | Every feature is its own function, called from a clean main()
|
| Data structures | Dictionary for stock, list of tuples for sales, set of unique items sold |
| Strings and files | Cleaning input with .strip(), .title(), .lower(); saving and loading .txt files |
Step 1: Design the data
Before writing any code, we decided how the data would look. Good structure makes everything after it easier.
Stock is a dictionary of dictionaries. The product name is the key, and the value holds price and quantity:
Looking up inventory["Pen"]["quantity"] is instant and readable, and checking whether a product exists is just name in inventory.
Sales are a list of tuples. Each sale never changes after it happens, which is exactly what a tuple is for:
Unique products sold are a set. A set ignores duplicates, so adding "Pen" twice still counts it once:
Step 2: Welcome and setup
The program opens by asking for the kiosk's name and the owner's name. Raw input() is never trusted: .strip() removes stray spaces and .title() fixes the capitalisation, so " navas herbert " becomes "Navas Herbert".
We split this into two small functions. One collects the information and returns two values; the other prints the banner.
The file names sit at the top as constants. If we ever rename a file, we change it in one place.
Step 3: Saving and loading data
Data that vanishes when the program closes is useless to a shopkeeper, so we used plain text files with one comma-separated record per line. This is what Dictionary.txt looks like:
On startup, os.path.exists() decides whether to load that file or begin with a small default inventory. Each line is turned back into a dictionary entry with .split(",").
to come back here!!!!
Saving uses three small functions, and the file mode matters:
Mode "w" overwrites, which is right for inventory because the file should always hold today's stock.
Mode "a" appends, which is right for sales because yesterday's sales must never be erased.
Step 4: Viewing stock
Aligned columns come from f-string padding: {product:<15} left-aligns a value in a 15-character column.
Step 5: Adding or restocking a product
One function handles both jobs. If the product exists, we add units to it; if not, we ask for a price and a starting quantity. Every input is checked with .isdigit() before converting with int(), so typing letters can never crash the program.
# ================function to add/ restock a product========================
def add_or_restock_product():
product = input("Enter product name: ").strip().title()
# Validate product name
if product == "" or product.isdigit():
print("Invalid product name. Please enter a valid product name.")
return
# Product already exists
if product in inventory:
quantity = input("Enter quantity to add: ").strip()
if not quantity.isdigit():
print("Quantity must be a number.")
return
quantity = int(quantity)
if quantity <= 0:
print("Quantity must be greater than 0.")
return
inventory[product]["quantity"] += quantity
save_inventory()
print(f"{product} has been restocked successfully.")
# Brand-new product
else:
price = input("Enter product price: ").strip()
quantity = input("Enter product quantity: ").strip()
if not price.isdigit() or not quantity.isdigit():
print("Price and quantity must be numbers.")
return
price = int(price)
quantity = int(quantity)
if price <= 0 or quantity <= 0:
print("Price and quantity must be greater than 0.")
return
inventory[product] = {
"price": price,
"quantity": quantity
}
save_inventory()
print(f"{product} has been added successfully.")
The repeated return statements are _"guard clause_s": the moment something is wrong, the function stops. The successful path stays flat and easy to read.
Step 6: Selling a product
This is where the business logic lives. A sale only goes through if the product exists, the quantity is a positive whole number, and there is enough stock.
#=============== Function to Sell a Product================
def sell_product():
product = input('Which product to sell: ').strip().title()
# Validate product name
if product == "" or product.isdigit():
print("Invalid product name. Please enter a valid product name.")
return
if product not in inventory:
print(f"{product} is not available in the inventory.")
return
if product in inventory:
quantity = input('Enter the units to be sold: ').strip()
if not quantity.isdigit():
print('Quantity must be a digit')
return
quantity = int(quantity)
if quantity <= 0:
print('Quantity should be greater than 0')
return
if quantity > inventory[product]["quantity"]:
print(
f"Not enough stock. Only "
f"{inventory[product]['quantity']} units available.")
return
price = inventory[product]['price']
total = quantity * price
inventory[product]["quantity"] -= quantity
sale = (product, quantity, total)
sales_log.append(sale)
sold_products.add(product)
save_inventory()
save_sales_log()
save_sold_products()
print(f"Sold {quantity} units of {product} sucessfuly for {total}")
print("Sale successful.")
print(f"Product: {product}")
print(f"Quantity: {quantity}")
print(f"Total: KES {total}")
Nothing changes until every check has passed. That is why a refused sale never leaves the stock half-updated.
Step 7: The sales report
One loop does three jobs:
- it prints each sale,
- adds to a revenue accumulator,
- builds a dictionary of units sold per product.
After the loop, max() picks the best seller.
#===============Function to print the sales report==============================
def sales_report():
if not sales_log:
print("No sales yet this session.")
return
print("\nSALES REPORT")
print("-" * 40)
print(f"{'Product':<15}{'Quantity':<10}{'Total (KES)':<12}")
print("-" * 40)
revenue = 0
units_by_product = {}
for product, quantity, total in sales_log:
print(f"{product:<15}{quantity:<10}{total:<12}")
revenue += total
units_by_product[product] = units_by_product.get(product, 0) + quantity
best = max(units_by_product, key=units_by_product.get)
print("-" * 40)
print(f"Total revenue: KES {revenue}")
print(f"Unique products sold: {len(sold_products)}")
print(f"Best seller: {best} ({units_by_product[best]} units)\n")
.get(product, 0) returns 0 the first time a product appears, which avoids a missing-key error. We define "best seller" as most units sold, and stay consistent with that.
Step 8: Searching products
A forgiving search lowercases both the search term and the product name, then uses the in operator, so typing calc finds Calculator.
# ===============Function to search products====================
def search_products():
term = input("Search for a product: ").strip().lower()
if term == "":
print("Please type something to search for.")
return
found = False
for product, details in inventory.items():
if term in product.lower():
print(f"{product:<15}KES {details['price']:<8}Qty: {details['quantity']}")
found = True
if not found:
print("No matches found.")
The found flag is a classic pattern: assume nothing matched, flip it to True on the first match, and print "No matches found" only if it never flipped.
Step 9: The main program
Because every feature is its own function, the main flow reads almost like plain English: ask, welcome, load, then loop until the user exits.
kiosk_name, owner_name = ask_kiosk_info()
show_owner_details(kiosk_name, owner_name)
print()
inventory = load_inventory()
sales_log = []
sold_products = set()
while True:
print('==============MAIN MENU============== ')
print('1. View Stock')
print('2. Add/ Restock a Product')
print('3. Sell a Product')
print('4. View Sales Report')
print('5. Search Products')
print('6. Exit')
choice = input('Enter your choice (Between 1 and 6): ').strip()
if not choice.isdigit():
print('Invalid choice. Please enter a number between 1 and 6')
continue
choice = int(choice)
if choice < 1 or choice > 6:
print('Invalid choice. Please enter a number between 1 and 6')
continue
if choice == 1:
view_inventory()
elif choice == 2:
add_or_restock_product()
elif choice == 3:
sell_product()
elif choice == 4:
sales_report()
elif choice == 5:
search_products()
else:
save_inventory()
save_sales_log()
save_sold_products()
print(f'Exiting. Goodbye, {owner_name}!')
break
The menu choice is validated in two steps: .isdigit() first (is it a number at all?), then a range check (is it 1 to 6?). continue sends the loop straight back to the menu instead of crashing.
Conclusion
Kiosk Manager is small, but it is a complete program: it takes input, applies business rules, stores data, reports results and survives a restart. That is the full loop of real software, and if every group member can explain every function above, we built something a real shopkeeper could use tomorrow morning.
Top comments (1)
This is a good start. You might want to look into sqlite so that you can save all of your data to a local database.