In this module — 10 sections
  1. The map
  2. What decides your score here
  3. The four costs that are actually bugs
  4. The semantics that bite
  5. Generators, which is where they go deep
  6. Objects, memory and the hash contract
  7. The GIL, briefly
  8. The stdlib worth having reflexive
  9. Say it
  10. Now do this

08 — Python under interview conditions

The email is explicit: "you will be expected to know a fair amount of detail about your favorite programming language." Choosing Python buys you speed and costs you scrutiny — they will push on the implementation, and this module is what they push on.

Prereqs: 01 · Reading: 7 min · Cards: 12 · Reference: operation costs

The map

Three kinds of Python knowledge get tested, and they are tested differently. The first is operation costs, which show up as bugs in code you write — an accidental quadratic that the interviewer spots before you do. The second is semantics: mutable defaults, shallow copies, identity versus equality, floor division on negatives. These arrive as "what does this print?" or as a silent wrong answer in your own solution. The third is the language's own machinery — generators, __slots__, the equality-and-hash contract — which is what gets asked when the interviewer wants to see how deep your knowledge goes.

You do not need all of it. You need the four costs that create real bugs, the handful of semantics that bite, and enough machinery to survive one follow-up.

What decides your score here

Not writing an accidental quadratic. Four operations do this, and an interviewer reading Python is actively watching for them.

Justifying the stdlib you reach for. Using Counter is fine and fast. Being unable to say what it is underneath when asked is not. Reach for the shortcut, and have the implementation ready.

Answering the machinery question without bluffing. If generators or __slots__ come up and you half-know them, say what you do know from first principles. Bluffing is the worst outcome here, because the follow-up will find it.

The four costs that are actually bugs

Membership on a list is linear. So x in some_list inside a loop is quadratic, silently. Use a set.

Popping or inserting at index zero is linear, because everything shifts. That single choice turns a BFS from O(V+E) into O(V²). Use collections.deque, which is O(1) at both ends.

String concatenation in a loop is quadratic, because strings are immutable and each += allocates and copies everything so far. Collect the parts in a list and "".join() once.

And a slice copies, so lst[a:b] costs the length of the slice — which matters when you slice inside a recursion and turn a linear algorithm into a quadratic one without noticing.

Everything else is in the reference card. Memorise these four, because these are the ones that appear in code written under pressure.

The semantics that bite

Mutable default arguments. def f(x, acc=[]) creates the list once, at definition time, and shares it across every call. Use None and create it inside. It is the language's most famous bug and it still catches people.

[[0]*3]*3 creates three references to the same row. Write one cell and three change. Use a comprehension. This is a real bug in grid problems, not a curiosity.

is versus ==. is compares identity. Small integers from −5 to 256 are cached, so is works on them by accident and fails on larger ones. Use == for values, is only for None.

Floor division and modulo on negatives. -7 // 2 is -4, not -3, because // floors rather than truncates. And -7 % 2 is 1, because the sign follows the divisor — unlike C. Both matter the moment you touch circular indices.

Integers are arbitrary precision. There is no overflow, which is why (lo + hi) // 2 is safe in Python and not in C or Java — mentioning that contrast shows you know both. The flip side: bit manipulation on negative numbers needs an explicit 32-bit mask, or a carry loop never terminates.

Truthiness. if not lst covers empty, but if not x is also true when x is 0. In a problem where zero is a valid value, test if x is None.

Generators, which is where they go deep

An iterable returns a fresh iterator each time, which is why you can walk a list twice. A generator is both iterable and its own iterator, so it exhausts — consume it once and the second pass yields nothing. That is the classic bug.

A function containing yield returns a generator when called, and the body does not run until the first next(). It suspends at the yield, keeping the whole frame alive — locals and execution point — and resumes from exactly there.

The one-sentence answer: "A generator is a function that suspends and resumes, keeping its local frame alive between calls. The practical win is memory — it produces items lazily, so you can iterate over something larger than RAM. The cost is that it's single-pass and you can't index into it."

yield from delegates to another iterable, which is how you write a lazy recursive tree walk. There is a complexity subtlety worth having ready: nested yield from at depth d makes every value bubble up d levels, turning the traversal into O(n·d). On a balanced tree that is O(n log n) and fine; on a degenerate one it is quadratic. Being able to point that out unprompted is exactly the kind of detail worth a 4.

And the practical distinction: sum(x*x for x in data) never materialises a list — O(1) memory instead of O(n). Use a generator when you consume once, a list when you need to index or iterate again.

Objects, memory and the hash contract

__slots__ removes the per-instance dictionary. That cuts memory by roughly forty to fifty percent on a small class and speeds up attribute access, at the cost of not being able to add attributes dynamically. It is the answer to "how would you hold ten million tree nodes in Python?"

__eq__ and __hash__ travel together. Define __eq__ without __hash__ and Python clears the inherited hash, making the class unhashable — it silently stops working in a set or as a dict key. Equal objects must hash equal, which is the contract module 03 relies on. dataclass(frozen=True) generates both correctly for free.

The GIL, briefly

It belongs to concurrency, but it gets asked as a language question, so have the short version ready here.

The GIL means only one thread executes Python bytecode at a time. So threading gives no CPU parallelism in CPython — though it does give I/O concurrency, because the GIL is released during blocking I/O and inside C extensions.

And the trap: the GIL does not make your code thread-safe. It guarantees one thread runs bytecode at a time; it does not make compound operations atomic. An increment compiles to load, add, store, and the interpreter can switch between them. You still need a lock.

Which model to choose follows from that. Multiprocessing for CPU-bound work, because the GIL blocks real parallelism and you pay in serialisation. Threading for a few dozen blocking I/O calls, because it is simplest. Asyncio for thousands of connections, because a coroutine costs far less than a thread — with the trap that one blocking call inside a coroutine stalls the whole event loop.

The stdlib worth having reflexive

defaultdict and Counter for grouping and counting. deque for every BFS. heapq for top-k, negating for a max-heap. bisect_left and bisect_right for binary search. functools.cache for memoisation in one line, remembering it requires hashable arguments. math.comb for n-choose-k. And itertools.accumulate for prefix sums, product and combinations for backtracking scaffolding.

One idiom that comes up constantly and nobody remembers under pressure: sorting by one field descending and another ascending is key=lambda x: (-x[1], x[0]).

Say it

Cover the answers. Out loud, in English.

?Name the Python operations that silently cost you a factor of n.

Membership on a list, popping or inserting at index zero, and string concatenation in a loop.

?What is wrong with def f(x, acc=[])?

The list is created once at definition time and shared across calls. Use None and create it inside.

?What is the bug in [[0]*3]*3?

Three references to the same row. Use a comprehension.

?What is -7 // 2 in Python, and -7 % 2?

Minus four, because it floors rather than truncates. And one, because the sign follows the divisor.

?What is a generator, in one answer?

A function that suspends and resumes, keeping its frame alive. The win is lazy memory; the cost is single-pass with no indexing.

?What does __slots__ buy, and cost?

It removes the per-instance dict — forty to fifty percent less memory and faster attribute access — at the cost of no dynamic attributes.

?Does the GIL make your Python code thread-safe?

No. It guarantees one thread runs bytecode at a time; it does not make compound operations atomic. An increment is load, add, store, and the switch can land in between.

?What happens if you define __eq__ without __hash__?

Python clears the inherited hash and the class becomes unhashable, so it stops working in sets and as a dict key.

Now do this

There is no separate problem list here: the whole bank is your Python practice. What this module adds is a discipline to apply while solving them.

Every time you reach for a stdlib shortcut, say why out loud, and be ready to implement it. If you use Counter, say "that's a dict from value to count, and I'd write it by hand if you prefer". If you use deque, say "popping from the front of a list is linear, so this would be quadratic".

Do that on the next five drill problems, whatever they are. It costs no extra time and it converts ordinary practice into practice for the language questions.

Stop when narrating the language choice happens without you deciding to do it.