☰ Learn Python Tutorial Menu
Practice Challenges: Put It All Together
Written by CSA mentors · Updated 26 Sept 2026 · 7 min read
Here's what the first week looks like for a junior analyst at a Karachi fintech we've placed students with. Monday is cleaning phone numbers and computing fees. Wednesday is an ageing report. By Friday they've loaded a month of wallet transactions into SQLite and produced a flag list for the compliance team. These ten challenges are that week, condensed. Each is graded from warm-up to build-it. Attempt every one in the playground before opening the solution, then compare and improve your version. None of them needs pandas, and all ten run in the playground. If you're preparing for a technical interview, several of these are close cousins of questions our interview preparation mentors hear candidates being asked.
Tier 1: Warm-up
Challenge 1: GST calculator
Write gst_breakdown(gross) that takes a GST-inclusive price (17%) and returns a tuple of (net, tax), each rounded to two decimals. Print the result for Rs 4,500.
Solution
def gst_breakdown(gross, rate=0.17):
net = gross / (1 + rate)
return round(net, 2), round(gross - net, 2)
net, tax = gst_breakdown(4500)
print(f"Net Rs {net:,.2f}, GST Rs {tax:,.2f}") # Net Rs 3,846.15, GST Rs 653.85
Challenge 2: Grade classifier
Given a list of (name, marks) tuples, print each student's grade (A 80+, B 70+, C 60+, D 40+, F below) and then the count of each grade. Notice how the solution uses a list of cut-offs instead of an elif ladder. It's the same idea as the tariff dictionary from Lesson 10.
Solution
from collections import Counter
def grade(marks):
for cutoff, letter in [(80, "A"), (70, "B"), (60, "C"), (40, "D")]:
if marks >= cutoff:
return letter
return "F"
students = [("Hira", 91), ("Ali", 58), ("Sana", 74), ("Bilal", 35), ("Zara", 80)]
grades = Counter()
for name, marks in students:
g = grade(marks)
grades[g] += 1
print(f"{name:<6} {marks:>3} {g}")
print(dict(sorted(grades.items())))
Challenge 3: Word counter
Count the five most common words in a paragraph, ignoring case and punctuation. This one comes up in interviews more than any other on this page.
Solution
import re
from collections import Counter
text = """Sales in Lahore rose again. Lahore now leads Karachi, and Karachi
leads Multan. Sales teams in Lahore expect sales to rise further."""
words = re.findall(r"[a-z]+", text.lower())
for word, n in Counter(words).most_common(5):
print(f"{word:<8} {n}")
Tier 2: Data tasks
Challenge 4: Sales by city
From a list of (city, amount) tuples, print each city's total, average and share of overall revenue, sorted by total descending.
Solution
from collections import defaultdict
rows = [("Lahore", 2500), ("Multan", 1800), ("Lahore", 3200), ("Karachi", 9100),
("Lahore", 1500), ("Karachi", 2700), ("Multan", 8400)]
by_city = defaultdict(list)
for city, amount in rows:
by_city[city].append(amount)
grand = sum(a for _, a in rows)
for city, vals in sorted(by_city.items(), key=lambda kv: sum(kv[1]), reverse=True):
total = sum(vals)
print(f"{city:<8} total {total:>6,} avg {total / len(vals):>7,.0f} share {total / grand:.1%}")
Challenge 5: Clean phone numbers
Normalise a list of Pakistani mobile numbers to the form 03XXXXXXXXX, and report any that are invalid after cleaning (must be 11 digits starting with 03). Watch the last test number. It's a Lahore landline, and a good cleaner should reject it rather than mangle it.
Solution
def clean_phone(raw):
digits = "".join(ch for ch in raw if ch.isdigit())
if digits.startswith("92"):
digits = "0" + digits[2:]
return digits if len(digits) == 11 and digits.startswith("03") else None
raw_numbers = ["0300-1234567", "+92 321 7654321", "0345 12345", "92-333-9876543", "042-35761234"]
for raw in raw_numbers:
cleaned = clean_phone(raw)
print(f"{raw:<18} -> {cleaned or 'INVALID'}")
Challenge 6: Overdue invoices
Given invoices as (ref, issued_iso, amount) with 30-day terms, list those overdue on 2026-09-24 with days overdue and a late fee of Rs 50 per day after the first three days.
Solution
from datetime import date, timedelta
today = date(2026, 9, 24)
invoices = [("INV-201", "2026-06-30", 45000), ("INV-214", "2026-08-20", 12500),
("INV-230", "2026-09-15", 78000), ("INV-233", "2026-08-01", 6000)]
for ref, issued, amount in invoices:
due = date.fromisoformat(issued) + timedelta(days=30)
late = (today - due).days
if late > 0:
fee = max(0, late - 3) * 50
print(f"{ref} due {due} {late:>3} days late fee Rs {fee:,}")
Challenge 7: CSV summary with bad rows
Write a CSV with a few rows including two invalid amounts, then read it back and print the total of valid rows and a list of the rejected rows.
Solution
import csv
with open("orders.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["id", "city", "amount"])
w.writerows([[1, "Lahore", "2500"], [2, "Multan", "N/A"], [3, "Karachi", "9100"], [4, "Lahore", ""]])
total, rejected = 0.0, []
with open("orders.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
try:
total += float(row["amount"])
except ValueError:
rejected.append(row)
print(f"Total Rs {total:,.0f}; rejected {len(rejected)} rows: {[r['id'] for r in rejected]}")
Tier 3: Build it
Challenge 8: Inventory class
Build an Inventory class with add(sku, qty, price), sell(sku, qty) (raising an error if stock is insufficient), value() and a __len__. Demonstrate it, including a rejected sale.
Solution
class OutOfStock(Exception):
pass
class Inventory:
def __init__(self):
self._items = {} # sku -> {"qty": int, "price": float}
def add(self, sku, qty, price):
item = self._items.setdefault(sku, {"qty": 0, "price": price})
item["qty"] += qty
item["price"] = price
def sell(self, sku, qty):
item = self._items.get(sku)
if item is None or item["qty"] < qty:
raise OutOfStock(f"{sku}: have {item['qty'] if item else 0}, need {qty}")
item["qty"] -= qty
return qty * item["price"]
def value(self):
return sum(i["qty"] * i["price"] for i in self._items.values())
def __len__(self):
return len(self._items)
inv = Inventory()
inv.add("NB-01", 40, 250)
inv.add("PN-07", 100, 40)
print("Sold for", inv.sell("NB-01", 5))
try:
inv.sell("PN-07", 500)
except OutOfStock as e:
print("Rejected:", e)
print(len(inv), "SKUs worth Rs", f"{inv.value():,.0f}")
Challenge 9: sqlite3 mini report
Load the sales rows from Challenge 4 into an in-memory SQLite table and answer with SQL: total per city, the single largest sale, and the number of sales above the overall average.
Solution
import sqlite3
rows = [("Lahore", 2500), ("Multan", 1800), ("Lahore", 3200), ("Karachi", 9100),
("Lahore", 1500), ("Karachi", 2700), ("Multan", 8400)]
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE sales (city TEXT, amount REAL)")
con.executemany("INSERT INTO sales VALUES (?, ?)", rows)
for city, total in con.execute("SELECT city, SUM(amount) FROM sales GROUP BY city ORDER BY 2 DESC"):
print(f"{city:<8} {total:,.0f}")
print("Largest:", con.execute("SELECT city, MAX(amount) FROM sales").fetchone())
print("Above average:", con.execute(
"SELECT COUNT(*) FROM sales WHERE amount > (SELECT AVG(amount) FROM sales)").fetchone()[0])
Challenge 10: JSON transaction audit
Parse a JSON array of mobile-wallet transactions and flag any account that either sent more than Rs 50,000 in total or made more than two transactions between 00:00 and 05:00. Output the flags as JSON. This is a simplified version of a real compliance rule, and the shape (accumulate per account, then test thresholds) is one you'll write again and again.
Solution
import json
from collections import defaultdict
feed = json.loads('''[
{"from": "0300111", "amount": 20000, "time": "2026-09-24T01:10:00"},
{"from": "0300111", "amount": 35000, "time": "2026-09-24T13:00:00"},
{"from": "0321222", "amount": 900, "time": "2026-09-24T02:00:00"},
{"from": "0321222", "amount": 1200, "time": "2026-09-24T03:30:00"},
{"from": "0321222", "amount": 700, "time": "2026-09-24T04:45:00"},
{"from": "0333444", "amount": 4000, "time": "2026-09-24T11:00:00"}]''')
sent = defaultdict(float)
night = defaultdict(int)
for t in feed:
sent[t["from"]] += t["amount"]
if int(t["time"][11:13]) < 5:
night[t["from"]] += 1
flags = []
for acct in sent:
reasons = []
if sent[acct] > 50000:
reasons.append("high volume")
if night[acct] > 2:
reasons.append("night activity")
if reasons:
flags.append({"account": acct, "sent": sent[acct], "night_txns": night[acct], "reasons": reasons})
print(json.dumps(flags, indent=2))
Friday afternoon at the fintech: The night-activity flag list from Challenge 10 is, give or take a few columns, what that new analyst handed to the compliance team at the end of week one. They still receive it daily. Nothing in it goes beyond what you've just practised.
Where to go next
Most analytics tasks break down into parse, clean, group, aggregate, report. Functions and classes keep each step testable, and exceptions keep bad rows from stopping the run. Counter, defaultdict, datetime, csv, json and sqlite3 cover a remarkable amount without a single install. When you compare your solution with the reference, ask whether yours is clearer, safer or faster, and improve one thing. Then take the CSA SQL track, and come back to the pandas lessons with a real dataset of your own.
Stretch tasks
- Extend Challenge 7 to also write the rejected rows to
rejected.csvwith areasoncolumn. - Combine Challenges 5 and 10: clean the
fromnumbers first, so the same account written two ways is aggregated together. - Rewrite Challenge 4 as a
Reportclass withadd(city, amount)and a__str__that prints the table, then feed it the rows from Challenge 9 via SQL.
Lesson 26 of 26
Sign in to track your progress and earn learning points for every lesson you finish.
