Python Execution Flow PAGE 1 — Loop Iteration Trace
Visual diagrams for loops, lists, mutation & memory s picks 'error', then 'warning'
Source Code Loop Unroll
logs = []
words = ['error', 'warning'] Iteration 1
s = 'error'
logs = [] + ['error'] => ['error']
for s in words:
logs = logs + [s]
Iteration 2
# Result: logs = ['error', 'warning'] s = 'warning'
logs = ['error'] + ['warning'] => ['error','warning']
State Trace (what lives in memory each step)
Step s logs (before) logs (after) New list?
start — — [] no
iter 1 'error' [] ['error'] YES
iter 2 'warning' ['error'] ['error','warning'] YES
done — ['error','warning'] same —
Memory Diagram — why logs = logs + [s] creates a NEW list every time
BEFORE iter 1 AFTER iter 1 OLD list (abandoned)
logs [] logs ['error'] []
logs + ['error']
creates NEW object
id: 0x001 object @ 0x001 id: 0x002 NEW object @ 0x002 id: 0x001
empty list logs reassigned no variable points here
=> garbage collected
Python Execution Flow | Page 1 of 4
append() vs logs = logs + [s] PAGE 2 — In-place vs Concatenation
Side-by-side execution and memory comparison
Method A — .append() Method B — logs = logs + [s]
MUTATES the existing list in-place Creates a BRAND NEW list every iteration
append() code concat code
logs = [] logs = []
for s in words: for s in words:
[Link](s) logs = logs + [s]
Memory — only ONE list object exists throughout: Memory — a NEW list object created each time:
start start
logs -> [ ] id: 0xAAA (unchanged!) logs -> [ ] id: 0xAAA (different!)
iter 1 iter 1
logs -> ['error'] id: 0xAAA (unchanged!) logs -> ['error'] NEW id: 0xBBB (different!)
iter 2 iter 2
logs -> ['error','warn'] id: 0xAAA (unchanged!) logs -> ['error','warn'] NEW id: 0xCCC (different!)
SAME object throughout DIFFERENT object each time
No copies made. Efficient! Old lists orphaned, then GC'd
Performance & Behavior Comparison
Property append() logs + [s] Winner
Creates new list? NO YES, every loop append
Memory usage O(1) amortized O(n) per step append
Original list changed YES (mutates) NO (new binding) depends
Result identical? YES YES tie
Use when... building list immutability needed both valid
Python Execution Flow | Page 2 of 4
Default Argument Mutation Trap PAGE 3 — Function Call Memory
Why mutable defaults are dangerous — and how Python really handles them
The Bug (mutable default) The Fix (None sentinel)
def add_item(item, bag=[]): def add_item(item, bag=None):
[Link](item) if bag is None:
return bag bag = []
[Link](item)
r1 = add_item('apple') return bag
r2 = add_item('banana')
r3 = add_item('cherry') r1 = add_item('apple')
r2 = add_item('banana')
# SURPRISE!
# r1 = ['apple','banana','cherry'] # CORRECT
# r2 = ['apple','banana','cherry'] # r1 = ['apple']
# r3 = ['apple','banana','cherry'] # r2 = ['banana']
Why it happens — the default list lives inside the FUNCTION OBJECT
Function Object: add_item
Created ONCE at def time
__name__: 'add_item'
__code__: <compiled bytecode>
__defaults__: ( [ ] ) <-- lives HERE
same list object, never reset
Call trace (all calls share the same default list object):
call 1: add_item('apple')
bag = <default> -> ['apple']
call 2: add_item('banana')
bag = SAME default -> ['apple','banana']
call 3: add_item('cherry')
bag = SAME default -> ['apple','banana','cherry']
The None fix — each call gets its OWN fresh list:
call 1: add_item('apple')
bag=None -> bag=[] -> ['apple'] new id: 0x111
call 2: add_item('banana')
bag=None -> bag=[] -> ['banana'] new id: 0x222
Golden Rule
NEVER use mutable objects (list, dict, set) as default arguments.
They are evaluated ONCE when the function is defined, not each time it is called.
Always use None, then assign inside the function body.
Python Execution Flow | Page 3 of 4
Nested Loop Execution Map PAGE 4 — Execution Order Grid
Exactly which (i,j) pairs execute and in what order
Nested Loop Execution grid — each cell shows (i, j) and call order
for i in range(3):
for j in range(4):
print(i, j)
j=0 j=1 j=2 j=3
i=0
(0,0) (0,1) (0,2) (0,3)
call #1 call #2 call #3 call #4
i=1
(1,0) (1,1) (1,2) (1,3)
call #5 call #6 call #7 call #8
i=2
(2,0) (2,1) (2,2) (2,3)
call #9 call #10 call #11 call #12
Execution order (reading left to right, top to bottom):
1 2 3 4 5 6 7 8 9 10 11 12
(0,0) (0,1) (0,2) (0,3) (1,0) (1,1) (1,2) (1,3) (2,0) (2,1) (2,2) (2,3)
How break and continue change execution:
Pairs printed with break at j==2:
break example
(0,0) (0,1) j==2 (0,3)
for i in range(3): i=0 printed printed BREAK skipped
for j in range(4):
if j == 2: (1,0) (1,1) j==2 (1,3)
break # stops INNER loop i=1 printed printed BREAK skipped
print(i, j)
(2,0) (2,1) j==2 (2,3)
i=2 printed printed BREAK skipped
break only exits INNER loop. Outer loop still runs for all i.
Key Takeaways
Outer loop runs n times, inner loop runs m times PER outer iteration => n x m total calls
break stops only the loop it lives in — not any outer loops
continue skips the rest of that iteration and moves to the next one
Use enumerate() to track iteration index without a manual counter variable
Python Execution Flow | Page 4 of 4