Rules Tell You What, Principles Tell You Why
Rules tell you what to do. Principles tell you why. An engineer who only knows the rules follows them into places they were never meant to go, and drops them the moment they get inconvenient. An engineer who knows the principles can tell which situation they’re in, and that judgment is where the real value comes from.
A rule is a compressed principle
Every rule started as someone’s reason. RuboCop’s Metrics/MethodLength check flags any method longer than ten lines by default. Behind that number is a principle: small units of code are easier to read, name, and test. The rule is the principle squeezed into something a machine can count.
That compression is useful. A linter can’t evaluate “is this easy to read,” but it can count lines, and it can do it on every commit without a meeting. Rules scale in a way principles don’t.
Compression also loses information. The rule keeps the what and drops the why, and once the why is gone, nobody can tell when the rule has stopped doing its job.
Satisfying the rule, defeating the principle
Say a method is eighteen lines long and the linter flags it. An engineer who only knows the rule splits it into process_part_one and process_part_two. The linter goes quiet. The code got worse. You now jump between two places to follow one idea, and the names tell you nothing about either half.
An engineer who knows the principle asks a different question: would splitting this make it easier to understand? If the method fetches a record, validates it, transforms it, and saves it, those are real seams. Extract them with names that mean something, and the original method becomes a four-line summary of what happens. If the method is one long SQL query, or a case statement mapping error codes to messages, there are no seams. Splitting it would only scatter it. The right move there is to disable the check for that method, with a comment that says why.
Style guides work the same way. “No abbreviations in names” exists so readers don’t have to decode cust_addr_ln2. Applied without the why, it gives you uniform_resource_locator where everyone would have understood url, and index in a three-line loop where i was clearer. The principle is that names should be easy for the reader. Sometimes the abbreviation is the easy one.
“Always write tests”
“Always write tests” is a rule. The principles behind it: tests let you change code without fear, they document what the code is supposed to do, and they tell you within seconds when you’ve broken something.
Know those reasons and the edge cases sort themselves out. A script that backfills one column once and then gets deleted doesn’t need the same suite as the billing code. A test that stubs a method to return 42 and then asserts it returned 42 satisfies the rule and protects nothing. It only checks its own setup.
Coverage targets fail the same way. Make 100% the target and you’ll hit it, partly with tests that execute lines without asserting anything about them. That’s Goodhart’s law: once a measure becomes a target, it stops being a good measure.
The principles also tell you where the rule doesn’t ask for enough. The date math around daylight saving time, the rounding in a currency conversion, the retry logic that only runs when a dependency is down. “Always write tests” treats every line the same. Knowing why you test tells you where to test hardest.
Principles travel
Rules are local. They belong to a codebase, a language, a linter config. Principles transfer. “Small units are easier to understand” applies to a Ruby method, a SQL query, a Terraform module, and a design doc. The Agile Manifesto works the same way: four values and no required meetings, which is why a team can run every ceremony and still miss the point.
This is what I like about the Zen of Python. It’s nineteen aphorisms, and almost none of them can be enforced by a tool. Explicit Is Better Than Implicit doesn’t tell you how long a method should be. It tells you what to care about and leaves the judgment to you. The Zen even writes the tension down: “Special cases aren’t special enough to break the rules,” followed on the next line by “Although practicality beats purity.” You can only apply both lines if you know what the rules were for.
Write down the why
When you introduce a rule on a team, put the reason next to it. A comment on each non-default setting in the linter config. A “because” on each entry in the style guide. A code review comment that explains the concern along with the requested change, so the author can apply it next time without you.
Rules that come with reasons get followed where they fit and questioned where they don’t. Rules without reasons get followed blindly or ignored, and from the outside you can’t tell which.
Learn the why behind the rules you follow. That’s how you’ll know when to follow them and when to set them aside.