6 Don’t surprise your users
This chapter emphasizes designing Python software that behaves the way users expect, avoiding logic and performance surprises by following the Principle of Least Astonishment. It frames “users” as both fellow programmers and end users, and argues that clarity in behavior and cost (time and memory) is essential. The material surveys common sources of astonishment and how to prevent them: careful naming, attention to indexing and iteration, choosing efficient algorithms and data structures, systematic refactoring, and explicitly defining expectations through Programming by Contract.
At the code level, classic traps include off-by-one errors (for example, 0-based indexes clashing with human 1-based month numbers) and misleading function names that hide extra behavior, such as a “merge” routine that also removes duplicates. The fixes pair intent with implementation: normalize indexes (or structure data to remove the mismatch), name functions to reflect all effects, and refactor algorithms to avoid unnecessary nested loops—e.g., eliminate duplicates with a set or by preventing them during a linear merge. Performance surprises also surface when we ignore language features: list comprehensions typically beat manual append loops; slicing with strides is faster than explicit iteration; tuples can outpace lists for read-only use; and NumPy arrays with vectorized operations offer the largest speedups.
To further reduce surprises, the chapter promotes Programming by Contract: state preconditions (what must be true before a call), postconditions (what will be true on return), and class invariants (what must always hold for object state). Responsibilities are clear: callers ensure preconditions; functions ensure postconditions when preconditions are met; invariants must hold after construction and after each mutation. A circular buffer example shows assertions enforcing these rules, with exceptions as a production-ready alternative. Used judiciously on critical classes, contracts document intent, keep objects valid, and make behavior predictable.
Summary
- Code should behave as expected by its users.
- The Principle of Least Astonishment states that there should be few, if any, surprises in well-designed software. Some surprises are due to the way Python operates. We can also inadvertently put surprises in our code.
- Off-by-one errors are common, and they can lead to insidious runtime logic errors that are hard to find.
- A poorly named function can mislead a caller of the function to believe that it does something other than its name suggests.
- Poorly designed code can have unexpected bad runtime performance, which includes longer than expected running times or excessive use of memory and other resources.
- A Python list is easy to use and very powerful. However, we should use build-in features such as list comprehension and slicing to ensure good performance. Using tuples or numpy arrays instead of lists can improve performance.
- Programming by contract ensures users of a class that we wrote won’t be surprised by how its objects behave at run time.
- The Precondition Principle states that the caller of a function must ensure that the function’s precondition is true before making the call. If the precondition is not true but the function is called anyway, the function can behave in a manner that might not be convenient for the caller. A typical behavior in an application is for the function to raise an exception. On the other hand, if the precondition is true, the function must guarantee that it will perform correctly.
- The Postcondition Principle states that the function must ensure that its postcondition is true before returning.
- The Class Invariant Principle involves a condition that must remain true at run time. It ensures that none of the class’s objects are left in an invalid state. In an application, the class can raise an exception if any object violates its invariant.
FAQ
What is the Principle of Least Astonishment (PoLA)?
Write code that behaves as users expect, with few or no surprises. Surprises create logic/runtime errors, hurt performance, and make software harder to maintain.What is an off-by-one error and how do I avoid it?
An off-by-one error happens when a count, index, or loop runs one time too many or too few (for example, using <= instead of < in a loop, or mixing 1-based expectations with 0-based indexing). Avoid it by carefully reasoning about loop bounds and remembering that Python list/tuple indices start at 0.How can I fix the month indexing surprise in a Date class?
Two common fixes: 1) Subtract 1 when indexing:_MONTH_NAMES[self._month - 1]. 2) Add a dummy element at index 0 so months map directly: _MONTH_NAMES = ('', 'JAN', ..., 'DEC') and then use _MONTH_NAMES[self._month]. The first costs a subtraction; the second uses one extra tuple slot (now length 13).Why are misnamed functions dangerous?
A misleading name causes callers to assume the wrong behavior. Example: a function namedmerge that also removes duplicates surprises callers expecting a true merge. Rename to something accurate (e.g., merge_and_deduplicate) or separate concerns.How can poor algorithm design cause performance surprises?
Nested loops and unnecessary passes can make runtime grow much faster than linearly. In the merge example, deduplicating after building the list with nested loops is slow. Faster options: 1) uselist(set(merged)) to remove duplicates, or better 2) prevent duplicates while merging by checking the last appended value (leveraging sorted inputs).When should I prefer list comprehensions over for-loops with append?
Use list comprehensions to build lists; they are optimized and typically faster than afor loop with repeated append(). For large lists this can deliver noticeable speedups.Which is faster for strided operations: list, tuple, or NumPy array?
For slicing with a stride ([::stride]): tuple often beats list (immutability and lower overhead), and a NumPy array with vectorized operations (e.g., np.mean) is usually fastest. Choose the structure that best fits your performance and library constraints.What is refactoring and how does it help?
Refactoring is redesigning code to improve qualities like performance or readability without changing its external behavior or how it’s used. Example: rewriting a merge to avoid nested loops while preserving its output contract.In Programming by Contract, who checks preconditions and who guarantees postconditions?
The caller must ensure preconditions are true before calling; the function must ensure postconditions are true before returning. In Python, you can expose methods or properties to let callers check preconditions, and useassert or raise exceptions to enforce contracts.What is a class invariant and can it be temporarily violated?
A class invariant is a condition about the object’s state that must hold after construction and after every mutating method returns. It may be transiently false during a method’s internal steps, but must be true again before returning. Example: a circular buffer keeps0 ≤ head, tail < capacity, maintained via modulo arithmetic.
Software Design for Python Programmers ebook for free