Python Basics — Mini Projects

8 minute read

Mini Projects

The best way to learn programming is to build things. Here are 5 complete projects that use everything you’ve learned. Each project is broken into steps — try coding each step before looking at the solution.


Project 1: CLI Calculator

A command-line calculator that supports basic operations and keeps a history.

Skills used: functions, loops, error handling, dictionaries

Requirements

  • Support +, -, *, / operations
  • Keep a history of calculations
  • Handle invalid input gracefully
  • Let the user type “history” to see past calculations
  • Type “quit” to exit
Show Solution
def calculator():
    """Interactive CLI calculator with history."""
    history = []
    
    operations = {
        "+": lambda a, b: a + b,
        "-": lambda a, b: a - b,
        "*": lambda a, b: a * b,
        "/": lambda a, b: a / b if b != 0 else "Error: Division by zero",
    }
    
    print("=" * 40)
    print("  Python Calculator")
    print("  Type 'history' or 'quit'")
    print("=" * 40)
    
    while True:
        user_input = input("\n> ").strip().lower()
        
        if user_input == "quit":
            print(f"\nGoodbye! ({len(history)} calculations performed)")
            break
        
        if user_input == "history":
            if not history:
                print("No calculations yet.")
            else:
                print("\n--- History ---")
                for i, entry in enumerate(history, 1):
                    print(f"  {i}. {entry}")
            continue
        
        try:
            parts = user_input.split()
            if len(parts) != 3:
                print("Format: <number> <operator> <number>")
                print("Example: 10 + 5")
                continue
            
            num1 = float(parts[0])
            op = parts[1]
            num2 = float(parts[2])
            
            if op not in operations:
                print(f"Unknown operator '{op}'. Use: +, -, *, /")
                continue
            
            result = operations[op](num1, num2)
            
            if isinstance(result, str):  # Error message
                print(f"  {result}")
            else:
                display = f"{num1} {op} {num2} = {result}"
                print(f"  {display}")
                history.append(display)
        
        except ValueError:
            print("Invalid numbers. Example: 10 + 5")

# Run: calculator()

Project 2: To-Do List Manager

A file-based to-do list that persists between runs.

Skills used: file I/O, JSON, functions, loops, error handling

Requirements

  • Add, complete, delete, and list tasks
  • Tasks saved to a JSON file (persists between runs)
  • Each task has: title, status (pending/done), creation date
  • Display tasks with numbering
Show Solution
import json
import os
from datetime import datetime

TODO_FILE = "todos.json"

def load_todos():
    """Load todos from file."""
    if os.path.exists(TODO_FILE):
        with open(TODO_FILE, "r") as f:
            return json.load(f)
    return []

def save_todos(todos):
    """Save todos to file."""
    with open(TODO_FILE, "w") as f:
        json.dump(todos, f, indent=2)

def show_todos(todos):
    """Display all todos."""
    if not todos:
        print("\n  📋 No tasks yet. Add one with 'add'.")
        return
    
    print(f"\n  📋 To-Do List ({len(todos)} tasks)")
    print("  " + "-" * 40)
    for i, todo in enumerate(todos, 1):
        status = "✅" if todo["done"] else "⬜"
        print(f"  {i}. {status} {todo['title']}")
        print(f"       Added: {todo['created']}")

def add_todo(todos):
    """Add a new task."""
    title = input("  Task title: ").strip()
    if not title:
        print("  Title cannot be empty.")
        return
    
    todos.append({
        "title": title,
        "done": False,
        "created": datetime.now().strftime("%Y-%m-%d %H:%M"),
    })
    save_todos(todos)
    print(f"  Added: '{title}'")

def complete_todo(todos):
    """Mark a task as complete."""
    show_todos(todos)
    if not todos:
        return
    
    try:
        num = int(input("  Complete task #: "))
        if 1 <= num <= len(todos):
            todos[num - 1]["done"] = True
            save_todos(todos)
            print(f"  Completed: '{todos[num - 1]['title']}'")
        else:
            print(f"  Invalid number. Choose 1-{len(todos)}")
    except ValueError:
        print("  Please enter a number.")

def delete_todo(todos):
    """Delete a task."""
    show_todos(todos)
    if not todos:
        return
    
    try:
        num = int(input("  Delete task #: "))
        if 1 <= num <= len(todos):
            removed = todos.pop(num - 1)
            save_todos(todos)
            print(f"  Deleted: '{removed['title']}'")
        else:
            print(f"  Invalid number. Choose 1-{len(todos)}")
    except ValueError:
        print("  Please enter a number.")

def main():
    """Main application loop."""
    todos = load_todos()
    
    commands = {
        "list": lambda: show_todos(todos),
        "add": lambda: add_todo(todos),
        "done": lambda: complete_todo(todos),
        "delete": lambda: delete_todo(todos),
    }
    
    print("\n  📝 To-Do Manager")
    print("  Commands: list, add, done, delete, quit")
    
    while True:
        cmd = input("\n> ").strip().lower()
        
        if cmd == "quit":
            print("  Goodbye!")
            break
        elif cmd in commands:
            commands[cmd]()
        else:
            print("  Unknown command. Use: list, add, done, delete, quit")

# Run: main()

Project 3: Number Guessing Game

A game where the computer picks a random number and the player tries to guess it.

Skills used: random module, loops, conditionals, input validation

Requirements

  • Random number between 1 and 100
  • Give “higher/lower” hints after each guess
  • Track number of attempts
  • Show high score (fewest guesses)
  • Option to play again
Show Solution
import random

def play_round():
    """Play one round of the guessing game."""
    secret = random.randint(1, 100)
    attempts = 0
    
    print("\n  🎯 I'm thinking of a number between 1 and 100...")
    
    while True:
        try:
            guess = int(input("  Your guess: "))
        except ValueError:
            print("  Please enter a valid number.")
            continue
        
        attempts += 1
        
        if guess < secret:
            print("  📈 Higher!")
        elif guess > secret:
            print("  📉 Lower!")
        else:
            print(f"  🎉 Correct! You got it in {attempts} attempts!")
            return attempts

def guessing_game():
    """Main game loop with high score tracking."""
    high_score = None
    games_played = 0
    
    print("=" * 40)
    print("  Number Guessing Game")
    print("=" * 40)
    
    while True:
        attempts = play_round()
        games_played += 1
        
        if high_score is None or attempts < high_score:
            high_score = attempts
            print(f"  🏆 New high score: {high_score} attempts!")
        else:
            print(f"  High score: {high_score} attempts")
        
        print(f"  Games played: {games_played}")
        
        again = input("\n  Play again? (y/n): ").strip().lower()
        if again != "y":
            print(f"\n  Thanks for playing! Best: {high_score} attempts in {games_played} games.")
            break

# Run: guessing_game()

Project 4: Password Generator

Generate secure, customizable passwords.

Skills used: random, string module, functions, command-line arguments

Requirements

  • Configurable length (default 16)
  • Toggle: uppercase, lowercase, digits, special characters
  • Option to exclude ambiguous characters (0/O, 1/l/I)
  • Generate multiple passwords at once
  • Check password strength
Show Solution
import random
import string

def generate_password(length=16, uppercase=True, lowercase=True,
                      digits=True, special=True, exclude_ambiguous=False):
    """Generate a secure random password."""
    chars = ""
    required = []  # Ensure at least one of each selected type
    
    if lowercase:
        lc = string.ascii_lowercase
        if exclude_ambiguous:
            lc = lc.replace("l", "")
        chars += lc
        required.append(random.choice(lc))
    
    if uppercase:
        uc = string.ascii_uppercase
        if exclude_ambiguous:
            uc = uc.replace("O", "").replace("I", "")
        chars += uc
        required.append(random.choice(uc))
    
    if digits:
        dg = string.digits
        if exclude_ambiguous:
            dg = dg.replace("0", "").replace("1", "")
        chars += dg
        required.append(random.choice(dg))
    
    if special:
        sp = "!@#$%^&*()-_=+[]{}|;:,.<>?"
        chars += sp
        required.append(random.choice(sp))
    
    if not chars:
        raise ValueError("At least one character type must be selected")
    
    if length < len(required):
        raise ValueError(f"Length must be at least {len(required)}")
    
    # Fill remaining length with random chars
    remaining = length - len(required)
    password_chars = required + [random.choice(chars) for _ in range(remaining)]
    
    # Shuffle so required chars aren't always at the start
    random.shuffle(password_chars)
    return "".join(password_chars)


def check_strength(password):
    """Rate password strength."""
    score = 0
    checks = {
        "Length >= 8": len(password) >= 8,
        "Length >= 12": len(password) >= 12,
        "Length >= 16": len(password) >= 16,
        "Has lowercase": any(c.islower() for c in password),
        "Has uppercase": any(c.isupper() for c in password),
        "Has digits": any(c.isdigit() for c in password),
        "Has special": any(c in string.punctuation for c in password),
    }
    
    for check, passed in checks.items():
        if passed:
            score += 1
    
    if score <= 3:
        strength = "Weak"
    elif score <= 5:
        strength = "Medium"
    else:
        strength = "Strong"
    
    return strength, score, checks


def password_generator():
    """Interactive password generator."""
    print("=" * 40)
    print("  🔐 Password Generator")
    print("=" * 40)
    
    try:
        count = int(input("  How many passwords? [3]: ").strip() or "3")
        length = int(input("  Password length? [16]: ").strip() or "16")
    except ValueError:
        count, length = 3, 16
    
    print(f"\n  Generated {count} passwords (length={length}):\n")
    
    for i in range(count):
        pw = generate_password(length=length)
        strength, score, _ = check_strength(pw)
        print(f"  {i+1}. {pw}  [{strength} {score}/7]")

# Run: password_generator()

Project 5: Contact Book (File-Based)

A contact management system that stores contacts in a CSV file.

Skills used: CSV, file I/O, functions, error handling, data structures

Requirements

  • Add, search, edit, delete contacts
  • Fields: name, phone, email
  • Store in CSV file
  • Case-insensitive search by name
  • Display contacts in a formatted table
Show Solution
import csv
import os

CONTACTS_FILE = "contacts.csv"
FIELDNAMES = ["name", "phone", "email"]

def load_contacts():
    """Load contacts from CSV file."""
    contacts = []
    if os.path.exists(CONTACTS_FILE):
        with open(CONTACTS_FILE, "r", newline="") as f:
            reader = csv.DictReader(f)
            contacts = list(reader)
    return contacts

def save_contacts(contacts):
    """Save contacts to CSV file."""
    with open(CONTACTS_FILE, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
        writer.writeheader()
        writer.writerows(contacts)

def show_contacts(contacts):
    """Display all contacts."""
    if not contacts:
        print("\n  📒 No contacts yet.")
        return
    
    print(f"\n  📒 Contacts ({len(contacts)})")
    print(f"  {'#':<4} {'Name':<20} {'Phone':<15} {'Email':<25}")
    print("  " + "-" * 64)
    for i, c in enumerate(contacts, 1):
        print(f"  {i:<4} {c['name']:<20} {c['phone']:<15} {c['email']:<25}")

def add_contact(contacts):
    """Add a new contact."""
    name = input("  Name: ").strip()
    phone = input("  Phone: ").strip()
    email = input("  Email: ").strip()
    
    if not name:
        print("  Name is required.")
        return
    
    contacts.append({"name": name, "phone": phone, "email": email})
    save_contacts(contacts)
    print(f"  Added: {name}")

def search_contacts(contacts):
    """Search contacts by name (case-insensitive)."""
    query = input("  Search name: ").strip().lower()
    if not query:
        return
    
    results = [c for c in contacts if query in c["name"].lower()]
    
    if results:
        print(f"\n  Found {len(results)} match(es):")
        for c in results:
            print(f"    {c['name']} | {c['phone']} | {c['email']}")
    else:
        print(f"  No contacts matching '{query}'")

def edit_contact(contacts):
    """Edit an existing contact."""
    show_contacts(contacts)
    if not contacts:
        return
    
    try:
        num = int(input("  Edit contact #: ")) - 1
        if not 0 <= num < len(contacts):
            print("  Invalid number.")
            return
    except ValueError:
        print("  Please enter a number.")
        return
    
    contact = contacts[num]
    print(f"  Editing: {contact['name']} (press Enter to keep current value)")
    
    name = input(f"  Name [{contact['name']}]: ").strip() or contact["name"]
    phone = input(f"  Phone [{contact['phone']}]: ").strip() or contact["phone"]
    email = input(f"  Email [{contact['email']}]: ").strip() or contact["email"]
    
    contacts[num] = {"name": name, "phone": phone, "email": email}
    save_contacts(contacts)
    print(f"  Updated: {name}")

def delete_contact(contacts):
    """Delete a contact."""
    show_contacts(contacts)
    if not contacts:
        return
    
    try:
        num = int(input("  Delete contact #: ")) - 1
        if 0 <= num < len(contacts):
            removed = contacts.pop(num)
            save_contacts(contacts)
            print(f"  Deleted: {removed['name']}")
        else:
            print("  Invalid number.")
    except ValueError:
        print("  Please enter a number.")

def main():
    """Main application loop."""
    contacts = load_contacts()
    
    commands = {
        "list": lambda: show_contacts(contacts),
        "add": lambda: add_contact(contacts),
        "search": lambda: search_contacts(contacts),
        "edit": lambda: edit_contact(contacts),
        "delete": lambda: delete_contact(contacts),
    }
    
    print("\n  📒 Contact Book")
    print("  Commands: list, add, search, edit, delete, quit")
    
    while True:
        cmd = input("\n> ").strip().lower()
        if cmd == "quit":
            print(f"  Goodbye! ({len(contacts)} contacts saved)")
            break
        elif cmd in commands:
            commands[cmd]()
        else:
            print("  Unknown command. Use: list, add, search, edit, delete, quit")

# Run: main()

Building Tips

  1. Start small — get the basic version working first
  2. Test often — run your code after every few lines
  3. Add features incrementally — don’t try to build everything at once
  4. Handle errors — think about what could go wrong
  5. Read your own code — if it’s confusing, simplify it

Next up: Coding Challenges →

Updated: