name: least-astonishment description: "Principle of Least Astonishment (POLA) for edit-time code changes. Activate when modifying, editing, refactoring, fixing, patching, extending, or adding to existing code — any time Claude is about to change code in an established codebase. Trigger keywords: edit, modify, change, update, refactor, fix, patch, extend, add to, integrate with, wire into, match the existing, follow the pattern, in this file, in this module, in this codebase, alongside, similar to. Do NOT activate for: greenfield, from scratch, new project, prototype, scaffold, throwaway."
Principle of Least Astonishment (POLA)
Every change should be predictable to someone familiar with the codebase.
If a change would surprise a developer who knows the project, either:
- Don't make it
- Make a smaller, less surprising change
- Explain clearly before proceeding
Before Making Changes
The Surprise Check
Ask yourself:
Would this surprise someone reading a diff?
- Unexpected file modifications
- Unrelated changes bundled together
- Renamed things without clear reason
Does this follow existing patterns?
- Check neighboring files first
- Match naming conventions already in use
- Use established abstractions, don't invent new ones
Is the scope what was requested?
- No unrequested refactors
- No "improvements" beyond the ask
- No adding features "while I'm here"
Would the function/API do what its name suggests?
- No hidden side effects
- No surprising return values
- No unexpected mutations
Common POLA Violations
Scope Creep
Asked: "Fix the null check in validateUser"
Did: Fixed null check + refactored 3 other functions + added logging
Why surprising: Reviewer expects a small fix, gets a large diff.
Hidden Side Effects
def get_user(id):
user = db.find(id)
user.last_accessed = now() # Mutation in a "get" function
db.save(user)
return user
Why surprising: "get" implies read-only; this writes.
Inconsistent Naming
# Existing codebase uses:
fetch_user(), fetch_orders(), fetch_products()
# New code adds:
retrieve_customer() # Different verb AND different noun
Why surprising: Breaks established vocabulary.
Unexpected File Changes
PR title: "Update README"
Files changed: README.md, config.py, utils.py, test_utils.py
Why surprising: Unrelated files modified.
Silent Behavior Changes
# Before: returned empty list on error
# After: raises exception on error
Why surprising: Callers expecting old behavior will break.
POLA-Compliant Patterns
Minimal Diffs
Change only what's necessary. If you notice something else that needs fixing, mention it separately.
Match Existing Vocabulary
# Codebase uses "fetch_*" pattern
def fetch_payments(): # Follows convention
...
Explicit Over Implicit
# Instead of hidden side effect:
def get_user(id):
return db.find(id)
def get_and_update_access(id): # Name reveals behavior
user = db.find(id)
user.last_accessed = now()
db.save(user)
return user
Backward-Compatible Changes
# Adding parameter with default preserves existing behavior
def process(data, validate=True): # Old callers unaffected
...
Predictable Return Types
# Always return same type
def find_users(query) -> list[User]:
# Return [] not None when empty
return results or []
When Larger Changes Are Needed
Sometimes breaking POLA is necessary. When it is:
- Announce it - "This will change behavior for X"
- Explain why - "Current approach causes Y problems"
- Offer alternatives - "We could also do Z, which is less disruptive"
- Get explicit approval - Don't assume silence means consent
Quick Reference
| Situation | POLA-Compliant Approach |
|---|---|
| Fixing a bug | Fix only that bug |
| Adding a feature | Add only that feature |
| Refactoring | Only when explicitly requested |
| Renaming | Match existing conventions |
| Changing return types | Add new function, deprecate old |
| Modifying APIs | Backward-compatible by default |
Integration with Other Principles
- KISS - Simple solutions are usually less surprising
- DRY - But don't create abstractions that surprise (premature DRY violates POLA)
- Consistency - Following patterns is core to POLA
TL;DR
Do what the code reader expects. Match existing patterns. Keep changes focused. No surprises.
AI-Assistant-Specific Anti-Patterns
LLM coding agents have a distinct set of POLA failure modes. Run this checklist before committing an edit:
- Drive-by reformatting — don't touch whitespace, quote style, or import order in regions you weren't asked to change.
- Opportunistic renaming — don't rename variables/functions across files when the user asked for one local fix.
- Idiom substitution — don't rewrite
for→ comprehension (or vice versa) if the surrounding file uses the other style. - Unrequested abstraction — don't extract a helper/base class/config object on the first occurrence of duplication. Three similar lines beat a premature abstraction.
- New dependency for solved problems — don't add a library when the file already uses a stdlib equivalent.
- Error-handling style drift — don't introduce
Result/exceptions/early-returns mid-file when the surrounding code uses a different convention. - Logging vs print divergence — don't use
printin a module that uses a structured logger (or vice versa). - Test framework drift — don't write
unittest.TestCasetests in apytestcodebase (or mixassertandexpect). - Silent default changes — don't flip a kwarg default, timeout, or retry count "to be safer" without being asked.
- Cross-file collateral — don't modify call sites, types, or fixtures beyond the file the user pointed at without flagging it first.
If you catch yourself about to do any of these, stop and either (a) make the smaller edit, or (b) surface the broader change to the user before proceeding.
References
- Raymond, E. S. The Art of Unix Programming — Rule of Least Surprise: https://www.catb.org/~esr/writings/taoup/html/ch11s01.html
- Wikipedia — Principle of least astonishment: https://en.wikipedia.org/wiki/Principle_of_least_astonishment
- DevIQ — Principle of Least Astonishment: https://deviq.com/principles/principle-of-least-astonishment/