programming-principles-lab execution · functions · scope · data · paradigms

1. How code runs — compiled vs interpreted

Session 1's hardware/software interface. A compiler translates the whole source to machine code once, then the CPU runs it; an interpreter walks the source statement by statement every run. Step the pipeline and watch where time is spent.

phase
CPU instructions run
portable binary?

2. Flow control & loops

A for loop over range(n) with an if guard. Choose what happens when the guard fires — continue skips the rest of the body, break exits the loop. Step to trace the control flow and the running total.

i
total0

3. Recursion & the call stack

Session 6 — a function calling itself. Each call pushes a frame holding its own arguments; frames pop as they return, multiplying results back up. Step through $\texttt{fact}(n)$ or $\texttt{fib}(n)$ and watch the stack grow and unwind.

stack depth0
max depth0
result

4. Recursion tree & memoization

Naive $\texttt{fib}(n)$ recomputes the same subproblems exponentially often. Memoizing — caching each result the first time — collapses the tree to $O(n)$ calls. Toggle the cache and compare the call counts.

fib(n)
function calls
naive calls

Green nodes were computed; faint nodes were served from the cache.

5. Scope, lifetime & closures

Session 7 — local vs global names and side effects. Python resolves a name by the LEGB rule: Local, Enclosing, Global, Built-in. Pick a scenario and trace which binding each reference resolves to, including a closure that captures an enclosing variable.

x resolves via
value used inside
global x after call

6. Higher-order functions — map · filter · reduce

Session 7's functional tools. A lambda is applied across a list: map transforms every element, filter keeps those where the predicate is true, reduce folds them to one value. Step to see each element flow through the pipeline.

processed
output

7. Eager vs lazy evaluation (generators)

A list builds all its values up front; a generator with yield produces them one at a time, only when pulled. With a take(k) downstream, the lazy pipeline computes just what it needs. Step the consumer and compare work done.

elements computed
elements wasted

8. Mutability & function side effects

Session 12 — "modify in-place" vs "return a new object". Python passes object references: mutating a list inside a function is visible to the caller, but rebinding the name, or working on a copy, is not. Run each variant and watch the caller's list.

same object?
caller changed?

9. Dictionaries as hash maps

Session 10 — key/value pairs. A dict hashes each key, then hash % buckets chooses a slot. Two keys landing in the same slot collide and chain together — which is why average lookup is $O(1)$ but degrades when buckets fill. Add keys and watch them distribute.

hash(key)
slot = hash % buckets
load factor
collisions

10. NumPy — vectorization & broadcasting

Module 4 — array programming. A vectorized op applies element-wise across a whole array at once; broadcasting stretches a smaller shape to match without copying. Pick operands and see which cells combine and whether the shapes are broadcast-compatible.

result shape
broadcastable?