NUS TCX1002 exam-focused gotchas & patterns. For general Python notes, see my Python Notebook.
1TCX1002 | Python Notebook
2│
3├── 1. Iteration Traps
4│ ├── Mutation during iteration (pop/remove shifts indices → crash)
5│ ├── Loop variable scope leaking (Python ≠ JS/Java block scope)
6│ ├── range() locking (for: ONCE at start, while: every iteration)
7│ └── Safe patterns (backwards iteration, list comprehension copy)
8│
9├── 2. Data Structure Gotchas
10│ ├── List multiplication mutability ([[]] * 3 = shared refs!)
11│ ├── Negative indexing silent bug (tree[-1] returns last, not error)
12│ ├── Dict overwrites duplicate keys (use defaultdict)
13│ ├── map() returns iterator (not list — must convert)
14│ └── round() banker's rounding vs math.ceil()
15│
16├── 3. Functional Programming
17│ ├── Lambda with sorted() (key= required!)
18│ ├── map / filter / reduce trio
19│ ├── Functional composition (filter→map→sum pipeline)
20│ ├── Python 3: no tuple unpack in lambda (PEP 3113)
21│ └── for...else pattern ("if not broken")
22│
23├── 4. Algorithm Patterns
24│ ├── Tuple (index, value) sorting pattern
25│ ├── Sliding window template (+ O(1) optimization)
26│ ├── K-length optimization (group by length)
27│ └── Boyer-Moore string search (bad character heuristic)
28│
29├── 5. Meta-Learning
30│ ├── "Tests pass ≠ logic correct" (Q6 convergence)
31│ └── "Tweak until green" anti-pattern (Q3 trailing whitespace)
32│
33└── 6. Memory Tricks (speed reference)
34 ├── sorted() vs .sort() ("ED = nEw Data")
35 ├── Lambda = "Lazy function"
36 └── * = "explode operator"
1: Iteration Traps
1.1 Mutation During Iteration
Modifying a list while iterating over it causes index shifts that skip elements or crash.
1nums = [1, 2, 3, 4, 5]
2for i in range(len(nums)):
3 if nums[i] % 2 == 0:
4 nums.pop(i) # IndexError! After pop, list shrinks but range doesn't
Why it crashes — the index shift trace:
| Step | i | List before | Action | List after |
|---|---|---|---|---|
| 1 | 0 | [1, 2, 3, 4, 5] | 1 is odd, skip | [1, 2, 3, 4, 5] |
| 2 | 1 | [1, 2, 3, 4, 5] | 2 is even, pop(1) | [1, 3, 4, 5] |
| 3 | 2 | [1, 3, 4, 5] | 4 is even, pop(2) | [1, 3, 5] |
| 4 | 3 | [1, 3, 5] | IndexError! len=3, i=3 | 💥 |
After pop(1) removes 2, everything shifts left: 3 slides from index 2 → index 1. But i increments to 2 regardless, so it now points at 4 (original index 3) — 3 was never checked! Then pop(2) shrinks the list again, and when i=3 the list only has 3 elements (indices 0-2), so nums[3] crashes with IndexError. The core problem: range(len(nums)) was locked to 5 at the start, but each pop() makes the list shorter.
Safe alternatives:
1# Option 1: Backwards iteration (indices don't shift for earlier items)
2for i in range(len(nums) - 1, -1, -1):
3 if nums[i] % 2 == 0:
4 nums.pop(i)
5
6# Option 2: List comprehension (creates new list, no mutation)
7nums = [x for x in nums if x % 2 != 0]
8
9# Option 3: Copy and iterate
10for x in nums[:]: # [:] makes a copy
11 if x % 2 == 0:
12 nums.remove(x)
1.2 Loop Variable Scope Leaking
Python loop variables survive after the loop — unlike Java/JS block scope.
1for x in range(3):
2 pass
3print(x) # 2 (NOT an error!)
4
5# Even in list comprehensions (Python 2 leaked, Python 3 doesn't):
6result = [i for i in range(5)]
7# print(i) # NameError in Python 3 (list comp has own scope)
8# BUT regular for loops still leak!
Trap: If you use
xafter a loop, it holds the last value from the iteration.
1.3 range() Evaluation Timing
1# FOR loop: range() evaluated ONCE at start
2x = [1, 2, 3]
3for i in range(len(x)): # range(3) — locked!
4 x.append(99) # List grows, but range stays 3
5# Runs exactly 3 times (not infinite)
6
7# WHILE loop: condition re-evaluated EVERY iteration
8x = [1, 2, 3]
9i = 0
10while i < len(x): # len(x) re-checked each time!
11 x.append(99) # List grows AND condition updates
12 i += 1
13# Infinite loop!
| Loop Type | When Evaluated | Mutation Effect |
|---|---|---|
for i in range(len(x)) | Once at start | Safe from growth |
while i < len(x) | Every iteration | Dangerous with mutation |
Note:
N = len(x)before the loop is just readability —range(len(x))is already locked.
2: Data Structure Gotchas
2.1 List Multiplication with Mutables
1# SAFE — immutables (each is independent)
2zeros = [0] * 5 # [0, 0, 0, 0, 0]
3zeros[0] = 1 # [1, 0, 0, 0, 0] ✓
4
5# DANGEROUS — mutables (all share SAME reference!)
6matrix = [[]] * 3 # [[], [], []]
7matrix[0].append(1) # [[1], [1], [1]] ← ALL changed!
8# matrix[0] is matrix[1] is matrix[2] → True
9
10# SAFE alternative
11matrix = [[] for _ in range(3)] # Each [] is a NEW object
12matrix[0].append(1) # [[1], [], []] ✓
Rule:
[mutable] * ncreatesnreferences to the same object. Use list comprehension for independent copies.
2.2 Negative Indexing (Silent Bug)
Python doesn’t error on negative indices — it wraps around:
1tree = [10, 20, 30]
2pointer = -1
3tree[pointer] # 30 (last element, NOT an error!)
The trap: When using -1 as a sentinel value (like “no child” in BST):
1# ❌ Wrong — accesses last element instead of signaling "not found"
2(value, left, right) = tree[pointer] # tree[-1] = last element!
3
4# ✅ Correct — check sentinel BEFORE indexing
5if pointer == -1:
6 return -1
7(value, left, right) = tree[pointer] # Now safe
Lesson: Always validate bounds BEFORE array access when -1 is a sentinel.
2.3 Dict Overwrites Duplicate Keys
1rules = [("a", "X"), ("a", "Y")]
2d = {}
3for pattern, replacement in rules:
4 d[pattern] = replacement # Last wins!
5# d = {"a": "Y"} ← Lost "X"!
When you need first match wins (rule precedence), use defaultdict(list):
1from collections import defaultdict
2
3grouped = defaultdict(list)
4for pattern, replacement in rules:
5 grouped[pattern].append((pattern, replacement))
6
7# Now iterate in insertion order:
8for match, res in grouped["a"]:
9 if match == target:
10 return res # First match wins! ✓
When: Rule-based systems, config precedence, any time order matters for duplicate keys.
2.4 map() Returns Iterator (Not List)
1out = map(lambda x: x**2, [1, 2, 3])
2out[0] # ❌ TypeError: 'map' object is not subscriptable
3len(out) # ❌ TypeError: object of type 'map' has no len()
4out.sort() # ❌ AttributeError: 'map' object has no attribute 'sort'
Fix: Convert or use iterator-accepting functions:
1# Convert to list
2out = list(map(lambda x: x**2, data))
3out.sort() # Now works!
4
5# Or use sorted() directly (accepts iterators)
6out = sorted(map(lambda x: x**2, data))
7
8# These accept iterators natively (no conversion needed):
9sum(map(...)) # ✓
10any(map(...)) # ✓
11all(map(...)) # ✓
Why iterators? Memory efficiency —
map()computes on demand instead of loading everything into memory.
2.5 round() vs math.ceil()
1import math
2
3# round() = nearest integer (banker's rounding!)
4round(2.2) # → 2
5round(2.5) # → 2 ← Surprise! Banker's rounds to nearest EVEN
6round(2.6) # → 3
7round(3.5) # → 4 ← Rounds to nearest even (4, not 3)
8
9# math.ceil() = always rounds UP
10math.ceil(2.2) # → 3
11math.ceil(2.5) # → 3
12math.ceil(2.0) # → 2 (already integer)
Lesson: “Round up” in requirements =
math.ceil(), NOTround(). Read carefully!
3: Functional Programming
3.1 Lambda with sorted()
1# ❌ Common mistake — missing key=
2sorted(data, lambda x: -x[1]) # TypeError!
3
4# ✅ Must use key= keyword
5sorted(data, key=lambda x: -x[1]) # Descending by index 1
90% use cases:
1# Sort by specific element
2sorted(data, key=lambda x: x[1])
3
4# Sort descending
5sorted(nums, key=lambda x: -x)
6
7# Sort by multiple keys (first ascending, second descending)
8sorted(data, key=lambda x: (x[0], -x[1]))
9
10# Sort with string comparison
11sorted(words, key=lambda x: x.lower())
Memory trick: “Lambda = Lazy function” (too lazy to write
def)
3.2 map / filter / reduce
1from functools import reduce
2
3# filter: keep items matching condition
4evens = filter(lambda x: x % 2 == 0, [1, 2, 3, 4]) # → 2, 4
5
6# map: transform each item
7squares = map(lambda x: x**2, [1, 2, 3]) # → 1, 4, 9
8
9# reduce: aggregate to single value
10total = reduce(lambda a, b: a + b, [1, 2, 3, 4]) # → 10
Python vs JavaScript:
| Feature | JavaScript | Python |
|---|---|---|
| Syntax | array.map(fn) | map(fn, array) |
| Return | Array (eager) | Iterator (lazy) |
| Chaining | arr.filter().map() | Nested: map(fn, filter(fn, arr)) |
| reduce | arr.reduce(fn, init) | reduce(fn, arr, init) (needs import) |
Key: Python puts function first, data second. Args are flipped compared to JS.
3.3 Functional Composition (filter → map → sum)
1# Pipeline: filter → map → aggregate
2significant = filter(lambda x: abs(x) > threshold, changes)
3squared = map(lambda x: x**2, significant)
4result = sum(squared)
5
6# One-liner (read right-to-left, like math):
7result = sum(map(lambda x: x**2, filter(lambda x: abs(x) > threshold, changes)))
Tip: When readability suffers, use list comprehension instead:
1result = sum(x**2 for x in changes if abs(x) > threshold)
3.4 Python 3: No Tuple Unpack in Lambda (PEP 3113)
1# ❌ Python 2 syntax (doesn't work in Python 3!)
2map(lambda (k, v): (k, v**2), dict.items()) # SyntaxError
3
4# ✅ Python 3: use indexing
5map(lambda kv: (kv[0], kv[1]**2), dict.items())
6
7# ✅ Or use a named function (clearer for complex logic)
8def process(kv):
9 k, v = kv # Unpacking OK in regular functions!
10 return (k, v**2)
11
12# ✅ Or list comprehension (built-in unpacking)
13[(k, v**2) for k, v in dict.items()]
3.5 for…else Pattern
else on a for loop runs only if the loop completes without break.
1# "If not broken" pattern
2for item in collection:
3 if item == target:
4 print("Found!")
5 break
6else:
7 # Only runs if NO break (target not found)
8 print("Not found")
Mental model: Think of else as “if exhausted” or “if not broken”:
1for each item:
2 if found → break (skip else)
3else (if not broken):
4 handle not-found case
When to use:
- Search with fallback (“if not found, do X”)
- Validation loops (“if all items pass, proceed”)
- Replacing
found = Falseflag pattern
4: Algorithm Patterns
4.1 (index, value) Tuple Sorting
Problem: Need indices sorted by some computed value.
1# Build (index, value) tuples
2data = [(3, 1), (4, 1), (5, 1.67), (6, 2), (7, 1.33)]
3
4# Sort by value descending
5sorted_data = sorted(data, key=lambda x: -x[1])
6# → [(6, 2), (5, 1.67), (7, 1.33), (3, 1), (4, 1)]
7
8# Extract just indices
9indices = [i for i, _ in sorted_data]
10# → [6, 5, 7, 3, 4]
Pattern reuse: Used in Q2 (Olympic ranking), Q8 (Moving Average) — same template, different problems.
4.2 Sliding Window
1# Basic template
2L = 0
3R = window_size - 1
4while R < len(data):
5 window = data[L:R+1] # Process current window
6 L += 1
7 R += 1
8
9# O(1) optimization: track running sum instead of re-slicing
10window_sum = sum(data[:window_size])
11for i in range(window_size, len(data)):
12 window_sum = window_sum - data[i - window_size] + data[i]
13 # O(1) per step instead of O(window_size)
4.3 K-Length Optimization (Group by Pattern Length)
Naive: Check all R rules at every position → O(N x R)
Optimized: Group by length, check only relevant lengths → O(N x K) where K = unique lengths
1from collections import defaultdict
2
3# Pre-process: group patterns by length
4by_length = defaultdict(list)
5for pattern, replacement in rules:
6 by_length[len(pattern)].append((pattern, replacement))
7
8# Search: longest first, only check matching lengths
9for i in range(len(text)):
10 for length in sorted(by_length.keys(), reverse=True):
11 for pattern, replacement in by_length[length]:
12 if text[i:i+length] == pattern:
13 # Found match!
14 break
When K « R: Huge speedup. 100 rules but only 5 unique lengths = check 5 groups, not 100 patterns.
4.4 Boyer-Moore String Search (Bad Character Heuristic)
Uses adaptive jumps to skip unnecessary comparisons (faster than brute force):
1def boyer_moore(text: str, pat: str) -> int:
2 # 1. Build 'last' map: char → last index in pattern
3 last = {}
4 for idx, c in enumerate(pat):
5 last[c] = idx
6
7 # 2. Search with adaptive jumps
8 i = 0
9 while i <= len(text) - len(pat):
10 j = 0
11 while j < len(pat) and text[i + j] == pat[j]:
12 j += 1
13
14 if j == len(pat):
15 return i # Full match!
16
17 # Mismatch: compute shift
18 bad_char = text[i + j]
19 idx = last.get(bad_char, -1)
20 if idx == -1:
21 i += j + 1 # Char not in pattern → skip past it
22 else:
23 i += max(j - idx, 1) # Align last occurrence, shift at least 1
24
25 return -1
Why it’s faster:
- Bad char not in pattern → jump entire pattern length
- Bad char in pattern → align its last occurrence
Common mistakes:
- Variable name collision:
for i, c in enumerate(pat)overwrites outeri - Wrong parameter to shift: pass
j(pattern index), noti(text position) - Wrong slice:
text[i:i+K]for K-length slice, NOTtext[i:K+1]
Not a sliding window! Boyer-Moore jumps vary in size — it’s a two-pointer pattern.
5: Meta-Learning
5.1 “Tests Pass ≠ Logic Correct”
The Q6 Newton’s sqrt bug:
1# ❌ Wrong convergence check (line 24)
2if round(n, 5) == round(x_next, 5):
3 return x_next
4# Checks: "Is my guess equal to the TARGET number?"
5# For sqrt(2.0): x_next == 2.0? (never true!)
6
7# ✅ Correct convergence check
8if abs(x_next - x_prev) < tol:
9 return x_next
10# Checks: "Has my guess STOPPED CHANGING?"
Why it “passed” tests: The wrong check NEVER triggered → ran all 1000 iterations → Newton’s formula converged anyway after ~10 iterations. Tests passed because the final answer was correct, but it wasted 990 iterations.
The two checks are asking completely different questions:
(a) Bug: round(n, 5) == round(x_next, 5) asks “is my guess equal to the input number?” For sqrt(2.0), this checks x_next == 2.0 — but the answer is 1.41421..., so the check is never true. The condition never fires, the loop runs all 1000 iterations, and Newton’s formula converges to the correct answer anyway by iteration ~10.
(b) Correct: abs(x_next - x_prev) < tol asks “has my guess stopped improving?” When consecutive guesses differ by less than the tolerance, the algorithm has converged — that’s the actual definition of convergence.
The accident: It only “worked” because max_iter=1000 gave Newton’s method enough runway. If max_iter=5, the wrong check still never triggers, but 5 iterations might not be enough to converge — you’d get an inaccurate answer. The correct check would return early at iteration ~10 with a precise result regardless of max_iter.
5.2 “Tweak Until Green” Anti-Pattern
The Q3 trailing whitespace mystery:
- Output had 1 extra trailing space per row
- Fixed by subtracting 1 in the last column:
remaining -= 1 - But couldn’t explain WHY — arrived at fix by trial and error
The danger:
1# What you THINK you learned:
2"Last column needs COLUMN_SIZE - 2"
3
4# What you ACTUALLY learned:
5"Subtracting 2 makes tests pass" # ← Magic numbers!
Better approach:
- Stop. Write down your hypothesis
- Test hypothesis on paper (not in code)
- If hypothesis holds → implement
- If tests fail → hypothesis wrong, go to step 1
Senior SWE discipline: Derive the logic, don’t guess-and-check. Even when frustrated with “tedious” problems.
6: Memory Tricks (Speed Reference)
| Concept | Trick |
|---|---|
sorted() vs .sort() | “sortED = nEw Data” (ED suffix = returns new list) |
| Lambda | “Lambda = Lazy function” (too lazy to write def) |
* operator | “Explode operator” (flattens/unpacks) |
map() return | “Map = Maybe convert” (returns iterator, not list) |
| Merge complexity | Must output M+N elements → can’t beat O(M+N) |
| Dense ranking | 1-2-2-3 (NOT 1-2-2-4) — rank increments only when values change |
| Backwards iteration | “Reverse to Remove” (safe mutation pattern) |
| for…else | “else = if not broken” |
Quick Reference: Python vs JavaScript
| Feature | JavaScript | Python |
|---|---|---|
| Block scope | let/const block-scoped | Loop vars leak after loop |
| Array methods | arr.map(fn) | map(fn, arr) — args flipped |
| map() return | Array (eager) | Iterator (lazy) |
| Negative index | arr.at(-1) (ES2022) | arr[-1] (always works, silent) |
| Round up | Math.ceil() | math.ceil() |
| Tuple unpack | Destructuring everywhere | Not in lambda (PEP 3113) |
| for…else | N/A | else runs if no break |
| List copy | [...arr] | arr[:] or arr.copy() |
Last updated: 2026-02-10 Sources: T2 REVIEW, T3 notes (8 files), Jan 29 session notes Midterm: Feb 14, 10:30am