AiTechWorlds
AiTechWorlds
University learning companion
Data Structures · Dynamic Arrays and How Python Lists Really Work
Use this course-level map to place “Dynamic Arrays and How Python Lists Really Work” in the larger system. It complements—not replaces—a topic-specific worked diagram.
A dynamic array is an array that automatically resizes itself by allocating a bigger block and copying elements over whenever it runs out of room, which is exactly how a Python list works under the hood.
You are moving apartments. You start with one box labeled "Books." You pack 4 books, and the box is full. You need to add a 5th book.
You have two choices:
Python lists use option 1 but with a clever twist. Instead of getting a box that holds exactly 5, Python gets a box that holds 8 (roughly double). Then 9 through 16 books fit without any moving. When the 17th book arrives, Python gets a box that holds 25, moves everything, and continues.
This capacity doubling strategy is the secret behind why Python lists feel both unlimited and fast.
A static array has a fixed size set at creation. You declare "I need 10 slots" and you get exactly 10. If you need 11, you are stuck you must manually allocate a new array and copy everything over.
A dynamic array handles this automatically. It maintains:
When length equals capacity and you try to add one more item, the dynamic array:
This copying step is expensive O(n). But because it only happens occasionally (each time the array doubles), the average cost per append remains O(1).
Python's sys.getsizeof() reports the memory footprint of a list object in bytes. As the list grows, you can watch it resize:
import sys
lst = []
prev_size = sys.getsizeof(lst)
print(f"Empty list: {prev_size} bytes")
for i in range(26):
lst.append(i)
new_size = sys.getsizeof(lst)
if new_size != prev_size:
print(f"After {len(lst):2d} items: {new_size} bytes ← capacity increased")
prev_size = new_sizeSample output (exact bytes vary by Python version and platform):
Empty list: 56 bytes
After 1 items: 88 bytes ← capacity increased
After 5 items: 120 bytes ← capacity increased
After 9 items: 184 bytes ← capacity increased
After 17 items: 248 bytes ← capacity increased
After 25 items: 312 bytes ← capacity increasedThe jumps happen at 1, 5, 9, 17, 25 this is CPython's growth formula. It is not a pure doubling, but a formula roughly equivalent to new_capacity = old_capacity + old_capacity >> 3 + (3 or 6). The result is a pattern of 0 → 4 → 8 → 16 → 25 → 35 → 46 → 58... capacity slots.
The key observation: the list does not resize on every append. Resizes cluster around specific thresholds. Between thresholds, every append is just placing an item in a pre allocated slot an extremely fast operation.
Amortized analysis spreads the cost of expensive but rare operations across many cheap operations.
Think of it this way: imagine appending 16 items to an empty list that starts with capacity 4:
| Appends | Resize? | Cost of this append |
|---|---|---|
| Items 1–4 | No | O(1) each |
| Item 5 | Yes, resize to 8 | O(n) copies 4 items |
| Items 6–8 | No | O(1) each |
| Item 9 | Yes, resize to 16 | O(n) copies 8 items |
| Items 10–16 | No | O(1) each |
Total work: 16 appends + 4 copies + 8 copies = 28 operations for 16 items.
28 ÷ 16 = 1.75 operations per append on average.
As the list grows larger, the ratio approaches 1. The expensive copies become a smaller and smaller fraction of total work. This is why we say append is O(1) amortized not always fast, but fast on average across any sequence of appends.
Amortized O(1) means: if you perform n appends, the total cost is O(n). It does not mean each individual append takes constant time.
Every Python developer should have this table memorized. It explains countless performance bugs.
| Method / Operation | Time Complexity | Notes |
|---|---|---|
lst.append(x) | O(1) amortized | Occasional O(n) resize, but rare |
lst.pop() | O(1) | Removes last element no shifting |
lst.pop(i) | O(n) | Removes at index shifts remaining elements |
lst.insert(i, x) | O(n) | Shifts all elements from index i onward |
lst.remove(x) | O(n) | Linear search + shift |
len(lst) | O(1) | Length is stored as a field, not computed |
lst[i] | O(1) | Direct index calculation |
lst[i] = x | O(1) | Direct write by index |
x in lst | O(n) | Linear scan for membership |
lst.sort() | O(n log n) | CPython uses Timsort |
sorted(lst) | O(n log n) | Returns new sorted list |
lst.reverse() | O(n) | In place reversal |
lst.copy() | O(n) | Shallow copy of all n elements |
lst.extend(other) | O(k) | k = length of other |
lst + other | O(n + k) | Creates a new list of size n+k |
lst.clear() | O(1) | Drops all references, resets length |
The dangerous one is x in lst. Developers frequently write:
# This is O(n) on every loop iteration → O(n²) total
for item in large_list:
if item in another_large_list: # DO NOT DO THIS
process(item)Converting another_large_list to a set makes the in check O(1), reducing the whole thing to O(n).
List comprehensions are not just syntactic sugar they are meaningfully faster than equivalent for loops that call append.
import time
n = 1_000_000
# Method 1: for loop with append
start = time.perf_counter()
result1 = []
for i in range(n):
result1.append(i * 2)
loop_time = time.perf_counter() - start
# Method 2: list comprehension
start = time.perf_counter()
result2 = [i * 2 for i in range(n)]
comp_time = time.perf_counter() - start
print(f"Loop: {loop_time:.4f}s")
print(f"Comprehension: {comp_time:.4f}s")Sample output:
Loop: 0.0821s
Comprehension: 0.0392sThe comprehension is typically 2× faster because Python avoids repeatedly looking up the append method on each iteration the entire list building operation runs in optimized C code internally.
Python lists are convenient but memory heavy. When working with large datasets of uniform type, consider:
array module typed arraysimport array
import sys
regular_list = list(range(10_000))
typed_array = array.array('i', range(10_000)) # 'i' = 4-byte int
print(f"list size: {sys.getsizeof(regular_list):,} bytes")
print(f"array size: {sys.getsizeof(typed_array):,} bytes")Sample output:
list size: 85,176 bytes
array size: 40,056 bytesThe typed array uses roughly half the memory because it stores raw integers directly rather than Python object references.
numpy arrays for numerical workimport numpy as np
import sys
np_array = np.array(range(10_000), dtype=np.int32)
print(f"numpy size: {np_array.nbytes:,} bytes") # Output: 40,000 bytesNumPy arrays are the foundation of scientific computing in Python. Operations on NumPy arrays are vectorized (run in C), making them 10–100× faster than equivalent Python loops for numerical operations.
One of the most common uses of a dynamic array is implementing a stack a structure where you can only push items on top and pop them from the top (Last In, First Out).
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item) # O(1) amortized
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self._data.pop() # O(1) — removes from end
def peek(self):
if self.is_empty():
raise IndexError("peek at empty stack")
return self._data[-1] # O(1)
def is_empty(self):
return len(self._data) == 0 # O(1)
def size(self):
return len(self._data) # O(1)
def __repr__(self):
return f"Stack({self._data})"
# Demo
browser_history = Stack()
browser_history.push("google.com")
browser_history.push("github.com")
browser_history.push("docs.python.org")
print(browser_history) # Output: Stack(['google.com', 'github.com', 'docs.python.org'])
print(browser_history.peek()) # Output: docs.python.org
browser_history.pop()
print(browser_history) # Output: Stack(['google.com', 'github.com'])
print(browser_history.size()) # Output: 2Every operation here is O(1) because lists are optimized for end operations. This is the ideal usage pattern for a list never inserting or deleting from the middle.
append is O(1) amortized fast on average across any sequence of appendssys.getsizeof() reveals that lists allocate capacity in jumps, not one slot at a timeappend, pop(), lst[-1]) are O(1); middle/beginning operations are O(n)x in lst is O(n) convert to a set when membership testing repeatedlyarray.array or numpy arrays reduce memory usage by 50–75%The next lesson moves into two dimensions 2D arrays and matrices where rows and columns let you model the real world: images, game boards, distance tables, and more.
University learning companion
Data Structures · Dynamic Arrays and How Python Lists Really Work
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises