Chapter 39: Clean Code and Refactoring
Section 1
Clean code is code that is easy to read, easy to understand, and easy to change safely, or clever, or fast, for genuine bottlenecks only. Clean code expresses its intent plainly: a reader can tell what it does, and why, from reading it once, without needing to run it, or to ask its author, what it was meant to do, in a particular case, with particular data, for their own work, on that code, in the future. That readability is not a luxury, or a matter of taste alone, understood, and changed, rather than being written, for the first time, or adding features, that users actually need, from the program, as a whole, over its lifetime.
Clean code also matters for correctness, and hard to change, without introducing new bugs, in places, that the changer did not realise, were affected, by their change, to a part, of the program, that they only partly understood, before changing it, for their purpose, in that moment, during maintenance, of the program, as a whole, over its run, generally. When code is clean, with clear names, small functions, a reader can see, what will be affected, by a change, before making it, and can test, that change, with confidence, that its effects, are understood, and intended, for the program, as a whole, over its run, generally. When code is unclear, with vague names, long functions, and tangled structure, a changer must guess, at its effects, and must rely, on tests, alone, to catch, mistakes, that clear code, would have prevented, by making, the changer's intent, and the code's behaviour, obvious, to anyone, reading it, for the first time, without prior knowledge, of its history, or its author's intentions, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time, generally.
Finally, clean code is a team practice, not merely a personal preference, because code is read, by many people, over its lifetime, and because, a team's shared standards, for naming, for structure, and for formatting, make, every member's code, familiar, to every other member, without needing, to learn, a new personal style, for each author, whose code, they must read, or change, during their work, on the program, as a whole, over its run, generally. Shared standards, do not mean, rigid uniformity, for its own sake, or rules, that serve, no purpose, beyond conformity, to a style, chosen arbitrarily, by a team, for its own sake, alone, without regard, for readability, or for maintainability, in practice, for real code, with real readers, who must, understand it, quickly, and accurately, for their work, on that code, over its lifetime, generally. They mean, agreement, on the practices, that genuinely help, readability, such as, clear names, small functions, and consistent formatting, so that, a reader, can focus, on what code, does, rather than, on how it, is written, in a personal style, that distracts, from its meaning, for that reader, specifically, rather than, generally, for all readers, at once, as a whole, over time, generally. This chapter, covers, those practices, and the companion, practice, of refactoring, which is, how unclear code, is improved, gradually, without changing, its behaviour, for users, who depend on it, for their work, over the program's lifetime, generally.
Check: Why is readability, rather than brevity or cleverness, the defining quality of clean code?
Section 2
Naming is the simplest, because, names, are how, code, communicates, its meaning, to readers, every time, they encounter, a variable, a function, generally. A good name, states, is, or does, plainly, in terms, familiar, will understand, immediately, without needing, to read, surrounding code, to infer, its meaning, from context, alone, which is, unreliable, when context, is large, or when, the name, is used, far from, its definition, generally. A variable, holding, a list, should be, named, rather than, alone, generic term, that could, generally. A function, that calculates, a total, should be, named, specifically, rather than, that could, mean anything, generally. Those, plain names, cost, nothing, in performance, and little, in typing, compared with, the time, they save, every reader, generally, and specifically, pays off, repeatedly, over time, generally.
Good names, follow, a few, consistent conventions, that help, readers, recognise, what kind, of thing, a name, refers to, alone, without reading, its definition, in detail, specifically, rather than, generally, at once, over time, generally. Variables, and functions, are typically, named, separated, for readability, while, classes, are named, that state, what kind, of thing, the class, represents, specifically, rather than, generally, at once, over time, generally. Constants, whose values, should not, change, are typically, named, signalling, to readers, that they, should not, be reassigned, casually, in code, generally. Those, conventions, are not, arbitrary, style, but, signals, that help, readers, parse, code, quickly, what kind, of thing, they are, looking at, before they, in detail, specifically, rather than, generally, at once, over time, generally, and consistent, use, of conventions, across a program, makes, every name, more predictable, for every reader, who encounters it, in their work, on that code, over its lifetime, generally.
Naming, also, involves, avoiding, misleading names, which are, worse, than vague names, because, they actively, mislead, readers, about what, a thing, is, or does, for that thing, specifically, rather than, generally, for all things, at once, as a whole, over time, generally. A variable, named, for one kind, of data, that actually, holds, another kind, will, cause, readers, to make, wrong assumptions, about code, that uses it, for its purpose, in the program, as a whole, over its run, generally, leading, to bugs, when those assumptions, are acted on, in changes, to that code, specifically, rather than, generally, for all code, at once, as a whole, over time, generally. A function, named, for one action, that actually, performs, another action, in addition, will, surprise, callers, who did not, expect, that side effect, for their call, specifically, rather than, generally, for all calls, at once, as a whole, over time, generally. Renaming, is therefore, a legitimate, and valuable, refactoring, as discussed, generally, for refactoring, as a practice, for improving, code, without changing, its behaviour, for users, for their work, generally, and it, should be, done, promptly, whenever, a misleading name, is noticed, during reading, or during review, of code, that uses it, for its purpose, in the program, as a whole, over its run, generally, because, the cost, of renaming, is small, compared with, the cost, of confusion, causes, repeatedly, for every reader, who encounters it, generally, and specifically, in a program, where its correction, pays off, immediately, who meets it, in their work, on that code, over time, generally.
Check: Why is a misleading name worse than a merely vague name?
Section 3
Functions should be small, one thing, well, because, clear purpose, is easier, to understand, that does, many things, hard-to-follow sequence, of steps, must hold, to understand, does, generally. A small function, can be, read, can state, its purpose, plainly, so that, a reader, of code, need not, at all, to understand, specifically, rather than, generally, at once, over time, generally, unless, they specifically, want, to understand, its implementation, in detail, itself, generally. A large function, by contrast, forces, every reader, to read, to understand, any part, of it, because, its parts, are intertwined, long sequence, where, is not, obvious, from reading, any part, alone, without reading, in order, as written, specifically, rather than, generally, at once, over time, generally, and specifically, makes, reading, and testing, harder, generally.
Small functions, also, help testing, because, each function, can be, tested, in isolation, specifically, rather than, generally, at once, over time, generally, without needing, that does, many things, together, alone, in isolation, specifically, rather than, generally, at once, over time, generally. A failing test, points, directly, specifically, rather than, generally, at once, over time, generally, while, could, be caused, by any part, of that function, requiring, debugging, to find, the faulty part, for that test, specifically, rather than, generally, for all parts, at once, as a whole, over time, generally. Small functions, also, help reuse, because, a function, that does, one thing, well, can be, reused, in other contexts, where, that one thing, is needed, in the program, as a whole, over its run, generally, while, a large function, that does, many things, together, cannot, be reused, alone, without also, doing, the others, unnecessarily, specifically, rather than, generally, for all uses, at once, as a whole, over time, generally, and specifically, in a program, makes, reuse, straightforward, for future uses, of that purpose, in the program, as a whole, over its lifetime, generally.
Splitting, a large function, into smaller ones, is a refactoring, discussed, generally, for refactoring, as a practice, for improving, code, without changing, its behaviour, for users, for their work, generally, and it, should be, done, by identifying, distinct steps, each of which, can be, named, plainly, for its purpose, specifically, rather than, generally, for all steps, at once, as a whole, over time, generally, and extracting, each step, into its own, small function, that states, its purpose, for that step, specifically, rather than, generally, for all steps, at once, as a whole, over time, generally. The original, large function, then, becomes, a short, readable sequence, of calls, in order, that states, plainly, involves, step by step, for a reader, who can, then, drill down, into any step, they need, to understand, in detail, for their work, on that step, specifically, rather than, generally, for all steps, at once, as a whole, over time, generally, without being, forced, to read, all steps, in detail, to understand, the overall task, as a whole, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time, generally. That, layered reading, from overall task, to individual steps, on demand, is, the essence, for programs, of any size, and any purpose, in practice, generally, and specifically, for each function, designed, over its lifetime, as it evolves, through maintenance, and refactoring, generally.
Check: How do small, single-purpose functions help both reading and testing?
Section 4
A comment that merely restates the code beside it adds nothing. If the code says total = price * quantity, a comment saying "multiply price by quantity" only repeats what any reader can already see. Worse, it creates a second place that must be kept correct. When the code changes and the comment does not, the comment becomes a lie that a future reader may trust. That is why the useful question to ask before writing a comment is not "what does this line do?" but "what would a reader misunderstand, or get wrong, without this note?"
The answer to that question is usually about why a decision was made. Code can show the choice that was taken, but it cannot easily show the choices that were rejected, or the constraint that forced the choice. Perhaps an apparently odd order of operations is required by an external system. Perhaps a simpler approach was tried and failed for a reason that is not obvious from the code alone. Perhaps a value that looks arbitrary comes from a specification, a legal requirement or a quirk of the data being handled. In each case a short comment recording the reason protects the code. Without it, a well-meaning maintainer may "tidy" the code into a form that looks cleaner but breaks the hidden requirement.
Consider a concrete example. Suppose a list must be sorted in a particular way before it is passed on:
# The receiver expects names sorted by surname, then first name.
# Sorting by full name would put "Mary Ann" before "Mary Beth" wrongly
# when surnames differ, so sort on the two fields separately.
names.sort(key=lambda person: (person.surname, person.first_name))
The code alone shows a sort key. The comment explains why that key was chosen and what mistake it prevents. A reader who understands the reason is far less likely to simplify the sort into something that looks equivalent but is not.
Good why-comments are brief and focused. A long essay beside a short function is a warning sign: either the code is unclear and should be rewritten with better names or a clearer structure, or the comment is explaining things the code already makes plain. Prefer the clearer code first. Rename a variable so its purpose is obvious. Extract a step into a small function whose name states its purpose. Only then add a comment for the part that clarity alone cannot convey, namely the reasoning behind a decision.
There is one more habit worth building. When you write a why-comment, write it for the person who will change the code next, not for the person who wrote it. State the reason plainly, name the constraint if there is one, and say what would go wrong if the code were changed in the obvious way.
Finally, treat comments as code that needs maintenance. When you change the behaviour a comment describes, update the comment in the same change. A stale comment is not harmless clutter. It actively misleads, because readers tend to trust prose over code when the two disagree, and they may then "fix" correct code to match an outdated explanation. Short, accurate, why-focused comments, kept in step with the code they describe, are one of the cheapest ways to make a program easier to maintain.
Check: Why should comments explain why code was written a certain way, rather than restating what it does?
Section 5
Readers understand code visually before they understand it logically. Indentation shows nesting. Blank lines show where one idea ends and another begins. Spaces around operators show which parts of an expression belong together. When those visual cues are consistent, a reader can grasp the shape of a program at a glance and then read the details with confidence. When they are inconsistent, the reader must spend attention on layout instead of meaning, working out whether an odd indent is significant or merely careless. That extra work slows reading and, worse, it can hide real errors behind visual noise.
Consistency across a whole program matters more than any single formatting choice because readers learn a pattern once and then apply it everywhere without thinking. It does not matter very much whether a team indents with four spaces or some other width, or exactly where it breaks a long line, provided the choice is the same in every file. A reader who opens an unfamiliar file written in the team's usual style already knows how to read it. A reader who finds a different style in every file must relearn the visual language each time, and that friction accumulates across a working day.
This is why automated formatters are so valuable. A formatter applies the team's conventions uniformly, without requiring anyone to remember every rule while writing code. It also removes formatting from code review. Reviewers no longer need to argue about spacing or line breaks, and can spend their attention on logic, naming and design, which are the things that actually affect correctness. Running a formatter before review, or as part of a project's standard tooling, turns consistency from a matter of discipline into a matter of habit.
Long lines deserve particular attention. A line that forces horizontal scrolling breaks concentration, because the reader cannot see the whole statement at once. Breaking such a line at a natural point, for example between arguments in a call or between parts of a long expression, makes each part visible separately:
result = process_order(
customer, items, discount_code, shipping_address
)
Here the call is easy to scan, and adding or removing an argument later is a small, clear change. Blank lines serve a similar purpose between logical sections of a function or module. They mark boundaries without needing a comment for every section, and they let the eye rest at a natural pause.
None of this means formatting is more important than logic. It means formatting is the surface through which logic is read, and a rough surface makes the logic harder to reach. Small visual practices, applied consistently, accumulate into a program that feels pleasant to open and invites careful review. A program whose formatting is chaotic, by contrast, feels like a chore before a single line has been read, and reviewers are less likely to give it the careful attention it needs. Choose conventions as a team, automate them where you can, and then leave them alone so that readers can focus on what the code does.
Check: Why does consistency across a program matter more than any single formatting choice?
Section 6
Duplicated code is a maintenance problem because every copy must be kept correct. When a rule changes, a developer must find every place that implements it and change each one. If one copy is missed, the program becomes inconsistent: it behaves one way in one place and another way elsewhere, for what should be the same operation. Bugs of this kind are hard to spot because each copy looks correct on its own. They often appear only for some inputs or some uses, long after the change was made, when the forgotten copy is finally exercised.
The principle often called "do not repeat yourself" captures the remedy. Every piece of knowledge in a program should have a single authoritative representation, so that a change is made once and is reflected everywhere automatically. In practice this usually means extracting shared logic into a function that all the duplicated places call, with parameters for the values that genuinely vary between uses:
def discounted_price(price, rate):
return price * (1 - rate)
total_a = discounted_price(100, 0.10)
total_b = discounted_price(250, 0.10)
Now the discount rule lives in one place. If the rule changes, it changes once, and both uses follow.
Extraction must be done carefully, however, because two pieces of code that look similar may not represent the same knowledge. They may coincide for now while representing different ideas that will diverge as requirements change. Forcing such code into a single function creates false coupling: a future change to one idea will wrongly affect the other, and unravelling that coupling later is often harder than the duplication was. The classic example is two calculations that happen to use the same formula today but mean different things, such as a tax rate and a discount rate that are both ten percent at present but are governed by different rules.
A useful test separates the two cases. Ask whether a change to one copy would necessarily require the same change to the other copy to keep the program correct. If the answer is yes, the copies represent shared knowledge and should be unified. If the answer is no, because a change to one copy might not apply to the other for reasons specific to its context, then the copies represent different knowledge that merely looks alike for now, and they should be left duplicated until their relationship becomes clearer.
This test also explains why duplication is not always a failure. Early in a program's life, it may be unclear whether two similar pieces of code are truly the same idea. Duplicating them, and waiting to see how they evolve, is often wiser than unifying them prematurely and then discovering that they needed to diverge. The goal is not zero duplication at any cost. It is a program in which each genuine piece of knowledge has one home, and in which accidental similarity is not mistaken for shared meaning.
Check: How do you distinguish genuine shared knowledge from coincidentally similar code?
Section 7
Clean error handling means dealing with errors in a predictable way, rather than scattering checks through code so that the reader cannot tell where failures are handled or what happens when they occur. Errors are a normal part of a program's life: files may be missing, networks may fail, and users may supply input the program did not expect. A program that handles those cases in a clear, consistent place is easier to understand than one in which every function quietly guards against every possible failure in its own way.
A common clean practice is to handle an error where it can be dealt with meaningfully. Low-level code often cannot know what to do about a failure, so it should report the error upward, by raising an exception, rather than inventing a fallback. Higher-level code, which knows the context, can then decide whether to retry, use a fallback, show a message to the user, or stop gracefully. Centralising handling in this way makes the flow of a program easier to follow: the main logic describes the successful path, and the error handling describes what happens when that path fails.
Equally important is not hiding errors silently. A program that catches an exception and carries on as if nothing happened may appear to work while its state is inconsistent or its output is incomplete. Silent failures are particularly harmful because they are discovered late. By the time their effects become visible, the original failure is far in the past, and there may be no record of what went wrong. Users cannot report a problem they do not know exists, and developers cannot fix a failure they cannot reproduce or trace.
Every caught error should therefore be handled deliberately. Either handle it meaningfully, for example by retrying with a fallback or by telling the user what happened and what they can do next, or log it with enough detail for later diagnosis, or re-raise it to code that can handle it. An empty handler that swallows an exception should be treated as suspicious in review, and if it is genuinely the right choice, it deserves a comment explaining why ignoring that particular error is safe:
try:
cache_file.unlink()
except FileNotFoundError:
# Already gone: nothing to clean up.
pass
Finally, error messages themselves should be clear and actionable. A message that merely says something went wrong helps nobody. A good message states what operation failed, what input or condition caused the failure, and what the reader can do next. For users, that might be a concrete next step. For developers reading a log, it might be the values involved and where the failure occurred. In both cases, avoid exposing sensitive details such as internal paths or credentials in messages shown to users. Clarity for the reader and safety for the system are both part of clean error handling, and both deserve attention when an error path is written.
Check: Why is silently swallowing an error particularly harmful for later diagnosis?
Section 8
Refactoring is the practice of restructuring existing code to improve its internal structure without changing its observable behaviour. The code becomes clearer, better organised or easier to change, but for every input the program currently handles correctly, it produces the same result as before. That last condition is the defining feature of refactoring, and it is what separates refactoring from other kinds of change. If behaviour changes, the work is not refactoring alone. It is refactoring combined with a functional change, and the two should be done separately so that each can be verified on its own.
Keeping them separate matters because tests are the main tool for verifying a refactoring. A test suite can only confirm that behaviour has not changed if the only change made is the refactoring itself. If a functional change is mixed in, and a test then fails, the cause is unclear. It could be that the refactoring broke existing behaviour, or that the new behaviour is correct and the test needs updating, or that the new behaviour itself is wrong. Sorting out which of those is true wastes time and risks masking a genuine regression behind a change that was expected to alter test results. When the two kinds of change are separate, a failing test after a refactoring has one clear meaning: the refactoring went wrong.
Separation also makes review easier. A reviewer looking at a pure refactoring can check that the structure improved and that no behaviour changed, without also having to judge whether a new feature is correct. A reviewer looking at a functional change can focus on the new behaviour, knowing the structure it sits in is already understood. Mixing the two forces the reviewer to do both jobs at once, and makes it harder to spot mistakes in either.
Refactoring is not a separate phase that happens only when code becomes unbearably messy. It is a continuous practice that happens alongside ordinary development. A developer who notices a misleading name while reading code for another purpose can rename it immediately, in a small step that does not change behaviour. A developer who notices duplication while adding a feature can extract the shared logic as part of that work, again in a small, tested step. Because each step is small, its cost is low and its safety is easy to verify.
Those small, continuous refactorings accumulate. A codebase that is tidied a little at a time stays clean without needing large, disruptive efforts that compete with feature work and risk introducing bugs through large-scale changes to previously working code. The discipline is simple to state and worth practising: first refactor, verify with tests, and only then change behaviour, or the other way round, but never both in the same change. A team that follows that rule can improve its code steadily while keeping its test suite as a reliable witness that nothing else changed.
Check: Why must refactoring and functional changes be kept as separate changes?
Section 9
Refactoring safely depends on tests, because tests are how you verify that behaviour has not changed after restructuring code. Without tests, refactoring is guesswork. A developer may hope that the restructured code behaves exactly as the original did, but cannot verify that hope except by trying cases by hand, and hand trials easily miss the case where behaviour actually changed. A regression introduced by refactoring may then be discovered later by users, who must report it, and by then the change that caused it may be hard to identify among many other changes.
Before refactoring code that lacks adequate tests, therefore, the usual first step is to write tests that capture its current behaviour. These are sometimes called characterisation tests, because they characterise what the code actually does. They do not need to assert that the behaviour is correct. In fact, for legacy or poorly understood code, the behaviour may include quirks that are wrong in principle but relied upon in practice. The point of the test is to record what the code does now, so that the refactoring can be checked against that record. If the refactoring changes behaviour, even in a way that looks like an improvement, the test will catch it, and the developer can decide deliberately whether that change is wanted, rather than discovering it by accident.
Characterisation tests should cover representative inputs, including edge cases that are known to matter. An empty list, a boundary value, an unusual but valid input, and an input that currently causes an error are all worth recording, because those are exactly the cases where a restructuring is most likely to slip. Writing these tests is often the most valuable part of the work, since it forces the developer to understand the code's actual behaviour before touching its structure:
def test_total_with_empty_order():
assert order_total([]) == 0
def test_total_applies_discount_once():
assert order_total([100, 50]) == 135
With such tests in place, refactoring can proceed in small steps, with the tests run after each step. A failing test after a small step points directly at that step, and the step can be reverted and tried differently. A failing test after a large, multi-step refactoring, by contrast, could have been caused by any step, and finding the faulty one requires debugging across the whole change. Reverting a single small step is cheap. Reverting a large change that contains some correct steps and some broken ones is not.
That rhythm, small step, run tests, small step, run tests, is what makes refactoring safe rather than merely hopeful. Tests do not make refactoring risk-free, and they cannot prove that behaviour is unchanged for every possible input. But a good set of tests, written before the restructuring begins, turns refactoring from a leap of faith into a checked, repeatable process, and it gives the developer the confidence to improve code that would otherwise be left alone for fear of breaking it.
Check: Why should you write tests capturing current behaviour before refactoring untested code?
Section 10
A code smell is a surface indication that something in the code may be wrong, in the same way that a smell in a kitchen may indicate that something needs attention without proving what it is. Code smells are useful triggers for refactoring, but they should prompt investigation first, not automatic refactoring. A long method, a long parameter list, a class with many unrelated methods, duplicated code and feature envy, where a method uses another object's data far more than its own, are all familiar smells. Each one suggests a possible problem, and each one deserves a look. None of them, on its own, proves that a problem exists.
Consider a long method. It may be a sign that the method does several things and should be split into smaller functions, each with a clear purpose. But it may also be a single coherent sequence of steps that belongs together for readability, and splitting it into many tiny functions could make it harder to follow, because the reader would have to jump between fragments whose individual meaning is lost outside the sequence. The length alone cannot decide between those two readings. Only reading the method, and understanding what it does, can.
A long parameter list raises a similar question. It may mean that several values belong together and should be grouped into a structured value, or that the function does too much and should be split. Or it may mean that the function genuinely needs those values, and grouping them would only hide the complexity rather than reduce it. Duplicated code, too, may be genuine shared knowledge that should be extracted, or it may be coincidentally similar code that will diverge, as discussed for duplication. Feature envy may show that a method belongs in another class, or it may show that the method legitimately coordinates several objects and cannot be moved without making the design worse.
Treating smells as prompts for investigation, rather than as automatic triggers, protects against refactoring that makes code worse. A refactoring undertaken without understanding why the smell is present can destroy a deliberate design, introduce coupling where none was needed, or split a coherent sequence into fragments that are harder to maintain. It also wastes effort on code that was never a problem, while real problems elsewhere go unexamined.
The right response, then, is a short investigation followed by a decision. Read the smelly code and ask what it is doing, why it is shaped that way, and whether that shape causes a real difficulty for readers or for future changes. If the investigation confirms a real problem, refactor in small, tested steps, as for any safe refactoring. If it shows a good reason for the current shape, leave the code alone, and consider adding a comment recording the reason, so that the next reader does not repeat the investigation. Used this way, code smells are a valuable guide to where attention is most likely to pay off, without becoming a mechanical rule that overrides judgement.
Check: Why should code smells trigger investigation before refactoring, rather than automatic refactoring?
Section 11
Extract method is the most common refactoring, small function of its own, with a name that states what that block does. You use it when a function has grown long, with a clear purpose, even though it is currently buried inside a larger sequence of steps. The new function takes as parameters the values the block needs, each naming one step, that a reader must follow, line by line, to understand the overall task. That transformation is valuable even when the extracted block is used only once, because its purpose is clarity, at a glance, without forcing a reader to understand every detail, of every step, before they understand what the function does, as a whole, for its purpose, in the program.
Extracting a method well requires choosing a good name for the new function, leaving readers still unsure what the step does, even though it is now a separate function, with its own name, and its own body, that they must read, to understand it, for their work, on that code, in the future. The name should state the step's purpose, in terms of the problem domain, rather than restating its mechanics, in terms of loops, or conditions, that the step happens to use, internally, for its implementation, at present, which may change, over time, while its purpose, remains stable, for the program, as a whole, over its run. It also requires deciding what the new function should take, and return, cleanly, without passing, a large number, of values, back and forth, through its parameters, and its return value, which would suggest, that the block, was not, a clean step, after all, but was, entangled, with surrounding code, in ways, that extraction, alone, cannot fix, without further, restructuring, of the surrounding code, itself, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time.
After extracting a method, run the tests for the affected code, to verify that behaviour has not changed, for any input, that the code, currently handles, correctly, for that input, specifically, rather than, generally, for all inputs, at once, as a whole, over time. If tests fail, the extraction introduced a mistake, most often in what was passed in, or returned, from the new function, for that extraction, specifically, rather than, generally, for all extractions, at once, as a whole, over time, and that mistake should be fixed, before proceeding, to further refactoring, of the same code, for its purpose, in the program, as a whole, over its run. Extract method is often the first step, in a larger refactoring, because once steps are named, as separate functions, can be reused, through its new function, rather than, duplicated again, for a new use, of that step, specifically, rather than, generally, for all steps, at once, as a whole, over time; a step that is complex, can be simplified, in isolation, without affecting, other steps, in the original function, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time; and a step that is wrong, can be fixed, in one place, for all its uses, together, for that step, specifically, rather than, generally, for all steps, at once, as a whole, over time.
Check: Why is extract method valuable even when the extracted block is used only once?
Section 12
Rename is the simplest refactoring, costs little, and benefits, immediately, is complete, specifically, rather than, generally, at once, over time. You should rename whenever you notice a name that does not match what its thing actually is, or does, changed, or because, hastily, about what, would need, to understand, alone, without reading, surrounding code, to infer, its meaning, from context, alone, which is, unreliable, when context, is large, or when, the name, is used, far from, when done, with tooling, that finds, every use, and updates, them all, consistently, specifically, rather than, generally, at once, over time, because, leaves, the program, inconsistent, referring, and others, specifically, rather than, generally, at once, over time.
Renaming a public name, by code, or package, specifically, rather than, generally, at once, over time, than renaming, used, only within, or function, specifically, rather than, generally, for all names, at once, as a whole, over time, because, external users, may depend, for their work, with the program, as a whole, over its run, and because, changing it, without notice, breaks, their code, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time. For public names, consider, whether, the rename, is worth, breaking, external users, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time, or whether, the old name, should be, kept, during which, users, can update, their code, gradually, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time, before, the old name, is eventually, removed, of the program, as a whole, over its run. That judgement, depends, on how widely, the name, is used, externally, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time, and on, how misleading, the old name, actually is, for new users, who encounter it, without prior knowledge, of its history, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time.
Renaming should also be applied, to concepts, not just, to individual names, in isolation, for a program, as a whole, over its run, because, when, a domain concept, changes, its name, itself, for that concept, specifically, rather than, generally, for all concepts, at once, as a whole, over time, all names, that refer, to that concept, throughout the program, should, change, consistently, for that concept, specifically, rather than, generally, for all concepts, at once, as a whole, over time, so that, readers, do not, encounter, two different terms, in different parts, of the program, as a whole, over its run, and wonder, whether, they refer, to different things, under different names, for that concept, specifically, rather than, generally, for all concepts, at once, as a whole, over time. That consistency, of terminology, across a program, is part, of clean naming, generally, for naming, as a practice, for readability, and for maintainability, and it, requires, attention, during review, and during, refactoring, when, domain language, evolves, as understanding, of the problem, deepens, over time, and during, its maintenance, by developers, who must, keep terminology, consistent, across all, its code, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time.
Check: Why does renaming a public name require more care than renaming a private one?
Section 13
Move method, are refactorings, that relocate, a method, to another, where, belongs, based on, that uses, far more, specifically, rather than, generally, at once, over time, as discussed, generally, for smells, as prompts, for investigation, into whether, refactoring, is warranted, specifically, rather than, generally, at once, over time, and moving, that method, whose data, specifically, rather than, generally, at once, over time, places, its logic, closer, specifically, rather than, generally, at once, over time, making, both, who encounter, that method, specifically, rather than, generally, at once, over time.
Moving a method, involves, more than, merely, cutting, and pasting, its code, specifically, rather than, generally, at once, over time, because, its parameters, may need, to change, to reflect, specifically, rather than, generally, at once, over time, and because, callers, must be, updated, to call, it, instead of, specifically, rather than, generally, at once, over time, which, tooling, can help, with, by finding, all callers, specifically, rather than, generally, at once, over time, and updating, them, consistently, specifically, rather than, generally, at once, over time, provided, is done, with tests, run, as discussed, for safe refactoring, generally, of this chapter, specifically, rather than, generally, for all sections, at once, as a whole, over time, to verify, that behaviour, has not changed, for any input, that the program, currently handles, correctly, for that input, specifically, rather than, generally, for all inputs, at once, as a whole, over time.
Move field, follows, the same logic, for data, as move method, does, for behaviour, for a program, as a whole, over its run, because, a field, that is, used, far more, by another class, than by, its own class, for that field, specifically, rather than, generally, for all fields, at once, as a whole, over time, probably, belongs, instead, for that field, specifically, rather than, generally, for all fields, at once, as a whole, over time, where, its data, will be, closer, to the code, that actually, uses it, for its purpose, in the program, as a whole, over its run. Moving, a field, requires, updating, all code, that reads, or writes, that field, throughout the program, to use, its new location, for that field, specifically, rather than, generally, for all fields, at once, as a whole, over time, which, again, tooling, can help, with, by finding, all uses, for that field, specifically, rather than, generally, for all fields, at once, as a whole, over time, and updating, them, consistently, for that field, specifically, rather than, generally, for all fields, at once, as a whole, over time, provided, that the move, is done, carefully, and tested, after each step, for that move, specifically, rather than, generally, for all moves, at once, as a whole, over time, to verify, that behaviour, has not changed, for any input, that the program, currently handles, correctly, for that input, specifically, rather than, generally, for all inputs, at once, as a whole, over time. Together, move method, and move field, help, keep, classes, coherent, with data, and behaviour, that belong together, staying together, for that class, specifically, rather than, generally, for all classes, at once, as a whole, over time, which, is a central, principle, of clean, object-oriented design, for programs, of any size, and any purpose, in practice, generally.
Check: What sign suggests a method should be moved to a different class?
Section 14
Replacing a conditional with polymorphism means removing a long chain of checks, or type, and instead, letting, each kind, implement, share, together, specifically, rather than, generally, at once, over time. A typical, example, is code, that checks, what kind, of item, it has, and then, performs, different actions, long conditional, that grows, added, over time, and during, its maintenance, by developers, who must, individually, where, kind-based behaviour, each kind, becomes, a class, that implements, the operation, specifically, rather than, generally, at once, over time, simply, calls, the operation, it has, without checking, which kind, it is, first, specifically, rather than, generally, at once, over time, because, determines, which implementation, runs, specifically, rather than, generally, at once, over time.
That transformation, is valuable, when, kind-based conditionals, appear, specifically, rather than, generally, of kinds, at once, over time, because, each place, must be, updated, added, over time, and during, its maintenance, generally, while, with polymorphism, is added, by writing, specifically, rather than, generally, at once, over time, without changing, that calls, the operation, specifically, rather than, generally, at once, over time, provided, implements, that interface, correctly, specifically, rather than, generally, at once, over time. That, benefit, discussed, generally, specifically, rather than, generally, at once, over time, where, varying behaviour, is encapsulated, in separate objects, rather than, in conditionals, scattered, through client code, for that behaviour, specifically, rather than, generally, for all behaviour, at once, as a whole, over time, and where, adding, a new variant, does not, require, changing, existing code, that uses, the common interface, for that behaviour, specifically, rather than, generally, for all behaviour, at once, as a whole, over time.
Polymorphism, should not, be used, for every conditional, in a program, as a whole, over its run, because, a simple conditional, with two, or three, branches, that appears, for that conditional, specifically, rather than, generally, for all conditionals, at once, as a whole, over time, is often, clearer, than a full, class hierarchy, with a class, for each branch, of that conditional, specifically, rather than, generally, for all conditionals, at once, as a whole, over time, which, adds, several new classes, for a decision, that a reader, could, understand, immediately, from the conditional, itself, for that decision, specifically, rather than, generally, for all decisions, at once, as a whole, over time, without needing, to follow, through a hierarchy, to see, what each class, does, for its branch, specifically, rather than, generally, for all branches, at once, as a whole, over time. The, decision, to replace, a conditional, with polymorphism, should therefore, be, based on, how often, the conditional, appears, in a program, as a whole, over its run, how many, kinds, it handles, for that conditional, specifically, rather than, generally, for all conditionals, at once, as a whole, over time, and how, likely, new kinds, are, to be added, over time, and during, its maintenance, generally, because, those factors, determine, whether, the hierarchy's complexity, is justified, by the benefit, of adding, new kinds, without changing, client code, for that conditional, specifically, rather than, generally, for all conditionals, at once, as a whole, over time, or whether, a simple conditional, remains, the clearer, choice, for that decision, specifically, rather than, generally, for all decisions, at once, as a whole, over time.
Check: When is replacing a conditional with polymorphism worth its added class complexity?
Section 15
Long parameter lists are a smell, is needed, specifically, rather than, generally, at once, over time, since, must be, updated, to supply, specifically, rather than, generally, at once, over time, and since, callers, must remember, the order, that may, and that, look alike, without names, clearly, from memory, incomplete, or outdated, as discussed, for documentation, generally, misleads readers, was written, behaves differently, than documented, in ways, that cause, failures, for callers, who trusted, the documentation, itself, in practice, generally, and specifically, makes, calling, and reading, harder, generally.
One refactoring, that groups, related parameters, structured value, specifically, rather than, generally, at once, over time, so that, a function, takes, that object, instead of, specifically, rather than, generally, at once, over time. That grouping, is appropriate, when, the parameters, genuinely, belong together, specifically, rather than, generally, at once, over time, such as, an address, city, specifically, rather than, generally, at once, over time, with its start, and end, for that range, specifically, rather than, generally, for all ranges, at once, as a whole, over time, because, in those cases, the grouped object, itself, has meaning, beyond, the individual values, it contains, for that group, specifically, rather than, generally, for all groups, at once, as a whole, over time, and because, callers, can then, construct, the object, once, with named fields, that state, plainly, what each value, means, for that object, specifically, rather than, generally, for all objects, at once, as a whole, over time, instead of, supplying, many values, in order, without names, to distinguish them, clearly, for a reader, writing the call, from memory, or from documentation, that may be, incomplete, or outdated, as noted above, for parameter order, generally.
Parameter objects, should not, be created, merely, to shorten, a parameter list, without any, coherent grouping, for the parameters, in question, specifically, rather than, generally, for all parameters, at once, as a whole, over time, because, an arbitrary grouping, of unrelated values, merely, moves, complexity, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time, without making, the code, any clearer, for readers, who must, now understand, both, the function, it takes, as a parameter, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time, to know, what values, are actually needed, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time. In such, cases, a better, refactoring, may be, to split, the function, itself, into smaller functions, each taking, fewer parameters, specifically, rather than, generally, for all purposes, at once, as a whole, over time, as discussed, for function size, generally, of this chapter, specifically, rather than, generally, for all sections, at once, as a whole, over time, because, may actually, indicate, that the function, does, too many things, together, specifically, rather than, generally, for all purposes, at once, as a whole, over time, and that, splitting it, addresses, the root cause, of its length, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time, instead of, merely hiding, its length, that groups, unrelated values, arbitrarily, for that function, specifically, rather than, generally, for all functions, at once, as a whole, over time.
Check: When is introducing a parameter object appropriate, and when is it merely hiding complexity?
Section 16
Dead code is code that is never executed, actually handles, in practice, that determine, during execution, specifically, rather than, generally, at once, over time, and it, should be, removed, promptly, when identified, specifically, rather than, generally, at once, over time, because, dead code, misleads readers, who assume, that code, is actually, used, and who, spend time, understanding, code, run, actually handles, in practice, that determine, which paths, are taken, during execution, specifically, rather than, generally, at once, over time, and because, dead code, must still, be maintained, in principle, whenever, surrounding code, changes, specifically, rather than, generally, at once, over time, even though, it never, actually runs, handles, in practice, generally, and specifically, costs, reader time, and maintenance, attention, for users, generally.
Dead code, arises, in several, common ways, and during, its maintenance, by developers, who must, keep it, clean, over time, as requirements, change, are added, or removed, specifically, rather than, generally, at once, over time. A feature, that is, removed, may leave, behind, helper functions, that were, used, and that, are now, unused, feature, generally, and specifically, left behind, specifically, rather than, generally, at once, over time. A condition, that was, once, possible, may become, impossible, specifically, rather than, generally, at once, over time, leaving, a branch, be taken, currently handles, correctly, specifically, rather than, generally, at once, over time, but that, remains, for readers, specifically, rather than, generally, at once, over time. A function, that was, written, that was, never implemented, specifically, rather than, generally, at once, over time, may remain, unused, generally, and specifically, should be, questioned, during review, and during, refactoring, specifically, rather than, generally, at once, over time.
Removing, dead code, should be, done, carefully, because, code, that appears, dead, may actually, be used, dynamically, through mechanisms, cannot see, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time, such as, through, a plugin system, where, code, is loaded, by name, at run time, for a plugin, specifically, rather than, generally, for all plugins, at once, as a whole, over time, or through, external callers, outside the program, itself, that is, used, by other programs, for their work, with the program, as a whole, over its run, generally, and specifically, for each piece, where its uses, must be, checked, thoroughly, before removal, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time. Once, removal, is confirmed, as safe, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time, by checking, its uses, and by running, tests, after removal, to verify, that behaviour, has not changed, for any input, that the program, currently handles, correctly, for that input, specifically, rather than, generally, for all inputs, at once, as a whole, over time, the removal, itself, is straightforward, and its benefit, is immediate, who encounters, cleaner code, without dead branches, or unused helpers, to puzzle over, for their work, on that code, over its lifetime, generally, and specifically, for each removal, of dead code, in a program, where its benefit, is clarity, for future readers, who need not, waste time, on code, that never runs, for any input, that the program, handles, in practice, generally.
Check: Why must apparently dead code be checked carefully before removal?
Section 17
Guard clauses simplify conditionals by handling special cases early and returning immediately, instead of nesting the main logic inside layers of conditions. The idea is simple: check for the cases that should stop the function first, deal with each one, and then let the rest of the function describe the normal path without further indentation. A reader can then read the function from top to bottom, dismissing each early exit as it is handled, and arrive at the main logic knowing exactly which cases have already been excluded.
Without guard clauses, a function often takes the shape of a deep nest. Each condition adds a level of indentation, and the main logic ends up buried at the deepest level, surrounded by conditions that the reader must hold in mind simultaneously. Consider a function that validates its input before processing it. Written with nesting, the reader must track every enclosing condition to understand what is true at the point where the real work happens. Written with guard clauses, each invalid case is rejected at the top, and the real work stands out clearly at the normal indentation level:
def average(numbers):
if not numbers:
return 0
if any(n < 0 for n in numbers):
raise ValueError("numbers must not be negative")
return sum(numbers) / len(numbers)
Here the two guard clauses state their conditions plainly and exit at once. By the time the final line runs, the reader knows the list is not empty and contains no negative numbers, without having to trace through nested blocks to establish those facts.
Guard clauses also make each special case easier to test and to change. Because each guard is a separate, early check, a test can target it directly, and a change to one special case does not disturb the others or the main logic. In nested code, by contrast, changing one condition can affect the meaning of everything inside it, and the interaction between conditions becomes a source of bugs. Early returns reduce that interaction by turning a set of overlapping conditions into a sequence of independent decisions.
There are limits to the technique. A function with a very large number of guard clauses may itself become hard to read, and that is a sign that the validation logic should be extracted into its own function, or that the function is doing too much. Guard clauses also need to be used consistently: mixing early returns with deeply nested logic in the same function can confuse readers about which style to expect. But used in moderation, they are one of the simplest ways to flatten a function and make its intent visible.
The habit to build is to look for nesting when you read a function and ask whether the outer conditions are really part of the main logic or merely preconditions. If they are preconditions, moving them to the top as guard clauses will usually make the function shorter, flatter and easier to follow, and the main logic will finally be where the reader can see it.
Check: How do guard clauses make a function's main logic easier to read?
Section 18
Encapsulating a collection means controlling how outside code can access and modify a collection that belongs to an object, instead of exposing the collection directly. If a class holds a list of items and hands that list out as it is, any caller can add, remove or reorder items without the class knowing, and without any of the class's rules being applied. The object's state can then become inconsistent in ways that are hard to trace, because the change did not go through any method of the class and left no trace in its own code.
The invariants that justify encapsulation are the rules that must always hold for the collection. Perhaps items must be unique, or must be kept in sorted order, or must satisfy some validity condition, or must be accompanied by a count or total that is kept in step with the collection. Perhaps removing an item must trigger some other update, such as releasing a resource or notifying another part of the system. If no such rule exists, and the collection is simply a bag of items with no constraints, exposing it may be harmless. But as soon as there is a rule that must hold after every change, direct exposure becomes a risk, because nothing stops a caller from breaking the rule.
Encapsulation solves this by making the collection private and providing methods that enforce the rules. Callers add items through a method that checks the rule, removes them through a method that performs the necessary follow-up, and read them through a method that returns a copy or a read-only view, so that they cannot mutate the internal collection by accident:
class Playlist:
def __init__(self):
self._songs = []
def add_song(self, song):
if song not in self._songs:
self._songs.append(song)
def songs(self):
return list(self._songs)
In this example, uniqueness is enforced by add_song, and songs returns a copy, so a caller who appends to the returned list does not corrupt the playlist. The invariant, that the playlist contains no duplicates, is maintained no matter how the class is used.
This control also makes future change easier. If the internal representation needs to change, for example from a list to a set, or if a new rule needs to be added, the change can be made inside the class, and callers that use the public methods are unaffected. Callers that had direct access to the list would all need to be checked and possibly changed, which is exactly the kind of scattered change that encapsulation prevents.
Encapsulation is therefore not about secrecy for its own sake. It is about giving the class sole responsibility for keeping its own state valid, so that its invariants hold by construction rather than by the goodwill of every caller. When a collection carries rules that matter, that responsibility is worth protecting, and the small cost of writing accessor methods is repaid by the bugs that never happen.
Check: What invariants justify encapsulating a collection instead of exposing it directly?
Section 19
Refactoring and performance interact, because restructuring code for clarity can affect its speed. A refactoring that introduces an extra function call, an extra copy of a collection, or an extra pass over data may be entirely harmless in most code, yet noticeable in a hot path, a section of code that runs very frequently or over very large inputs. The clean-code habit of preferring clarity is still the right default, but it should not be based on assumptions about speed, in either direction. Neither "this refactoring will be too slow" nor "this refactoring cannot matter" is a safe claim without evidence.
The reason measurement matters is that performance is often surprising. Code that looks expensive may run rarely enough that its cost is irrelevant, while code that looks trivial may run millions of times and dominate the total time. A developer who avoids a clear refactoring on performance grounds, without measuring, may preserve a speed problem that does not exist while keeping code that is harder to maintain. A developer who applies a refactoring blindly to a hot path, without measuring, may introduce a slowdown that only appears under real load, when it is expensive to diagnose. In both cases the error is the same: deciding on the basis of a guess instead of a measurement.
Measuring on hot paths means identifying where the program actually spends its time, and then checking the effect of the change there. Profiling tools can show which functions consume the most time for realistic inputs, and that picture should guide where optimisation effort, and performance caution, is spent. Once the hot paths are known, a refactoring can be tested against them directly: run a representative workload before and after the change, and compare. If the difference is negligible, clarity wins. If the difference is significant, the developer can decide deliberately, perhaps by keeping the clearer structure in most places and using a faster form only where it is measured to matter, with a comment explaining why.
It is also worth remembering that clarity itself can help performance work. A well-structured program, with small functions and clear boundaries, is easier to profile and easier to optimise in the one place that matters, because the hot path can be found and changed without disturbing the rest of the system. Tangled code, by contrast, makes it hard to tell where time is spent or to change one part without affecting others.
The practical rule, then, is straightforward. Refactor for clarity by default. When the code involved is on a hot path, or when a refactoring is suspected to be expensive, measure before and after with realistic data, and let the measurement decide. That habit protects both goals: code stays readable where speed does not matter, and it stays fast where speed genuinely does, with the distinction grounded in evidence rather than in habit or fear.
Check: Why should refactoring's performance impact be measured on hot paths rather than assumed?
Section 20
Refactoring legacy code, poorly understood, from refactoring, well-tested, familiar code, specifically, rather than, generally, at once, over time, because, without tests, refactoring, cannot be, verified, safely, specifically, rather than, generally, at once, over time, and because, without understanding, actually does, specifically, rather than, generally, at once, over time, a refactoring, may change, behaviour, did not, know about, specifically, rather than, generally, at once, over time, leading, introduced, by refactoring, that is, discovered, later, by users, who encounter, generally, and specifically, must be, managed, through careful, incremental steps, and through, test creation, before refactoring, itself, specifically, rather than, generally, at once, over time.
The first step, is therefore, to build, understanding, before changing, anything, specifically, rather than, generally, at once, over time, by reading, the code, carefully, by tracing, its inputs, and outputs, that are, known, to matter, specifically, rather than, generally, at once, over time, and by, talking, to users, who know, is supposed, to do, generally, and specifically, must be, understood, before refactoring, is attempted, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time. Next, write, characterisation tests, as discussed, for safe refactoring, generally, of this chapter, specifically, rather than, generally, for all sections, at once, as a whole, over time, that capture, current behaviour, specifically, rather than, generally, for all cases, at once, as a whole, over time, so that, refactoring, can then, be verified, against those tests, after each step, for that refactoring, specifically, rather than, generally, for all refactorings, at once, as a whole, over time, before, the refactored code, is shared, with users, for their work, with the program, as a whole, over its run, generally, and specifically, must be, built, before refactoring, itself, begins, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time.
Legacy refactoring, should also, be, scoped, narrowly, to the part, of the code, that actually, needs, changing, in the program, as a whole, over its run, rather than, attempting, to refactor, an entire, legacy system, at once, in a large, risky change, that could, introduce bugs, throughout the system, for that system, specifically, rather than, generally, for all systems, at once, as a whole, over time, and that, would compete, with other work, for time, and for review, effort, to review it, thoroughly, and safely, before merging it, into shared code, where it, would affect, everyone, immediately, whether they, were ready, for it, or not, at that moment, generally, and specifically, where its refactoring, must be, incremental, and focused, on parts, that are, actually being, changed, for other reasons, in the program, as a whole, over its run, generally. That, incremental approach, sometimes, called, for leaving, code, cleaner, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time, means, that, legacy code, improves, gradually, over time, as developers, touch it, for other tasks, in the program, as a whole, over its run, generally, without needing, a separate, large refactoring, effort, that would, disrupt, other work, and that, would risk, introducing bugs, through large-scale, changes, to code, that was, previously working, correctly, for its users, in practice, generally, and specifically, maintained, through incremental, boy scout refactorings, over its lifetime, generally.
Check: Why should legacy refactoring start with building understanding and tests before changing code?
Section 21
Refactoring in a team requires coordination, structure, behaviour, for users, other developers, who are, working, that uses, or method, specifically, will create, a conflict, are combined, even though, neither change, alone, refactoring intentions, before undertaking, specifically, rather than, generally, at once, over time, and keeping, refactorings, small, and frequent, rather than, large, and infrequent, so that, conflicts, remain, small, and easy, to resolve, specifically, rather than, generally, at once, over time, when they, do occur
Small, to review, since, a reviewer, can verify, quickly, and confidently, that it, preserves behaviour, currently handles, correctly, specifically, rather than, generally, at once, over time, while, with many, changed files, and many, changed functions, specifically, rather than, generally, at once, over time, is hard, to review, thoroughly, because, a reviewer, must hold, many changes, at once, to judge, whether, behaviour, is preserved, together, specifically, rather than, generally, at once, over time, which, increases, the chance, slips through, review, unnoticed, specifically, rather than, generally, at once, over time, and that, later, by users, of small, well-tested refactorings, submitted, separately, for that refactoring, specifically, rather than, generally, for all changes, at once, as a whole, over time, as discussed, for separating, refactoring, and functional changes, generally, of this chapter, specifically, rather than, generally, for all sections, at once, as a whole, over time, therefore, improves, both review quality, and coordination, for the team, as a whole, over its work, on shared code, in the program, as a whole, over its run.
Team refactoring also benefits, from shared standards, for what, counts, as clean code, in that team, specifically, rather than, generally, for all teams, at once, as a whole, over time, because, without shared standards, one developer's refactoring, toward clarity, may be, another developer's, unnecessary churn, specifically, rather than, generally, for all code, at once, as a whole, over time, leading, to disputes, in review, about whether, a refactoring, was worthwhile, for that code, specifically, rather than, generally, for all code, at once, as a whole, over time, instead of, agreement, that it, improved, the code, for future readers, who will, work with it, over its lifetime. Shared standards, documented, briefly, with examples, of preferred, and discouraged, practices, for that team, specifically, rather than, generally, for all teams, at once, as a whole, over time, give, reviewers, a common basis, for judging, refactorings, for that team, specifically, rather than, generally, for all teams, at once, as a whole, over time, and give, developers, confidence, that their refactorings, toward those standards, will be, welcomed, in review, rather than, questioned, as personal preference, for that developer, specifically, rather than, generally, for all developers, at once, as a whole, over time, in the team, as a whole, over its work, on shared code, in the program, as a whole, over its run. Those standards, should evolve, as the team, learns, what works, for its code, specifically, rather than, generally, for all code, at once, as a whole, over time, through discussion, in review, and through, retrospectives, on refactorings, that helped, or that hindered, future work, on the code, they touched, for that team, specifically, rather than, generally, for all teams, at once, as a whole, over time, together, on shared code, in the program, as a whole, over its run.
Check: Why do small, frequent refactorings coordinate better in a team than large, infrequent ones?
Section 22
Automated refactoring tools help by performing mechanical refactorings, such as renaming a name everywhere it is used, extracting a block into a method, or moving a method to another class, quickly and consistently. A human performing the same steps by hand might miss a use, introduce a typo, or update callers inconsistently. A good tool, backed by an understanding of the program's structure, does not make those mistakes, and it frees the developer to think about design rather than about search and replace. For that reason, tools should be used whenever they are available, and their results should still be verified with tests.
What tools cannot do is decide whether a refactoring should happen at all. Renaming is safe mechanically, but choosing a better name requires understanding what a thing means and what name will make that meaning clear to future readers. Extracting a method is safe mechanically, but choosing which block to extract, and what to call it, requires judgement about the structure of the problem and the purpose of each step. Moving a method requires judgement about which class the method truly belongs to, and about whether the move improves cohesion or merely shuffles code. A tool can perform the move once the decision is made, but it cannot make the decision, because the decision depends on intent, on the domain, and on the future direction of the program, none of which are visible in the code alone.
Tools also cannot judge trade-offs. A refactoring that improves clarity in one place may introduce coupling elsewhere, or may be unnecessary because the code is stable and rarely changed, or may conflict with a larger change already planned. A human reviewer weighs those factors against the cost of the change and the risk of disturbing working code. A tool, which sees only the current structure, cannot weigh them, and applying every refactoring a tool suggests would produce churn without benefit, and would risk breaking code for no good reason.
There is a further limit. Tools work best on code they can analyse precisely. Code that relies on dynamic behaviour, on names resolved at run time, or on external configuration may not be fully understood by the tool, and a mechanical change may then miss a use or change a meaning the tool did not see. Tests help to catch such mistakes, but the developer must still review the result and ask whether it is what was intended.
The right relationship, then, is a division of labour. Let tools do the mechanical work, because they do it more reliably than people. Keep the judgement with people, because only people can decide what the code should mean, whether a change is worth its cost, and whether the result is genuinely clearer. A developer who uses tools without judgement will refactor busily and learn little. A developer who uses judgement without tools will waste effort on mechanics and make avoidable mistakes. Together, the two produce refactorings that are both safe and worthwhile.
Check: Why do automated refactoring tools not replace human judgement about whether to refactor?
Section 23
Refactoring code that works with a database involves an extra dimension that ordinary code refactoring does not. Code can be changed and redeployed in one step, but stored data persists. Data written by the old version of the code must continue to work with the new version, and data written by the new version must not break anything that still reads the old form. A refactoring that changes only the code, while ignoring the data already stored, risks corrupting that data or making it unreadable, and such damage is often far harder to undo than a code change.
The most common form of this problem is a change to the shape of stored data. Renaming a column, splitting a table, changing the type of a field, or moving information from one place to another all affect existing rows, not just future ones. A code refactoring that assumes the new shape will fail, or worse, silently misread, rows written in the old shape. The safe approach is to treat the data change as a migration: a planned, tested step that transforms existing data from the old shape to the new shape, with a way to verify that the transformation was correct and, where possible, a way to roll back if it was not.
Timing matters as well. In many systems, old and new versions of the code may run at the same time during a deployment, or old data may be read by tools and reports outside the main program. A refactoring that requires all data to change at once, and all readers to change at once, creates a fragile moment in which any part that lags behind will break. Safer patterns expand first and contract later: add the new shape alongside the old one, migrate the data, move readers to the new shape, and only then remove the old shape. Each step is small and reversible, in the same spirit as small, tested code refactorings.
Testing database refactoring therefore needs to cover data, not just code. Tests should exercise the migration against realistic stored data, including old rows, edge cases and rows that may be incomplete or unusual. They should check that data survives the migration unchanged in meaning, even if its shape changes, and that code reading the new shape handles rows that have not yet been migrated, if that situation can occur in production.
Finally, database refactoring deserves the same separation of concerns as code refactoring. A migration that changes the shape of data should be kept separate from a change that changes what the data means, so that each can be verified on its own and rolled back on its own. Treating stored data as part of the system being refactored, rather than as an afterthought, is what makes database refactoring safe. The code may be easy to redeploy, but the data it guards may be irreplaceable, and that asymmetry should shape every decision about how the refactoring is planned and carried out.
Check: Why must database refactoring consider stored data as well as code?
Section 24
Refactoring a public interface, one used by code outside your own program or package, requires care, because changing it without notice breaks the code of people who depend on it. A function, method or class that is part of a public interface is a promise. Other developers have written code against that promise, and they cannot all update their code at the moment you change yours. Deprecation provides a safe migration path by keeping that promise for a while, while clearly signalling that it will change, so that users can move to the new interface at their own pace.
The pattern works in stages. First, introduce the new interface alongside the old one, so that both exist and both work. Then mark the old interface as deprecated, with a warning that tells users what to use instead and, if possible, when the old form will be removed. Users who run their code will see the warning and can plan their migration, while their code continues to work in the meantime. Only after a reasonable period, and after the warning has had time to reach users, is the old interface removed. Each stage is small, and each can be verified separately, which is the same discipline that makes ordinary refactoring safe.
A deprecation warning should be specific and helpful. It should name the replacement, not merely say that something is deprecated, and it should be issued in a way that users will actually see, rather than buried in documentation they may not read. In Python, the standard warnings mechanism provides a conventional way to do this:
import warnings
def old_function():
warnings.warn(
"old_function is deprecated; use new_function instead.",
DeprecationWarning,
stacklevel=2,
)
return new_function()
Here the old function still works, by delegating to the new one, so existing callers are not broken. The warning tells them exactly what to change, and the delegation ensures that behaviour stays consistent between the old and new forms.
Deprecation also needs a timetable and a commitment. A deprecated interface that is never removed accumulates forever, and the codebase ends up maintaining two versions of everything. A deprecated interface that is removed without warning, or after too short a period, breaks trust. The right balance depends on how widely the interface is used and how disruptive the change is, and it should be stated plainly, in the warning or in the release notes, so that users can plan.
Used well, deprecation turns a breaking change into a gradual migration. Users are not forced to update everything at once, maintainers are not forced to support old forms forever, and the interface can evolve without breaking the ecosystem that depends on it. That is why deprecation, rather than a sudden rename or removal, is the standard tool for refactoring a public interface safely.
Check: How does deprecation provide a safe migration path when refactoring a public interface?
Section 25
Code review for clean code focuses on readability and maintainability in addition to correctness. A reviewer is not only checking whether the code works, but whether a future reader will understand it, whether its names are clear, whether its functions are small enough to follow, and whether its structure will make the next change easy or hard. Comments about those qualities can be genuinely helpful, but only if they are phrased in a way that the author can act on. A comment that merely says "this is messy" or "this is bad style" tells the author nothing useful and is likely to be experienced as obstructive, however well it is meant.
Helpful review comments are specific. Instead of a general judgement, they point to a particular line or decision, explain the problem it creates for a reader or a future change, and suggest a concrete alternative. For example, "this variable name could be misread as a count rather than a list; pending_orders would make its type clear" gives the author something to do, and explains why it matters. A comment that explains the reasoning behind a suggestion also teaches, so that the author is less likely to repeat the same pattern, whereas a bare instruction to change something teaches nothing and may feel arbitrary.
Tone matters as much as content. Review is a conversation between colleagues, and the author has usually invested effort in the code under review. Phrasing a suggestion as a question or a possibility, "would it be clearer to extract this block into a function named validate_order?" invites discussion and leaves room for the author to explain a reason the reviewer missed. Phrasing it as a command, or as a judgement of the author's ability, shuts discussion down and makes the author defensive, which is the opposite of what review is for. The goal is better code, not winning an argument about code.
It is also important to distinguish between issues that must be fixed and suggestions that are a matter of taste. A misleading name that could cause a bug, or a structure that will make the next change dangerous, is worth blocking a change over. A preference for one equally clear naming style over another is not, and insisting on it wastes both people's time and erodes goodwill for the occasions when a strong objection genuinely matters. Reviewers should say plainly which category a comment falls into, so that the author knows what requires action and what is open for discussion.
Finally, good review comments acknowledge what is good as well as what needs work. A clear function, a well-chosen name, or a thoughtful comment is worth noting, not only because encouragement matters, but because it shows the author, and other reviewers, what the team's standard looks like in practice. Review conducted in this spirit, specific, reasoned, kind and proportionate, improves code without obstructing the people who write it, and it builds the shared understanding of clean code that makes future reviews faster and easier.
Check: How should clean code review comments be phrased to be helpful rather than obstructive?
Section 26
Clean code helps onboarding, because new developers must learn a codebase quickly to become productive, and a codebase that is readable, well named and well structured is far easier to learn than one that is tangled or obscure. But onboarding also works in the other direction. New developers bring fresh questions, and those questions are a valuable diagnostic tool for the clarity of a codebase. A question that a newcomer asks is often a question that the code failed to answer on its own, and that failure is worth fixing, not merely answering in conversation.
When a new developer asks what a function does, or why a particular step exists, or what a variable means, the first response should be to listen carefully, because the question reveals a gap. Perhaps a name is misleading, and the newcomer was led to expect something different from what the code does. Perhaps a step lacks a why-comment, and the reason for it is known only to the original author. Perhaps a structure is more complex than it needs to be, and the newcomer is struggling for a good reason. In each case, the question points to a specific improvement: a rename, a comment, or a simplification that would help the next newcomer as well as this one.
Treating fresh questions this way requires a deliberate habit. It is easy, and often tempting, to answer the question verbally and move on, leaving the code exactly as unclear as it was. The newcomer leaves satisfied, but the next newcomer will ask the same question again, and the knowledge will remain trapped in people's heads rather than recorded in the code. A better habit is to fix the code as part of answering the question, or to record the fix as a small task to be done promptly. If a name was confusing, rename it. If a reason was missing, add the comment. If a structure was obscure, consider whether a small refactoring would make it plain.
This approach also benefits the newcomer. Being invited to point out unclear code, and seeing their questions lead to real improvements, signals that the team values clarity and that asking questions is welcome rather than a sign of weakness. New developers are often reluctant to criticise code written by more experienced colleagues, so the invitation needs to be explicit. A team that says, plainly, that fresh eyes are an asset, and that questions about unclear code are contributions, will hear more of them, and will learn more from them.
There is a limit, of course. Not every question indicates a problem in the code. Some questions arise because the newcomer has not yet learned the domain, and the right response is an explanation, not a change. The judgement lies in asking whether a reasonable reader, with the relevant background, would have understood the code without asking. If the answer is no, the code should change. If the answer is yes, the explanation is enough. Applied with that judgement, new developers' questions become a steady, low-cost source of improvements, and onboarding becomes not just a process of bringing people up to speed, but a process that leaves the codebase clearer than it was before.
Check: How can new developers' fresh questions help improve a codebase's clarity?
Section 27
Measuring code quality helps a team track whether its code is getting cleaner or messier over time. Metrics such as function length, the number of parameters, the depth of nesting, or the amount of duplication can be computed automatically, and they can highlight parts of a codebase that deserve attention. Used well, they are a useful prompt for investigation. Used badly, as absolute thresholds that code must pass, they become a source of distortion, because developers will optimise for the metric rather than for the quality the metric was meant to represent.
The problem with absolute thresholds is that they treat every part of a codebase as if it were the same. A function that is long because it implements a single coherent algorithm, with steps that belong together, is not necessarily worse than a shorter function that hides its complexity elsewhere. A module with more duplication than some threshold allows may contain coincidentally similar code that should not be unified. A strict limit, enforced without judgement, will push developers to split functions that should stay together, to group parameters that should stay separate, or to extract shared code that represents different ideas, all in order to satisfy a number, while making the code harder to read. The metric improves, and the code gets worse.
Metrics are far more useful as trends. If the average function length in a codebase is rising steadily, or if duplication is growing month by month, that trend is a signal worth investigating, even if no single function breaches a limit. It suggests that the codebase is accumulating complexity faster than it is being tidied, and that the team may need to invest more effort in refactoring, or to look at why new code is being written in a tangled way. Conversely, if the trend is improving, the team can be confident that its habits are working, without needing to celebrate any particular number.
Trends also allow for context. A team can look at where a metric is changing, rather than merely at its overall value, and can ask whether the change reflects a genuine problem or a legitimate feature, such as a complex new area of the program that is inherently harder to express simply. That investigation, prompted by the metric but not dictated by it, is where the value lies. The metric tells the team where to look. Human judgement decides what, if anything, to do.
A practical approach, then, is to track a small number of metrics that the team understands, to review their direction regularly, and to treat sudden changes as prompts for conversation rather than as failures to be fixed mechanically. Avoid turning metrics into targets for individuals, since that invites gaming and discourages honest work on genuinely hard code. Used as trend prompts, metrics support the same habit that runs through clean code generally: notice a signal, investigate it with judgement, and act only where investigation confirms a real problem.
Check: Why are code quality metrics more useful as trend prompts than as absolute thresholds?
Section 28
Refactoring and documentation must be kept in step, because documentation that describes a program's structure becomes misleading after refactoring changes that structure. If a guide says that a particular task is performed by a particular function, or that data flows through a particular set of classes, and a refactoring renames those functions, splits a class, or reorganises the flow, the guide is now wrong, even though the program still works. A reader who follows the outdated guide will be confused, and may make changes based on a picture of the program that no longer exists.
Documentation that focuses on behaviour is far more resilient to this problem. Behaviour, what the program does, what inputs it accepts, what outputs it produces, and what guarantees it offers, changes much less often than structure. A refactoring, by definition, does not change behaviour, so documentation that describes behaviour survives refactoring untouched. A guide that says "the program validates an order before charging the customer, and rejects orders with missing items" remains true whether validation lives in one function or several, and whether charging is performed by one class or another. Structure-focused documentation, by contrast, must be updated after almost every refactoring, and in practice it often is not, which is how it becomes stale.
This does not mean structure should never be documented. For a newcomer learning a codebase, a map of its main parts can be genuinely helpful. But such maps should be kept short, high-level, and clearly marked as an overview, so that they are easy to update and so that readers do not rely on details that may have changed. Detailed structural documentation, naming every function and describing every internal step, is a maintenance burden that refactoring will quickly outgrow, and it is usually better replaced by clear code itself, with good names and small functions that make the structure self-evident.
A useful rule is to document the contract, not the implementation. State what a component promises to do, what it requires from its callers, and what it guarantees about its results, and leave the internal steps to the code. When the implementation is refactored, the contract stays the same, and the documentation stays correct. When the contract itself changes, that is a functional change, not a refactoring, and the documentation should be updated as part of that change, alongside the code.
Finally, treat documentation updates as part of the refactoring checklist. Before finishing a refactoring, ask whether any documentation describes the structure that has just changed, and update it or remove it. Documentation that is known to be outdated is worse than no documentation, because it misleads with confidence. Behaviour-focused documentation, kept in step with the program's promises rather than its internal shape, avoids that trap and remains a reliable guide through many rounds of refactoring.
Check: Why is behaviour-focused documentation more resilient to refactoring than structure-focused documentation?
Section 29
Knowing when not to refactor is as important as knowing how to refactor, because refactoring carries risk and cost, and spending that risk and cost where it brings no benefit is wasteful. Stable, rarely changed code is a clear example. If a piece of code works correctly, is well tested, and is seldom touched, refactoring it will not make future changes easier in any meaningful way, because there are few future changes to make easier. The benefit is hypothetical, while the risk, that the refactoring introduces a bug into working code, and the cost, in time and review effort, are real and immediate.
Code that is about to be replaced is an even clearer case. If a component is scheduled for removal, or for a major rewrite, in the near future, tidying its internal structure now is effort that will be thrown away when the replacement arrives. The time would be better spent on the replacement, or on code that will live longer. Similarly, code that is about to be changed for functional reasons should usually be refactored, if at all, as a separate step before the functional change, not as part of a large combined effort, and if the functional change will reshape the code anyway, a preliminary refactoring may be wasted work.
This is not an argument for leaving messy code alone forever. It is an argument for judging refactoring by its return. Refactoring pays off when code is changed frequently, when it is hard to understand and that difficulty is causing bugs or slowing work, when it blocks a new feature, or when it is about to be extended in a way that its current structure makes dangerous. In those cases, the investment is repaid by safer, faster future changes. When none of those conditions holds, the return is low, and the effort is better spent elsewhere.
A practical test is to ask what will be different after the refactoring, in concrete terms. Will the next change to this code be easier, and is such a change actually expected? Will a bug that is currently likely become less likely? Will a new developer understand this code materially better, and will that understanding matter soon? If the honest answers are vague or negative, the refactoring is probably not worth doing now, however untidy the code looks. Tidiness for its own sake is not a sufficient reason to disturb working code.
None of this means stable code should never be touched. If a bug is found in stable code, fix it, and refactor only the part needed to make the fix safe. If stable code must be extended after a long quiet period, refactor it then, when the benefit is real and immediate. The discipline is to let the need drive the refactoring, rather than refactoring on principle alone. A codebase kept clean by refactoring where it matters, and left alone where it does not, will be both healthier and less risky than one in which every untidy corner is chased regardless of cost.
Check: When is refactoring stable, rarely changed code usually a poor use of effort?
Section 30
A refactoring checklist helps you apply the practices of safe refactoring consistently, without forgetting a step that matters for safety. Refactoring is made of small decisions, and under pressure it is easy to skip one, to mix a functional change into a structural one, or to forget to run the tests. A checklist turns the discipline into a habit, and it gives a reviewer a clear picture of what was done and what was verified. The steps below form a practical checklist that can be adapted to a team's needs, but the order and the principles behind them are worth keeping.
The first step is to understand the code before changing it. Read the function or class to be refactored, and make sure you know what it does, what inputs it handles, and what its callers expect. If the code is unclear, that understanding may itself require investigation, and it may reveal that the refactoring you planned is not the right one. Rushing into restructuring code you do not understand is a common source of refactoring mistakes.
The second step is to ensure tests exist that capture current behaviour. If adequate tests are already in place, run them to confirm they pass before you start. If they are not, write characterisation tests first, covering representative inputs and edge cases, and confirm that they pass against the unchanged code. These tests are your safety net, and without them the rest of the checklist cannot be verified.
The third step is to make one small change at a time. Rename one name, extract one function, or move one method, and nothing else. Do not combine a refactoring with a functional change, and do not bundle several refactorings into a single step. Small steps make failures easy to diagnose, because a failing test after a small step can only have been caused by that step.
The fourth step is to run the tests after each change. If they pass, the step is safe, and you can proceed to the next one. If they fail, stop, diagnose the failure, and either fix the step or revert it and try a different approach. Do not press on in the hope that later steps will fix an earlier failure, because later steps built on a broken foundation are likely to be broken themselves.
The fifth step is to review the result for clarity, not merely for passing tests. A refactoring that passes tests but leaves code no clearer than before has not achieved its purpose. Read the refactored code as a stranger would, and ask whether its names, structure and flow now make its intent plain. If they do not, consider whether a different refactoring, or a further small step, is needed.
The final step is to commit the refactoring separately from any functional change, with a clear description of what was restructured and why, and with the test results noted. A separate, well-described commit makes the change easy to review, easy to understand in the project's history, and easy to revert if a problem is discovered later. Followed in order, these steps, understand, test, change small, verify, review for clarity, and commit separately, turn refactoring from a risky improvisation into a repeatable, safe practice.
Check: What are the main steps of a safe refactoring checklist?
Section 31
Clean code looks slightly different in a short script than in a library that many programs depend on, and applying the same standard rigidly to both is a mistake. A script written to process one file, once, for its author alone, documentation, wastes the author's time, and makes the script harder, not easier The right standard for a script is modest: clear names, a readable flow, a month later, if they needed to rerun it, with slightly different data, for a similar task, without rewriting it, from scratch, because they had forgotten, what it did, or how, it worked, in detail, for its original purpose.
A library, by contrast, is read by strangers, who cannot ask its author, what it was meant to do, and who must rely, on its interface, and its documentation, alone, to use it, correctly, for their own programs, with their own data, and their own requirements, that the library's author, never anticipated, in detail, for each user, individually. Its code must therefore be clearer, more carefully named, and more stable, in its public interface, than a script's code, because every unclear name, or surprising behaviour, in a library, is multiplied, across all its users, each of whom, must spend time, understanding it, or working around it, for their own use, of the library, in their program, as a whole, over its run. A library's internal code, should also be clean, because libraries are maintained, over years, by developers, who were not, its original authors, and who must, understand its internals, to fix bugs, or to add features, without breaking, existing users, who depend on it, for their work, with the library, as a whole, over its lifetime. That difference, in audience, and in lifetime, is why, the same clean code practices, apply, with different intensity, to scripts, and to libraries, rather than, with identical strictness, to both, regardless, of their purpose, or their audience, in practice.
Between those extremes, or a script that gradually grows, into a lasting tool, used, by many people, over time, as its usefulness, becomes apparent, to others, beyond its original author, for their own work, with similar tasks, that the tool, can help them, with, in practice. For such programs, clean code practices should grow, with the program's audience, and lifetime, so that, a tool, that starts, as a personal script, gains, clearer names, better structure, and better documentation, as more people, start, to use it, for their work, with similar tasks, over time. That growth, in cleanliness, alongside growth, in use, is natural, and healthy, because it, invests effort, where it pays off, for real readers, rather than, in advance, for hypothetical readers, who may never, actually appear, for a given program, in practice. The practical rule, is to write, code, that is clear, enough, for its current readers, and to, improve it, when its readership, grows, or when, its maintenance, becomes harder, for its current readers, who must, work with it, over its lifetime, rather than, aiming, from the start, for a perfection, that its current use, does not require, and that would, waste effort, better spent, on the program's actual purpose, for its real users, at that time, during its development, or its maintenance.
Check: Why should clean code standards differ between a throwaway script and a shared library?
Section 32
Naming conventions in a team are agreements about how names are formed, without needing, to learn, whose code, capitalisation, of names, such as, variables, functions, classes, and constants, as discussed, for naming, generally, and they, prefixes, or suffixes, that signal, such as, a prefix, that marks, a name, as private, specifically, rather than, generally, at once, over time. Those conventions, are not, arbitrary, style, but, signals, that help, readers, parse, code, quickly, what kind, of thing, they are, looking at, before they, in detail, specifically, rather than, generally, at once, over time, and consistent, use, of conventions, makes, every name, more predictable
Conventions should be documented, briefly, with examples, of preferred, and discouraged, names, specifically, rather than, generally, at once, over time, so that, new members, quickly, without guessing, alone, older names, specifically, rather than, generally, at once, over time, due to, conventions, changing, over time, learns, what works, specifically, rather than, generally, at once, over time, together Documentation, for conventions, should focus, behind them, specifically, rather than, generally, at once, over time, such as, why, helps readers, distinguish, classes, from functions, specifically, rather than, generally, at once, over time, because, reasons, help, developers, apply, conventions, thoughtfully, that documentation, does not, explicitly cover, specifically, rather than, generally, at once, over time, while, bare rules, without reasons, invite, mechanical compliance, without understanding, specifically, rather than, generally, at once, over time, and invite, disputes, in review, about whether, a rule, was actually, violated, specifically, rather than, generally, at once, over time, in the program, as a whole, over its run.
Conventions should also be enforced, lightly, through automated checks, where possible, specifically, rather than, generally, for all code, at once, as a whole, over time, rather than, through lengthy, review debates, about capitalisation, specifically, rather than, generally, for all names, at once, as a whole, over time, which, waste review time, that could, be spent, on logic, and design, for the change, under review, specifically, rather than, generally, for all changes, at once, as a whole, over time, in practice, generally. Automated checks, for naming, can flag, names, that do not, follow conventions, specifically, rather than, generally, for all code, at once, as a whole, over time, without human intervention, for each name, specifically, rather than, generally, for all names, at once, as a whole, over time, in a way, that is, consistent, and impersonal, for all developers, on the team, specifically, rather than, generally, for all teams, at once, as a whole, over time, together, on shared code, in the program, as a whole, over its run. That automation, frees, reviewers, to focus, on whether, names, are clear, and meaningful, for their purpose, in the program, as a whole, over its run, which, automation, cannot judge, specifically, rather than, generally, for all names, at once, as a whole, over time, because, clarity, depends, on domain meaning, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time, that only, a human reader, familiar, with the domain, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, can assess, accurately, for that name, specifically, rather than, generally, for all names, at once, as a whole, over time, in practice, generally.
Check: Why should naming conventions be documented with reasons, not just as bare rules?
Section 33
Functions should take as few arguments as possible, because each argument is a burden on callers, who must supply it correctly, and on readers, who must understand what each value means and how it affects the function's behaviour. A function with many arguments is harder to call correctly, harder to test, because each combination of arguments may need a test, and harder to understand, because the reader must hold all the arguments in mind at once. A guideline that suggests keeping the number small, often stated as three or fewer, captures that burden in a memorable form, and it is a useful prompt to stop and think.
It is a prompt for thought rather than a rigid rule, however, because the right number of arguments depends on what the function does. Some functions genuinely need several values that do not belong together and cannot sensibly be grouped. Forcing such a function to take fewer arguments by bundling unrelated values into a single object does not reduce complexity. It hides it, because the caller must still supply all the values, and the reader must now unpack the bundle to understand what the function actually needs. In that case, the guideline has been satisfied in form while the underlying problem remains, and the code is arguably worse, because the function's true requirements are now less visible.
Conversely, a function with few arguments is not automatically clean. A function that takes a single large object, and then reaches deep inside it for many values, may be harder to understand and test than a function that takes those values explicitly, because its dependencies are hidden. Counting arguments alone cannot capture that difference. What matters is whether the function's inputs are clear, whether they belong together, and whether the function can be understood and tested without excessive effort.
The guideline is therefore best used as a question, not as a limit. When a function grows beyond a few arguments, ask whether it is doing too much and should be split, whether some arguments belong together as a coherent group that deserves its own type, or whether some arguments are really configuration that could be supplied once rather than on every call. Each of those questions may lead to a genuine improvement. But if none of them leads anywhere, if the function does one thing well and simply needs its inputs, then keeping the arguments is the honest choice, and the guideline has still done its job by making you check.
A related habit is to be suspicious of arguments that change the function's behaviour in a fundamental way, such as a flag that switches between two different tasks. Such a flag often indicates that the function is really two functions, and splitting it will usually be clearer than any argument-count rule could require. In short, treat the three-argument guideline as a nudge to examine a function's design, not as a rule to be obeyed by reshaping code into a form that looks compliant while remaining hard to use. Judgement, applied to the function's purpose and its callers, decides when the guideline points to a real improvement and when it does not.
Check: Why is a three-argument guideline a prompt for thought rather than a rigid rule?
Section 34
Comments in legacy code deserve special care, because a comment that was accurate when written may have become inaccurate since, as the code around it changed without the comment being updated. In legacy code, where many hands have touched the program over a long period, and where the original authors may no longer be available to ask, the chance that a comment has gone stale is high. That makes the situation where code and comment disagree a common one, and it calls for investigation rather than a quick decision to trust one or the other.
Trusting the comment without question is risky. If the comment is stale, following it may mean "fixing" code that is actually correct, introducing a bug into a program that was working, or reverting a deliberate change whose reason was never recorded. The code, after all, is what actually runs, and if it has been tested and used in production, its behaviour, however surprising, may be the behaviour that users rely on. Changing it to match an old comment, without understanding why it differs, is a gamble.
Trusting the code without question is also risky. Code can contain bugs that have gone unnoticed, especially in paths that run rarely, and a comment may preserve the original intent that the code now fails to implement. If the code was changed by mistake, or if an earlier refactoring introduced an error that tests did not catch, the comment may be the only record of what was intended. Dismissing it outright would lose that information and leave the bug in place.
Investigation, therefore, is the right response. Start with the history, if it is available: when was the comment written, when did the code last change, and were those changes described in a way that explains the difference? Then look at the tests. Do they support the code's current behaviour or the comment's description? Then look at the surrounding system. Which behaviour do callers expect, and which behaviour would break them if it changed? In some cases, a small experiment, running the code with a revealing input, can settle the question quickly.
Whatever the investigation finds, record the outcome. If the code is correct and the comment is stale, update the comment so the next reader is not misled. If the comment is correct and the code is wrong, fix the code, and treat that fix as a functional change, not a refactoring, with tests to confirm the new behaviour. If the investigation cannot settle the question, say so in a comment, and note what is unknown, rather than silently choosing one side. Legacy code rewards this caution, because its history is often the only guide to intent, and intent is exactly what is at stake when code and comment disagree.
Check: When code and a comment disagree in legacy code, why investigate rather than simply trusting one?
Section 35
Version control supports clean refactoring, because it lets you make small changes, commit them separately, and describe each change clearly, so that the history of a program tells the story of how it evolved. That history is valuable for reviewers, for future maintainers, and for anyone who needs to understand why a piece of code looks the way it does. It is also a safety net: a change that turns out to be mistaken can be reverted, but only if it can be identified and separated from other changes. Both benefits depend on commits being small and separately described.
Small refactoring commits matter for the same reason that small refactoring steps matter. A commit that contains only a rename, or only an extraction, can be reviewed quickly, because the reviewer can see exactly what changed and can check that behaviour did not change. A commit that mixes a refactoring with a functional change, or that bundles many refactorings together, forces the reviewer to untangle several kinds of change at once, and makes it harder to spot a mistake in any of them. It also makes reversion harder: if the functional change is good but the refactoring is flawed, the two cannot be separated without manual work.
Separately describing each commit matters just as much. A commit message for a refactoring should say what was restructured and why, not merely that code was cleaned up. For example, "extract order validation into validate_order so that the checkout function reads as a sequence of named steps" tells a future reader what changed and what problem it solved. A vague message, by contrast, leaves the reader to reconstruct the reasoning from the diff alone, which is often impossible long after the change was made. Good messages also help when searching history for the origin of a bug or a design decision.
The discipline extends to the order of commits. Refactoring commits should come before the functional changes that build on them, so that the history reads as a sequence: first the structure was improved, then the new behaviour was added on top of the improved structure. A reviewer following that sequence can verify each step in turn. A history in which refactoring and functionality are interleaved is much harder to follow, and it hides the point at which behaviour actually changed.
There is a practical cost to this discipline, since it requires committing more often and writing more messages. That cost is repaid quickly, in faster reviews, in easier debugging when a regression appears, and in a history that future maintainers can actually use. A useful habit is to commit after each small, tested refactoring step, with a message that states the step and its reason, before moving on to the next step or to new functionality. Over time, that habit produces a history that is itself a form of documentation, showing not just what the code is, but how and why it came to be that way.
Check: Why should refactoring commits be small and separately described from functional changes?
Section 36
Module structure affects clean code, because how a program is organised into modules shapes how easily it can be read, understood and changed. A module should have a clear responsibility, and its dependencies, the other modules it uses, should be few and easy to follow. When that structure is sound, a reader can understand one module without needing to understand the whole program, and a change to one module can be made with confidence that its effects are contained. Circular dependencies break that property, and that is why they are a sign of poor structure.
A circular dependency occurs when two modules depend on each other, directly or through a chain of other modules. Module A imports something from module B, and module B imports something from module A, so neither can be understood, loaded or tested without the other. In Python, such a cycle can cause import errors or surprising behaviour, because the order in which modules are loaded determines which definitions are available when. Even when the program happens to work, the cycle makes the structure fragile: a change in either module may require a change in the other, and the two modules can no longer evolve independently.
The deeper problem is what the cycle reveals about the design. If two modules genuinely need each other, it usually means that a shared idea has been split across them, or that a responsibility has been placed in the wrong module. Perhaps a function in A uses data from B, while a function in B uses data from A, because the two functions really belong together, or because a third concept that both depend on has not been identified and given its own module. The cycle is a symptom of that confusion, not merely a technical inconvenience, and fixing it by rearranging imports without addressing the underlying design will usually leave the confusion in place, ready to reappear.
Breaking a cycle therefore starts with understanding it. Identify what each module uses from the other, and ask whether those uses represent a genuine shared concept. If they do, extract that concept into a third module that both can depend on, so that the dependency flows in one direction. If one use is incidental, move the function or value that causes it to the module where it belongs, so that the dependency disappears. In either case, the result should be a structure in which dependencies flow in one direction, from higher-level modules to lower-level ones, without loops.
That one-way flow is sometimes called a layered structure, and it is worth aiming for because it makes the program easier to reason about. A reader can start at the top layer and follow dependencies downward, knowing that no lower module will unexpectedly reach back upward. Testing becomes easier too, because a lower module can be tested without the higher modules that use it. Circular dependencies deny all of those benefits, and removing them, by clarifying responsibilities rather than merely shuffling imports, is one of the more valuable structural refactorings a team can undertake.
Check: Why are circular dependencies between modules a sign of poor structure?
Section 37
Magic numbers and magic strings are literal values buried in code without any explanation of what they mean, such as 0.2 in a calculation, 7 in a loop, or "PENDING" in a comparison. A reader encountering such a value must guess its meaning from context, and may guess wrongly. A future maintainer who needs to change the value must find every place it appears and change each one, risking a missed copy. Replacing a magic value with a named constant solves both problems at once, by giving the value a name that states its meaning and a single home where it can be changed.
Consider a discount calculation written with a literal:
total = price * 0.8
The reader must work out that 0.8 means a twenty percent discount, and must hope that no other meaning of 0.8 is intended. Written with a constant, the same line becomes self-explanatory:
DISCOUNT_RATE = 0.20
total = price * (1 - DISCOUNT_RATE)
Now the purpose is plain, and if the discount rate changes, it changes in one place. The name also documents the value's role for every reader, without needing a comment, and it makes the code easier to search: finding all uses of the discount rate is a matter of searching for one name, rather than searching for a number that may appear for unrelated reasons.
Named constants also help changing code safely in a subtler way. When the same literal appears in several places for the same reason, giving it one name unifies those places, so that a change is made once and applies everywhere. When the same literal appears for different reasons, giving each use its own name keeps them separate, so that changing one does not wrongly change the other. The act of naming forces the developer to decide which case applies, and that decision itself clarifies the code. Two values that happen to be equal today, such as a tax rate and a discount rate that are both twenty percent, should have different names if they are governed by different rules, because they will diverge in future.
There are exceptions. Small, universally understood values, such as 0 or 1 in a simple count, or an index that is clearly explained by its context, do not always need a name, and naming every trivial literal can clutter code with constants that add no understanding. The test is whether a reader would need to stop and ask what the value means, or whether a future change to the value would need to be made in more than one place. If either answer is yes, a named constant is warranted.
Used with that judgement, named constants are a small refactoring with a large payoff. They make code easier to read, because values carry their meaning with them, and easier to change, because each piece of knowledge has a single home. That combination of readability and maintainability is exactly what clean code aims for, and it is why replacing magic values is one of the first refactorings worth learning.
Check: How does replacing a magic value with a named constant help both reading and changing code?
Section 38
Clean code helps debugging, because when a program fails, a developer must find the part of the program responsible for the failure, and clean structure makes that search short. In tangled code, a failure could have been caused by any part of a long, intertwined sequence, and finding it requires reading and reasoning about the whole sequence. In clean code, the failure can usually be localised quickly, because the program is divided into small functions with clear purposes, and the evidence from the failure points to one of them.
Small functions localise a failure in several ways. First, a stack trace, the record of which functions were running when the failure occurred, becomes informative. If the trace shows a failure inside a function named calculate_discount, the developer knows immediately which piece of logic failed, and can focus there, rather than searching through a large function that does many things.
Second, small functions can be tested in isolation. Once a failing case is known, the developer can reproduce it by calling the suspect function directly, with the same inputs, and observe its behaviour without needing to run the whole program or set up a complex scenario. That isolation makes it possible to confirm or rule out a suspect quickly, and to experiment with a fix without disturbing other parts of the system. In a large function, the same experiment would require reproducing the entire sequence that leads to the failure, which is slower and more error-prone.
Third, small functions limit the scope of what can be wrong. A function that does one thing well has a small number of inputs, a clear purpose, and a limited set of ways it can fail. A developer examining it can reason about all of its behaviour in a short time. A large function, with many variables and many steps interacting, has a much larger space of possible faults, and reasoning about it requires holding all of those interactions in mind at once. That difference is often the difference between a bug found in minutes and a bug found in hours.
Consider a concrete example. Suppose an order total is wrong. In a program where the total is computed by a single function that also validates items, applies discounts and adds tax, the developer must examine all of those steps to find the error. In a program where each step is a small function, the developer can check each step's output in turn, perhaps by calling the functions directly or by adding temporary output, and the faulty step will reveal itself as the first one whose output is wrong. The search becomes a short walk down a chain of named steps, rather than a hunt through an undifferentiated block.
Clean code does not prevent bugs, and it does not make every failure trivial to find. But by dividing a program into small, named, testable parts, it ensures that when a failure occurs, the developer has a clear place to start, a short list of suspects, and a practical way to narrow that list down. That is a large part of what makes a codebase maintainable in practice, long after it was first written.
Check: How do small functions localise a failure during debugging?
Section 39
Shotgun surgery is a smell in which a single logical change requires making many small changes scattered across a codebase, because the logic involved is itself scattered rather than held together in one place. Divergent change is a different smell in which a single module or class must be changed in many different ways, for many different reasons, because it contains unrelated responsibilities that happen to live together. Both are cohesion problems, meaning that they arise from code not being grouped according to what belongs together, but they point in opposite directions, and confusing them leads to the wrong fix.
Shotgun surgery appears when a concept is spread too thinly. Suppose a new kind of order status is added, and that change requires edits in validation, in display, in reporting, in storage, and in several other places, each edit small, but together forming a scattered change that is easy to get partly wrong. The problem is that the knowledge about order status lives in many places, so no single place can be changed to effect the whole change. The risk is a missed edit: one place is updated and another is forgotten, leaving the program inconsistent. The remedy is to gather the scattered logic together, so that the concept has a single home, and a change to it can be made in one place.
Divergent change appears when a concept is packed too tightly. Suppose a single class handles user authentication, order processing and report generation, because those features were added to it over time. A change to authentication requires editing that class, a change to order processing requires editing the same class, and a change to reporting requires editing it again, for entirely different reasons. The problem is not that a change is scattered, but that unrelated changes all land in the same place, making the class harder to understand, harder to test, and riskier to change, because an edit for one purpose may disturb code serving another purpose. The remedy is to split the class, giving each responsibility its own home, so that each kind of change touches only the part that owns it.
The two smells can be told apart by asking where a change lands. If one logical change requires edits in many places, that is shotgun surgery, and the fix is to bring the scattered pieces together. If many unrelated changes all require edits in one place, that is divergent change, and the fix is to split that place apart.
Recognising the difference matters because the remedies are opposites, and applying the wrong one makes the problem worse. Splitting a class that suffers from shotgun surgery would scatter its logic further. Gathering the responsibilities of a class that suffers from divergent change would make it even more overloaded. The shared principle behind both fixes is cohesion: code that changes together, for the same reason, should live together, and code that changes for different reasons should live apart. Judged by that principle, each smell points clearly to its own remedy, and a codebase organised by cohesion will suffer from neither.
Check: How does shotgun surgery differ from divergent change as a cohesion problem?
Section 40
This chapter has covered clean code and refactoring, through naming, small functions, comments, formatting, duplication, to refactoring, its safety, through tests, such as, extract method, rename, move method, and replacing, conditionals, with polymorphism, generally, and specifically, covered, were described, specifically, rather than, generally, at once, but, for programs, read, understood, and changed, safely, who were, specifically, rather than, generally, at once, over time, and that, refactoring, is how, clean code, is achieved, and maintained, gradually, over time, without needing, large, disruptive efforts, that compete, for time, and that, risk, introducing bugs, through large-scale, changes, to code, that was, previously working, correctly, in practice, generally, and specifically, maintained, through continuous, small refactorings, generally.
The habit to carry forward is simple: write code, could understand, once, specifically, rather than, generally, at once, over time, without needing, to ask, its author, what it, was meant, to do, specifically, rather than, generally, at once, over time, and when, you encounter, code, specifically, rather than, generally, at once, over time, improve it, tested step, specifically, rather than, generally, at once, over time, before, moving on, specifically, rather than, generally, at once, over time, in practice, generally. That habit, of writing, clearly, and improving, incrementally, for code, that you, encounter, accumulates, that stays, clean, over time, without needing, heroic efforts, up, specifically, rather than, generally, at once, over time, when, unbearably messy, for developers, who must, generally, and specifically, maintained, generally. It, also, makes, your work, more enjoyable, because, working, is pleasanter, than working, who must, spend, reading, and changing, existing code, rather than, writing, new code, from scratch, generally, and specifically, who benefits, generally.
Finally, remember, and refactoring, serve, users, ultimately, because, programs, that are, are easier, to fix, when they, break, for users, and easier, to improve, that users, request, generally, and specifically, enables, faster, safer changes, over its lifetime, generally. A program, whose code, is messy, may, still work, correctly, for its users, today, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, but, it will, become, harder, to maintain, with every change, made, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, until, eventually, changes, become, so risky, and so slow, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, that users, suffer, from delayed, fixes, and delayed, features, for their work, with the program, as a whole, over its run, generally, and specifically, for each program, whose messy code, eventually, harms, its users, through slow, risky maintenance, over its lifetime, generally. Investing, in clean code, and in refactoring, therefore, is investing, in users, as well as, in developers, for a program, as a whole, over its run, because, maintainable code, serves, users, better, over time, than code, that is, merely working, today, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, but that, cannot be, maintained, safely, for tomorrow, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, in practice, generally, and specifically, for each program, you will write, or maintain, with clean code, as a practice, from now on, generally.
Check: What habit summarises this chapter's approach to clean code and refactoring?
Section 41
Data processing code deserves particular attention for readability, messy inputs, can produce, for many records, before anyone, notices, the error, in practice. A processing step that filters, converts, and aggregates data, in a single, long function, all three operations, together, is correct, for its purpose. Splitting processing into stages, tested, and verified, on its own, with sample data, that shows, to its input, it merely produces, values, that look plausible, for the records, they describe, in a way, that only careful, stage-by-stage checking, can reveal, before those results, are used, for decisions, for real users, with real data, at stake, in practice.
Naming matters even more, in data processing, because data fields, often have, similar names, across different stages, of a pipeline, and because, a field, that means, one thing, at input, may mean, something slightly different, after conversion, for that field, of the pipeline, as a whole. A variable that holds, raw input values, should not, be given, the same name, as a variable, that holds, cleaned values, after conversion, because, a reader, encountering, either variable, in later code, will not know, which stage's values, it actually holds, for that variable, in that part, of the program, without tracing, its history, through earlier stages, of the pipeline, as a whole. Giving, each stage's output, a distinct, descriptive name, that states, what transformation, has been applied, to it, so far, in the pipeline, as a whole, prevents, that confusion, and makes, each stage's input, and output, clear, at a glance, for a reader, who is trying, to understand, one stage, in isolation, without reading, the whole pipeline, for that stage, in practice. That naming discipline, combined, with stage-by-stage testing, using, small samples, of real data, that exercise, each stage's logic, for its purpose, in the pipeline, as a whole, gives, data processing code, a level, of trustworthiness, that monolithic processing, rarely achieves, for programs, that handle, important data, for real users, with real decisions, depending on it, over time.
Finally, its assumptions, about its input, plainly, in comments, or in documentation, for that code, because, assumptions, about data, are often, the source, of silent errors, when input, changes, in ways, that violate, those assumptions, without any, visible failure, in the program, as a whole, over its run. If a stage, assumes, that its input, is sorted, or that, a field, is never empty, or that, a value, is always, for that stage, in the pipeline, as a whole, that assumption, should be, stated, where the stage, is defined, for that stage, in the pipeline, as a whole, so that, a future maintainer, who changes, an earlier stage, or who, supplies, new input, for that pipeline, as a whole, can see, whether, their change, violates, an assumption, depends on, for its correctness, in practice. Stating assumptions, explicitly, also, makes them, testable: a test, can supply, input, that violates, an assumption, and verify, that the stage, handles it, gracefully, for that input, rather than, producing, silently wrong output, for that input, in the pipeline, as a whole. That combination, of clear stages, distinct names, and stated assumptions, is what, clean, in the sense, that matters, most, merely readable, but trustworthy, for decisions, for real users, with real data, at stake, in practice.
Check: Why are distinct names for each pipeline stage's output important in data processing code?
Section 42
Refactoring toward a pattern, are related, but distinct, activities, because, the first, introduces, as discussed, while, the second, improves, readability, and maintainability, in general, without necessarily, introducing, at all, being improved, in practice. A refactoring, that extracts, a method, to make, a function, smaller, and clearer, who must, even though, it introduces, no pattern, that introduces, a factory, to centralise, object creation, because, it introduces, with known, roles, and consequences, even though, it also, improves, readability, of centralising, creation, together Recognising, that distinction, helps you, choose, because, sometimes, the goal, is simply, clearer code, to introduce, in practice, while, the goal, that solves, will recognise, in code
In practice, of refactoring, often, go together, where, comes first, follows, once, the code, is clear, enough, to be, that does, many things, together, hard-to-follow sequence, of steps, may hide, could solve, but, that problem, is hard, to see, until, the function, is first, split, each with, the function, is clear, may become, obvious: perhaps, its steps, vary, by kind, of object, it handles, in the program, as a whole, over its run, suggesting, polymorphism, or strategy, for that variation, in the program, as a whole, over its run, or perhaps, its creation logic, is duplicated, in many places, for that logic, in the program, as a whole, over its run, suggesting, a factory, for that creation, in the program, as a whole, over its run. That sequence, from clarity, first, to pattern, second, is safer, than jumping, directly, to a pattern, for tangled code, because, a pattern, applied, to unclear code, often, merely, reorganises, its confusion, structure, without actually, clarifying, it, for future readers, who must, work with it, over its lifetime, in practice.
Conversely, removing, a pattern, that no longer, earns its complexity, of a program, during its evolution, and during, its maintenance, is also, clean code refactoring, in effect, because, it simplifies, code, for future readers, who need not, understand, pattern machinery, that serves, no current purpose, for that part, of the program, in the program, as a whole, over its run, even though, the change, is described, as removing, a pattern, rather than, as improving, readability, in general, for that part, of the program, in the program, as a whole, over its run. Whether, you are, introducing, a pattern, or removing, one, or simply, clarifying, code, without any pattern, involved, for that code, in the program, as a whole, over its run, the discipline, remains, the same: change, in small steps, verify, behaviour, after each step, through tests, for that step, in the refactoring, sequence, for that code, in the program, as a whole, over its run, and stop, when, the identified problem, is solved, for that code, in the program, as a whole, over its run, rather than, continuing, toward ever-greater, cleanliness, or ever-more, patterns, for that code, in the program, as a whole, over its run, without a specific, problem, to solve, for that code, in the program, as a whole, over its run, in practice. That discipline, shared, by both kinds, of refactoring, is what, makes them, safe, and useful, for programs, that must evolve, over long lifetimes, without breaking, for their users, for their work, with the program, as a whole, over its run, in practice.
Check: How does clean code refactoring often reveal a design problem that a pattern can then solve?
Section 43
Error messages are part of clean code, during development, when, code work, rather than, on what, when it, fails, during execution, that determine, states, what operation, failed, what, input, or condition, caused, the failure, and what, the reader, can do, next, whether, that reader, who needs, to correct, their input, who needs, to diagnose, the failure, alone, without needing, to reproduce, the failure, from scratch, input, and conditions, originally, in practice. A message, that merely, states, that something, went wrong, without saying, what, or why, helps, neither audience, because, users, cannot, and developers, cannot, diagnose, from it, alone, without additional, context, in practice.
Error messages should also be, safe, in what, they reveal, because, a message, shown, to users, that includes, internal details, such as, file paths, such as, credentials, can, expose information, that helps, an attacker, understand, or that, for users, in practice. A clean approach, distinguishes, between messages, for users, and messages, for developers, giving, users, a clear, actionable message, and giving, developers, through logs, to diagnose, the failure, without exposing, sensitive data, in practice. That distinction, is part, of defensive programming, as discussed, on security, for Python developers, where safe defaults, and careful handling, of sensitive information, were covered, for programs, that must be, trustworthy, for their users, over their lifetimes, in practice, and it, applies, to error messages, as much, part, of a program, that communicates, with users, or with logs, for that program, in the program, as a whole, over its run, in practice.
Finally, consistent, in style, across a program, as a whole, over its run, because, users, and developers, learn, to read, them, quickly, when, they follow, a predictable pattern, for that program, in the program, as a whole, over its run, such as, starting, with what, failed, for that failure, in the program, as a whole, over its run, followed, by why, it failed, for that failure, in the program, as a whole, over its run, and ending, with what, to do, next, for that failure, in the program, as a whole, over its run, in practice. Consistency, in messages, also, helps, automated tools, that parse, logs, for failures, in the program, as a whole, over its run, to find, and count, them, reliably, for that program, in the program, as a whole, over its run, without needing, to handle, many different, message formats, for the same, kind of failure, in the program, as a whole, over its run, in practice. When, you write, an error message, therefore, ask, whether, a stranger, reading it, for that failure, in the program, as a whole, over its run, would know, what went wrong, for that failure, in the program, as a whole, over its run, and what, to do, about it, for that failure, in the program, as a whole, over its run, without needing, to read, the code, that produced it, for that failure, in the program, as a whole, over its run, in practice. If, not, then, the message, is not, yet clean, for that failure, in the program, as a whole, over its run, and should, be improved, before, it is shared, with users, for understanding, failures, in their work, with the program, as a whole, over its run, in practice.
Check: What three things should a good error message state for its reader?
Section 44
Functions that avoid unexpected side effects are easier to understand, test, and reuse, rather than, changed, during execution, that determine, in practice. A function that reads a file, of computing, a result, surprises, callers, who expected, and who, their call, to change, in ways, that affect, later calls, in practice. That surprise, makes, code, who must, track, not only, returns, but also, what it, changes, elsewhere, in practice, and it, makes, testing, harder, because, a test, and check, not only, but also, in practice
That does not mean, that functions, should never, because, many programs, exist, precisely, to produce, side effects, such as, writing files, sending messages, or updating, databases, and because, a program, at all, would be, useless, for changes, itself, in practice. The clean code principle, side effects, explicit, and expected, rather than, hidden, or surprising, by naming, functions, that produce, side effects, actually do, such as, saving, or sending, or updating, rather than, naming them, as if, they merely, computed, a value, without any, side effect, in practice. It, also, means, separating, functions, that compute, values, from functions, that produce, side effects, where possible, so that, computation, can be, tested, in isolation, without external state, for that computation, in the program, as a whole, over its run, and so that, side effects, can be, managed, centrally, in a few, well-named places, for that program, in the program, as a whole, over its run, rather than, scattered, through computation, code, that callers, expect, to be pure, for that computation, in the program, as a whole, over its run, in practice.
Separating computation, from side effects, also, helps, refactoring, because, a pure computation, can be, moved, or reused, safely, without worrying, about what, else, it changes, for that computation, in the program, as a whole, over its run, while, a function, must be, moved, carefully, with attention, to its effects, on external state, for that function, in the program, as a whole, over its run, for each move, of that function, in the program, as a whole, over its run, in practice. When, you write, a function, therefore, ask, whether, its name, accurately, describes, everything, it does, for that function, in the program, as a whole, over its run, including, its side effects, for that function, in the program, as a whole, over its run, or whether, it hides, a side effect, behind a name, that suggests, pure computation, for that function, in the program, as a whole, over its run, in practice. If, it hides, one, then, either, rename it, to reflect, its side effect, for that function, in the program, as a whole, over its run, or split it, into a computation, part, part, for that function, in the program, as a whole, over its run, so that, callers, can see, plainly, what, each part, does, for that function, in the program, as a whole, over its run, and can, test each part, appropriately, for its purpose, in the program, as a whole, over its run, in practice. That, explicitness, about side effects, is a hallmark, of clean code, for programs, that must be, understood, and maintained, over long lifetimes, by many developers, who were, for that program, in the program, as a whole, over its run, in practice.
Check: Why should side effects be explicit in a function's name or separated from computation?
Section 45
Clean code supports security, to review, than code, that is, tangled, or unclear, where, can hide, in complexity, that reviewers, cannot follow, in practice. A function, that validates, user input, step-by-step way, named, plainly, can be, reviewed, quickly, for whether, it checks, everything, it should, and whether, any check, is missing, in practice. A function, that validates, input, with checks, scattered, through long, nested conditionals, is hard, to review, for completeness, because, a reviewer, must hold, all checks, at once, to judge, whether, in practice, and because, is easy, to overlook, who is, already struggling, to follow, in practice. That, why, such as, small functions, clear names, for input, are also, security practices, for programs, trustworthy, in practice, as discussed, generally, on security, were covered, for programs, secure, by design, rather than, by hope, that flaws, found, by attackers, who study, code, for weaknesses, in practice.
Clean code also supports security, by making, safe defaults, obvious, because, a function, is safe, and documentation, state, that default, plainly, to be, misused, unsafely, by callers, understand, its default, in practice, whose default, is unsafe, or unclear, and whose, callers, must guess, in practice. For example, a function, that opens, a file, should, by default, use, the safest, reasonable settings, and should, require, callers, explicitly, if they, in practice, rather than, requiring, callers, to safety, which, to do, in practice. That, safe-by-default design, is clearer, when, code, is clean, because, defaults, are visible, rather than, hidden, in tangled logic, for that function, in the program, as a whole, over its run, where, a reviewer, cannot tell, what the default, actually is, for that function, in the program, as a whole, over its run, without reading, its entire body, in detail, for that function, in the program, as a whole, over its run, in practice.
Finally, clean code helps, incident response, when, a security problem, is discovered, in a program, as a whole, over its run, because, clear code, can be, understood, quickly, under pressure, for that program, in the program, as a whole, over its run, by developers, who must, fix it, promptly, for users, for their work, with the program, as a whole, over its run, in practice, while, unclear code, slows, diagnosis, for that problem, in the program, as a whole, over its run, and increases, the chance, that a fix, introduces, new problems, for that program, in the program, as a whole, over its run, because, developers, fixing, under pressure, cannot, fully understand, code, that is, already hard, to understand, in calm conditions, for that code, in the program, as a whole, over its run, in practice. That, is a practical, security benefit, of clean code, that is, often overlooked, in discussions, of clean code, as merely, a matter, of style, or taste, for developers, who write it, in the program, as a whole, over its run, in practice, because, it, affects, how quickly, and how safely, a program, can be, fixed, when it, matters most, for its users, for their work, with the program, as a whole, over its run, in practice, and specifically, in a program, determines, the speed, and safety, of its fix, for that incident, in the program, as a whole, over its run, in practice, generally.
Check: How does clean code make security review and incident response easier?
Section 46
Tests themselves need clean code practices, read, understood, and maintained, just as, production code, must be, and because, unclear tests, can hide, bugs, in tests, themselves, that then, either pass, incorrectly, giving, false confidence, that is, actually broken, or fail, incorrectly, wasting time, in diagnosing, that is, at all, caused, in practice. A clean test, states, plainly, what, which, should describe, the behaviour, being tested, under which, rather than, merely naming, the function, being tested, without saying, what, about it, in practice. A test, named, and condition, can be, understood, alone, without reading, its body, in detail, in practice, which, helps, developers, who must, diagnose, quickly, immediately, what behaviour, broke, in practice.
Clean tests also follow, typically, setting up, the conditions, then, performing, the action, being tested, and then, checking, the result, against, expected behaviour, can follow, easily, without needing, to untangle, setup, action, and checking, that are, mixed together, hard-to-follow block, of code, in practice. That, structure, sometimes, called, arrange, act, and assert, for tests, in general, for testing, for programs, helps, readers, locate, quickly, where, a test, sets up, its conditions, where, it performs, its action, and where, it checks, its result, in practice, which, speeds diagnosis, when, a test, fails, because, a developer, can see, immediately, whether, the failure, in practice, rather than, searching, in practice.
Test duplication, should also, be, reduced, through shared, setup helpers, for tests, that production duplication, should be, reduced, through shared functions, for production code, in the program, as a whole, over its run, as discussed, for duplication, generally, of this chapter, specifically, rather than, generally, for all sections, at once, as a whole, over time, because, duplicated setup, across many tests, for that setup, in the program, as a whole, over its run, must be, updated, in every test, when, setup requirements, change, for those tests, in the program, as a whole, over its run, in practice, leading, to the same, inconsistency risks, as production duplication, for those tests, in the program, as a whole, over its run, in practice. Shared helpers, for tests, should, themselves, be, clean, with clear names, for their purpose, in the program, as a whole, over its run, and with, a single responsibility, for each helper, in the program, as a whole, over its run, so that, test readers, can understand, what, a helper, does, for a test, in the program, as a whole, over its run, without reading, its body, in detail, for that helper, in the program, as a whole, over its run, in practice, just as, production readers, can understand, a production function, from its name, alone, for that function, in the program, as a whole, over its run, in practice. That, attention, to test cleanliness, pays off, as it, grows, with a program, as a whole, over its run, because, is easier, to maintain, for that suite, in the program, as a whole, over its run, and because, tests, that are, easy to maintain, are more likely, to be, kept current, as production code, changes, for that program, in the program, as a whole, over its run, rather than, being, neglected, or deleted, when they, become too hard, to update, for that suite, in the program, as a whole, over its run, in practice.
Check: Why should tests follow the same clean code practices as production code?
Section 47
Configuration code deserves clean code attention, grows, messily, over time, are added, without any, clear organisation, in practice, until, eventually, no one, can tell, which settings, which, are obsolete, and which, interact, in practice. A clean approach, groups, related settings, together, so that, a reader, can see, which settings, belong, and which, are global, in practice, rather than, searching, to find, the ones, relevant, they are, working on, in practice. Grouping, also, helps, validation, because, settings, can be, validated, together, with checks, that consider, their interactions, rather than, validating, each setting, in isolation, without considering, how, it interacts, in practice, which, can miss, invalid combinations, of settings, that are, individually valid, but, jointly invalid, in practice.
Configuration defaults, should also, be, clear, and safe, as discussed, generally, specifically, rather than, generally, at once, over time, and security, together, because, a default, that is, unsafe, or surprising, can cause, problems, for users, explicitly set, that setting, and who, therefore, in practice. Defaults, should be, documented, alongside, their settings, so that, a reader, can see, what, will happen, is not, explicitly provided, without needing, to search, through code, value, in practice, who changes, a default, can see, will affect, all users, who rely, in practice, rather than, changing it, silently, over its run, without realising, its impact, for them, in the program, as a whole, over its run, in practice.
Finally, configuration, should be, kept, separate, from code, that uses it, for that program, in the program, as a whole, over its run, so that, changing, a setting, for a deployment, does not, require, changing, code, itself, for that deployment, in the program, as a whole, over its run, as discussed, for configuration patterns, generally, on design patterns, specifically, rather than, generally, for all chapters, at once, as a whole, over time, where configuration, loaded, from external sources, at startup, into a single, configuration object, for that program, in the program, as a whole, over its run, makes, settings, easier, to manage, for different deployments, with different settings, for each deployment, in the program, as a whole, over its run, in practice. That, separation, also, makes, configuration, easier, to test, because, tests, can supply, different configuration, values, for a test, in the program, as a whole, over its run, without changing, code, itself, for that test, in the program, as a whole, over its run, in practice, and can, verify, that the program, behaves, correctly, for each configuration, it supports, for that program, in the program, as a whole, over its run, in practice, rather than, testing, only the configuration, that happens, to be, in use, of the program, in the program, as a whole, over its run, in practice. Clean configuration, therefore, is not, a minor detail, for a program, as a whole, over its run, but, a significant part, for that program, in the program, as a whole, over its run, because, configuration, affects, how a program, behaves, in every deployment, for that program, in the program, as a whole, over its run, and because, unclear configuration, leads, to deployment errors, that are, hard to diagnose, for operators, who must, deploy the program, in new environments, for that program, in the program, as a whole, over its run, in practice.
Check: Why should related configuration settings be grouped and validated together?
Section 48
A code review checklist for clean code helps reviewers, remember, to check, readability, in addition, to correctness, under review, without turning, review, mechanical process, that slows, development, without improving, code quality, significantly, in practice. A useful, checklist, is short, focusing, that matter, most, rather than, on every, conceivable, in practice, because, will be, ignored, by reviewers, who cannot, remember, they review, in practice, while, high-impact items, will actually, be used, consistently, reviewed, in practice. Typical, items, include, whether, names, are clear, and accurate, under review, whether, functions, are small, and single-purpose, whether, duplication, has been, introduced, or removed, and whether, error handling, is clear, and safe, in practice, can be, checked, quickly, reading, the change, would, in practice.
A checklist, should also, be, adapted, actual problems, rather than, copied, unchanged, or from, another team, whose problems, differ, in practice, because, a team, that frequently, struggles, should, include, function size, prominently, while, a team, that frequently, struggles, should, include, naming, prominently, in practice, rather than, including, both, equally, without regard, actually affects, each team, more, a checklist, makes, it relevant, real work, and makes, reviewers, more likely, thoughtfully, they review, rather than, treating it, to be, ticked off, without thought, in practice, which, would waste, review time, without improving, code quality, over its run, in practice, and specifically, for each team, where its checklist, must reflect, its actual, recurring problems, for that team, in the program, as a whole, over its run, over its work, together, on shared code, in the program, as a whole, over its run, generally.
Finally, a checklist, should be, reviewed, itself, periodically, for that team, in the program, as a whole, over its run, to see, whether, its items, still reflect, the team's, current problems, for that team, in the program, as a whole, over its run, or whether, some items, have become, irrelevant, because, the team, has mastered, those practices, for that team, in the program, as a whole, over its run, and other items, should be, added, for new problems, that have, since emerged, for that team, in the program, as a whole, over its run, in practice, together, on shared code, in the program, as a whole, over its run. That, periodic review, of the checklist, itself, for that team, in the program, as a whole, over its run, prevents, it, from becoming, stale, and ignored, for that team, in the program, as a whole, over its run, that stale documentation, becomes, ignored, for a program, as a whole, over its run, when, it no longer, reflects, current reality, for that program, in the program, as a whole, over its run, in practice, and it, keeps, review, focused, on what, actually matters, for code quality, for that team, during its work, together, on shared code, in the program, as a whole, over its run, rather than, on historical problems, that the team, has already, solved, for that team, in the program, as a whole, over its run, in practice, and specifically, for each team, where its checklist, must evolve, alongside, its code, and its practices, for that team, in the program, as a whole, over its run, over its lifetime, together, on shared code, in the program, as a whole, over its run, generally.
Check: Why should a review checklist be short and adapted to a team's actual recurring problems?
Section 49
Daily clean code habits matter more, than occasional, large cleanups, because, code, becomes messy, gradually, through many, small decisions, made, rather than, large decision, to write, messy code, deliberately, in practice, and because, it, is therefore, cleaned, most effectively, through many, small decisions, made, during ordinary, development work, in practice, rather than, through occasional, heroic efforts, to clean, at once, which, compete, for time, and which, risk, introducing bugs, through large-scale, changes, to code, that was, previously working, correctly, in practice, generally, and specifically, maintained, through daily, small habits, generally. A useful, daily habit, code, cleaner, whenever, by renaming, or by, extracting, that you, found hard, to read, or by, removing, that you, discovered, in practice, safe improvement, that costs, little time, performing it, in practice, and that, benefits, who encounters, that code, generally.
Another, daily habit, automated checks, for formatting, code quality, before, sharing, your work, so that, formatting, issues, do not, waste, review time, that could, be spent, on logic, and design, under review, in practice, basic, quality problems, are caught, early, by tools, rather than, by reviewers, who must, manually, in practice, which, slows review, and frustrates, authors, whose work, is returned, that tools, could have, caught, automatically, in practice. That, habit, of running, checks, before sharing, also, builds, confidence, because, you know, that it, meets, basic standards, before, anyone else, sees it, in practice, and because, it, prevents, the embarrassment, of sharing, work, with obvious, formatting errors, that tools, would have, caught, immediately, in practice, generally, and specifically, who adopts, that habit, generally.
A third, daily habit, code, before, changing it, rather than, jumping, directly, based on, a guess, about what, the code, does, in practice, because, reading, first, often, reveals, is unnecessary, or that, it would, break, something, else, had not, considered, as a whole, over its run, in practice, before, reading, the code, carefully, for that code, in the program, as a whole, over its run, in practice. Reading, first, also, reveals, opportunities, for small, cleanups, for that code, in the program, as a whole, over its run, that can be, made, alongside, the planned change, for that code, in the program, as a whole, over its run, in a small, tested step, for that cleanup, in the program, as a whole, over its run, as discussed, for continuous refactoring, generally, of this chapter, specifically, rather than, generally, for all sections, at once, as a whole, over time, for clean code, and refactoring, together, for that program, in the program, as a whole, over its run, in practice. Together, those three habits, of leaving, code cleaner, running, checks, before sharing, and reading, before changing, for that code, in the program, as a whole, over its run, form, a daily practice, of clean code, that requires, no special, effort, beyond, ordinary, careful development, work, on shared code, in the program, as a whole, over its run, but that, accumulates, into a codebase, that stays, clean, over time, without needing, large, disruptive cleanups, for that codebase, in the program, as a whole, over its run, in practice, generally, and specifically, for each developer, who adopts, those habits, for their work, on shared code, in the program, as a whole, over its run, over their career, generally.
Check: What three daily habits help keep a codebase clean without large cleanups?
Section 50
This chapter has covered clean code and refactoring, through naming, small functions, comments, formatting, duplication, to refactoring, its safety, through tests, such as, extract method, rename, move method, and replacing, conditionals, with polymorphism, generally, and specifically, covered, were described, specifically, rather than, generally, at once, but, for programs, read, understood, and changed, safely, who were, specifically, rather than, generally, at once, over time, and that, refactoring, is how, clean code, is achieved, and maintained, gradually, over time, without needing, large, disruptive efforts, that compete, for time, and that, risk, introducing bugs, through large-scale, changes, to code, that was, previously working, correctly, in practice, generally, and specifically, maintained, through continuous, small refactorings, generally.
The habit to carry forward is simple: write code, could understand, once, specifically, rather than, generally, at once, over time, without needing, to ask, its author, what it, was meant, to do, specifically, rather than, generally, at once, over time, and when, you encounter, code, specifically, rather than, generally, at once, over time, improve it, tested step, specifically, rather than, generally, at once, over time, before, moving on, specifically, rather than, generally, at once, over time, in practice, generally. That habit, of writing, clearly, and improving, incrementally, for code, that you, encounter, accumulates, that stays, clean, over time, without needing, heroic efforts, up, specifically, rather than, generally, at once, over time, when, unbearably messy, for developers, who must, generally, and specifically, maintained, generally. It, also, makes, your work, more enjoyable, because, working, is pleasanter, than working, who must, spend, reading, and changing, existing code, rather than, writing, new code, from scratch, generally, and specifically, who benefits, generally.
Finally, remember, and refactoring, serve, users, ultimately, because, programs, that are, are easier, to fix, when they, break, for users, and easier, to improve, that users, request, generally, and specifically, enables, faster, safer changes, over its lifetime, generally. A program, whose code, is messy, may, still work, correctly, for its users, today, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, but, it will, become, harder, to maintain, with every change, made, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, until, eventually, changes, become, so risky, and so slow, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, that users, suffer, from delayed, fixes, and delayed, features, for their work, with the program, as a whole, over its run, generally, and specifically, for each program, whose messy code, eventually, harms, its users, through slow, risky maintenance, over its lifetime, generally. Investing, in clean code, and in refactoring, therefore, is investing, in users, as well as, in developers, for a program, as a whole, over its run, because, maintainable code, serves, users, better, over time, than code, that is, merely working, today, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, but that, cannot be, maintained, safely, for tomorrow, for that program, specifically, rather than, generally, for all programs, at once, as a whole, over time, in practice, generally, and specifically, for each program, you will write, or maintain, with clean code, as a practice, from now on, generally.
Check: What habit summarises this chapter's approach to clean code and refactoring?
Practical Task
Pick a function in a program you have written that is longer than you can read at a glance, or that has a vague or misleading name. First write tests that capture its current behaviour for representative inputs, including at least one edge case. Then refactor it in small, tested steps: rename it clearly if needed, extract at least one step into its own small function with a clear name, and replace any magic value with a named constant. Run the tests after each step, and keep functional changes out of the refactoring entirely. Write a short note stating what was unclear before, what each step changed, that the tests still pass, and whether a stranger reading the refactored code could understand its purpose without asking you.
Chapter Questions
- Why is a misleading name worse than a vague name, and when should renaming be done promptly?
- How does extract method improve both readability and testability, even when the extracted code is used only once?
- Why must refactoring and functional changes be kept separate, and how do tests verify a refactoring?
- When is duplication acceptable, and how do you distinguish shared knowledge from coincidental similarity?
- Why is refactoring stable, rarely changed code, or code about to be replaced, usually a poor use of effort?