Python Lists: Watching a Dynamic Array Grow, and Why insert(0) Is Slower

A Python list is a dynamic array: it backs onto a contiguous block of memory that has to be resized as the list grows. This post tracks a list’s actual byte size across 20 appends with sys.getsizeof, then times append() against insert(0, x) to see what adding at the front costs compared to adding at the end.

The code

import sys
import time

lst = []
prev_size = sys.getsizeof(lst)
print(f"{'len':>4} | {'bytes':>6} | {'grew?':>6}")
print(f"{len(lst):>4} | {prev_size:>6} | {'':>6}")

# Append one at a time and watch the underlying byte size.
# CPython over-allocates, so size only jumps on some appends, not every one.
for i in range(20):
    lst.append(i)
    size = sys.getsizeof(lst)
    grew = "yes" if size != prev_size else ""
    print(f"{len(lst):>4} | {size:>6} | {grew:>6}")
    prev_size = size

# Now compare append() (adds at the end) against insert(0, x)
# (adds at the front, shifting every existing element over by one).
n = 20000
print(f"\ntiming {n} appends vs {n} insert(0, x) calls")

start = time.perf_counter()
appended = []
for i in range(n):
    appended.append(i)
append_time = time.perf_counter() - start

start = time.perf_counter()
inserted = []
for i in range(n):
    inserted.insert(0, i)
insert_time = time.perf_counter() - start

print(f"append():     {append_time:.6f}s")
print(f"insert(0, x): {insert_time:.6f}s")

First loop appends 20 items one at a time and prints sys.getsizeof(lst) after each; second part times n = 20000 calls to append() against n calls to insert(0, x) on separate empty lists.

Running it

Real output, Python 3.14.3:

 len |  bytes |  grew?
   0 |     56 |       
   1 |     88 |    yes
   2 |     88 |       
   3 |     88 |       
   4 |     88 |       
   5 |    120 |    yes
   6 |    120 |       
   7 |    120 |       
   8 |    120 |       
   9 |    184 |    yes
  10 |    184 |       
  11 |    184 |       
  12 |    184 |       
  13 |    184 |       
  14 |    184 |       
  15 |    184 |       
  16 |    184 |       
  17 |    248 |    yes
  18 |    248 |       
  19 |    248 |       
  20 |    248 |       

timing 20000 appends vs 20000 insert(0, x) calls
append():     0.001019s
insert(0, x): 0.023445s

The byte size didn’t grow on every append — it jumped 4 times (at lengths 1, 5, 9, and 17) and stayed flat the rest of the time, going from 56 bytes empty to 248 bytes at length 20. In the timing comparison, 20000 append() calls took 0.001019s while 20000 insert(0, x) calls took 0.023445s — about 23x longer for the same number of calls.

Takeaway

The real run showed two things: the list’s byte size grows in occasional jumps rather than on every single append, and adding at the front with insert(0, x) measured about 23x slower than append() for 20000 calls in this run.