Write a contract before a function
A function has inputs, a result and rules about invalid inputs. “Summarize expenses” is too vague. A better contract is: receive non-negative integer amounts in paise and a non-negative budget; return the total, largest amount or None if empty, and a boolean indicating whether total exceeds budget.
Use functions to separate calculation from reading files and printing. Pure calculations are easier to test because the same inputs produce the same result without external effects.
Choose the collection that matches the question
| Collection | Use when | Example |
|---|---|---|
| List | Order and duplicates matter | Expenses in entry order |
| Set | Membership or uniqueness matters | Unique categories |
| Dictionary | Look up a value by key | Category totals |
| Tuple | A fixed grouping of values | Hours and leftover minutes |
A set does not preserve a sorted order for presentation. Sort it explicitly. Dictionary lookup is expected O(1), not an unconditional worst-case guarantee. A nested loop over all pairs is usually O(n²); replacing repeated searches with a set can reduce work when extra memory is acceptable.
Worked expense summary
def summarize_expenses(amounts, budget):
if not isinstance(budget, int) or isinstance(budget, bool) or budget < 0:
raise ValueError("Budget must be a non-negative integer")
total = 0
largest = None
for amount in amounts:
if not isinstance(amount, int) or isinstance(amount, bool) or amount < 0:
raise ValueError("Expenses must be non-negative integers")
total += amount
if largest is None or amount > largest:
largest = amount
return {"total": total, "largest": largest, "over_budget": total > budget}
assert summarize_expenses([], 0) == {
"total": 0, "largest": None, "over_budget": False
}
assert summarize_expenses([120, 80], 200)["over_budget"] is False
assert summarize_expenses([120, 80], 199)["over_budget"] is True
Python booleans are instances of integer, so the additional boolean check is intentional. This function processes n expenses in O(n) time and keeps O(1) additional state. The return dictionary has a fixed number of fields.
Group related records
def totals_by_category(records):
totals = {}
for category, amount in records:
if amount < 0:
raise ValueError("Amount must be non-negative")
totals[category] = totals.get(category, 0) + amount
return totals
assert totals_by_category([
("food", 100), ("travel", 50), ("food", 75)
]) == {"food": 175, "travel": 50}
A missing key is different from a key with a value of zero. get(category, 0) supplies a starting total for this specific operation. Do not apply default values mechanically to identifiers or missing required fields.
Mutation and shared references
original = [1, 2]
alias = original
copy = original.copy()
alias.append(3)
assert original == [1, 2, 3]
assert copy == [1, 2]
Assignment does not copy a list. A shallow copy makes a new outer list but still shares nested mutable objects. Before modifying input, decide whether your contract allows it. In interviews, state whether the solution changes the input.
Avoid a mutable default argument. Prefer items=None and create the list inside the function. Default objects are evaluated once when the function is defined, not freshly for every call.
Persist data in a local file
The following file-writing example runs locally, not in every browser sandbox. Place it in a temporary practice folder. It stores non-sensitive exercise data; do not put passwords or tokens in this file.
import json
from pathlib import Path
path = Path("expenses.json")
records = [{"category": "food", "amount": 120}]
path.write_text(json.dumps(records), encoding="utf-8")
loaded = json.loads(path.read_text(encoding="utf-8"))
assert loaded == records
Reading may fail because the file is missing, inaccessible or malformed. Catch the exception you can meaningfully handle. Catching every exception and returning an empty list would make corrupted data look like no expenses. Production multi-user storage also needs concurrency control and access rules; a local JSON file is a learning step, not a database substitute.
Practice with answers
Task A: Return unique lowercase email strings from a list, preserving the first occurrence. For this exercise, normalize by stripping whitespace and lowercasing; this is a simplified product rule, not a universal statement about email identity.
def unique_emails(emails):
result = []
seen = set()
for email in emails:
normalized = email.strip().lower()
if normalized not in seen:
seen.add(normalized)
result.append(normalized)
return result
assert unique_emails([" A@example.com ", "a@example.com", "b@example.com"]) == [
"a@example.com", "b@example.com"
]
Task B: Find the most frequent category. If frequencies tie, return the lexicographically smallest category; return None on empty input.
def most_frequent(categories):
counts = {}
for category in categories:
counts[category] = counts.get(category, 0) + 1
if not counts:
return None
return min(counts, key=lambda category: (-counts[category], category))
assert most_frequent(["travel", "food", "travel", "food"]) == "food"
assert most_frequent([]) is None
The tie rule is part of the specification. Without it, two correct-looking implementations could disagree. Explain the O(n + k) expected time, where k is the number of distinct categories.
Checkpoint
Build a small command-line expense report that reads JSON, validates required fields, prints totals by category and reports bad input clearly. Include tests for empty data, repeated categories, negative expenses and malformed JSON. Continue to Debugging.
Reference: Python data structures and input/output.