"Some operations are usually instant and occasionally brutal. Amortized analysis is how you charge them honestly: spread the rare brutal cost across all the cheap ones."
The Puzzle of list.append
Appending to a Python list is O(1). Except sometimes it isn't — sometimes appending one item makes Python copy the entire list to a bigger chunk of memory, which is O(n). So how can the docs honestly call append "O(1)"? The answer is a beautiful idea called amortized analysis: even though individual operations vary, the average cost per operation across a long sequence is what we report.
How a Dynamic Array Actually Grows
A list keeps spare capacity beyond its current length. When that space runs out, it allocates a larger block and moves the existing references, an O(n) event. The important invariant is geometric growth with spare capacity, not an exact doubling rule. CPython's precise growth factor is an implementation detail that can vary by version.
For a model that clicks, pretend capacity doubles. After growing to 8 slots, as many as 4 cheap appends arrive before the next copy; at 16 slots, as many as 8. The moved-element total is 1+2+4+…, less than 2n after n appends. That makes total copying O(n) and append amortized O(1). This is a teaching model, not a promise that CPython doubles exactly. The real invariant is geometric growth that puts many cheap appends between expensive copies.
The Banker's Intuition
Here's the mental model that makes it click. Imagine every cheap append secretly pays a little extra — say, three coins instead of one. Two coins go in a savings jar. When the rare expensive copy comes, the jar has exactly enough saved up to pay for it. No single operation ever overdraws. The expensive copy was "pre-paid" by all the cheap appends before it. That's why the average stays flat even though one operation occasionally spikes.
Amortized Is Not Worst-Case
Crucial caveat: amortized O(1) does not mean every append is O(1). One specific append — the one that triggers the copy — really is O(n). If you're writing real-time code where a single operation must never stall (a heart monitor, an audio buffer), amortized isn't good enough; you need worst-case guarantees, and you might pre-allocate to avoid the spike. For throughput (total work over time), amortized is exactly the right lens. Knowing which lens your problem needs is the skill.