In this module — 10 sections
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.
Membership on a list, popping or inserting at index zero, and string concatenation in a loop.
def f(x, acc=[])?The list is created once at definition time and shared across calls. Use
None and create it inside.
[[0]*3]*3?Three references to the same row. Use a comprehension.
-7 // 2 in Python, and
-7 % 2?Minus four, because it floors rather than truncates. And one, because the sign follows the divisor.
A function that suspends and resumes, keeping its frame alive. The win is lazy memory; the cost is single-pass with no indexing.
__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.
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.
__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.