Functions
Chapter 2 was about naming a thing well. This chapter is about the next unit up: the function. Functions are the sentences of a program, and just like sentences, most of them are badly written on the first draft. Robert C. Martin's rules for functions in Clean Code are less about elegance and more about a blunt, testable question: can someone read this function once and immediately trust what it does?
Small, and then smaller
The first rule for functions is that they should be small. The second rule is that they should be smaller than that. A function that fits inside a screen, ideally under about twenty lines, is easy to hold in your head; one that scrolls is already asking you to juggle more state than a reader should have to.
Smallness isn't a vanity metric. A short function has fewer branches, fewer local variables, and fewer places a bug can hide. Long functions accumulate size the same way a junk drawer accumulates junk: one "just one more thing" at a time, until the function does five unrelated jobs and its name only describes the first one.
Smallness constrains shape, too: the body of an if or a loop should ideally be a single
line, usually a call to a well-named function, and a function's indentation shouldn't run
deeper than one or two levels. A function with room for nested blocks is a function with
room to keep growing.
Do one thing
"Do one thing" is the rule smallness is actually chasing. A function that reads a file, validates its contents, transforms the data, and writes a report is not one function, it's four functions wearing a trenchcoat. The test isn't line count, it's whether you can extract a piece of the function into its own well-named function and have that extraction mean something on its own. If you can, the original function was doing more than one thing.
A useful companion rule here is the Stepdown Rule: every function should sit at one
consistent level of abstraction, and the functions it calls should each be one level lower.
A function that mixes parse_config() with x = x + 1 is switching altitude mid-sentence,
forcing the reader to zoom in and out on every line. Read well-decomposed code top to
bottom and it should read like a newspaper article: the headline (the top function) gives
you the gist, and each paragraph below (each extracted function) gives you one more level
of detail, only as deep as you choose to keep reading.
Bury switch statements
A switch (or a long if/elif chain) on a type code is rarely wrong on its own, but it's
almost always a smell about where it lives. The problem isn't the branch, it's that the
same branch tends to get copy-pasted everywhere that type code shows up, so adding one new
case means hunting down every copy. The fix isn't to ban switch, it's to bury it: let it
appear exactly once, inside a factory or constructor that uses it to build the right
polymorphic object, and let every other call site talk to a shared interface instead of
re-running the same branch. That single burial is the seed of the abstract factory
pattern, and it turns "add a new case" from "edit five files" into "add one new class."
Descriptive names
A function's name should be a small sentence describing what it does, and a long,
descriptive name beats a short, vague one every time - filter_active_users tells you
everything, process tells you nothing. Naming a function well is also a design act: if
you can't come up with a name shorter than a paragraph, that's often a sign the function
is doing too much for one name to cover.
Function arguments
The ideal number of arguments is zero. One argument is fine, two is starting to ask more of the reader, and three or more should make you look for a way to group them. Every argument adds a case the reader has to mentally track, and every argument in the signature is another thing a caller has to get right, in order, every time.
Two special cases are worth calling out. A flag argument - a boolean parameter that
picks between two behaviors - is an admission that the function actually does two things;
split it into two clearly named functions instead. And when a function's argument list
starts to reflect a set of values that always travel together (x, y, z for a point,
from_date, to_date for a range), that's a sign those arguments want to be a single
argument object - one parameter that also happens to name the concept.
Watch for output arguments too - a parameter a function writes into instead of returning from is a double-take waiting to happen, since readers default to assuming arguments flow in and results flow out. If a function needs to change state, let it change the state of the object it's already a method on, rather than mutate one of its arguments.
No side effects
A function should do exactly what its name promises, no more. A function named
check_password that, on success, also resets the user's session state has a side effect
its name never warned you about - and side effects like that are how "just calling a
getter" ends up corrupting state three call frames away. If a function has to do a second
thing, say so in the name, or better, split it into two functions the caller can call
explicitly.
Command-query separation
A function should either do something or answer something, not both. A set()
that also returns whether the set succeeded invites code like
if (attribute_name = "username"), where a reader can no longer tell, without checking the
function's body, whether that line is an assignment or a comparison. Keep commands (which
change state and return nothing meaningful) and queries (which return a value and change
nothing) as separate functions, and callers stop having to hold both possibilities in their
head at once.
Prefer exceptions to error codes
Returning an error code forces the caller to check it immediately, which nests the "happy
path" logic inside an if block and buries what the function actually does under
error-checking scaffolding. Throwing an exception instead lets the calling code express the
normal case plainly and handle the error path separately, wherever it's actually dealt
with. This is really a case of "do one thing" applied at the call site: business logic and
error handling are two different concerns, and an error code welds them into the same line.
The Error Handling chapter covers this in depth.
Once the happy path is separated out, pull the try and catch bodies into their own
functions too - one function that does nothing but the normal-case work, and one that does
nothing but handle what went wrong. A function that interleaves try/except with real
logic is doing two things by definition.
Don't repeat yourself
Duplicated logic is a maintenance liability even at the function level: two functions with the same five-line validation block mean every future fix to that validation has to be made twice, correctly, forever. Extracting the shared logic into its own function isn't just about saving keystrokes, it's about giving that logic exactly one place to be right.
How do you actually write functions like this
No one writes a twenty-line, single-purpose, well-named function on the first try. The realistic process is to get something working first, however messy, with all the logic and nested conditionals it takes to make the tests pass, and then to spend real time afterward splitting it apart: extracting functions, renaming things, and rearranging until each piece does one thing at one level of abstraction. Clean functions are the result of editing, not of typing them clean the first time. Chapter 14 shows this process end-to-end.