46

Least Astonishment

#
  • heuristic
  • established
  • fragment

The measure of astonishment is not the author’s taste but the established idiom of the language and the surrounding code: what astonishes is whatever behaves differently from ninety-nine similar places. Rule of thumb: if a behaviour needs a comment to explain it, removing the behaviour is cheaper than writing the comment — a reader who skips the comment will get it wrong anyway (47).

Code example (fragment)
fragment
Python
# The classic violation — a mutable default
def add_item(item, bucket=[]): ...      # bucket is shared across calls

def add_item(item, bucket=None):        # the expected behaviour
    bucket = bucket if bucket is not None else []
Java
// equals without hashCode: the object goes into a HashSet and “vanishes”
class Money {
    @Override public boolean equals(Object o) { ... } // hashCode is Object's
}
// set.add(new Money(5)); set.contains(new Money(5)) → false

// record generates equals and hashCode together and keeps them in sync
record Money(long cents, Currency currency) {}
Go
// a nil pointer in an interface is not a nil interface: it has two fields,
// type and value, and a non-nil type makes the whole thing non-nil
func find() *MyErr { return nil }

var err error = find() // type *MyErr, value nil
if err != nil {        // true — and that surprises every other reader
    log.Print("an error that isn't there")
}

// CORRECT: return error, not a concrete error type
func find() error { return nil } // here err == nil, as expected
Rust
// Deref is meant for pointers, not for is-a: through it the methods of
// Inner silently appear on Wrapper, and a reader can't find where they're declared
impl Deref for Wrapper {
    type Target = Inner;
    fn deref(&self) -> &Inner { &self.0 }
}

// CORRECT: delegate explicitly exactly what is promised outward (88)
impl Wrapper {
    pub fn id(&self) -> Id { self.0.id() }
}

Last technical review: 2026-08-04 · Reference · 112 principles