Composing functions and single responsibility
Single responsibility is the idea that each function should do *one* well-named thing. A function that parses input *and* applies a rule *and* formats output is doing three jobs — hard to name, hard to test, hard to reuse.
Composing means building bigger behaviour out of small functions that call each other. A high-level function reads like a sentence: "get the count, decide if it's an alert, format the result" — each step delegated to a focused helper.
The payoff is real. Small functions are easy to test in isolation (check extract_count once, trust it forever), easy to reuse (the same helper serves many callers), and easy to read (the top-level function tells the story; the detail lives below). When one part needs changing, you change one small function, not a tangled monolith.
A good smell test: if you struggle to name a function without using "and", it's probably doing too much — split it.
Syntax
# One job each:
def extract_count(line): ... # parse
def is_alert(count, limit): ... # decide
# Compose them in a higher-level function:
def report(line, limit):
count = extract_count(line) # step 1
if is_alert(count, limit): # step 2
return f"ALERT: {count}" # step 3
return f"OK: {count}"Worked examples
Three focused functions, composed by a fourth
def extract_count(line):
for part in line.split():
if part.startswith("count:"):
return int(part.split(":")[1])
return 0
def is_alert(count, threshold):
return count > threshold
def report(line, threshold):
count = extract_count(line)
if is_alert(count, threshold):
return f"ALERT: {count}"
return f"OK: {count}"
print(report("errors count:12 today", 10)) # ALERT: 12
print(report("errors count:3 today", 10)) # OK: 3Each helper is testable on its own
print(extract_count("errors count:12 today")) # 12
print(is_alert(12, 10)) # True
print(is_alert(5, 10)) # FalseOne function's output feeds the next
def normalise(name):
return name.strip().lower()
def initials(name):
return "".join(word[0] for word in name.split())
print(initials(normalise(" Sam Hembury "))) # shCommon mistakes & gotchas
The 'and' test for doing too much
If the honest name of a function is parse_and_validate_and_format, that's three functions wearing a trenchcoat. Split on the 'and's — each piece gets a clear single-purpose name.
Helpers that secretly depend on outside state
A helper should take what it needs as arguments and return its result, not read or mutate global variables. Hidden dependencies make a function impossible to reuse or test in isolation — the whole point of splitting it up.
Over-splitting into one-line functions
Single responsibility doesn't mean one line per function. Wrapping a + b in a def adds indirection for no gain. Split when a chunk has a meaningful name and a real job — not reflexively.
Why it matters
Every large codebase is a tree of small functions calling smaller ones. Code written this way is the code that survives — it can be tested, reused, and changed without fear. Refactoring a tangled function into focused pieces is one of the most common things a developer does, and it's the habit that separates code you can maintain from code you dread touching.