Unit 2 · Software engineering · Lesson 8 of 15
Python fundamentals for interviews and research code
Step 1: Learn the ideas
- Names, objects, identity and mutability
- Mutable default arguments and late-binding closures
- Shallow versus deep copies
- Iterators, generators and generator pipelines
- Context managers and contextlib
- Exceptions: hierarchy, chaining and cleanup
- collections (deque, Counter, defaultdict) and heapq
- The GIL: threads versus processes versus asyncio
- asyncio tasks, timeouts and blocking calls
- Optional free-threaded CPython builds
- NumPy broadcasting and vectorization
- NumPy views versus copies and dtype pitfalls
Why it matters
Python is the default language for training code, data pipelines, evaluation harnesses and agent tooling, and a common choice for coding rounds. Interviewers rarely ask trivia; they notice when a candidate mutates a shared list by accident, calls list.pop(0) inside a BFS, or claims threads will speed up CPU-bound Python. In ML and research code the expensive bugs are silent: an in-place edit through a NumPy view, an integer array that overflows, an evaluation loop that blocks the event loop.
Core ideas
Names bind to objects. Assignment never copies; it makes another name refer to the same object. Mutability belongs to the object (lists, dicts, sets, most class instances), not to the variable. Three consequences come up repeatedly:
- A default argument is evaluated once, when
defruns, sodef add(x, acc=[])shares one list across calls. UseNoneas the default and create the list inside. [[0] * n] * mholds m references to one inner list; build rows with a comprehension.- Closures capture variables, not values: lambdas created in a loop all see the loop variable's final value unless you bind it (
lambda i=i: i).
copy.copy duplicates only the outer container, so nested objects stay shared; copy.deepcopy copies recursively, handles cycles, and can be expensive.
Iteration. An iterable returns an iterator from __iter__; the iterator produces items from __next__ until it raises StopIteration. A generator function returns a lazy iterator whose state lives in a suspended frame, so a pipeline such as sum(parse(l) for l in f if l.strip()) runs in constant memory. Generators are single-pass: iterating one a second time yields nothing.
Context managers pair setup with guaranteed cleanup. with calls __enter__, runs the body, then always calls __exit__, which receives any exception and suppresses it only by returning a true value. contextlib.contextmanager turns a generator with one yield inside try/finally into a context manager; ExitStack handles a variable number of resources.
Exceptions. Catch the narrowest type you can handle. except Exception does not catch KeyboardInterrupt or SystemExit; a bare except: does. When translating errors at a module boundary, raise ConfigError(...) from err keeps the original cause. finally runs on every exit path, including return.
Containers and costs. Appending to and popping from the end of a list are amortized O(1), but pop(0) and insert(0, x) shift every element; use collections.deque for queues. Counter counts hashable items and offers most_common; defaultdict(list) groups without key checks but inserts a key whenever you index a missing one, even just to read it. heapq keeps a min-heap inside a plain list; push (priority, count, item) tuples so ties never compare the items themselves.
Concurrency. In the default CPython build the global interpreter lock (GIL) lets one thread execute Python bytecode at a time. Threads still help with I/O-bound work, because blocking I/O releases the GIL, and many NumPy operations release it inside compiled loops. CPU-bound pure-Python work needs processes (ProcessPoolExecutor), paying for pickling and duplicated memory. asyncio runs many coroutines on one thread: it scales concurrent waiting, but one blocking call (time.sleep, a synchronous HTTP client, heavy computation) stalls every task, so offload it with asyncio.to_thread or an executor. Keep references to tasks you create, or use asyncio.TaskGroup, because the loop holds only weak references to tasks. CPython 3.13 added an experimental free-threaded build with the GIL disabled; 3.14 made it officially supported but still optional, and the standard build keeps the GIL. Extensions not marked as compatible can re-enable the GIL at import, and single-threaded code runs somewhat slower, so never assume a deployment uses it.
NumPy. Vectorized operations move loops into compiled code. Broadcasting aligns shapes from the trailing dimension; two dimensions are compatible when they are equal or one of them is 1. Basic slicing returns a view that shares memory; advanced (integer or boolean) indexing returns a copy. Fixed-width integer arrays wrap on overflow without raising, and an in-place operation cannot change an array's dtype.
import numpy as np
x = np.arange(6).reshape(2, 3) # reshape returns a view when it can
row = x[0] # view: writing row[0] also changes x
picked = x[[0, 1], [0, 2]] # advanced indexing: a copy
centered = x - x.mean(axis=0) # (2, 3) minus (3,) broadcasts per row
np.array([250], dtype=np.uint8) + 10 # array([4], dtype=uint8): wrapped
# x += 0.5 raises: a float64 result cannot be cast into the int64 array
Common misconceptions
- "Assignment copies." It binds another name to the same object; only explicit copies create new objects, and shallow copies still share nested ones.
- "Threads never help in Python." They help when threads mostly wait on I/O or run C code that releases the GIL; they do not speed up pure-Python CPU work in the default build.
- "async makes code faster." asyncio improves concurrency for I/O waits on a single thread. It adds overhead to CPU work, and one blocking call freezes the whole loop.
- "Slicing always copies." True for lists, false for NumPy arrays: basic slices are views, so in-place edits propagate. Call
.copy()when you need independence. - "
isis a faster==."istests identity. Use it forNoneand sentinels; caching of small integers and some strings makesison values appear to work by accident.
How to approach it
Rebuild the model with predict-then-run experiments in a REPL rather than rereading. At graduate level, interviewers mostly probe aliasing, the cost of built-in operations and clean generator or context-manager code. Mid-level and senior conversations add choosing a concurrency model for a workload, explaining why an asyncio service stalled, or finding a hidden copy or dtype problem in array code. While coding, state the cost of each container operation as you use it and name the edge cases: empty input, an exhausted iterator, ties in a heap.
Suggested order
- 1Read "3.1. Objects, values and types" in the Data model reference, then predict and run five aliasing puzzles: a mutable default, [[0] * 3] * 3, lambdas created in a loop, copy.copy versus copy.deepcopy on nested lists, and is versus ==.
- 2Read the Iterators and Generators sections of the Functional Programming HOWTO; write a generator pipeline that streams a large log file and counts events per key in constant memory.
- 3Read "3.3.9. With Statement Context Managers" and the contextlib page; write a timing context manager twice (as a class and with @contextmanager) and test that cleanup runs when the body raises.
- 4Skim the collections and heapq pages, including the priority-queue notes; solve Group Anagrams with defaultdict, Top K Frequent Elements with Counter and heapq, and Sliding Window Maximum with deque.
- 5Read "Coroutines and tasks" and "Developing with asyncio"; simulate twenty I/O calls with asyncio.sleep under a TaskGroup with a timeout, then insert a blocking time.sleep and watch debug mode report the slow callback.
- 6Time a CPU-bound function under ThreadPoolExecutor and ProcessPoolExecutor and explain the result; then read the free-threading HOWTO to see what the optional build changes and what it costs.
- 7Read the NumPy Broadcasting, Copies and views and Data types chapters; rewrite a pairwise-distance double loop as one broadcasted expression, check np.shares_memory for slices versus fancy indexing, and reproduce a silent uint8 overflow.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
Python Language Reference: Data modelStart here
Python Software Foundation (docs.python.org)Section: 3.1. Objects, values and types
Identity, type, value and mutability as the language defines them; continue with 3.3.9 for the protocol behind the with statement.
Open(opens in a new tab)Link responded, 5 Oct 2026Python HOWTOs (official documentation)
Python Software Foundation (docs.python.org)Section: Functional Programming HOWTO: Iterators
The iterator protocol, generators and itertools; the free-threading HOWTO in the same collection covers the optional build without the GIL.
Open(opens in a new tab)Link responded, 5 Oct 2026The Python Standard Library (module reference)
Python Software Foundation (docs.python.org)Section: collections — Container datatypes
deque, Counter and defaultdict semantics and costs; open the heapq, copy and contextlib pages listed under this resource for heaps, copying and context-manager helpers.
Open(opens in a new tab)Link responded, 5 Oct 2026asyncio — Asynchronous I/O (Python documentation)
Python Software Foundation (docs.python.org)Section: Coroutines and tasks
Tasks, TaskGroup, timeouts, cancellation and the weak-reference pitfall; then Developing with asyncio for debug mode and blocking calls.
Open(opens in a new tab)Link responded, 5 Oct 2026NumPy user guide: NumPy fundamentalsStart here
NumPy project (numpy.org)Section: Broadcasting
Broadcasting rules, then the Copies and views and Data types chapters for view semantics and dtype pitfalls.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Solve these in deliberately idiomatic Python: defaultdict for Group Anagrams, Counter plus heapq.nlargest for Top K, deque for Sliding Window Maximum, a size-k heap for Kth Largest, two heaps with negated values for the running median, OrderedDict (then a hand-built doubly linked list) for LRU Cache, bisect for the time-based store, and threading.Event, Lock or Semaphore for the two concurrency problems. Then do three exercises outside LeetCode: write a context manager that times a block and always logs, even when the block raises; write a generator that yields fixed-size batches from any iterable without materializing it; and vectorize a pairwise-distance loop in NumPy, confirming with np.shares_memory which intermediates are views.
- Group AnagramsLeetCode(opens in a new tab)Not attemptedLeetCode
- Top K Frequent ElementsLeetCode(opens in a new tab)Not attemptedLeetCode
- Sliding Window MaximumLeetCode(opens in a new tab)Not attemptedLeetCode
- Kth Largest Element in an ArrayLeetCode(opens in a new tab)Not attemptedLeetCode
- Find Median from Data StreamLeetCode(opens in a new tab)Not attemptedLeetCode
- LRU CacheLeetCode(opens in a new tab)Not attemptedLeetCode
- Time Based Key-Value StoreLeetCode(opens in a new tab)Not attemptedLeetCode
- Print in OrderLeetCode(opens in a new tab)Not attemptedLeetCode
- Print FooBar AlternatelyLeetCode(opens in a new tab)Not attemptedLeetCode
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1Why does a mutable default argument accumulate state across calls, and what is the idiomatic fix?
- Question 2What does copy.copy share with the original that copy.deepcopy does not?
- Question 3What happens when you iterate the same generator twice?
- Question 4When does __exit__ suppress an exception?
- Question 5Why is collections.deque preferred over a list for a BFS queue?
- Question 6Why can threads speed up a downloader but not a pure-Python prime sieve in the default CPython build?
- Question 7Is x[::2] a view or a copy for a NumPy array? What about x[[0, 2]]?
- Question 8What value does a uint8 element holding 250 have after adding 10?
You’re done when you can
- Predict the output of five unseen aliasing or closure puzzles correctly and explain each with the name-binding model.
- Write a correct context manager with contextlib.contextmanager from memory, including cleanup when the body raises.
- Solve two collection-heavy problems in idiomatic Python in under 25 minutes each, stating the cost of every container operation used.
- Explain in under two minutes when to use threads, processes or asyncio, including what the GIL does and does not prevent.
- Rewrite a nested Python loop as broadcasted NumPy and say which intermediate arrays are views and which are copies.
Finish the lesson
Unlocks next: Training loops, optimizers and model debugging, PyTorch and autograd fluency


