Python Interview Questions and Answers
26 questions8 beginner · 10 intermediate · 8 advancedLast reviewed
This set is for anyone interviewing for a Python developer, backend, data or automation role, from a first job to a senior screen. The questions are the recurring ones: how the language really behaves, where it surprises people, and how you reason about concurrency and memory.
Practise by answering each question out loud in under a minute before you open the model answer, then compare. Run the code blocks yourself and change one line to predict what happens. If a topic feels shaky, the Python fundamentals course covers it step by step.
1What is the difference between mutable and immutable objects in Python? Give examples of each.Beginner
An immutable object cannot change after it is created; a mutable one can be modified in place. Strings, ints, floats, bools, tuples and frozensets are immutable. Lists, dicts, sets and most user-defined class instances are mutable.
The practical consequence is aliasing: if two names point to the same list, a change through one name is visible through the other. With a string, operations like concatenation create a new object instead. A subtle case is a tuple holding a list: the tuple's slots cannot be reassigned, but the list inside can still change.
python cart = ("tea", ["milk"]) cart[1].append("sugar") # fine: mutates the list inside print(cart) try: cart[0] += "x" # tries to rebind a tuple slot except TypeError as e: print("err", e)Output
('tea', ['milk', 'sugar']) err 'tuple' object does not support item assignmentWhat the interviewer is checking: Whether you know immutability is about the object itself and can spot the tuple-containing-a-list edge case.
Likely follow-up: Why does that matter for using something as a dictionary key?
2When would you pick a list, a tuple, a set or a dict?Beginner
Pick by what you need to do with the data. A list is an ordered, growable sequence, good for collections you append to and iterate over. A tuple is a fixed-size record, such as a (latitude, longitude) pair, and it is hashable if its contents are. A set stores unique items with fast membership tests and set algebra. A dict maps keys to values with fast lookup, and keeps insertion order.
A common rule: if you keep asking whether x is in a list inside a loop, you probably want a set or a dict, because membership is average O(1) there versus O(n) for a list.
What the interviewer is checking: Choosing the structure from access patterns and complexity rather than reciting definitions.
Likely follow-up: What does a set require of its elements, and why?
3What is the difference between
isand==in Python?Beginner==asks whether two objects have equal values, by calling__eq__.isasks whether two names refer to the very same object in memory, which is an identity check.Use
==for comparing data andisfor singletons, almost alwaysNone:if result is None. Never rely onisfor numbers or strings. CPython may reuse small ints or interned strings, which makesisappear to work in a quick test and then fail with other values or on another interpreter.What the interviewer is checking: That you separate value equality from identity and know the one place
isis the right tool.Likely follow-up: Why can a custom
__eq__make==return something surprising, andiscannot be overridden?4Explain
*argsand**kwargs. How do keyword-only arguments fit in?Beginner*argscollects extra positional arguments into a tuple, and**kwargscollects extra keyword arguments into a dict. They let a function accept a flexible call signature, and are common in wrappers that forward everything to another function.Any parameter after
*args(or a bare*) is keyword-only, so callers must name it. That is a good API habit for flags likedry_run=True, since it prevents unreadable calls such asship(order, True, False). The same stars also work at call sites to unpack a sequence or mapping.What the interviewer is checking: Practical command of the argument rules, including the keyword-only part many candidates skip.
Likely follow-up: What error do you get if you omit the required keyword-only argument?
5What is a list comprehension, and when should you avoid one?Beginner
A list comprehension builds a list from an iterable in one expression, optionally filtering:
[n for n in readings if n > 0]. It is usually clearer and faster than the equivalent loop withappend.Avoid it when the logic needs several statements, nested conditions that hurt readability, or side effects such as printing or writing to a file. If you only need to loop over results once, a generator expression avoids building the whole list in memory. In Python 3 the loop variable does not leak out of the comprehension.
What the interviewer is checking: Taste: knowing the tool and also when readability or memory argues against it.
Likely follow-up: What is the difference between a list comprehension and a generator expression?
6Why can a dictionary key be a tuple but not a list?Beginner
Dict keys must be hashable: they need a hash value that never changes during their lifetime, and equality that agrees with the hash. A list is mutable, so its contents and therefore its hash could change after insertion, and the dict would never find the entry again.
A tuple is hashable only if everything inside it is hashable, so a tuple of ints works as a key but a tuple containing a list does not. Common uses are coordinates or composite keys like
(user_id, date). If you need a list-like key, convert it withtuple(...)first.What the interviewer is checking: Whether you connect hashability to mutability and to how hash tables work.
Likely follow-up: What happens to hashability if a class defines
__eq__and nothing else?7What are dunder methods? Explain
__repr__versus__str__.BeginnerDunder (double underscore) methods are hooks that let your class plug into Python's syntax and built-ins:
__init__for construction,__len__forlen(),__add__for+,__iter__for loops,__enter__and__exit__forwith.__repr__is for developers: an unambiguous string, ideally one that could rebuild the object, and it is what you see in the REPL, in logs and inside containers.__str__is for end users and is used byprintandstr(). If you define only one, define__repr__, becausestr()falls back to it.What the interviewer is checking: Awareness of the data model and the repr-for-debugging convention.
Likely follow-up: Which dunder methods would you add so your class supports sorting?
8Why use virtual environments, and how do you set one up for a project?Beginner
A virtual environment is an isolated Python install with its own site-packages. Without one, every project shares the interpreter's global packages, so two apps needing different versions of the same library collide, and installs can break system tools.
Create one with
python -m venv .venv, activate it, thenpip install -r requirements.txt. Keep dependencies pinned in a requirements or lock file, and keep.venvout of version control so teammates and CI rebuild the same environment. Tools such as poetry, uv or pipenv wrap the same idea and add dependency resolution and lock files.What the interviewer is checking: Basic engineering hygiene: reproducible, isolated dependencies.
Likely follow-up: How do you make sure production installs exactly what you tested?
9What is wrong with this function, and how do you fix it?
def add_stop(stop, route=[])IntermediateThe default list is created once, when the function is defined, not on each call. Every call that omits
routeshares the same list, so state leaks between calls and the bug shows up far from its cause.The standard fix is to use
Noneas the default and create a fresh list inside the function. The same trap applies to any mutable default: dicts, sets and objects. Useis Nonerather than a truthiness check, since an empty list passed deliberately by the caller is falsy and you would wrongly replace it.python def add_stop(stop, route=[]): route.append(stop) return route print(add_stop("A")) print(add_stop("B")) def add_stop2(stop, route=None): if route is None: route = [] route.append(stop) return route print(add_stop2("A")) print(add_stop2("B"))Output
['A'] ['A', 'B'] ['A'] ['B']
What the interviewer is checking: Knowing when defaults are evaluated, and fixing it with the correct sentinel check.
Likely follow-up: Is there a case where this behaviour is deliberately useful?
10Explain shallow copy versus deep copy. When does the difference bite?Intermediate
A shallow copy creates a new outer container but reuses the same inner objects. A deep copy recursively copies everything, so the result shares nothing mutable with the original.
copy.copy,list.copy(),dict.copy(), slicing anddict(d)are all shallow;copy.deepcopyis deep.It bites with nested data: change an inner list through the shallow copy and the original changes too. The classic version is
[[0]*3]*2, which makes two references to one row. Deep copies cost time and memory and can be unnecessary; often copying only the layer you mutate is enough.python import copy order = {"id": 7, "items": [{"sku": "A1", "qty": 1}]} shallow = order.copy() deep = copy.deepcopy(order) order["items"][0]["qty"] = 5 print(shallow["items"][0]["qty"], deep["items"][0]["qty"]) grid = [[0]*3]*2 grid[0][0] = 1 print(grid) grid2 = [[0]*3 for _ in range(2)] grid2[0][0] = 1 print(grid2)Output
5 1 [[1, 0, 0], [1, 0, 0]] [[1, 0, 0], [0, 0, 0]]
What the interviewer is checking: Whether you understand shared references and can recognise the aliasing bug in real code.
Likely follow-up: How does
deepcopyhandle objects that refer to themselves?11What is the difference between an iterator and a generator, and why would you use a generator?Intermediate
An iterator is any object with
__iter__and__next__that produces values one at a time and raisesStopIterationwhen done. A generator is a convenient way to build one: a function containingyieldreturns a generator object that pauses at eachyieldand resumes where it left off.Generators are lazy, so you can process a huge file or an endless stream in constant memory, and you can chain them as a pipeline. The catch is that they are single-use: once exhausted they yield nothing, which silently breaks code that loops over the same generator twice.
python def read_batches(lines, size): batch = [] for line in lines: batch.append(line) if len(batch) == size: yield batch batch = [] if batch: yield batch gen = read_batches((f"row{i}" for i in range(5)), 2) print(next(gen)) print(list(gen)) print(list(gen))Output
['row0', 'row1'] [['row2', 'row3'], ['row4']] []
What the interviewer is checking: Understanding laziness, memory trade-offs and the single-use pitfall.
Likely follow-up: What does
yield fromadd over a plain loop that yields each item?12How does a decorator work? Write one that retries a flaky call.Intermediate
A decorator is a callable that takes a function and returns a replacement, usually a wrapper that adds behaviour before or after the original.
@retry(3)above a function is shorthand forfn = retry(3)(fn). With arguments you need three layers: the factory, the decorator and the wrapper.Always apply
functools.wrapsto the wrapper so the decorated function keeps its name and docstring, which matters for debugging, logging and tools that inspect functions. Catch only the exceptions you expect to be transient, and re-raise on the final attempt rather than swallowing the error.python import functools def retry(times): def deco(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): for attempt in range(1, times + 1): try: return fn(*args, **kwargs) except ConnectionError: if attempt == times: raise return wrapper return deco calls = {"n": 0} @retry(3) def fetch_rate(): """Fetch the rate.""" calls["n"] += 1 if calls["n"] < 3: raise ConnectionError return 1.08 print(fetch_rate(), calls["n"], fetch_rate.__name__, fetch_rate.__doc__)Output
1.08 3 fetch_rate Fetch the rate.
What the interviewer is checking: Closures, higher-order functions and the habit of preserving metadata with
wraps.Likely follow-up: How would you add exponential backoff, and why add jitter?
13What is a context manager? How would you write your own?Intermediate
A context manager guarantees setup and cleanup around a block via the
withstatement. It implements__enter__and__exit__;__exit__runs even if the block raises, which is why files, locks and database transactions use it. Returning a truthy value from__exit__suppresses the exception, which you should rarely do.The shortest way to write one is
contextlib.contextmanageron a generator: code beforeyieldis setup, code after it is cleanup. Wrap theyieldintry/finally, otherwise an exception inside the block skips your cleanup.python from contextlib import contextmanager @contextmanager def locker(name): print("open", name) try: yield name finally: print("close", name) try: with locker("L7") as l: print("using", l) raise ValueError("jammed") except ValueError as e: print("caught", e)Output
open L7 using L7 close L7 caught jammed
What the interviewer is checking: Resource-safety thinking and knowing the
try/finallydetail inside generator-based managers.Likely follow-up: What does
with open(a) as f, open(b) as gguarantee if the second open fails?14Why does this print
[2, 2, 2], and how do you fix it?[lambda: i for i in range(3)]IntermediateClosures capture variables, not values. All three lambdas refer to the same
ifrom the enclosing scope, and by the time you call them the loop has finished withi == 2. This is called late binding.Fix it by binding the current value at definition time with a default argument,
lambda i=i: i, or by usingfunctools.partial, or a small factory function that takes the value as a parameter. The problem is not specific to lambdas: any function defined in a loop and called later has it, for example callbacks registered for a list of buttons or tasks.python def make(): return [lambda: i for i in range(3)] print([f() for f in make()]) def make2(): return [lambda i=i: i for i in range(3)] print([f() for f in make2()])Output
[2, 2, 2] [0, 1, 2]
What the interviewer is checking: A real understanding of closure scoping rather than a memorised trick.
Likely follow-up: Does the same thing happen in JavaScript with
letin a for loop?15Explain Python's LEGB rule. What do
globalandnonlocaldo, and what isUnboundLocalError?IntermediateName lookup goes Local, Enclosing function, Global (module), then Built-in. Reading a name searches outward; assigning to a name makes it local to the current function unless you declare it
globalornonlocal.nonlocalrebinds a variable in the nearest enclosing function,globalrebinds a module-level one.UnboundLocalErrorappears when a function assigns to a name anywhere in its body, which makes that name local for the whole function, and then reads it before the assignment runs. The fix is to rename, pass the value in, or declare itglobal. Mutating an object, likeitems.append(x), does not need a declaration because it is not rebinding.python rate = 10 def outer(): rate = 20 def inner(): nonlocal rate rate += 1 return rate return inner() print(outer(), rate) def bad(): print(rate) rate = 5 try: bad() except UnboundLocalError as e: print(type(e).__name__, e)Output
21 10 UnboundLocalError cannot access local variable 'rate' where it is not associated with a value
What the interviewer is checking: Scoping fundamentals, including the compile-time decision that a name is local.
Likely follow-up: Why is relying on
globalstate usually a design smell?16What does
@dataclassgive you, and what is the catch with mutable defaults?Intermediate@dataclassgenerates boilerplate from annotated fields:__init__,__repr__and__eq__, and optionally ordering methods, plusfrozen=Truefor immutability. That makes it ideal for plain data carriers, such as a fare quote or an API payload, with less code and fewer typos.For mutable defaults like a list, you must use
field(default_factory=list); writing= []raises aValueErrorprecisely to avoid the shared-default bug. Note that a frozen dataclass witheq=Trueis hashable, so it can be a dict key or set member, but only if its fields are themselves hashable.python from dataclasses import dataclass, field @dataclass(frozen=True) class Fare: km: float surge: float = 1.0 def total(self): return round(self.km * 12 * self.surge, 2) f = Fare(3.5, 1.5) print(f, f.total(), f == Fare(3.5, 1.5)) try: f.km = 2 except Exception as e: print(type(e).__name__) @dataclass class Good: items: list = field(default_factory=list) print(Good(), Good().items is Good().items)Output
Fare(km=3.5, surge=1.5) 63.0 True FrozenInstanceError Good(items=[]) False
What the interviewer is checking: Knowing when to reach for dataclasses and how they avoid the mutable-default trap.
Likely follow-up: When would you choose a dataclass over a NamedTuple or a plain dict?
17If you define
__eq__on a class, what happens to__hash__, and why does it matter?IntermediateIn Python 3, defining
__eq__without__hash__sets__hash__toNone, making instances unhashable. That is intentional: objects that compare equal must have equal hashes, and the default identity-based hash would violate that.If instances should be usable in sets or as dict keys, define
__hash__from the same fields that__eq__compares, and make those fields effectively immutable. Hashing on a field that later changes corrupts the container, because the object sits in the wrong bucket and lookups fail. If instances are mutable, leaving them unhashable is the safer choice.What the interviewer is checking: Understanding the equality/hash contract and the risk of hashing mutable state.
Likely follow-up: How does
@dataclass(eq=True)decide whether to generate__hash__?18Why is removing items from a list while looping over it a bug, and what should you do instead?Intermediate
The list iterator walks by index. When you remove an element, everything after it shifts left, so the next element is skipped. The loop does not raise an error, it silently produces wrong results, which makes it a nasty bug.
Build a new list instead: a comprehension with the condition you want to keep, or
filter. If you must mutate in place, iterate over a copy (for x in items[:]) or assign back withitems[:] = [...]. Dicts and sets behave differently: changing their size during iteration raises aRuntimeError, so loop overlist(d)when you need to delete keys.What the interviewer is checking: Whether you can explain the mechanism and not just state the rule.
Likely follow-up: What happens if you add a key to a dict inside a loop over that dict?
19What is the GIL, and what does it mean for multithreaded Python code?Advanced
In standard CPython, the Global Interpreter Lock lets only one thread execute Python bytecode at a time. It exists because CPython's memory management, especially reference counting, is not thread-safe without it, and it makes the interpreter and many C extensions simpler and fast for single-threaded code.
Consequences: threads do not speed up CPU-bound pure-Python work, but they work well for I/O-bound work because the lock is released while waiting on sockets, files or sleeps. The GIL does not make your code thread-safe either:
count += 1is several bytecodes, so you still need locks or queues around shared state. Some C extensions, such as NumPy, release the GIL during heavy computation.What the interviewer is checking: Accurate nuance: what the GIL protects, what it does not, and where threads still help.
Likely follow-up: Does the GIL mean you never need a
threading.Lock?20You need to speed up a Python service. How do you choose between threading, multiprocessing and asyncio?Advanced
Start by identifying the bottleneck. For CPU-bound work, such as image resizing or number crunching in pure Python, use
multiprocessingorProcessPoolExecutor: separate processes have separate interpreters and GILs, at the cost of startup time and serialising data between them.For I/O-bound work with a modest number of tasks, threads are the simplest, especially with blocking libraries. For thousands of concurrent connections,
asynciohas the lowest per-task overhead, but every library in the path must be async-aware, because one blocking call stalls the whole event loop. Measure first; sometimes a better algorithm, caching or a vectorised library beats any concurrency model.What the interviewer is checking: Judgement: matching the concurrency model to the workload and naming the costs.
Likely follow-up: How would you call a blocking library from inside an asyncio program?
21How does
asynciorun concurrent work on a single thread? What doesawaitactually do?Advancedasyncioruns an event loop that schedules coroutines. A coroutine runs until it hits anawaiton something not ready, such as a network read or a sleep; it then yields control to the loop, which runs another ready coroutine. Nothing runs in parallel, tasks simply interleave at await points.asyncio.gatherstarts several awaitables together and collects results, so three one-second waits take about one second in total. The big pitfall is a blocking call liketime.sleepor a synchronous HTTP request inside a coroutine: it freezes the entire loop. Use the async equivalent or run it withasyncio.to_thread. Also, an un-awaited coroutine never runs.python import asyncio, time async def call(name, t): await asyncio.sleep(t) return name async def main(): s = time.perf_counter() r = await asyncio.gather(call("a", 0.3), call("b", 0.3), call("c", 0.3)) print(r, round(time.perf_counter() - s, 1)) asyncio.run(main())Output
['a', 'b', 'c'] 0.3
What the interviewer is checking: A correct mental model of cooperative multitasking and its main failure mode.
Likely follow-up: What is the difference between
gatherand creating tasks withcreate_task?22How does Python manage memory? What is reference counting, and how are cycles handled?Advanced
CPython tracks how many references point to each object. When the count drops to zero, the object is freed immediately, which makes cleanup mostly deterministic. Objects live on a private heap managed by the interpreter, with its own allocator for small objects.
Reference counting alone cannot free cycles, such as two nodes that point to each other, so CPython also has a cyclic garbage collector that periodically finds unreachable groups. Typical leaks are not true leaks but objects kept alive by a global cache, a long-lived list, or a closure. Use
weakreffor caches or back-references that should not keep objects alive. Other implementations like PyPy use different strategies.python import gc, weakref class Node: def __init__(self): self.other = None n1, n2 = Node(), Node() n1.other, n2.other = n2, n1 w = weakref.ref(n1) del n1, n2 print(w() is not None) # cycle keeps both alive gc.collect() print(w())Output
True None
What the interviewer is checking: Depth beyond 'Python has garbage collection': the two mechanisms and their limits.
Likely follow-up: When would you disable or tune the garbage collector?
23What is the iterator protocol, and how does a
forloop use it?AdvancedAn iterable is anything with
__iter__that returns an iterator. An iterator has__next__, returns the next value, and raisesStopIterationwhen exhausted; iterators are also iterable, returning themselves from__iter__. Aforloop callsiter()once on the object, then callsnext()repeatedly and stops quietly onStopIteration.This is why lists can be looped over many times, since each loop gets a fresh iterator, while a generator can only be consumed once. It also means you can make your own class loopable by implementing the two methods, or more simply by writing
__iter__as a generator function.What the interviewer is checking: Precision about iterable versus iterator and what the language does behind a loop.
Likely follow-up: How would you make this class re-iterable?
24How does
super()work with multiple inheritance, and what is the MRO?AdvancedThe method resolution order is the linear order in which Python searches classes for an attribute. It is computed with the C3 algorithm, which keeps each parent before its own bases and preserves the order you listed bases in. You can inspect it with
Class.__mro__.super()does not mean 'my parent'; it means 'the next class after me in the instance's MRO'. In a diamond, that lets each class callsuper().method()and have every class run exactly once, as long as all of them cooperate by callingsuper(). If one class in the chain skips the call, the classes after it never run.What the interviewer is checking: Whether you know
super()follows the instance's MRO rather than the direct parent.Likely follow-up: When would you prefer composition over multiple inheritance?
25What does
__slots__do, and what are its trade-offs?AdvancedBy default every instance stores its attributes in a per-instance
__dict__. Declaring__slots__ = ("x", "y")replaces that dict with fixed storage for the named attributes, which reduces memory per instance and blocks accidental creation of new attributes.It pays off when you create very large numbers of small objects, such as millions of points or events. The trade-offs: no arbitrary new attributes, no
__dict__or weak references unless you add__weakref__, and slightly trickier inheritance, since each subclass needs its own slots to keep the benefit. Measure before using it; for ordinary code it is premature.What the interviewer is checking: Understanding of object layout and the discipline to optimise only with evidence.
Likely follow-up: How would you measure whether slots actually helped?
26A long-running Python service keeps growing in memory. How do you find the cause?Advanced
First confirm it is a real leak: plot resident memory over time and check it keeps climbing across requests rather than plateauing. Then compare snapshots. The standard library's
tracemallocrecords allocation sites; take a snapshot, run load, take another, and diff them to see which lines allocated the most surviving memory.Typical culprits are unbounded caches or dicts keyed by request data, lists that only append, references held by closures or global registries, and cycles involving objects with heavy state. Fixes include a bounded cache (
functools.lru_cache(maxsize=...)or a TTL cache), weak references, and clearing per-request state. Also rule out native memory from C extensions, whichtracemalloccannot see.What the interviewer is checking: A methodical debugging approach with the right tool, not guesswork.
Likely follow-up: What is the difference between a leak in Python objects and a leak in a C extension?
Fill the gaps with a free course
If one of these Python answers felt shaky, these course covers the same ground step by step: Python Fundamentals.
More interview questions
- Java
- Spring Boot
- JavaScript
- React
- Node.js
- C++
- Data Structures & Algorithms
- SQL & Databases
- System Design
- DevOps
- AWS & Cloud
- Security
- Generative AI & LLMs
- Machine Learning
- Behavioral & HR
Practising Python for a real opening? Upload your resume to see which open jobs list Python skills you already have, or browse open roles.