AI Study Notebook AI-generated
Study Guide: The Elements of Programming Style
Brian Kernighan and P. J. Plauger
By Best Books
This AI-generated study guide is a reading aid. The source-backed recommendation record and evidence for this book live on the book page.
On this page
The Elements of Programming Style — Chapter-by-Chapter Outline
Author: Brian W. Kernighan and P. J. Plauger First published: 1974 (McGraw-Hill); Second Edition 1978 Edition covered: Second Edition (1978, ISBN 0-07-034207-5). The second edition added an entirely new chapter on Program Structure (Chapter 4), incorporated structured-programming techniques throughout, introduced pseudo-code as a design tool, and rewrote or replaced many examples from the first edition.
Central thesis
Good programming style cannot be taught by stating abstract principles — it must be learned by reading real programs, identifying their flaws, rewriting them, and then deriving the general rule from the specific case. Style is not decoration; it is the primary determinant of whether a program is correct, maintainable, and comprehensible.
The book drives this claim through more than a hundred short programs drawn verbatim from published textbooks of the 1960s and 1970s. Each program is examined, its shortcomings are exposed, a better version is produced, and a crisp rule is extracted. The examples are in Fortran and PL/I, but the principles hold for any language.
The deeper argument is that clarity and correctness are the same thing. Programs that are obscure or tricky are not merely hard to read — they are genuinely more likely to be wrong, because the programmer cannot verify what they are doing, and because small modifications almost inevitably introduce bugs. Conversely, a clean program written in a straightforward style is not just easier to read; it is easier to debug, easier to modify, and — counterintuitively — often runs faster, because simple algorithms beat cleverly-tuned bad ones.
If a program doesn't work, it doesn't matter how fast it runs.
Chapter 1 — Introduction
Central question
What does it mean to write a program badly, and what does it mean to write one well? How should we approach reading and criticizing code?
Main argument
The identity-matrix example. The chapter opens with a Fortran fragment that initializes a matrix to the identity by exploiting integer-division truncation: V(I,J)=(I/J)*(J/I). This is technically correct but requires readers to re-derive the author's reasoning on every encounter. A version using a comment plus two explicit loops — zeroing each row, then setting the diagonal — is immediately clear and happens to execute faster. The first rule follows: Write clearly — don't be too clever.
The shape of the book. The method is announced directly: take a program from a real textbook, discuss its shortcomings, rewrite it, draw a rule. Readers will encounter the same failures in combination — real programs violate several rules at once — so the classification is sometimes arbitrary, and digressions are unavoidable.
What "better" means. The authors distinguish between code that merely works and code that can be read, debugged, and modified without heroic effort. They note that programs written for "a few weeks" of use routinely end up running for years. The discipline of writing cleanly the first time is not extra work; it is less work overall, because the programmer who skips it spends far more time redoing and debugging.
Language independence. The examples use Fortran IV and PL/I because those were the dominant languages of the era. Readers unfamiliar with either language should not be deterred; the programs are short, the reasoning is explained in plain English, and the principles transfer directly to any language.
Key ideas
- Obscure, "clever" code hides errors — the programmer cannot verify a program they cannot understand.
- The act of improving a program and the act of understanding it are the same act.
- Size is often an illusion: programs shrink when improved, because complexity is replaced by clarity.
- Most real programs will be modified; they are not written once and discarded.
- Rules of style sound like sweeping generalities until illustrated by a specific case; the specific case makes the rule memorable and applicable.
Key takeaway
The only reliable path to correct, maintainable software is to write it clearly and simply in the first place; cleverness is not a virtue, it is a liability.
Chapter 2 — Expression
Central question
How should individual statements — arithmetic expressions, logical conditions, and variable names — be written to maximize clarity and minimize error?
Main argument
Say what you mean, simply and directly. A ten-line Fortran fragment using four labels and six GOTO statements to compute the minimum of three values (X, Y, Z) is replaced first by three IF statements and then by a single call to the library function AMIN1. Both alternatives are far shorter and far clearer. The lesson: use library functions; every line of unnecessary code is an opportunity for a bug.
Don't sacrifice clarity for efficiency. A common antipattern is code like 10 F1=X1-X2*X2 / F2=1.0-X2 / FX=F1*F1+F2*F2 with a comment "NOTE THAT IT IS MORE EFFICIENT TO COMPUTE F1F1 THAN F12". The authors measure the compiler's output and show the "efficient" version actually generates more machine instructions on one compiler and the same on another. The readable form `FX = (X1 - X22)2 + (1.0 - X2)*2` eliminates temporary variables (which are a source of initialization errors), is more readable, and performs the same or better.
Replace repeated expressions with functions. A program computing the sides and angles of a triangle repeats a complex expression three times for the sides and three times for the angles. Defining arithmetic statement functions SIDE(XA,YA,XB,YB) and ANGLE(SAREA,SA,SB,SC) collapses six repetitions into six one-line calls, makes the code match the mathematical notation, and means that any correction (such as changing ATANF to ATAN2) needs to be made in exactly one place.
Avoid temporary variables. Every temporary variable is a name that must be mentally tracked, a storage location that might not be initialized, and a potential source of subtle aliasing bugs. Eliminating temporaries by expressing computations directly reduces program state and the cognitive load on the reader.
Parenthesize to avoid ambiguity. The chapter works through cases where operator-precedence rules in Fortran and PL/I differ from the reader's intuition — for instance, whether -X**2 means -(X**2) or (-X)**2. The rule is to parenthesize whenever the order of evaluation is not immediately obvious, not to save keystrokes.
Choose variable names that won't be confused. Names that differ only by one character (like REEL vs. REAL, or I vs. 1) invite transcription errors. Mnemonic names tied to the problem domain are preferred.
Use the telephone test. A program passes the telephone test if you can read it aloud over the phone and the listener can reconstruct it accurately. Programs full of statement numbers, obscure operators, and implicit defaults fail this test.
Key ideas
- Library functions exist to be used; writing what a library already provides is an error-prone waste.
- Temporary variables increase state complexity and should be avoided when a direct expression is possible.
- "Efficient" micro-optimizations at the expression level almost never produce measurable gains and consistently produce less readable code.
- Repetition of identical expressions in source code is a signal that a function is needed and a source of divergent modifications.
- Operator precedence rules differ between Fortran and PL/I; explicit parenthesization removes the ambiguity.
- Mnemonic variable names are part of the program's documentation, not an afterthought.
- The arithmetic IF in Fortran (branching three ways on sign) is particularly dangerous and should be avoided in favor of the logical IF.
Key takeaway
At the statement level, every expression should be written in the most direct, least clever form possible; the compiler will handle any trivial optimizations, and the programmer gains correctness and readability at no cost.
Chapter 3 — Control Structure
Central question
How should loops, decisions, and flow of control be organized to make programs easy to follow and hard to break?
Main argument
Statement grouping and DO-END. PL/I's DO-END block allows a group of statements to be treated as a single unit following an IF. Code that uses GOTO to branch around a group — a Fortran habit imported into PL/I — should be replaced by a DO-END. The visible indentation of the controlled group directly shows the reader what the IF governs. In Fortran, which lacks block structure, the equivalent improvement is to reverse the condition and indent the controlled statements.
IF-ELSE for mutual exclusion. The ELSE should be used when, and only when, exactly one of two alternatives is to be performed. A payroll program that tests HRS_WORKED <= 40 and then separately tests HRS_WORKED >= 40 accidentally pays double to someone who works exactly forty hours. Replacing both tests with a single IF-ELSE makes the mutual exclusion explicit and eliminates the overlap.
DO and DO-WHILE for explicit loops. A PL/I sorting routine that builds its outer loop with a label and IF-GOTO is restructured as a DO-WHILE, immediately signaling to the reader "this is a loop, and here is its termination condition." The rewrite also introduces Boolean named constants YES and NO for bit literals '1'B and '0'B, making the conditions readable.
IF…ELSE IF chains for multi-way decisions. A sales-commission calculation requires three mutually exclusive tiers. The case statement pattern — a chain of IF … ELSE IF … ELSE IF … ELSE — is the clearest implementation. The conditions are read top to bottom; once one is satisfied, the rest are skipped. Because the tests execute in order, redundant bounds checks can be eliminated: if the first test (AMT_OF_SALES <= 50.00) fails, the second test need only check the upper bound. The chapter shows how to align all ELSE clauses at the same indent level to emphasize that all legs have equal status.
Programs should read from top to bottom. A bowling-score program implements its two-level loop (outer loop over frames, inner three-way decision for strike/spare/regular) using labels and GOTO statements that force the reader to trace control across the listing. Rewriting with explicit DO … TO … END and IF … ELSE IF … ELSE DO structures produces code that executes in the order it is written.
Write first in pseudo-code; then translate. The authors use a quadratic-equation solver as a worked example of top-down design. The full multi-way decision (handle degenerate cases A=0, B=0, C=0; linear case A=0; root-on-axis case C=0; general discriminant) is written first in a language-neutral pseudo-code. Once the pseudo-code is verified correct at the boundary conditions, it is translated into Fortran with explicit GOTOs substituting for the absent IF-ELSE. The translation preserves the structure found in the pseudo-code rather than inventing a new, worse structure directly.
Avoid GOTO-based loops. The chapter surveys the set of fundamental control structures — sequential groups, IF-ELSE, indexed loops (DO), condition-tested loops (DO-WHILE or WHILE), and procedures. These are collectively sufficient to express any algorithm. Code that uses GOTO to build these structures manually is opaque and error-prone.
Key ideas
- DO-END / BEGIN-END block structure eliminates labels and GOTO-around patterns.
- IF-ELSE should encode genuine mutual exclusion; overlapping IF tests lead to double-execution bugs.
- DO-WHILE makes loops explicit; hiding a loop inside an IF-GOTO is a lie about the code's structure.
- IF-ELSE-IF chains implement multi-way decisions clearly; the order of tests can be used to simplify individual conditions.
- Code should execute in the order it is written ("top to bottom"); structures that violate this are difficult to follow and prone to latent bugs.
- Pseudo-code as a design tool allows the programmer to verify logic at a high level before committing to the constraints of the target language.
- The structured-programming discipline — using nothing but properly nested basic constructions — does not solve all problems, but it eliminates an entire class of control-flow errors.
Key takeaway
Control structures are the skeleton of a program; when they are explicit and honest about what they do (loop, decide, group), the program can be read and verified; when they are disguised with GOTO and labels, bugs are hidden.
Chapter 4 — Program Structure
Central question
How should a large program be divided into modules, and what makes a module well-designed?
Main argument
Modularize. Use subroutines. A Fortran checker-playing subroutine that computes up to four legal moves for a piece uses a tangled nest of boundary tests and GOTO statements spread across 40 lines. Extracting a helper subroutine STORE(BOARD, IC, JC, ROW, COL, L) — which determines whether a given target square is on the board and unoccupied, and if so records it — reduces the main routine to four calls to STORE, one per direction, guarded by the piece-type constraints. The structural regularity becomes visible; the subroutine call summarizes the irregularities in its argument list and hides the regularities inside the subroutine body. The second edition notes that later adding jump-capture logic will be far easier.
Minimize coupling between modules. A module called OUT that must access the calling routine's variables AREA, LMTS, MSSG3, and K and also performs part of the AREA calculation is not truly independent. The minimal change is to pass the shared data as explicit arguments. The general principle: the "coupling" between modules should be visible in the interface (the argument list or return value), not hidden in shared globals. Minimum coupling maximizes independence and enables separate maintenance.
Each module should do one thing well. A median-computing program uses two specialized sub-procedures: EV_ARY (receives two adjacent elements and prints the average) and OD_ARY (receives one element and prints it). Both modules have two flaws: each does too little (neither actually computes the median for the general case) and each does too much (both mix computation with output). A true MEDIAN function takes the array and its length, computes and returns the median, and leaves printing to the caller. This version is broadly reusable; the originals are not.
Make sure every module hides something. The chapter introduces the concept that a good module's value can be measured by how well it hides one well-defined aspect of the problem from the rest of the program. Input/output is always messy and subject to change; hiding it inside a GETLIST function means that the rest of the program is insulated from the mechanics of end-of-file detection and format changes.
Let the data structure the program. A customer-account report program generates its header at the bottom of the code even though it is printed at the top of each page — a mismatch between code structure and output structure. A pseudo-code description of the desired output ("zero or more pages, each with a header and one to 46 detail lines") directly suggests the correct program structure: a WHILE loop that tests for input, prints a header at the top of each page, then processes detail lines. When the program's structure mirrors the structure of its output, the code is naturally easier to verify.
Top-down design and successive refinement. The chapter endorses designing programs from the top down: write the highest-level description first, then iteratively fill in details. This is illustrated with the median program, with the customer-accounts program, and with the quadratic solver from Chapter 3. The second edition of the book added this chapter specifically because structured programming had by 1978 become established practice, and the authors wanted to show how modular design — not just control-flow discipline — is indispensable.
Key ideas
- Subroutines are the building blocks of large programs; they permit separate development, testing, and maintenance.
- Coupling — the degree to which one module depends on the internals of another — should be minimized and made visible through explicit argument passing.
- A module that does two unrelated things (compute and print, for example) is harder to test, harder to reuse, and harder to understand than two modules each doing one thing.
- Shared state between modules (global variables) is a hidden coupling that makes programs fragile.
- The structure of a program should mirror the structure of the problem it solves; if they diverge, bugs accumulate at the divergence point.
- Premature optimization ("optimizing too early in the life of a program can kill its chances for growth") destroys modularity.
- Use recursive procedures for recursively-defined data structures; the structure of the code should reflect the structure of the data.
Key takeaway
A program is not modular merely because it has been divided into pieces; modularity requires that each piece does one thing, hides its details, and exposes a minimal, explicit interface to its neighbors.
Chapter 5 — Input and Output
Central question
How should programs read input and write output to be robust against bad data, easy to use correctly, and informative when things go wrong?
Main argument
Never trust any data. The chapter opens with a 1972 newspaper account: a keypunch error caused a 1967 Ford to be assessed at $7,000,950 instead of $950, costing the city of Woonsocket, Rhode Island $290,000 in lost tax revenue. The program contained no validity checks. A Fortran triangle-area program that calls SQRT(S*(S-A)*(S-B)*(S-C)) without first verifying that A, B, and C form a valid triangle terminates with "negative argument in SQRT" — an indirect and unhelpful error message. PL/I's ANY function makes the validation one line: IF ANY(T<=0) | ANY(T>S) THEN ....
Make sure input cannot violate the limits of the program. A sort program reads N numbers into an array declared DIMENSION X(300) using READ 1,N,(X(I),I=1,N) without checking that N does not exceed 300. If N is larger, memory outside the array is silently overwritten. If N is 1, the program compares X(1) with the uninitialized X(2). The fix: read N first, test it, then read the data.
Terminate input by end-of-file or marker, not by count. Requiring users to count their data cards and supply N is error-prone. Computers count better than people. Mark the end of data with an explicit sentinel or let the language's end-of-file mechanism (ON ENDFILE in PL/I, END= in Fortran) control the loop. This is how compilers work — they read until they find an END card.
Identify bad input; recover if possible. A hair-color counting program silently ignores any color string it cannot match against its table. A bad input card is processed without complaint, corrupting the counts. The corrected version prints the bad value and the card number; the program continues rather than aborting.
Treat end-of-file conditions uniformly. A student-grade-averaging program uses a negative sentinel in the first score field to signal end of data, but fails because the input loop requires exactly five scores per student and will hang if fewer are provided. PL/I's ON ENDFILE mechanism, which fires as soon as the end of the stream is reached, is far more reliable.
Make input easy to prepare and output self-explanatory. A 1971 newspaper report documents a school-scheduling snafu caused by a lost column in a punched card. The chapter argues for free-form input (values separated by spaces rather than packed into fixed columns), self-identifying input (where possible, the data carries labels), and output that echoes both the input values and the results so that errors in preparation are visible.
Localize input and output in subroutines. I/O format details change; centralizing them means one change instead of many.
Key ideas
- Input data is always potentially wrong; programs that do not validate their input are not merely incomplete but actively dangerous.
- Array bounds must be tested against user-supplied counts before the data is read, not after.
- Counting data is the computer's job; sentinel-terminated or end-of-file-terminated input removes a class of user errors.
- When an error in input is detected, the program should report it precisely (value, location) and, where safe, continue processing remaining data.
- Output should be self-explanatory: echo input values alongside results so that the reader can verify what was computed and from what.
- Free-form input is less error-prone than fixed-format input because there are no column boundaries to miscount.
Key takeaway
Programs that trust their input uncritically are programs that fail in unpredictable ways; defensive input handling — validation, bounds checking, end-of-file detection, and informative error messages — is not optional polish but a core correctness requirement.
Chapter 6 — Common Blunders
Central question
What are the most frequent categories of programming error, how do they arise, and how can they be prevented or detected?
Main argument
Initialization errors. A double-precision sine function accumulates the Maclaurin series sin(x) = x - x³/3! + x⁵/5! - … but never initializes SUM, so it begins with whatever value happens to be in that memory location. The chapter then shows that the same function contains four additional bugs: it does not take the absolute value of TERM before the convergence test (so the function exits immediately for negative x); the sign-alternation expression (-1**(I/2)) evaluates as minus one raised to a power when it should be negative one raised to a power; the accumulated term is tested for convergence before being added to the sum; and there is no guard against underflow on entry for very small x. All five are found and fixed systematically by translating the algorithm to pseudo-code first and then translating the pseudo-code to Fortran. Rule: Don't stop at one bug.
Data statements vs. executable initialization. Fortran's DATA statement initializes variables at compile time. If a free-standing program that uses DATA to initialize a count array is later converted to a subroutine, the count array will not be reset on the second call. Constants (tables of values that never change) belong in DATA; variables (counters, sums, indices) must be initialized by executable code.
Off-by-one errors. An insertion-sort subroutine contains DO 250 J=1,M where DO 250 J=1,M-1 is correct. When J reaches M, the program reads the non-existent element V(M+1). A binary-search routine tests IF HIGH<=LOW before examining the single-entry case, meaning a table with exactly one entry is never searched at all. The correct test is <, not <=. Rule: Watch out for off-by-one errors and Take care to branch the right way on equality.
Floating-point equality and accumulation. A program uses a floating-point variable incremented by 0.5 as a loop counter. Because 10.0 * 0.1 is not exactly 1.0 in binary floating point, programs that test floating-point variables for exact equality are unpredictable. Rule: Don't compare floating-point numbers just for equality.
Multiple exits from loops. A loan-amortization program has two exits from its DO loop that both arrive at the same label. One exit (from the side) leaves the interest variable C unrecomputed; the other (from the bottom) computes it correctly. The result is an interest-charge error that, incidentally, favors the bank. Replacing the two exits with a DO-WHILE that tests the termination condition at the top eliminates the inconsistency.
Defensive programming. A checker-playing fragment that counts red and black pieces with two parallel IFs is rewritten as an IF-ELSE-IF chain with a final ELSE that prints a diagnostic for any illegal piece value. The extra case never fires in a correct program, but if a bug elsewhere corrupts the board array, the diagnostic fires early and precisely. Rule: Program defensively.
Testing at boundary values. Boundary-condition errors — where code fails only at critical data values — are the most common class. The authors recommend systematically testing at the "null case" (zero items), the "one item" case, and values around powers of two when binary partitioning is involved. The average-of-N-numbers program is asymptotically correct but wrong for one item because COUNT is 1 too high at the point of division. Testing the program on zero and one items immediately reveals the error.
Key ideas
- Variables must be explicitly initialized before use; language defaults and storage residuals are unreliable.
- Constants and variables should be initialized differently: use compile-time mechanisms for true constants, executable code for mutable state.
- Off-by-one errors arise from incorrect loop bounds and wrong-direction boundary tests; they are found by testing edge cases.
- Floating-point arithmetic is not exact; never test floating-point variables for equality without a tolerance.
- Multiple loop exits that converge on the same point are a source of inconsistent state at exit.
- Defensive programming — including checks for "impossible" values — catches bugs early, when they are easier to diagnose.
- Boundary-condition testing is not optional; the boundary cases are where bugs live.
Key takeaway
Most real bugs belong to a small number of recurring categories — uninitialized variables, off-by-one errors, floating-point equality tests, inconsistent loop exits — and each category has a known antidote; applying the antidotes systematically is the difference between code that works and code that almost works.
Chapter 7 — Efficiency and Instrumentation
Central question
When should a programmer optimize for speed, how should optimization be approached, and how can programs be measured to guide the effort?
Main argument
Correctness before speed. A payroll program attempts to save time by computing the integer form of the employee ID (IEMPID) only on the regular-pay path and reusing it on the overtime path. But IEMPID is never set on the overtime path, so it retains the leftover value from the last non-overtime employee. The "efficient" structure introduced the bug. Removing the special case — testing HOURS twice, once for regular and once for overtime — produces a two-line calculation that is simpler, correct, and obviously correct. Rule: Make it right before you make it faster.
Clarity enables efficiency. An electric-company billing program is "optimized" by nesting the discount calculation inside the first billing tier. The resulting code omits the 100–500 usage tier, incorrectly pairs IF-ELSE branches, and gives a free 10% discount to customers who heat with electricity. The attempt to save one conditional test introduces two billing errors. Rule: Keep it right when you make it faster.
Let the compiler do trivial optimizations. A loop over 1000 elements replaces the straightforward X(J)=3 with a floating-point accumulator to avoid integer-to-float conversions. One compiler generates eight instructions for the "optimized" version versus nine for the original; a second compiler generates eleven instructions for the "optimized" version — making it slower. Meanwhile the code is less readable and the gain, even in the best case, is one percent of a program that is ten percent loop. Rule: Don't sacrifice clarity for small gains in efficiency.
Don't strain to re-use code; reorganize instead. A primality-testing program contorts its logic — including a transfer from within an ELSE into the corresponding THEN — to avoid duplicating a single PUT statement. The cure is to use a Boolean variable PRIME and a single output statement after the test loop, which is both longer and far clearer.
Algorithm beats tuning. The chapter presents a benchmark: a "carefully optimized" bubble sort with early exit, one-sided shrinkage of comparison range, and a switch to detect sorted order is compared with a plain O(n²) interchange sort. On arrays of 2000 random numbers, the "efficient" sort takes 38,500 ms versus 29,200 ms for the simple sort — the optimization made it 30% slower, probably because the bookkeeping overhead exceeds the comparison savings. Both interchange sorts are then compared with a Shell sort (after D. L. Shell), which runs at 3,200 ms for 2000 elements. The Shell sort's run time grows as approximately n^1.5 rather than n², so the gap widens. Rule: Don't diddle code to make it faster — find a better algorithm.
Instrument first, then optimize. A grade-collating program computes IWRONG = 5 - ISUM inside the loop, executing it five times per student when it only needs to run once after the loop. This is a "performance bug" — the code is correct but does unnecessary work, and the redundant execution is invisible without measurement. The authors introduce the concept of a program profile: a count of how many times each statement is executed (first named and described by Donald Knuth). Most computer centers of the era could provide automatic profiling. Adding counters NCOMP (comparisons) and NEXCH (exchanges) to the sort programs shows that the "efficient" sort and the simple sort do essentially the same number of exchanges; the "efficient" sort's higher count arises entirely from extra bookkeeping. Rules: Instrument your programs and Measure before making efficiency changes.
Summary of the chapter's argument. The authors conclude with five numbered points: (1) fix correctness first; (2) keep code clean during coding — "premature optimization is the root of all evil"; (3) let the compiler handle trivial calculations; (4) worry about the algorithm and data structure, not code details; (5) instrument and measure — leave instrumentation in as the program evolves.
Key ideas
- Correctness is prerequisite to performance; an optimized wrong program is worthless.
- Most local "optimizations" at the expression or statement level produce no measurable gain and consistently reduce readability.
- Algorithm choice determines asymptotic performance; no amount of tuning converts an O(n²) algorithm into an O(n^1.5) algorithm.
- The intuition about "where a program spends its time" is almost always wrong; measurement is required.
- Program profiles (statement execution counts) reveal performance bottlenecks precisely.
- Instrumentation code should be left in the program as it evolves, not removed "once debugging is done."
- Machine time is cheap; programmer time is expensive; optimizing for machine time at the cost of programmer comprehension is an inversion of priorities.
Key takeaway
Efficiency is not achieved by cleverness at the expression level; it is achieved by writing correct, clear code, measuring where time is actually spent, and then improving the algorithm — in that order.
Chapter 8 — Documentation
Central question
What forms of documentation are useful, what forms are harmful, and how should comments, variable names, and formatting be used?
Main argument
The best documentation is a clean structure. The chapter opens with a direct statement: "The best documentation for a computer program is a clean structure." Flowcharts and narrative descriptions are secondary; the only reliable documentation is the code itself. When code and comment disagree, the code wins, because the code is what actually runs. Multiple representations of a program — code, flowchart, comments, design document — multiply the opportunities for discrepancy and none of them can substitute for a correct program.
Comments must agree with code. A common failure mode: the comment says "TEST FOR NEGATIVE VALUE OF X" but the condition tests a different variable entirely. A more insidious case: the comment says "WE SHALL TEST FOR ODD NUMBERS" but IF MOD(X,2)=0 selects even numbers. The comment is believed subconsciously, preventing critical examination of the code. Weinberg's suggestion: write comments on the right side of the page during debugging so they can be covered.
Don't just echo the code — make every comment count. A program contains K13 = K13 + 1; /* INCREMENT COUNTER */. The comment says nothing the code does not already say. Useful comments explain the why, not the what: why a particular value was chosen, what invariant the code maintains, what the caller must guarantee before calling. Empty comments are not neutral — they take up space and train readers to ignore comments.
Don't comment bad code — rewrite it. A program checks IF(E-3.01)5,7,7 with a comment "TEST FOR VOLTAGE EXCEEDING 3.0" — the test is against 3.01, not 3.0, apparently as a floating-point guard, but the comment doesn't say so. Adding an arbitrary unexplained tolerance is a bad algorithm; the correct response is to rewrite the code so the tolerance is not needed, or to explain in the comment exactly why the tolerance is necessary.
Use variable names that mean something. A Fortran fragment declares twelve logical variables named EL, EM, EN, AKK, ELL, EMM, ENN, ELLL, EMMM, ENNN, ELLLL, EMMMM. The names have no mnemonic significance, differ from each other by a single repeated letter, and invite both misreading and typing errors. Names like BLTA (for B less than A) are better, though even these are strained. The best solution here is an array called E with descriptive format strings serving as documentation.
Format a program to help the reader. Indentation should reflect logical structure: each level of nesting is one indent level deeper. Statement labels should carry mnemonic meaning or at least follow a consistent numbering scheme (random statement numbers are repeatedly cited as a defect in the textbook examples). The multi-page maze program in Chapter 4 is revisited: it contains comments that explain why an END statement was omitted — a four-line comment that substitutes for four characters of code. The correct fix is to add the END, not to explain its absence.
Document data layouts. Every important data structure should have a brief specification at the point of declaration: what each field means, what units it uses, what range it may take. This is especially important for arrays where the indexing scheme is non-obvious.
Don't over-comment. The chapter is not an argument for more comments. The current-computing program from Chapter 6 has more comments than code and still contains multiple undetected errors. Over-commenting trains readers to ignore comments and provides a false sense of documentation. The right amount of commenting cannot be specified as a ratio; it depends entirely on how much the code needs explanation.
Key ideas
- Code is the primary documentation; all other representations are supplementary and potentially out of sync.
- A comment that contradicts the code, or simply restates it, is worse than no comment.
- Comments should explain intent, invariants, and non-obvious decisions — not narrate the code line by line.
- Mnemonic variable names, meaningful statement labels, and consistent indentation are part of documentation and cannot be replaced by prose comments.
- Pseudo-code written during design should be preserved as comments; it explains what the code was intended to do and why it was structured as it was.
- Bad code with good comments is still bad code; rewrite it.
- Programs grow and are maintained for far longer than originally anticipated; documentation that mismatches the code becomes actively misleading.
Key takeaway
Documentation is not prose attached to code — it is the code itself, written clearly, named meaningfully, formatted consistently, and commented only where something genuinely non-obvious needs explanation.
The book's overall argument
- Chapter 1 (Introduction) — establishes the method: take a real broken program, improve it, extract a rule; and argues that clarity and correctness are inseparable.
- Chapter 2 (Expression) — shows that at the statement level, directness beats cleverness; library functions, avoided temporaries, and consistent naming produce code that is simultaneously clearer and more correct.
- Chapter 3 (Control Structure) — shows that at the flow-of-control level, honest structures (IF-ELSE, DO-WHILE, CASE chains) produce code that reads in execution order and cannot hide control-flow bugs; pseudo-code is introduced as a design tool.
- Chapter 4 (Program Structure) — (new in the second edition) shows that modular design — single-purpose modules, minimal coupling, explicit interfaces, and structure mirroring the problem — is the large-scale analog of the small-scale clarity rules.
- Chapter 5 (Input and Output) — shows that programs must treat all input as suspect, validate it, report errors precisely, and terminate on end-of-file rather than by user-supplied count; robustness is a correctness requirement, not an optional feature.
- Chapter 6 (Common Blunders) — catalogs the most frequent error categories (uninitialized variables, off-by-one, floating-point equality, loop-exit inconsistency) and provides both detection strategies (boundary testing, debugging compilers) and prevention strategies (explicit initialization, DO-WHILE, defensive checks).
- Chapter 7 (Efficiency and Instrumentation) — argues that efficiency is achieved last, by measuring actual behavior and improving algorithms, not first by local cleverness; establishes that clean code is typically faster than "optimized" code because better algorithms outweigh micro-tuning.
- Chapter 8 (Documentation) — argues that the only reliable documentation is well-written code; all supplementary documentation must agree with the code or it becomes actively harmful; and that comments, names, and formatting are tools of communication, not bureaucratic obligation.
Common misunderstandings
Misunderstanding: The book is about Fortran and PL/I, so it is obsolete.
The examples happen to use Fortran IV and PL/I because those were the dominant languages when the book was written. Every rule — avoid cleverness, use library functions, write loops with explicit constructs, validate input, measure before optimizing — transfers directly to any language that has been invented since. The concrete examples are vehicles for principles, not the principles themselves.
Misunderstanding: "Write clearly" means write comments.
The authors explicitly argue the opposite. Comments that echo the code, or that contradict it, are worse than no comments. Clarity comes first from code structure (simple control flow, meaningful names, appropriate modularization) and only secondarily from comments that explain what the structure cannot express alone.
Misunderstanding: Efficiency and clarity are in tension, and real programmers optimize.
The book repeatedly measures this claim and finds it false. "Optimized" versions of programs are almost always slower than simple versions because the micro-optimizations save fractions of a percent while introducing structural complexity that prevents larger algorithmic improvements. The only efficiency gain that matters comes from algorithm choice, and algorithm choice requires clarity.
Misunderstanding: Structured programming solves all programming problems.
The authors explicitly note that a program using nothing but properly-nested IF, WHILE, and DO constructions can still be hard to understand and incorrect. Control-flow discipline is necessary but not sufficient; module structure, data representation, and input validation are equally important.
Misunderstanding: The book's rules are absolute.
The epilogue, echoing Strunk and White, notes that rules of programming style, like rules of English, are sometimes broken by good writers — but only when the specific situation demands it and the programmer is confident they are doing better. The default should be to follow the rules; departing from them requires justification, not just preference.
Central paradox / key insight
The book's deepest insight is a reversal of the intuition that drives most of the antipatterns it catalogues:
Cleverness is not a virtue. It is a cost.
Every piece of code that is too clever to read at a glance is code that cannot be verified, cannot be safely modified, and is almost certainly wrong in some subtle way. The programmer who writes V(I,J)=(I/J)*(J/I) to initialize an identity matrix has demonstrated intelligence and hidden a bug (since the expression is only correct when both I and J are positive and I equals J, which is accidentally always true for the identity). The programmer who writes two explicit loops with a comment has written code that can be read, tested, and changed by anyone.
This creates the paradox: the effort to make code shorter and faster — the effort that feels like good engineering — consistently produces code that is longer, slower, and more broken than the naive version. The authors illustrate this at every level:
- At the expression level, removing temporary variables and using direct library calls produces shorter code.
- At the control-flow level, explicit DO-WHILE loops are shorter than IF-GOTO constructions that do the same thing.
- At the module level, a well-designed MEDIAN function is shorter and more general than two specialized subprocedures.
- At the algorithm level, a straightforward Shell sort runs nine times faster than a carefully-tuned bubble sort on large arrays.
Simplicity is not the absence of effort. It is the result of the additional effort required to find the right structure.
Important concepts
Clarity
In Kernighan and Plauger's usage, a property of code (not of comments or documentation) meaning that the code's purpose and behavior can be understood on a linear reading without cross-referencing other parts of the program. Clarity is achieved through naming, direct expression, explicit control flow, and appropriate modularization.
Structured programming
In the narrow sense used in the book: programming using only properly-nested instances of the four basic constructions (sequential grouping, IF-ELSE decision, indexed loop, condition-tested loop) without unrestricted GOTO. The book endorses this discipline while noting it does not guarantee correct programs.
Coupling
The degree to which two modules depend on each other's internal state. High coupling (accessing global variables, relying on side effects, sharing data without explicit parameter passing) makes modules impossible to maintain independently. The book advocates minimizing coupling and making what remains visible in the module's argument list.
Off-by-one error
A bug in which a loop runs one iteration too many or too few, or a boundary test uses < where <= is correct (or vice versa). Caused by mismatched conventions about whether loop bounds are inclusive or exclusive. Found by testing the null case and the one-item case.
Pseudo-code
A design language, not tied to any specific programming language, used to express an algorithm's logic before translating to the target language. Used in the book as the primary tool for top-down design: write the algorithm in pseudo-code first, verify it is correct at the boundary cases, then translate. The authors' pseudo-code is based on Ratfor, described in their companion book Software Tools.
Profile
A record of how many times each statement in a program is executed during a test run. First described and named by Donald Knuth (Software Practice and Experience, 1971). A profile reveals where a program actually spends its time, as opposed to where the programmer believes it does. The authors note that professional computer centers of the era could provide automatic profiling.
Boundary condition
A data value at or near the edge of a program's input range: zero items, one item, maximum items, exact equality on a comparison threshold, powers of two in a binary search. Boundary conditions are where bugs live; testing them is the most efficient strategy for finding errors.
Defensive programming
The practice of writing code that explicitly checks for "impossible" conditions — illegal values in data structures, out-of-range indices, unexpected end of file — and reports them precisely before they propagate into corrupted output. The extra checks have near-zero cost in a correct program and provide early, precise diagnostics when a bug elsewhere in the program corrupts data.
Telephone test
An informal readability test proposed in the book: can the code be read aloud over a telephone so that the listener can transcribe it without ambiguity? Code that fails this test — full of arbitrary statement numbers, implicit precedence, and abbreviated identifiers — is code that will be misread.
Premature optimization
Optimizing code before it is correct and before measurement has identified the actual bottleneck. The book quotes (without attribution to Knuth) "premature optimization is the root of all evil" as a summary of Chapter 7's argument. The corollary is that optimization performed after correctness, guided by measurement, is neither premature nor evil.
References and Web Links
Primary book and edition information
- Kernighan, Brian W., and P. J. Plauger. The Elements of Programming Style, Second Edition. McGraw-Hill, 1978. ISBN 0-07-034207-5.
Background and overview
- Wikipedia — The Elements of Programming Style
- ACM SIGPLAN Notices — review of the first edition (1974)
Companion and related works by the same authors
- Kernighan, Brian W., and P. J. Plauger. Software Tools. Addison-Wesley, 1976. (The book that introduces Ratfor, the pseudo-code language used in the second edition of Elements.)
- Kernighan, Brian W., and Dennis M. Ritchie. The C Programming Language. Prentice-Hall, 1978. (Kernighan's companion volume, applying many of the same style principles to C.)
Related influential works cited in the book
- Brooks, Frederick P. The Mythical Man-Month. Addison-Wesley, 1975. (Listed in the book's supplementary reading.)
- Dahl, O.-J., E. W. Dijkstra, and C. A. R. Hoare. Structured Programming. Academic Press, 1972. (The theoretical foundation for the control-structure chapter.)
- Knuth, Donald E. "An Empirical Study of FORTRAN Programs." Software: Practice and Experience, 1971. (Source of the term "profile" for execution-count measurement.)
- Weinberg, Gerald M. The Psychology of Computer Programming. Van Nostrand Reinhold, 1971. (Cited in Chapter 8 for the suggestion about covering comments during debugging.)
Additional study resources
These are secondary summaries and should be used alongside, rather than instead of, the original book.