⚡ PHASE 2 · PROJECTS

Phase 2
Real Programs

Conditionals, loops, lists, dicts, sets, and functions — all working together. Build 4 programs that feel genuinely useful.

4
PROJECTS
~3h
BUILD TIME
6
CONCEPTS
🟡
BEGINNER+
LESSON 6
Conditionals
LESSON 7
Loops
LESSON 8
Lists & Tuples
LESSON 9
Dicts & Sets
LESSON 10
Functions
🎯
PROJECT 01 OF 04
Number Guessing Game
A full game loop with difficulty levels, attempt tracking, hint system, scoring, high-score tracking across rounds using a dictionary, and replay support.
while loops if/elif/else functions dictionaries random module
✅ Features
  • 3 difficulty levels (Easy/Medium/Hard)
  • Adaptive hints: hot/warm/cold feedback
  • Attempt counter with max-attempts limit
  • Score calculated from attempts used
  • High scores stored per difficulty in a dict
  • Replay loop — play multiple rounds
🧠 Concepts Used
  • random.randint() for secret number
  • Functions: setup_game(), get_hint(), calc_score()
  • Dict for difficulty config and high scores
  • while loop for game loop and retry
  • if/elif chain for hint logic
number_guessing.py
PYTHON
# ══════════════════════════════════════════════
#   Number Guessing Game — Phase 2 Project 1
# ══════════════════════════════════════════════
import random

LEVELS = {
    "easy"  : {"range": (1, 50),  "attempts": 10},
    "medium": {"range": (1, 100), "attempts": 7},
    "hard"  : {"range": (1, 200), "attempts": 5},
}
high_scores = {"easy": 0, "medium": 0, "hard": 0}

def get_hint(guess, secret):
    """Return hot/warm/cold hint based on distance."""
    diff = abs(guess - secret)
    if   diff == 0:  return "🎯 CORRECT!"
    elif diff <= 5:  return "🔥 Very hot!"
    elif diff <= 15: return "♨️  Warm"
    elif diff <= 30: return "❄️  Cold"
    else:             return "🧊 Freezing!"

def calc_score(attempts_used, max_attempts):
    """Score based on how few attempts were used."""
    return max(0, (max_attempts - attempts_used + 1) * 10)

def play_round(level):
    """Play one round and return score (0 if lost)."""
    cfg    = LEVELS[level]
    lo, hi = cfg["range"]
    secret = random.randint(lo, hi)
    max_att= cfg["attempts"]
    att    = 0

    print(f"\n  Guess a number between {lo} and {hi}. ({max_att} attempts)\n")

    while att < max_att:
        att += 1
        try:
            guess = int(input(f"  Attempt {att}/{max_att}: "))
        except ValueError:
            print("  Enter a whole number!"); att -= 1; continue

        hint = get_hint(guess, secret)
        print(f"  {hint}")

        if guess == secret:
            score = calc_score(att, max_att)
            print(f"  🏆 You got it in {att} attempts! Score: {score}")
            return score

    print(f"  😞 Out of attempts. Answer was {secret}.")
    return 0

# ── Main game loop ────────────────────────────
print("=" * 42)
print("         🎯  NUMBER GUESSING GAME")
print("=" * 42)

while True:
    print("\n  Difficulty: easy | medium | hard")
    lvl = input("  Choose: ").strip().lower()
    if lvl not in LEVELS:
        print("  ❌ Invalid level."); continue

    score = play_round(lvl)
    if score > high_scores[lvl]:
        high_scores[lvl] = score
        print(f"  ⭐ New high score on {lvl}: {score}!")
    print(f"  High scores: {high_scores}")

    if input("\n  Play again? (y/n): ").lower() != "y":
        print("  Thanks for playing! 👋")
        break
Extension ideas
Add a leaderboard list sorted by score. Add a timer using time.time() to penalise slow guessing. Add binary search hint: tell the user if their guess is too high or too low.
💰
PROJECT 02 OF 04
Personal Expense Tracker
A fully functional expense manager. Add expenses with category and description, view summaries grouped by category, find the biggest spender, and see a visual bar chart — all in the terminal.
lists of dicts dict grouping functions list comprehension sorted + lambda
✅ Features
  • Add expenses: amount, category, description
  • View all expenses in a formatted table
  • Summary grouped by category with totals
  • ASCII bar chart of spending by category
  • Filter expenses by category
  • Total budget vs spending report
🧠 Concepts Used
  • List of dicts as the data store
  • Functions for each menu action
  • Dict grouping with .setdefault()
  • sorted(key=lambda) for ranking
  • List comprehension for filtering
  • f-string :,.2f currency formatting
expense_tracker.py
PYTHON
# ══════════════════════════════════════════════
#   Expense Tracker — Phase 2 Project 2
# ══════════════════════════════════════════════
expenses = []

def add_expense(amount, category, desc):
    """Add an expense record to the list."""
    expenses.append({
        "amount"  : float(amount),
        "category": category.strip().title(),
        "desc"    : desc.strip()
    })

def view_all():
    """Print all expenses in a table."""
    if not expenses:
        print("  No expenses yet."); return
    print(f"\n  {'#':4} {'Category':14} {'Amount':>10}  Description")
    print("  " + "─" * 50)
    for i, e in enumerate(expenses, 1):
        print(f"  {i:4} {e['category']:14} PKR {e['amount']:>7,.0f}  {e['desc']}")
    print(f"\n  Total: PKR {sum(e['amount'] for e in expenses):,.2f}")

def summary_by_category():
    """Group and total expenses by category."""
    grouped = {}
    for e in expenses:
        grouped.setdefault(e["category"], 0)
        grouped[e["category"]] += e["amount"]

    ranked = sorted(grouped.items(), key=lambda x: x[1], reverse=True)
    max_amt= ranked[0][1] if ranked else 1

    print(f"\n  {'Category':16} {'Total':>10}  Chart")
    print("  " + "─" * 50)
    for cat, amt in ranked:
        bar = "█" * int(amt / max_amt * 20)
        print(f"  {cat:16} PKR {amt:>7,.0f}  {bar}")

def filter_by_category(cat):
    """Show expenses matching a category."""
    found = [e for e in expenses if e["category"].lower() == cat.lower()]
    if not found: print("  No expenses in that category."); return
    for e in found:
        print(f"  PKR {e['amount']:,.0f} — {e['desc']}")
    print(f"  Subtotal: PKR {sum(e['amount'] for e in found):,.2f}")

# ── Menu ─────────────────────────────────────
print("═" * 44)
print("        💰  EXPENSE TRACKER")
print("═" * 44)
while True:
    print("\n  1) Add  2) View all  3) Summary  4) Filter  5) Quit")
    ch = input("  Choice: ").strip()
    if   ch == "1":
        amt  = input("  Amount (PKR): ")
        cat  = input("  Category   : ")
        desc = input("  Description: ")
        add_expense(amt, cat, desc)
        print("  ✅ Added.")
    elif ch == "2": view_all()
    elif ch == "3": summary_by_category()
    elif ch == "4": filter_by_category(input("  Category: "))
    elif ch == "5": print("  Bye! 👋"); break
💡
ASCII bar chart technique
The bar chart uses "█" * int(amount / max_amount * 20) — normalising each amount to a 0–20 scale so the largest bar always fills 20 blocks. This scales correctly regardless of the actual currency amounts.
🔐
PROJECT 03 OF 04
Caesar Cipher
A classic encryption tool. Encrypt and decrypt any text using the Caesar cipher shift algorithm, with brute-force crack mode that tries all 26 shifts and a frequency-analysis hint to identify the most likely correct decryption.
string manipulation for loops functions chr() & ord() dict freq analysis
✅ Features
  • Encrypt text with any shift (1–25)
  • Decrypt with known shift
  • Brute-force: try all 26 shifts
  • Frequency analysis hint (most common letter)
  • Preserves case, spaces, punctuation
🧠 Concepts Used
  • ord() / chr() for ASCII math
  • Modulo % 26 for wrap-around
  • Functions: encrypt(), decrypt(), crack()
  • Dict comprehension for letter frequency
  • sorted(key=lambda) to find most common
caesar_cipher.py
PYTHON
# ══════════════════════════════════════════════
#   Caesar Cipher — Phase 2 Project 3
# ══════════════════════════════════════════════

def shift_char(ch, shift):
    """Shift a single letter by shift positions (preserves case)."""
    if ch.isalpha():
        base  = ord('A') if ch.isupper() else ord('a')
        return chr((ord(ch) - base + shift) % 26 + base)
    return ch   # non-letters unchanged

def encrypt(text, shift):
    """Encrypt text with Caesar cipher."""
    return "".join(shift_char(c, shift) for c in text)

def decrypt(text, shift):
    """Decrypt by shifting backwards."""
    return encrypt(text, -shift)

def crack(ciphertext):
    """Try all 26 shifts and highlight most likely."""
    # English letter frequencies — 'e' is most common
    freq  = {c: ciphertext.lower().count(c) for c in "abcdefghijklmnopqrstuvwxyz"}
    most_common = max(freq, key=freq.get)
    likely_shift= (ord(most_common) - ord('e')) % 26

    print(f"\n  Most common letter: '{most_common}' → likely shift: {likely_shift}\n")
    for s in range(26):
        marker = " ← likely" if s == likely_shift else ""
        print(f"  [{s:2}] {decrypt(ciphertext, s)[:50]}{marker}")

# ── Menu ─────────────────────────────────────
print("=" * 44)
print("           🔐  CAESAR CIPHER")
print("=" * 44)

while True:
    print("\n  1) Encrypt  2) Decrypt  3) Crack  4) Quit")
    ch = input("  Choice: ")

    if ch == "1":
        text  = input("  Text to encrypt: ")
        shift = int(input("  Shift (1-25)   : "))
        print(f"  🔒 Encrypted: {encrypt(text, shift)}")
    elif ch == "2":
        text  = input("  Encrypted text : ")
        shift = int(input("  Shift          : "))
        print(f"  🔓 Decrypted: {decrypt(text, shift)}")
    elif ch == "3":
        text = input("  Ciphertext to crack: ")
        crack(text)
    elif ch == "4":
        print("  Encrypting goodbye... Hmm 👋"); break
The magic behind shift_char
ord('A') gives 65. Subtracting base converts a letter to 0–25. Adding shift, modulo 26, then adding base back converts back to a character. The % 26 handles wrap-around (Z+1 = A). This is all standard ASCII arithmetic.
🎓
PROJECT 04 OF 04
Reusable Quiz Engine
A data-driven quiz engine powered by a list of dicts. Load any question bank, randomise order, time each question, track per-category performance, and generate a full analytics report with weak areas.
list of dicts functions dict comprehension random.shuffle time module
✅ Features
  • Questions stored as list of dicts
  • Randomised question order each run
  • Timer per question (penalises slow answers)
  • Per-category score tracking
  • Full analytics report with weak areas
  • Replayable with reshuffled questions
🧠 Concepts Used
  • random.shuffle() for question order
  • time.time() for per-question timing
  • Functions: run_quiz(), show_report()
  • Dict grouping for category analytics
  • List comprehension for weak categories
  • Nested dict for tracking scores
quiz_engine.py
PYTHON
# ══════════════════════════════════════════════
#   Reusable Quiz Engine — Phase 2 Project 4
# ══════════════════════════════════════════════
import random, time

QUESTIONS = [
    {"q": "What does type('42') return?",
     "opts": ["int", "str", "float", "None"], "ans": "b", "cat": "Types"},
    {"q": "What does 17 % 5 equal?",
     "opts": ["3", "3.4", "2", "0"], "ans": "c", "cat": "Operators"},
    {"q": "What is the output of 'Python'[::-1]?",
     "opts": ["Python", "nohtyP", "Pyt", "Error"], "ans": "b", "cat": "Strings"},
    {"q": "Which creates an empty set?",
     "opts": ["{}", "set()", "[]", "dict()"], "ans": "b", "cat": "Collections"},
    {"q": "What does *args collect?",
     "opts": ["dict", "tuple", "list", "set"], "ans": "b", "cat": "Functions"},
    {"q": "LEGB stands for…?",
     "opts": ["Local-Enclosing-Global-Built-in", "Loop-Else-Get-Break",
               "Lambda-Enum-Global-Block", "None of these"],
     "ans": "a", "cat": "Functions"},
]

def run_quiz(questions, time_limit=15):
    """Run the quiz, return results dict."""
    random.shuffle(questions)
    results  = {"correct": 0, "wrong": 0, "categories": {}}

    for i, q in enumerate(questions, 1):
        print(f"\n  Q{i}/{len(questions)} [{q['cat']}]  {q['q']}")
        for j, opt in zip("abcd", q["opts"]):
            print(f"    {j}) {opt}")

        t0 = time.time()
        ans= input("  Answer (a/b/c/d): ").strip().lower()
        elapsed = time.time() - t0

        cat = q["cat"]
        results["categories"].setdefault(cat, {"correct": 0, "total": 0})
        results["categories"][cat]["total"] += 1

        if ans == q["ans"] and elapsed <= time_limit:
            print(f"  ✅ Correct! ({elapsed:.1f}s)")
            results["correct"] += 1
            results["categories"][cat]["correct"] += 1
        elif elapsed > time_limit:
            print(f"  ⏰ Too slow ({elapsed:.1f}s). Answer: {q['ans']}")
            results["wrong"] += 1
        else:
            print(f"  ❌ Wrong. Answer: {q['ans']}")
            results["wrong"] += 1
    return results

def show_report(results, total):
    """Print analytics report."""
    pct  = results["correct"] / total * 100
    weak = [c for c, s in results["categories"].items()
             if s["correct"] / s["total"] < 0.5]
    print(f"""
  ╔═══════════════════════════════╗
    QUIZ REPORT
    Score   : {results['correct']}/{total} ({pct:.0f}%)
    ─────────────────────────────
    Category Breakdown:""")
    for c, s in results["categories"].items():
        print(f"    {c:15} {s['correct']}/{s['total']}")
    if weak: print(f"\n    ⚠️  Study more: {', '.join(weak)}")
    print("  ╚═══════════════════════════════╝")

# ── Run ──────────────────────────────────────
print("═" * 44 + "\n     🎓  QUIZ ENGINE  (15s per question)\n" + "═" * 44)
while True:
    res = run_quiz(QUESTIONS.copy())
    show_report(res, len(QUESTIONS))
    if input("\n  Play again? (y/n): ").lower() != "y": break
This is a real architecture pattern
Storing questions as a list of dicts is how real quiz apps work — swapping the QUESTIONS list for a JSON file or database means zero code changes. The engine is fully reusable for any subject just by changing the question data. This separation of data from logic is fundamental software design.
🏆
Phase 2 Complete!
You've built 4 real programs using every Phase 2 concept. You write structured, reusable, well-organised Python. Phase 3 introduces Object-Oriented Programming — where your code starts to mirror the real world.
Conditionals ✓ Loops ✓ Lists & Tuples ✓ Dicts & Sets ✓ Functions ✓ *args/**kwargs ✓ Lambda ✓ Scope (LEGB) ✓ 4 Real Projects ✓
Course Home
Phase 2 Complete4 projects built