Python Interview Preparation: Complete Guide for 2026
From the GIL and Memory Internals to Decorators, Async Concurrency & Modern Typing

What You Must Master to Clear This Track
- Explain reference counting and the generational garbage collector together, and know exactly when the GIL is released.
- Understand why threading suits I/O-bound work, multiprocessing suits CPU-bound work, and asyncio suits massively concurrent I/O.
- Master list/dict/set internals: dynamic array over-allocation, open-addressing hash tables, and average O(1) lookups.
- Be fluent in decorators, generators, and context managers as first-class tools, not just syntax sugar.
- Know Python 3.12+ changes: PEP 695 generic syntax, per-interpreter GIL, and the free-threaded (no-GIL) build effort.
Step-by-Step Study Plan
Follow this sequential roadmap designed to take you from core foundations to advanced architecture and mock interviews.
Language Internals & Built-in Data Structure Mastery
GIL mechanics, reference counting plus the generational GC, and the internal implementation of list, dict, set, and tuple.
- •Explain reference counting and the 3-generation cyclic GC well enough to debug a reference-cycle memory leak.
- •Describe dict's open-addressing hash table and why average-case lookups are O(1).
- •Identify when to use list vs deque vs tuple vs set based on required operations and complexity.
- •Rewrite 5 common coding problems focusing on choosing the right built-in structure, not just correctness.
- •Use the tracemalloc module to inspect a script's memory allocations and practice explaining the output.
Pythonic Idioms & Object Model Depth
Custom decorators, generators and context managers, dunder methods, metaclasses, and MRO / C3 linearization.
- •Write a parameterized decorator and a contextlib.contextmanager-based resource manager from scratch.
- •Explain the descriptor protocol behind @property and bound methods.
- •Trace Method Resolution Order for a diamond multiple-inheritance hierarchy.
- •Implement a small class hierarchy using __init_subclass__ before reaching for a custom metaclass.
- •Practice explaining WHY functools.wraps matters, not just that it exists.
Concurrency Models, Modern Typing & Production Readiness
threading vs multiprocessing vs asyncio trade-offs, asyncio event loop internals, typing/Protocol, and Python 3.12+ features.
- •Build a small asyncio program using gather, wait_for, and an executor offload for a blocking call.
- •Convert a TypeVar/Generic class to PEP 695 syntax and explain Protocol-based structural typing.
- •Complete a mock interview covering GIL trade-offs, memory leak diagnosis, and one live coding round in Python.
- •Benchmark the same CPU-bound task under threading, multiprocessing, and single-threaded code to internalize the GIL's real impact.
- •Review Python 3.12/3.13 changelog highlights (PEP 695, PEP 701, the free-threaded build) and be ready to discuss trade-offs.
1. The GIL, Reference Counting & Generational Garbage Collection
Nearly every Python interview probes whether you understand CPython's memory model beyond 'it just works' -- the GIL, reference counting, and the cyclic garbage collector working together.
A mutex ensuring only one thread executes Python bytecode at a time in CPython, simplifying refcount safety at the cost of true multi-core parallelism for pure Python code.
Every PyObject carries an ob_refcnt; it is incremented on each new reference and decremented when one goes out of scope, freeing the object the instant the count hits zero.
A supplementary collector groups objects into 3 generations (0, 1, 2) and periodically scans for unreachable reference cycles that refcounting alone can never free.
The GIL is released around blocking C-level I/O calls and at a configurable switch interval (sys.getswitchinterval, default 5ms), which is why threading still helps I/O-bound workloads.
How two Python threads interleave execution around a single blocking I/O call.
import gc
import sys
class Node:
def __init__(self, name):
self.name = name
self.parent = None
def __del__(self):
print(f"Node {self.name} destructor called")
def make_cycle():
a = Node("A")
b = Node("B")
a.parent = b
b.parent = a # reference cycle: each node's refcount never drops to 0
print("refcount(a):", sys.getrefcount(a))
make_cycle()
print("Local names 'a' and 'b' are gone, but the cycle keeps both alive")
gc.collect() # the generational cyclic collector finds and frees the cycle
print("Cycle collected")- When asked 'does Python have garbage collection?', answer with both mechanisms: immediate refcounting plus a periodic generational cycle collector -- most candidates only mention one.
- sys.getrefcount(obj) always reports one more than you expect, because passing obj as an argument creates a temporary extra reference.
- Assuming the GIL means Python programs can never use multiple cores -- it only serializes pure Python bytecode; C extensions and separate processes are unaffected.
- Defining __del__ on objects that participate in reference cycles and assuming they'll never be collected -- since PEP 442 (Python 3.4+) the cyclic GC safely finalizes these too, just with undefined ordering.
2. Mutable vs Immutable Types & Data Structure Internals
Beyond syntax, interviewers want to see that you understand what list, dict, and set actually are under the hood, and why that dictates their performance characteristics.
list, dict, set, and bytearray can change in place; tuple, str, frozenset, and numeric types cannot. Only immutable, hashable objects are safe as dict keys or set members.
A list is a contiguous array that over-allocates extra capacity on growth, giving amortized O(1) append but O(n) insert/delete anywhere except the end.
Since Python 3.7, dict combines a sparse index array with a dense, insertion-ordered entry array; it resizes once roughly two-thirds full to keep probe sequences short.
set reuses dict's open-addressing machinery but stores only keys, giving average O(1) add/contains -- far faster than scanning a list for membership.
# BUG: the default list is created ONCE, when the function is defined
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] <- unexpected! same list reused every call
# FIX: use a None sentinel and create a fresh object per call
def add_item_fixed(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add_item_fixed("a")) # ['a']
print(add_item_fixed("b")) # ['b']- Use collections.deque for O(1) appends/pops from both ends instead of list.insert(0, x) or list.pop(0), which are O(n).
- Prefer a tuple or frozenset as a dict key when you need a hashable, immutable grouping of values.
- Assuming `a is b` is a safe replacement for `a == b` on anything beyond None and small cached integers.
- Forgetting that slicing a list (lst[:]) or calling dict(d) makes a shallow copy -- nested mutable objects are still shared with the original.
3. Decorators, Generators & Context Managers
These three constructs are what separate someone who writes working Python from someone who writes idiomatic, production-grade Python -- and they show up constantly in live coding rounds.
`@decorator` above a def is pure syntax sugar for `func = decorator(func)`; a parameterized decorator is a factory function that returns the real decorator.
A function using yield returns a generator object immediately; its frame (locals + instruction pointer) is suspended between yields, giving O(1) memory regardless of sequence length.
yield from forwards values, exceptions, and the return value transparently to/from a sub-generator, simplifying generator composition.
`with obj:` calls __enter__(), runs the block, and unconditionally calls __exit__() even on an exception -- equivalent to a try/finally, but reusable and declarative.
import time
import functools
from contextlib import contextmanager
def retry(times=3, exceptions=(Exception,)):
"""Decorator factory: retries the wrapped function on failure."""
def decorator(func):
@functools.wraps(func) # preserves __name__, __doc__, signature
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as exc:
last_exc = exc
print(f"Attempt {attempt} failed: {exc}")
raise last_exc
return wrapper
return decorator
@retry(times=3, exceptions=(ConnectionError,))
def fetch(url):
...
@contextmanager
def timed_block(label):
start = time.perf_counter()
try:
yield # control returns to the 'with' block body here
finally:
elapsed = time.perf_counter() - start
print(f"{label} took {elapsed:.4f}s")
with timed_block("fetch"):
fetch("https://api.example.com")- Always apply functools.wraps in a decorator you write, or introspection tools, help(), and pickling break because __name__/__doc__ point to the wrapper instead of the original function.
- Generator expressions `(x for x in items)` are lazy and O(1) memory versus list comprehensions, which materialize the entire list up front.
- Reusing an already-exhausted generator -- iterating over it a second time silently yields nothing instead of raising an error.
- Forgetting that a `with` block's exception is passed to __exit__(exc_type, exc_val, exc_tb), and that returning a truthy value there silently swallows it.
4. OOP Internals, Concurrency Models & Modern Python (3.12+)
Senior Python interviews go beyond syntax into how the object model actually resolves attributes, and how to pick the right concurrency primitive for a given workload.
A metaclass (subclassing type) intercepts class creation itself via __new__/__init__ -- used by ORMs like Django to auto-register model fields; __init_subclass__ or a class decorator is usually the simpler modern alternative.
CPython resolves multiple inheritance via C3 linearization (inspectable as Cls.__mro__), guaranteeing a consistent, monotonic order even in diamond hierarchies; super() follows the MRO, not just the immediate parent.
threading suits I/O-bound work sharing memory (GIL releases during I/O); multiprocessing bypasses the GIL entirely for CPU-bound parallelism at the cost of IPC/pickling; asyncio gives single-thread cooperative concurrency for huge I/O fan-out.
PEP 695 introduces native generic syntax (`class Stack[T]:`), Protocol enables structural typing without inheritance, and type hints remain unenforced at runtime unless validated by mypy/pyright or pydantic.
How cooperative coroutines interleave on one OS thread without ever needing multiple cores.
import asyncio
import time
async def fetch_page(session_id: int, delay: float) -> str:
print(f"[{session_id}] request sent")
await asyncio.sleep(delay) # simulates non-blocking network I/O
print(f"[{session_id}] response received")
return f"page-{session_id}"
async def main():
start = time.perf_counter()
results = await asyncio.gather(
fetch_page(1, 1.0),
fetch_page(2, 1.0),
fetch_page(3, 1.0),
)
print(results, f"elapsed={time.perf_counter() - start:.2f}s") # ~1.0s, not 3.0s
asyncio.run(main())- Use multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor for CPU-bound number crunching; threads won't help because of the GIL.
- Reach for asyncio when you need thousands of concurrent connections with minimal per-task overhead; reach for threads when integrating with blocking third-party libraries you can't rewrite.
- Calling a blocking function (time.sleep, a synchronous requests.get) inside an async coroutine freezes the entire event loop for every other task.
- Assuming overriding __eq__ alone is enough -- Python sets __hash__ to None automatically unless you also define it, making instances unhashable.
Cutting API Latency by Replacing Threaded Polling with Asyncio at a Fintech Startup
A payments startup's Python backend polled six third-party bank APIs sequentially inside a Flask endpoint using a thread pool, and p99 latency on the /reconcile endpoint had crept past 4 seconds as the number of integrated banks grew. The on-call engineer was asked to bring it back under 1 second without adding new infrastructure.
- 1Profiled the endpoint with cProfile and confirmed nearly all wall-clock time was spent waiting on outbound HTTPS calls, not CPU work, ruling out multiprocessing.
- 2Rewrote the six bank API calls as async coroutines using an async HTTP client and fired them concurrently with asyncio.gather instead of the existing thread pool.
- 3Adapted the Flask view to run as an async view so the event loop could actually overlap the six requests on one thread.
- 4Added a per-call timeout with asyncio.wait_for so one slow bank couldn't stall the entire batch, falling back to cached data for stragglers.
- 5Load-tested with concurrent traffic to confirm the change held up before rolling it out behind a feature flag.
Top Must-Know Interview Questions & Model Answers
Q1: What is the Global Interpreter Lock (GIL) and why does CPython have one?
- •CPython's reference counting isn't atomic across threads by default; the GIL avoids costly fine-grained locking on every object's refcount.
- •The GIL is released periodically (based on sys.getswitchinterval, default ~5ms) and explicitly around blocking C-level I/O calls.
- •It is a CPython implementation detail, not a language requirement -- Jython, IronPython, and the experimental free-threaded CPython 3.13 build do not have it.
Q2: How does CPython's reference counting garbage collection work, and where does it fall short?
- •Refcounting gives deterministic, immediate cleanup (no stop-the-world pauses) but cannot free objects that reference each other in a cycle, since their counts never reach zero.
- •sys.getrefcount(obj) reports the count plus one, because passing obj as an argument creates a temporary extra reference.
- •CPython supplements refcounting with a generational cyclic garbage collector (the gc module) to catch unreachable reference cycles.
Q3: Explain Python's generational garbage collector and its three generations.
- •New objects start in generation 0; if they survive a generation-0 collection they're promoted to generation 1, and then generation 2.
- •Default thresholds (700, 10, 10) mean generation 0 is scanned after 700 net allocations, generation 1 after 10 generation-0 collections, and generation 2 after 10 generation-1 collections.
- •The collector only needs to examine 'container' objects capable of holding references (lists, dicts, instances) since only they can form cycles.
Q4: What actually happens to the GIL when a thread performs blocking I/O?
- •This is why threading is genuinely useful for I/O-bound workloads in Python despite the GIL -- the lock isn't held during the wait.
- •Pure CPU-bound Python bytecode loops only yield the GIL at the configured switch interval, not on every instruction.
- •C extensions like NumPy can release the GIL around their internal computation too, via Py_BEGIN_ALLOW_THREADS.
Q5: Compare threading, multiprocessing, and asyncio for concurrent Python workloads.
- •threading: cheap context switches and a shared address space, but the GIL serializes bytecode execution so there's no CPU parallelism.
- •multiprocessing: separate interpreters and memory spaces bypass the GIL entirely, but data must be pickled/copied between processes (or shared via multiprocessing.shared_memory).
- •asyncio: a single OS thread with cooperative scheduling via an event loop; scales to tens of thousands of concurrent sockets, but one blocking call stalls everything.
Q6: Walk through what happens inside the asyncio event loop when you await asyncio.sleep().
- •asyncio.sleep schedules a call_later callback on the loop instead of blocking the thread.
- •The event loop's core is effectively a select()/epoll()/kqueue() call plus a heap of scheduled timer callbacks.
- •When the coroutine resumes, execution continues exactly after the await statement with local state intact, because a coroutine is a suspendable generator-like frame.
Q7: Why is multiprocessing overhead often higher than expected, and how do you reduce it?
- •Use a Pool with chunksize tuning to amortize IPC overhead across many small work items.
- •Prefer multiprocessing.shared_memory or memory-mapped arrays for large datasets instead of pickling them per call.
- •On Linux the default 'fork' start method copies the parent's memory via copy-on-write, which is much cheaper than 'spawn' (default on Windows/macOS), which re-imports the module from scratch.
Q8: What is the difference between a mutable and an immutable type in Python, and why does it matter for function defaults and dict keys?
- •Hashability requires an object whose value (and hash) never changes during its lifetime -- this is why lists can't be dict keys but tuples of immutables can.
- •Mutable default arguments are evaluated once at function definition time and persist across calls, a classic interview trap.
- •Immutable objects enable safe sharing across threads without defensive copying, simplifying reasoning about concurrent code.
Q9: Explain how Python's dict achieves average O(1) lookup internally.
- •Since Python 3.6/3.7, dicts maintain insertion order using a compact layout: a sparse index array pointing into a dense array of (hash, key, value) entries.
- •The table resizes (growing roughly 4x when small, 2x when larger) once it's about two-thirds full, to keep probe sequences short.
- •Key lookup computes hash(key), probes the sparse table for a matching slot (checking hash equality then __eq__), giving amortized O(1) average time but O(n) worst case under pathological collisions.
Q10: How does a Python list grow, and what is the time complexity of append vs insert(0, x)?
- •CPython's list growth pattern roughly follows newsize + (newsize >> 3) + 6, over-allocating so not every append triggers a reallocation.
- •Because a list is contiguous memory, indexing lst[i] is O(1), but inserting or removing anywhere except the end requires shifting the remaining elements.
- •collections.deque is a doubly linked list of fixed-size blocks giving O(1) appends/pops from both ends, making it the right structure for queues.
Q11: How is a Python set implemented, and how does it differ from a dict internally?
- •set operations like union, intersection, and difference are implemented in C and are much faster than the equivalent manual loop-and-compare in Python.
- •frozenset is the immutable, hashable counterpart, usable as a dict key or set member.
- •Set membership testing (`x in my_set`) is O(1) average vs O(n) for `x in my_list`, a common performance-tuning interview point.
Q12: What is the difference between a shallow copy and a deep copy?
- •Mutating a nested mutable object (e.g. a list inside a list) through a shallow copy also affects the original, which surprises many candidates.
- •copy.deepcopy handles cyclic references safely via a memo dictionary that tracks already-copied objects.
- •Slicing (lst[:]) and dict(d) both produce shallow copies, not deep ones.
Q13: How do Python decorators work under the hood?
- •A parameterized decorator (`@retry(times=3)`) is really a decorator factory: an outer function that takes arguments and returns the actual decorator.
- •functools.wraps(func) copies __name__, __doc__, and __wrapped__ onto the wrapper so introspection, help(), and stack traces stay meaningful.
- •Decorators can stack; they apply bottom-up, so `@a` `@b` above def f() is equivalent to f = a(b(f)).
Q14: What is the difference between a generator and a regular function returning a list?
- •Calling a generator function returns a generator object immediately without running any code; execution only proceeds on each call to next() or a for-loop iteration.
- •State (local variables, instruction pointer) is preserved on the generator's frame between yields, unlike a normal function whose frame is discarded on return.
- •yield from delegates iteration to a sub-generator or iterable, forwarding values, exceptions, and the return value transparently.
Q15: How does the context manager protocol (the with statement) guarantee cleanup?
- •@contextlib.contextmanager turns a generator function into a context manager: code before yield is __enter__, the yielded value is bound by `as`, and code after yield (in a finally) is __exit__.
- •If __exit__ returns a truthy value, the exception is suppressed; returning None/False lets it propagate -- a subtle footgun.
- •Multiple context managers in one `with a, b:` statement nest right-to-left on exit, mirroring nested `with a:` `with b:` blocks.
Q16: What is a metaclass, and when would you actually use one?
- •`class Foo(metaclass=Meta):` runs Meta.__new__/Meta.__init__ with the class name, bases, and namespace dict, letting you inspect or rewrite the class before it's created.
- •Django's ORM and SQLAlchemy's declarative base both use metaclasses to turn class-level attribute declarations into database column mappings automatically.
- •Most 'I need a metaclass' problems are solvable more simply with __init_subclass__ (a hook for subclass creation) or a class decorator, the modern, less invasive alternative.
Q17: Explain Method Resolution Order (MRO) and C3 linearization in multiple inheritance.
- •You can inspect it directly via ClassName.__mro__ or ClassName.mro().
- •C3 guarantees a subclass always precedes its parents, and respects each class's locally declared base order consistently -- something a naive depth-first search fails to guarantee.
- •super() uses the MRO, not the immediate parent class, which is why cooperative multiple inheritance (every class calling super().__init__()) correctly chains through a diamond hierarchy exactly once per class.
Q18: What is the contract between __eq__ and __hash__, and what breaks if you violate it?
- •Defining __eq__ on a class sets __hash__ to None automatically unless you also define __hash__ explicitly, immediately breaking use as a dict key or set member.
- •Violating the contract (equal objects, different hashes) causes silent bugs: a set may contain 'duplicate' equal entries because they land in different buckets.
- •dataclasses handle this automatically -- @dataclass(frozen=True) generates a consistent __eq__/__hash__ pair for you.
Q19: What are Python's key dunder methods for making a custom class behave like a built-in container?
- •__iter__ should return an iterator (an object with __next__); a generator function naturally satisfies this.
- •__repr__ should return an unambiguous, ideally eval-able representation for debugging; __str__ is for user-friendly display and falls back to __repr__ if undefined.
- •__enter__/__exit__ make a class usable as a context manager; __call__ makes instances callable like functions.
Q20: How do Python type hints (the typing module) work at runtime, and what is a Protocol?
- •TypeVar and Generic let you write classes/functions parameterized over a type, e.g. class Stack(Generic[T]); PEP 695 (Python 3.12) introduces native syntax -- class Stack[T]: ... and def first[T](items: list[T]) -> T: ... -- without importing TypeVar.
- •Protocol (from typing) enables 'if it walks like a duck': a class satisfies class Sized(Protocol): def __len__(self) -> int: ... just by having a compatible __len__, with no inheritance required.
- •typing.TYPE_CHECKING guards imports that are only needed for type hints, avoiding runtime circular-import costs.
Q21: What notable changes did Python 3.12 introduce that interviewers now expect you to know?
- •PEP 695: `type Alias = list[int]` and `class Box[T]: ...` / `def f[T](x: T) -> T: ...` replace verbose TypeVar/Generic boilerplate.
- •PEP 701 relaxes f-string grammar: you can now reuse the same quote character inside the expression part and nest f-strings freely.
- •PEP 703 (targeted for 3.13+) is the effort to make the GIL optional (a --disable-gil build), a major potential shift for CPU-bound multithreading -- still experimental and not the default.
Q22: Why can calling a blocking function inside an async coroutine be catastrophic for performance?
- •The fix is to use the async-native equivalent (asyncio.sleep, an async HTTP client like httpx/aiohttp) or offload blocking work to a thread/process pool via loop.run_in_executor.
- •CPU-bound work inside a coroutine is just as damaging as blocking I/O -- it too monopolizes the single event-loop thread.
- •asyncio's debug mode (PYTHONASYNCIODEBUG=1) warns when a callback takes too long, helping surface accidental blocking calls.
Q23: Explain __slots__ and when it's worth using.
- •Without __slots__, every instance carries a __dict__ (a full hash table) even if it only stores 2-3 attributes -- significant overhead when creating millions of small objects.
- •Classes using __slots__ cannot have arbitrary new attributes added at runtime, and multiple inheritance with slots across unrelated classes gets tricky (layout conflicts).
- •@dataclass(slots=True) (Python 3.10+) generates a slotted dataclass automatically, combining ergonomics with the memory win.
Q24: What is the descriptor protocol, and how do @property and instance methods rely on it?
- •@property is sugar for a data descriptor: property(fget, fset, fdel) implements __get__/__set__ to intercept attribute access with custom logic.
- •Functions are themselves non-data descriptors -- accessing instance.method invokes the function's __get__, which returns a bound method with self pre-filled.
- •Data descriptors (defining __set__) take priority over instance __dict__ entries during attribute lookup; non-data descriptors (only __get__) do not.
Q25: How would you detect and prevent a memory leak in a long-running Python service?
- •Objects participating in a reference cycle and defining __del__ historically could not be collected at all in Python before 3.4; modern CPython (PEP 442) safely collects them, but __del__ order is still undefined, encouraging weakref-based cleanup instead.
- •weakref.ref / weakref.WeakValueDictionary let caches or observer registries hold non-owning references so they don't keep objects alive.
- •tracemalloc.take_snapshot() plus snapshot.compare_to() pinpoints exactly which allocation call sites are growing between two points in time.
Q26: What's the difference between == and is, and where does this commonly trip people up?
- •CPython caches small integers (-5 to 256) and some string literals, so `a is b` can accidentally appear to work for small ints in casual testing but fails for larger numbers or dynamically built strings.
- •The only correct, portable use of is is comparing against singletons: x is None, x is True, x is NotImplemented.
- •Using is to compare strings/numbers is an implementation-detail-dependent bug waiting to surface in a different CPython build or version.
Q27: Why is a bare except: clause considered bad practice?
- •Prefer except Exception: at minimum, which excludes BaseException-only signals like SystemExit/KeyboardInterrupt.
- •Catching overly broad exceptions also hides real bugs (typos, wrong types) that should surface immediately during development.
- •Always log or re-raise (a bare `raise` preserves the original traceback) rather than silently passing in an except block.
Q28: What happens when you modify a list while iterating over it, and how do you avoid the bug?
- •Iterating over list(original) (a copy) or reversed(original) while mutating the original avoids index-shifting bugs.
- •For filtering, prefer building a new list via a comprehension ([x for x in items if keep(x)]) rather than removing matches in place.
- •The same hazard applies to dict/set: RuntimeError: dictionary changed size during iteration is raised if you add/remove keys while iterating a dict directly.
Mistakes That Sink Otherwise Strong Candidates
Why it happens: Default argument expressions are evaluated once at function definition time, not on every call, which is unintuitive coming from most other languages.
The fix: Default to None and lazily create the mutable object inside the function body on first use.
Why it happens: Threads look like the obvious concurrency primitive, but the GIL serializes bytecode execution so multiple threads don't get parallel CPU time for pure Python code.
The fix: Use multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor for CPU-bound work, or vectorize with a library that releases the GIL internally.
Why it happens: It's easy to forget that async code shares a single OS thread, so any function that doesn't yield control stalls every other task.
The fix: Swap in async-native equivalents (asyncio.sleep, an async HTTP client) or push blocking calls to a thread pool via loop.run_in_executor.
Why it happens: Small integer and string interning caching makes is appear to work correctly in quick manual tests, hiding the bug until it hits larger values in production.
The fix: Reserve is exclusively for identity/singleton checks (is None); always use == for value comparison.
Why it happens: Python automatically sets __hash__ to None when __eq__ is defined, silently making the class unusable as a dict key or set member.
The fix: Define a consistent __hash__ alongside __eq__, or use @dataclass(frozen=True) which generates both correctly.
Why it happens: Names like list.copy() or x[:] sound like a full copy, and the bug only manifests once a nested object is mutated through one reference.
The fix: Use copy.deepcopy() whenever the structure contains nested mutable containers you need to fully isolate.
Why it happens: It looks harmless syntactically, and the bug (skipped elements or a RuntimeError) doesn't always surface with small test inputs.
The fix: Iterate over a copy (list(original)) or build a new filtered collection via a comprehension instead of mutating in place.
Why it happens: It feels like a safe catch-all during quick debugging, but it also swallows SystemExit and KeyboardInterrupt and hides real bugs.
The fix: Catch the narrowest exception class you can meaningfully handle, and re-raise or log anything unexpected.
Why it happens: A generator looks like a list from the outside, so reusing the same generator object in a second for-loop feels natural.
The fix: Materialize the values into a list if you need to iterate more than once, or re-invoke the generator function to get a fresh generator.
Quick-Reference Cheat Sheet
Recommended Practice Quizzes on QuizCluster
Test your retention and prepare for timed live coding and MCQ technical screening rounds:
Python Core & Data
Drill GIL behavior, data structure internals, decorators, and OOP fundamentals across hundreds of Python-specific questions.
Arrays, Two-Pointers & Sliding Window
Apply Python's list and string idioms to classic pointer and sliding-window coding interview patterns.
Frequently Asked Questions
Do I need to know the GIL in depth for a junior Python interview?
Yes, at a conceptual level -- expect at least one question distinguishing threading, multiprocessing, and asyncio and explaining why the GIL affects each differently. Deep internals like switch intervals and PEP 703 are more common at senior/staff level.
Is Python 2 knowledge still relevant?
Rarely. Interviews in 2026 assume Python 3.10+ idioms -- focus on match statements, the walrus operator, f-strings, and 3.12's generic syntax rather than Python 2/3 compatibility tricks.
Should I memorize exact CPython source constants like dict resize thresholds?
No -- interviewers care that you understand the underlying model (open addressing, amortized growth, generational GC) and can reason about performance trade-offs, not that you've memorized exact numeric constants.