Chapter 3 · Junior Developer

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: 3

Each 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))    # False

One 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 ")))   # sh

Common 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.