Problem Decomposition
Break a vague, overwhelming problem into smaller pieces, each with a clear job, before writing any code.
By the end of this lesson, you can
- Split a vague problem statement into smaller subproblems, each with a clear input and output
- Explain why decomposing a problem makes each piece easier to test and verify on its own
- Map a decomposed problem onto a set of small, cooperating functions
Why it matters
“Print a receipt for a shopping cart” is easy to say and hard to know where to start typing. Most real problems feel this way — not because they’re conceptually difficult, but because they’re several smaller problems tangled together. Decomposition is the skill of untangling them before writing any code, so each piece is small enough to actually think about clearly.
Mental model
Instead of asking “how do I write this whole program?”, ask “what are the separate jobs this program needs done?” For the receipt problem:
- Add up the prices → a subtotal
- Work out tax on that subtotal → a tax amount
- Add subtotal and tax together → a total
- Turn all of that into readable text → a receipt
Each of those is small enough to describe in one sentence, has an obvious input and output, and can be checked on its own before the others exist.
Small example
Decomposition maps naturally onto functions — one function per
subproblem. (sum(prices) is a built-in function that adds up every
number in a list — a small decomposition of its own, provided for you.)
def subtotal(prices):
return sum(prices)
def tax(subtotal, rate):
return subtotal * rate
def total(subtotal, tax_amount):
return subtotal + tax_amount
def format_receipt(subtotal, tax_amount, total_amount):
return "Subtotal: " + str(subtotal) + "\nTax: " + str(tax_amount) + "\nTotal: " + str(total_amount)
prices = [4.50, 2.00, 6.25]
sub = subtotal(prices)
tax_amount = tax(sub, 0.08)
grand_total = total(sub, tax_amount)
print(format_receipt(sub, tax_amount, grand_total))Subtotal: 12.75
Tax: 1.02
Total: 13.77Every function here does exactly one job. If the total is wrong, there are only four small, separately checkable places to look — not one long block where a mistake could be hiding anywhere.
Plan it
Before writing code, it helps to write down each subproblem’s interface — what goes in, what comes out — the same way you’d think about a function’s parameters and return value:
| Subproblem | Input | Output |
|---|---|---|
| subtotal | a list of prices | their sum |
| tax | a subtotal and a rate | the tax amount |
| total | a subtotal and a tax amount | their sum |
| format_receipt | subtotal, tax, and total | a readable string |
Notice that total doesn’t need to know how subtotal or tax were
calculated — only what values they produced. That separation is what
makes each piece independently understandable.
Check your understanding
Or reveal the answer without checking
Answer:average(scores) that returns a number, and passed(average) that returns True or False
average(scores) has one job: turn a list of scores into a single number. passed(average) has a separate job: turn that number into a yes/no answer. Each can be tested on its own without the other.
Or reveal the answer without checking
Answer:Each small piece can be checked independently, so a wrong result narrows down to one specific piece instead of the whole program
When each subproblem is its own function with a clear input and output, you can test and trust each one separately — so when something's wrong, you already know which piece to look at first.
Practice: warm-up
Decompose this problem into three or four subproblems, each with a one-sentence job description: “Given a list of email addresses, find which ones are valid, and report how many are from a specific company domain.”
Stuck? Reveal one hint at a time.
Hint 1
Start by asking what "valid" means as its own separate check, independent of counting anything.
Hint 2
Counting-by-domain and checking-validity can be two completely separate subproblems that don't need to know about each other.
Reveal one reasonable decomposition
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
is_valid_email(address) -> True or False
filter_valid(addresses) -> the addresses that pass is_valid_email
is_company_domain(address, domain) -> True or False
count_from_domain(addresses, domain) -> a numberThere’s more than one reasonable way to split this — the point is that each piece above has one job and doesn’t need to know how the others work.
Practice: apply it
Here’s an undecomposed version of the receipt program from this lesson:
prices = [4.50, 2.00, 6.25]
sub = 0
for price in prices:
sub = sub + price
result = sub + (sub * 0.08)
print("Total: " + str(result))
Or reveal the answer without checking
Answer:You would have to read through the whole block, since subtotal and tax are combined into one expression with no separate, testable piece
Here, sub * 0.08 is buried inside one combined expression with no name of its own — there's no separate, individually testable 'tax' step to check in isolation, unlike the decomposed version.
Modification challenge: rewrite this block as three small functions
(subtotal, tax, total), the way the lesson’s example did.
Summary
- Decomposition means splitting a problem into smaller subproblems, each with a clear input and output.
- Each subproblem should be small enough to describe in one sentence and check independently of the others.
- In Python, decomposed subproblems usually become separate functions that call each other.
- Decomposition pays off even for small problems — it isn’t only for large, complex ones.