Python interview questions
Language concepts, the classic traps, and what to say while you write code on a shared screen.
Python interviews mix language questions with a short coding task. The language half keeps returning to the same ideas: which objects are mutable, how names and references work, why a default argument can remember the last call, how generators differ from lists, and what the global interpreter lock does to threads. The coding half is usually a twenty-line function where the interviewer wants to hear your reasoning about the approach and its cost, not a clever one-liner.
What Python interview questions test
Interviewers rarely ask for syntax recall. They ask questions whose answers reveal whether you have debugged real Python: what happens when two names point at the same list, what a decorator actually returns, when a generator is consumed. Each of those has a short answer and a longer one, and the longer one is where the signal is.
The shape of the round depends on the job. A backend role leans on classes, error handling, packaging and the standard library. A data role leans on collections, iteration, pandas and SQL alongside Python. A test automation role leans on fixtures, mocking and pytest. Ask the recruiter which it is; they almost always tell you.
A practical way to prepare is to run every example you read. Every snippet here was executed on CPython first, and the comments are the output it printed. Typing them yourself and changing one line is worth more than rereading a list of definitions.
Mutability, identity and copies
Start with the question behind many others: which built-in types can change in place. Lists, dictionaries and sets are mutable. Strings, tuples, integers and frozensets are not, so any operation that looks like it edits a string actually builds a new one. That is also why a tuple can be a dictionary key and a list cannot.
The follow-up is the difference between == and is. The first compares values, the second asks whether two names refer to the same object. Then comes copying, and the copy module documentation gives the exact distinction: a shallow copy constructs a new compound object and then inserts references into it to the objects found in the original, while a deep copy recursively inserts copies of those objects instead.
import copy
a = [1, 2, [3, 4]]
b = a[:] # shallow copy of the outer list
b[2].append(5)
print(a) # [1, 2, [3, 4, 5]] - the inner list is shared
print(a == b, a is b) # True False
c = copy.deepcopy(a)
c[2].append(6)
print(a[2], c[2]) # [3, 4, 5] [3, 4, 5, 6]
The same idea explains the function-argument question. Python passes references to objects, so a function that calls append on a list it received changes the caller’s list, while rebinding the parameter with an assignment does not. If asked whether Python is call by value or call by reference, describe that behaviour rather than picking a label.
Dictionaries are worth a sentence of their own. Since Python 3.7 the insertion-order preservation nature of dict objects has been declared to be an official part of the Python language spec, so iterating a dict gives you the insertion order and you do not need an OrderedDict for that alone.
Default arguments, closures and decorators
The single most asked gotcha is the mutable default argument. The tutorial states the rule plainly: the default value is evaluated only once, which matters when the default is a mutable object such as a list or a dictionary. So the list is created when the function is defined and then reused on every call that omits the argument.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("apple")) # ['apple']
print(add_item("pear")) # ['apple', 'pear'] - same list again
def add_item_safe(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item_safe("apple"), add_item_safe("pear")) # ['apple'] ['pear']
Closures bring a second trap, late binding. A function defined inside a loop looks up the loop variable when it is called, not when it is created, so all three functions below see the final value. Binding the value as a default argument fixes it.
fns = [lambda: i for i in range(3)]
print([f() for f in fns]) # [2, 2, 2]
fns = [lambda i=i: i for i in range(3)]
print([f() for f in fns]) # [0, 1, 2]
Then decorators. A decorator is a function that takes a function and returns a replacement, and @name is shorthand for reassigning the name to that replacement. Mentioning functools.wraps is the detail interviewers listen for, because without it the wrapped function loses its name and docstring.
import functools
def log_calls(fn):
@functools.wraps(fn)
def wrapper(*args):
result = fn(*args)
print(f"{fn.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3) # prints: add returned 5
print(add.__name__) # add
That snippet also touches the star-arguments question. A star parameter collects any extra positional arguments into a tuple; a double-star parameter does the same for keyword arguments and gives you a dictionary. A wrapper that has to stand in for any function declares both, which is why you see that pair in almost every decorator.
Generators, comprehensions and iteration
Expect to be asked the difference between a list comprehension and a generator expression. A comprehension in square brackets builds the whole list in memory. A generator expression in round brackets produces values one at a time, so sum(x * x for x in range(1000)) never builds a thousand-element list. For a large file or a long pipeline that difference is the whole answer.
A function containing yield returns a generator. Calling it runs no body code until you iterate, and each value is produced on demand. The part candidates forget is that a generator is consumed as you go:
def squares(n):
for i in range(n):
yield i * i
gen = squares(4)
print(next(gen), next(gen)) # 0 1
print(list(gen)) # [4, 9] - the first two are gone
From there, two reliable follow-ups. The first is the difference between an iterable and an iterator: an iterable can produce an iterator, an iterator has next and raises StopIteration when it is done, and a for loop calls iter on the iterable for you. The second is when to choose a set, a dict or a list: membership tests on a set or dict are close to constant time on average, while the same test on a list scans it, which turns an innocent nested loop into quadratic work.
Sorting comes up in the same breath. Sorting with a key function is the idiomatic answer, and sorted is stable, so items with equal keys keep their relative order. A tuple key sorts on several fields at once:
scores = {"ana": 7, "ben": 9, "cleo": 7}
print(list(scores)) # ['ana', 'ben', 'cleo'] - insertion order
print(sorted(scores.items(), key=lambda kv: (-kv[1], kv[0])))
# [('ben', 9), ('ana', 7), ('cleo', 7)]
Classes, self and the shared-attribute trap
Object-oriented questions in Python interviews stay practical. What is self: the instance, passed to methods automatically. What does init do: it initialises an already created instance, which is why people are asked not to call it a constructor. What is the point of repr: it gives you a readable object in logs and in the REPL, and interviewers notice when you add one.
The trap here mirrors the default-argument one. An attribute defined in the class body belongs to the class, so every instance shares it:
class Team:
members = [] # one list for the whole class
def __init__(self, name):
self.name = name
red, blue = Team("red"), Team("blue")
red.members.append("ana")
print(blue.members) # ['ana']
The fix is to create the list in init as self.members. Class attributes are still useful for constants and counters, which is the nuance to offer after you have named the problem.
Two more questions turn up often enough to prepare. Static method versus class method: a staticmethod takes neither the instance nor the class and is just a function living in the class namespace, while a classmethod receives the class and is the usual way to write an alternative constructor. And inheritance versus composition: say that you reach for composition when one object uses another, and inheritance only when the subclass genuinely is a kind of the parent, then mention that super() lets you extend rather than replace the parent’s behaviour.
The GIL, threads, processes and asyncio
This is the question that most often exposes memorised answers, so use the documented wording. CPython’s glossary defines the global interpreter lock as the mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time, and adds two points people leave out: some extension modules release the lock during computationally intensive work such as compression or hashing, and the lock is always released when doing input and output.
That gives you the practical rule. Threads still help when the work is waiting on the network, the disk or a database, because the lock is released during the wait. Threads do not give you more CPU on pure Python computation; for that, the multiprocessing module runs separate processes with their own interpreters, at the cost of starting processes and of moving data between them. Recent CPython releases also ship an optional build configured without the lock, which is worth knowing exists, though most production code still runs on the default build.
Asyncio is the third option and a different model: a single thread, an event loop, and functions defined with async def that hand control back at each await. It suits thousands of concurrent network calls and does nothing for CPU-bound code. A blocking call inside a coroutine stalls the whole loop, which is a good thing to mention because it is the most common asyncio bug.
If you are asked which you would choose, answer with the bottleneck first. Input and output bound work goes to asyncio or threads, CPU bound work goes to processes, and the honest closing line is that you would measure before rewriting anything.
The coding task, and role-specific questions
The coding exercise is usually small and tests whether you can pick a data structure. A frequent one asks for the first character in a string that never repeats. The nested-loop answer works but scans the string for every character; counting once and then scanning once is linear and easier to defend.
from collections import Counter
def first_unique(text):
counts = Counter(text)
for ch in text:
if counts[ch] == 1:
return ch
return None
print(first_unique("swiss"), first_unique("aabb")) # w None
Say the complexity out loud, name the edge cases you handled (empty string, no unique character, case sensitivity, whitespace) and ask whether the input could be huge, because that changes whether you want Counter at all. Interviewers also like a quick note on testing: one normal case, one empty input, one input where every character repeats.
Finally, prepare for the layer above the language. For a web role that means request handling, an ORM and migrations, with Django or FastAPI specifics depending on the shop. For a data role it means pandas and SQL, where the questions are about grouping, joining and avoiding row-by-row loops over a data frame. For a testing role it means pytest fixtures, parametrised tests and where to use a mock. Read the job description, pick the one layer it stresses, and have a story about something you built with it.
Practice it now
Answer out loud, as if the interviewer just asked. Then check yourself against the list.
“Write a function that takes a list of dictionaries, each with the keys name and score, and returns the names of the top three by score, highest first, with ties broken alphabetically. Then explain its complexity.”
Aim for 15 minutes- You asked about input size, duplicate names and missing keys before coding.
- The sort uses a key function rather than manual swapping or repeated max calls.
- The tie-breaking rule is explicit in the key, not left to chance.
- You handled the case of fewer than three items without a crash.
- You stated the complexity and one alternative, such as heapq.nlargest for a long list.
- You named two test cases you would write, including one empty input.
Frequently asked questions
What are the most common Python interview questions?
Mutable versus immutable types, == against is, shallow versus deep copies, the mutable default argument, decorators, generators against list comprehensions, class versus instance attributes, and the global interpreter lock with threads, processes and asyncio. Most rounds also include one small coding task.
How much of the standard library should I know?
Know collections (Counter, defaultdict, deque), itertools in outline, json and csv, pathlib, datetime, and the context manager pattern with the with statement. Knowing that a tool exists and roughly what it does is enough; you can look up the arguments.
What should I say about the GIL if I have never hit it?
Quote the mechanism accurately, then reason from it: only one thread runs Python bytecode at a time, the lock is released during input and output, so threads help waiting work and processes help computation. Saying you have not profiled a parallel workload yourself is fine.
Do I need to know a web framework or pandas?
Only what the role uses. The language questions are shared, but a backend interview will go into request handling, an ORM and migrations, and a data interview will go into grouping and joining data frames. Prepare the one layer the job description names.
How do I practise for the live coding part?
Solve small problems while narrating your thinking out loud, in a plain editor with no autocomplete, under a timer. Then rerun each solution with an empty input and a single-element input, which is where most interview code breaks.