interviewpath
  • 0day streak
  • 10total XP
  • Level 1

Focus mode

Python fundamentals for interviews and research code

Read alongside

Python Language Reference: Data model: 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.

Objectives

  • Explain Python name binding and predict the result of aliasing, mutable-default-argument and late-binding-closure code; choose between copy.copy and copy.deepcopy.
  • Implement a generator pipeline and a context manager (class-based and with contextlib.contextmanager), and state exactly when cleanup runs if the body raises.
  • Choose among list, deque, dict, Counter, defaultdict and heapq for a task and state the cost of each operation used.
  • Compare threads, processes and asyncio for I/O-bound and CPU-bound work, explaining the effect of the GIL in the default build and what the optional free-threaded build changes.
  • Write vectorized NumPy code with broadcasting and diagnose whether an expression returns a view or a copy and whether its dtype can overflow or lose precision.
  • Use exceptions idiomatically: narrow except clauses, exception chaining with raise ... from, and cleanup through finally or with.

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 def runs, so def add(x, acc=[]) shares one list across calls. Use None as the default and create the list inside.
  • [[0] * n] * m holds 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.
  • "is is a faster ==." is tests identity. Use it for None and sentinels; caching of small integers and some strings makes is on 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.

Self-check before you finish

  1. Why does a mutable default argument accumulate state across calls, and what is the idiomatic fix?
  2. What does copy.copy share with the original that copy.deepcopy does not?
  3. What happens when you iterate the same generator twice?
  4. When does __exit__ suppress an exception?
  5. Why is collections.deque preferred over a list for a BFS queue?
  6. Why can threads speed up a downloader but not a pure-Python prime sieve in the default CPython build?
  7. Is x[::2] a view or a copy for a NumPy array? What about x[[0, 2]]?
  8. What value does a uint8 element holding 250 have after adding 10?