Intuition What this page is
The parent note taught the rules of lists. Here we stress-test them. We enumerate every kind of trap a list can throw at you — every sign of index, every degenerate slice, empty lists, aliasing, mutation-during-loop — then work through examples that touch every single cell so no scenario surprises you in an exam.
Before solving anything, let us map the territory. Every list problem you will meet is one of these case classes :
#
Case class
What makes it tricky
Covered by
A
Positive index in range
the baseline: offset from start
Ex 1
B
Negative index
must convert − k → n − k
Ex 1
C
Index out of range
raises IndexError (strict)
Ex 1
D
Slice, both ends in range
length = s t o p − s t a r t
Ex 2
E
Slice with step (fwd)
ceiling formula
Ex 2
F
Slice out of range
clamped , no error (forgiving)
Ex 3
G
Negative step / reversal
walks backward
Ex 4
H
Empty / degenerate slice
returns [], never crashes
Ex 3
I
Empty list []
length 0, no valid index
Ex 5
J
Aliasing (b = a)
one list, two names
Ex 6
K
Copy (a[:] / .copy())
independent object
Ex 6
L
Mutate-in-place vs rebind
append vs +
Ex 7
M
Word problem (real world)
model data as a list
Ex 8
N
Exam twist (mutate in loop)
the classic gotcha
Ex 9
The columns "positive / negative / zero / out-of-range / limiting" from the parent all appear here. Let's clear them one by one.
Worked example Cells A, B, C
a = [ 'p' , 'y' , 't' , 'h' , 'o' , 'n' ] # length n = 6
# 1) a[0] → ?
# 2) a[5] → ?
# 3) a[-1] → ?
# 4) a[-6] → ?
# 5) a[6] → ?
Forecast: cover the answers and guess all five. Write them down before reading on.
Step 1 — read a[0]. Answer 'p'.
Why this step? The index is an offset (distance from the start), so index 0 is the box 0 steps in — the first one. Look at figure s01: the top ruler labels each gap; index 0 sits under 'p'.
Step 2 — read a[5]. Answer 'n'.
Why this step? Valid indices run 0 to n − 1 = 5 . So 5 is the last legal forward index, pointing at the last box 'n'.
Step 3 — read a[-1]. Answer 'n'.
Why this step? Use the identity a [ − k ] ≡ a [ n − k ] . Here − 1 → n − 1 = 6 − 1 = 5 , the same box as step 2. In figure s01 the bottom ruler shows -1 sitting under 'n' — the two rulers meet at the same box.
Step 4 — read a[-6]. Answer 'p'.
Why this step? − 6 → n − 6 = 6 − 6 = 0 , the first box. That is the most-negative index still allowed.
Step 5 — read a[6]. IndexError.
Why this step? Indexing must point at one real box . The largest valid forward index is n − 1 = 5 ; 6 points past the end, so Python refuses. This is the strict behaviour — contrast with slicing in Ex 3.
Verify: n = 6. Positive twins: − 1 → 5 ✓, − 6 → 0 ✓. Legal range [ 0 , 5 ] ∪ [ − 6 , − 1 ] — 6 is outside, IndexError confirmed.
Worked example Cells D, E
a = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] # n = 10
# 1) a[2:6] → ?
# 2) a[1:8:2] → ?
# 3) a[3:3] → ?
Forecast: predict each result AND its length before checking.
Step 1 — a[2:6]. Answer [2, 3, 4, 5].
Why this step? A slice is half-open : it includes start=2 up to but not including stop=6. So indices 2,3,4,5. The length is the clean subtraction s t o p − s t a r t = 6 − 2 = 4 . Figure s02 draws the two cut-gaps (orange) — the slice is everything between them.
Step 2 — a[1:8:2]. Answer [1, 3, 5, 7].
Why this step? With a step we land on start, start+step, start+2·step, … while still < stop: 1 , 3 , 5 , 7 (next would be 9, but 9 < 8 ). Length by the ceiling rule:
⌈ 2 8 − 1 ⌉ = ⌈ 3.5 ⌉ = 4
Why the ceiling? You always get to take the very first landing point, so a partial final step still counts — round up .
Step 3 — a[3:3]. Answer [].
Why this step? s t o p − s t a r t = 3 − 3 = 0 , an empty span. Both cuts fall in the same gap , so nothing lies between them. This is a degenerate slice (cell H preview) — it returns an empty list, never an error.
Verify: lengths 4 , 4 , 0 match len of the answers [2,3,4,5], [1,3,5,7], []. ✓
Worked example Cells F, H
a = [ 10 , 20 , 30 ] # n = 3
# 1) a[1:100] → ?
# 2) a[5:9] → ?
# 3) a[2:0] → ?
Forecast: one of these looks like it should crash. Decide which — then discover none do.
Step 1 — a[1:100]. Answer [20, 30].
Why this step? A slice describes a range of gaps , not a single box. Python clamps stop=100 down to n=3, so you effectively get a[1:3] = [20, 30]. No IndexError — unlike a[100] which would crash.
Step 2 — a[5:9]. Answer [].
Why this step? start=5 is already past the end; clamped to 3, giving a[3:3], an empty span. Forgiving again — empty list, no error.
Step 3 — a[2:0]. Answer [].
Why this step? With the default positive step + 1 , you walk forward from 2, but stop=0 is behind you. There is nothing to collect, so the span is empty. (To walk backward you'd need a negative step — see Ex 4.)
Verify: all three lengths are ≥ 0 and none raised. len(a[1:100]) = 2, both others 0. The contrast with Ex 1 step 5 is the whole point: indexing is strict, slicing is forgiving. ✓
a = [ 0 , 1 , 2 , 3 , 4 , 5 ] # n = 6
# 1) a[::-1] → ?
# 2) a[4:1:-1] → ?
# 3) a[::-2] → ?
Forecast: with a negative step the defaults flip. Guess what start and stop default to.
Step 1 — a[::-1]. Answer [5, 4, 3, 2, 1, 0].
Why this step? A negative step walks from the end toward the start . With both ends omitted, start defaults to the last index and stop defaults to "just before the first" — so you sweep the whole list backward. This is the standard reverse idiom.
Step 2 — a[4:1:-1]. Answer [4, 3, 2].
Why this step? Start at index 4, step by − 1 : land on 4, 3, 2, stopping before stop=1 (still half-open, but now from above). Index 1's value is excluded . Figure s03 draws these backward hops as red arrows.
Step 3 — a[::-2]. Answer [5, 3, 1].
Why this step? From the end, jump backward by 2: 5, 3, 1. Next would be -1 which underruns the start, so we stop. Length by the ceiling rule using the magnitude of the step: ⌈ 6/2 ⌉ = 3 .
Verify: reversal length = 6 ✓; a[4:1:-1] collects 4,3,2 (3 items, stop 1 excluded) ✓; a[::-2] gives 5,3,1 (3 items) ✓.
e = [] # n = 0
# 1) len(e) → ?
# 2) e[0] → ?
# 3) e[:] → ?
# 4) e[0:5] → ?
Forecast: an empty list has no valid index. Predict which line crashes.
Step 1 — len(e). Answer 0.
Why this step? There are no boxes at all, so the count is zero. This is the limiting case n → 0 .
Step 2 — e[0]. IndexError.
Why this step? Valid indices are 0 to n − 1 = − 1 — an empty range, so no index is legal, not even 0. Indexing is strict, so it raises.
Step 3 — e[:]. Answer [].
Why this step? A full slice of an empty list is a new empty list. Slicing never crashes, even here.
Step 4 — e[0:5]. Answer [].
Why this step? Both ends clamp to 0; empty span. Forgiving as always.
Verify: len([]) = 0, [][:] and [][0:5] are both []; only the single-index access e[0] raises. Same strict-vs-forgiving pattern as Ex 3. ✓
Worked example Cells J, K
a = [ 1 , 2 , 3 ]
b = a # alias
c = a[:] # copy
b[ 0 ] = 99
c[ 2 ] = 77
# After these edits, what are a, b, c?
Forecast: guess all three lists. The trap is what b[0]=99 does to a.
Step 1 — b = a. Why this step? This ties a second label to the same memory box. There is still one list, now with two names. Figure s04 shows a and b as two arrows into one box; c gets its own separate box.
Step 2 — c = a[:]. Why this step? A full slice copies every element into a fresh list. c points at a different box, independent of a.
Step 3 — b[0] = 99. Since b and a share one list, this edit is visible through a too. So a = [99, 2, 3] and b = [99, 2, 3].
Why this step? Lists are mutable — editing in place changes the one shared object.
Step 4 — c[2] = 77. c is separate, so a and b are untouched. c = [1, 2, 77].
Result: a = [99, 2, 3], b = [99, 2, 3], c = [1, 2, 77].
Verify: a is b is True, a is c is False. The shared edit landed on both a and b; the copy's edit stayed local. ✓ (See Shallow vs Deep copy for when a[:] is not enough — nested lists.)
a = [ 1 , 2 ]
r1 = a.append( 3 ) # what is r1? what is a?
a = a + [ 4 ] # what is a now?
a = a.append( 5 ) # the classic bug — what is a now?
Forecast: two of these three lines surprise beginners. Which return None?
Step 1 — r1 = a.append(3). a becomes [1, 2, 3]; r1 is None.
Why this step? .append mutates in place and returns None. Its job is the side effect , not a value.
Step 2 — a = a + [4]. a becomes [1, 2, 3, 4].
Why this step? + builds a brand-new list and rebinds a to it. (If any alias pointed at the old list, it would not see the 4.)
Step 3 — a = a.append(5). a becomes None!
Why this step? .append(5) edits the list and returns None , and you assigned that None back to a. You lost your list. Never write a = a.append(...). See List methods — append, insert, pop, sort .
Verify: after step 1, a == [1,2,3] and r1 is None. After step 2, a == [1,2,3,4]. After step 3, a is None. ✓
A shop logs the last 7 days of sales:
sales = [ 120 , 90 , 0 , 150 , 200 , 0 , 175 ] # Mon..Sun
(a) Yesterday's sales? (Sunday is the last entry.) (b) Weekday sales only (Mon–Fri)? (c) Every other day starting Monday? (d) How many zero-sales days?
Forecast: which index gives "yesterday"? Guess before reading.
Step 1 — yesterday = sales[-1]. Answer 175.
Why this step? "Last entry" = the box 1 step from the end = index -1 = n-1 = 6. Negative indexing reads from the end without needing to know n.
Step 2 — weekdays = sales[0:5]. Answer [120, 90, 0, 150, 200].
Why this step? Mon–Fri are indices 0..4; half-open slice 0:5 excludes index 5 (Saturday). Length 5 − 0 = 5 days ✓.
Step 3 — every other day = sales[0:7:2]. Answer [120, 0, 200, 175].
Why this step? Start Monday, step 2: indices 0,2,4,6. Length ⌈ 7/2 ⌉ = 4 ✓.
Step 4 — zero days = sales.count(0). Answer 2.
Why this step? We ask "how many boxes hold 0?" The two zeros are Wednesday (index 2) and Saturday (index 5).
Verify: sales[-1] = 175; sales[0:5] has 5 items; sales[0:7:2] = [120,0,200,175]; two zeros total. Units: all values are rupees; counts are dimensionless days. ✓
a = [ 1 , 2 , 3 , 4 ]
for x in a:
if x % 2 == 0 :
a.remove(x)
# What is a afterwards? (This is the classic gotcha.)
Forecast: most people say [1, 3]. Predict — then trace the indices , not the values.
Step 1 — the loop walks by index under the hood. A for over a list visits position 0, 1, 2, …. It does not re-scan; it just moves an internal pointer forward. See for loops .
Why this step? Removing an item shifts everything after it left by one , but the pointer keeps advancing — so it skips the shifted element.
Step 2 — trace it.
pointer 0 → x=1, odd, keep. a=[1,2,3,4].
pointer 1 → x=2, even, remove. a=[1,3,4]. Now index 1 holds 3 — but pointer moves to 2.
pointer 2 → x=4 (the 3 at index 1 was skipped !), even, remove. a=[1,3].
pointer 3 → out of range, loop ends.
Step 3 — result. a = [1, 3].
Why this step? Here it looks correct by luck, but 3 was never even tested. Change the data and it breaks — mutating the list you iterate is a real bug. Fix: iterate a copy for x in a[:]: or build a new list [x for x in a if x % 2].
Verify: trace ends at [1, 3]. The safe version [x for x in [1,2,3,4] if x % 2 != 0] also gives [1, 3] — same answer here, but the safe version tests every element. ✓
Recall Self-quiz across the whole matrix
a=['p','y','t','h','o','n']; what is a[-6]? ::: 'p' (since − 6 → n − 6 = 0 ).
Does a[5:9] on a length-3 list raise? ::: No — clamped to []; slicing is forgiving.
What does a[4:1:-1] collect from [0,1,2,3,4,5]? ::: [4, 3, 2] (backward, stop 1 excluded).
After b=a; b[0]=99, what is a[0]? ::: 99 — one list, two names.
What is a after a = a.append(5)? ::: None — never do this.
Why can mutating a list mid-loop skip elements? ::: Removal shifts items left but the pointer still advances, skipping the shifted item.
Index: strict — out of range → IndexError; empty list has no legal index.
Slice: forgiving — clamps, returns [] for empty/degenerate spans, never crashes.
Negative step reverses direction; defaults flip to (last → before-first).
= shares one list; [:]/.copy()/list() make a real copy.
.append mutates + returns None ; + builds a new list.
Never mutate a list while looping it — iterate a copy instead.
Related: Strings — indexing & slicing (same rules, immutable), Tuples (ordered but immutable), Mutability & function arguments .