Principle of Least Astonishment (POLA)
Auto-activate when: Making code changes, refactoring, modifying existing functions, changing APIs, renaming things, moving files, or reviewing implementations. Should activate alongside development-philosophy for any non-trivial changes.
Core Principle
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.