These are the core-language Python questions that actually come up — grouped by topic, each with a short model answer and a note on what the interviewer is really checking. Python questions reward understanding the data model: most "gotchas" are the same few ideas (mutability, references, evaluation time) wearing different hats.
The tell on Python questions is whether you can explain why, not just what. "A tuple is immutable" is recall; "a tuple is immutable, which is why it can be a dict key and a list can't" is understanding. For each answer below, notice the pattern: name the mechanism, give the one consequence that matters, then stop.
The data model
What's the difference between mutable and immutable types?
A mutable object can be changed in place after creation; an immutable one can't. Lists, dicts and sets are mutable; ints, floats, strings, tuples and frozensets are immutable. It matters for three everyday reasons: only immutable objects can be dictionary keys or set members (they need a stable hash), immutable objects are safe to share without worrying someone mutates them under you, and "modifying" a string actually creates a new one. Most Python surprises trace back to this one distinction.
What they're testing: that you connect mutability to hashing, sharing, and the gotchas below.
List vs tuple — when would you use each?
A list is a mutable, ordered collection you grow and change; a tuple is an immutable, ordered collection you fix once. Use a list for a homogeneous sequence you'll modify (a queue of tasks); use a tuple for a fixed record whose shape has meaning (a coordinate (x, y), a row returned from a query). The practical payoffs of the tuple: it can be a dict key, it signals "this won't change," and it's slightly lighter.
What they're testing: that you pick by mutability and intent, not just "tuple is a faster list."
is vs == — what's the difference?
== checks equality (do these have the same value?); is checks identity (are these the exact same object in memory?). You almost always want ==. The one firm rule: compare to None with is (if x is None), because None is a singleton. A classic trap: small integers and short strings are cached, so is sometimes looks like it works for values — relying on that is a bug waiting to happen.
What they're testing: that you know identity vs equality, and the is None convention.
Shallow copy vs deep copy?
A shallow copy duplicates the outer object but shares the nested objects inside it; a deep copy recursively duplicates everything. So if you shallow-copy a list of lists and mutate an inner list, the change shows up in both copies — because they point at the same inner list. Use copy.deepcopy() when you need true independence, and know it's slower. This is just the mutability rule again: copying the container doesn't copy what it references.
What they're testing: that you understand references, and when a shallow copy quietly bites you.
Functions
What do *args and **kwargs do?
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. They let a function accept any number of arguments, and — used at the call site — unpack a sequence or dict back into arguments (f(*my_list), f(**my_dict)). The everyday use is writing wrappers and decorators that pass arguments through without knowing them: def wrapper(*args, **kwargs): return func(*args, **kwargs).
What they're testing: that you can write flexible, pass-through function signatures.
What is a decorator?
A decorator is a function that takes another function and returns a new one that wraps it — a clean way to add behaviour (logging, timing, caching, access checks) around a function without touching its body. @decorator above a definition is just sugar for func = decorator(func). The reason they feel magical and then obvious: functions in Python are ordinary objects you can pass around and return, so wrapping one is nothing special.
What they're testing: that you grasp functions-as-objects, which is the whole idea behind decorators.
Why is a mutable default argument a bug?
Because default arguments are evaluated once, when the function is defined — not on each call. So def add(x, items=[]) shares one list across every call that uses the default, and it accumulates between calls, which is almost never what you want. The fix is the standard idiom: default to None and create the list inside — def add(x, items=None): items = items or []. It's the single most common Python gotcha in interviews.
What they're testing: that you know default arguments are evaluated at definition time, not call time.
Iterators & generators
What is a generator, and why use one?
A generator is a function that uses yield to produce values lazily, one at a time, instead of building and returning a whole list. Each yield hands back a value and pauses, resuming where it left off on the next request. The win is memory: you can iterate over a billion-line file or an infinite sequence without ever holding it all in RAM. Reach for one whenever you're processing a large or streaming sequence and don't need every item at once.
What they're testing: that you see generators as a memory optimisation for sequences, not just "fancy loops."
What's the difference between an iterable and an iterator?
An iterable is anything you can loop over — it can produce an iterator (a list, a string, a dict). An iterator is the object that actually does the walking: it remembers its position and hands you the next item via next(), raising StopIteration when it's done. A for loop quietly calls iter() on the iterable to get an iterator, then next() until it's exhausted. A list is iterable but not its own iterator; a generator is both.
What they're testing: that you know the protocol a for loop actually runs under the hood.
List comprehension vs generator expression?
Same syntax, different brackets, different behaviour. [x*2 for x in data] builds the whole list in memory immediately. (x*2 for x in data) creates a generator that produces values lazily, one at a time. Use the list comprehension when you need the full list (you'll index it, reuse it, or it's small); use the generator expression when you're just going to iterate once over something large — it keeps memory flat.
What they're testing: that you choose eager vs lazy evaluation on purpose, by size and reuse.
Concurrency & resources
What is the GIL, and what does it mean for threads?
The Global Interpreter Lock is a mutex in CPython that lets only one thread execute Python bytecode at a time. The consequence: threads don't give you true parallelism for CPU-bound work — two threads crunching numbers run effectively one at a time. But threads still help for I/O-bound work (network, disk), because the GIL is released while waiting. So the rule is: threads for I/O-bound, multiprocessing (separate processes, separate GILs) or native extensions for CPU-bound.
What they're testing: that you know the GIL's real-world rule — threads for I/O, processes for CPU.
What is a context manager, and when would you write one?
A context manager is the object behind the with statement: it runs setup on entry and — crucially — guaranteed cleanup on exit, even if an exception is raised. with open(f) as file: closes the file no matter what. You write your own (with __enter__/__exit__, or the @contextmanager decorator) whenever you have a resource that must be released — a lock, a database connection, a temporary change to state — and you don't want to rely on remembering to clean it up.
What they're testing: that you reach for with for guaranteed cleanup, not manual try/finally everywhere.
How does Python manage memory?
Primarily with reference counting: every object tracks how many references point to it, and it's freed the instant that count hits zero. On top of that, a cyclic garbage collector handles the case reference counting can't — objects that reference each other in a cycle, which would otherwise never reach zero. You rarely manage memory by hand; the thing worth knowing is that holding a reference (in a long-lived list, a cache, a closure) is what keeps an object alive.
What they're testing: that you understand reference counting and what actually causes a "leak" in Python.
How to actually answer these
Almost every Python question above reduces to one of a few core ideas — mutability, references, when code is evaluated. The strongest candidates don't memorise each gotcha; they explain it from the principle: "default arguments evaluate once at definition, so a mutable default is shared — that's the same reason this bug and that one happen." Showing the underlying model beats reciting a list of tricks.
Two habits that quietly raise your score: give the consequence, not just the definition (a tuple is immutable → so it can be a dict key), and mention the standard idiom where there is one (is None, items=None defaults, with for cleanup) — it signals you've written real Python. Prepping the whole stack? Pair this with the Python backend, SQL, and system design questions.
Rehearse these out loud, scored. Peakblick asks you Python questions like these on a timer and scores every answer 1–10 with feedback — so you find the weak spots before the real interview. Free to try, no card.
Top comments (0)