Python Interview Prep
Top 30 Python interview questions with detailed answers and production-quality code examples for all levels.
Core Language
Q1: What is the GIL and how does it affect concurrency?
The Global Interpreter Lock is a mutex in CPython that allows only one thread to execute Python bytecode at a time. This means:
- Threads don’t parallelize CPU-bound work — only one core is used
- Threads do help I/O-bound work — the GIL is released during I/O, so threads can overlap waiting
multiprocessingbypasses the GIL by spawning separate processes
# CPU-bound: threads don't help
import threading, time
def cpu_task():
sum(i**2 for i in range(5_000_000))
# Sequential and threaded take roughly the same time for CPU work
# Use multiprocessing.Pool for real CPU parallelism
Q2: Explain mutable default arguments — what’s the trap?
Default argument values are evaluated once at function definition time, not on every call.
# Bug
def append(item, lst=[]):
lst.append(item)
return lst
append(1) # [1]
append(2) # [1, 2] — NOT [2]! Same list object reused.
# Fix: use None as sentinel
def append(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Q3: What is a decorator and how do you write one?
A decorator is a callable that takes a function and returns a replacement function. It adds behavior without modifying the original.
import functools, time
def timeit(func):
@functools.wraps(func) # preserves __name__, __doc__
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timeit
def slow():
time.sleep(0.1)
slow() # "slow took 0.1003s"
Q4: What’s the difference between @staticmethod and @classmethod?
class MyClass:
count = 0
@staticmethod
def utility(x):
# No access to class or instance — like a regular function in the class namespace
return x * 2
@classmethod
def create(cls):
# cls is the class itself — works correctly with subclasses
obj = cls()
cls.count += 1
return obj
@classmethod is used for alternative constructors. @staticmethod for namespace organization.
Q5: How does Python’s memory management work?
- Reference counting — every object has a reference count. Reaches zero → immediately freed.
- Cyclic GC — handles reference cycles (A → B → A) that ref counting can’t break.
- Memory pools — CPython pre-allocates pools for small objects to avoid fragmentation.
import sys
x = [1, 2, 3]
sys.getrefcount(x) # 2 (x + argument)
y = x
sys.getrefcount(x) # 3
del y
sys.getrefcount(x) # 2
Data Structures and Algorithms
Q6: What are Python’s built-in data structures and their complexities?
| Structure | Access | Search | Insert | Delete |
|---|---|---|---|---|
| list | O(1) | O(n) | O(1) amortized (end) | O(n) |
| dict | O(1) avg | O(1) avg | O(1) avg | O(1) avg |
| set | — | O(1) avg | O(1) avg | O(1) avg |
| deque | O(n) | O(n) | O(1) both ends | O(1) both ends |
Q7: How do you reverse a list in Python? Name all the ways.
lst = [1, 2, 3, 4, 5]
lst[::-1] # new reversed list (slicing)
list(reversed(lst))# new reversed list (iterator)
lst.reverse() # in-place, returns None
sorted(lst, reverse=True) # sorted reversed copy
Q8: How do you find duplicates in a list?
from collections import Counter
def find_duplicates(lst):
return [item for item, count in Counter(lst).items() if count > 1]
find_duplicates([1, 2, 2, 3, 3, 3, 4]) # [2, 3]
Q9: What is a generator and when would you use one?
A generator function uses yield to produce values lazily. Use when the dataset is large or you only need to iterate once.
def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip()
# Processes line by line — constant memory regardless of file size
for line in read_large_file("10gb_file.log"):
process(line)
Q10: How does list.sort() differ from sorted()?
list.sort() sorts in place and returns None. sorted() returns a new sorted list and works on any iterable.
lst = [3, 1, 4, 1, 5]
lst.sort() # modifies lst, returns None
sorted(lst) # returns new list, lst unchanged
sorted("hello") # works on any iterable: ['e', 'h', 'l', 'l', 'o']
OOP and Design
Q11: What is duck typing?
Python doesn’t check types — it checks behavior. If an object has the right methods, it works.
def process(stream):
for line in stream: # works for file, list, generator, StringIO...
print(line.strip())
process(open("file.txt"))
process(["line 1\n", "line 2\n"])
process(line + "\n" for line in ["a", "b"])
Q12: What are dunder (magic) methods?
Double-underscore methods customize Python’s built-in operations:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # repr(v)
return f"Vector({self.x}, {self.y})"
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __len__(self): # len(v)
return 2
def __eq__(self, other): # v1 == v2
return self.x == other.x and self.y == other.y
Q13: What is multiple inheritance and the MRO?
Python supports multiple inheritance. The Method Resolution Order (MRO) determines which class’s method is called using the C3 linearization algorithm.
class A:
def hello(self): return "A"
class B(A):
def hello(self): return "B"
class C(A):
def hello(self): return "C"
class D(B, C):
pass
D().hello() # "B" — follows MRO
D.__mro__ # (D, B, C, A, object)
Q14: What is super() and when do you use it?
super() delegates method calls to the next class in the MRO. Always use it instead of naming the parent class directly.
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # calls Animal.__init__
self.breed = breed
Functional Python
Q15: What is map(), filter(), and when should you use them?
numbers = [1, 2, 3, 4, 5]
list(map(lambda x: x**2, numbers)) # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]
# Prefer comprehensions for readability:
[x**2 for x in numbers]
[x for x in numbers if x % 2 == 0]
Use map/filter with named functions when the function already exists.
Q16: What is functools.partial?
Creates a new function with some arguments pre-filled:
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
square(5) # 25
cube(3) # 27
Q17: Explain closures.
A closure is a function that captures variables from its enclosing scope:
def make_multiplier(factor):
def multiply(x):
return x * factor # factor is captured from outer scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
Concurrency and I/O
Q18: When would you use asyncio vs threading vs multiprocessing?
asyncio— I/O-bound, async-native code (web APIs, database, sockets). Thousands of concurrent connections in one thread.threading— I/O-bound, blocking libraries. GIL limits true CPU parallelism.multiprocessing— CPU-bound work. Each process has its own GIL and memory.
Q19: What is the difference between @property and a regular attribute?
@property makes a method behave like an attribute, adding computed logic and validation:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
import math
return math.pi * self._radius ** 2
c = Circle(5)
c.radius = -1 # raises ValueError
c.area # computed, not stored
Advanced Topics
Q20: What are __slots__ and when do they help?
__slots__ restricts instance attributes to a fixed set, eliminating the per-instance __dict__. Saves 40-60% memory when creating millions of instances.
class FastPoint:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x; self.y = y
Q21: What is a context manager and how do you create one?
from contextlib import contextmanager
@contextmanager
def managed_resource():
resource = acquire()
try:
yield resource
finally:
release(resource)
with managed_resource() as r:
use(r)
Q22: How does @lru_cache work?
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
It caches results in a dict keyed by arguments. The “LRU” (Least Recently Used) eviction policy keeps the most recently used results up to maxsize.
Q23: What is the difference between is and ==?
== tests value equality (calls __eq__). is tests identity (same object in memory).
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True
a is b # False
x = None
x is None # correct
x == None # works but style violation
Q24: How do you handle circular imports?
Move the import inside the function that uses it, or restructure to extract shared code into a third module.
# Circular: a.py imports b.py, b.py imports a.py
# Fix: import inside the function
def get_user():
from myapp.models import User # deferred import
return User.find(1)
Q25: What is *args and **kwargs?
def variadic(*args, **kwargs):
print(args) # tuple of positional arguments
print(kwargs) # dict of keyword arguments
variadic(1, 2, 3, name="Alice", age=30)
# (1, 2, 3)
# {'name': 'Alice', 'age': 30}
# Unpack into a function call
def add(a, b, c): return a + b + c
args = (1, 2, 3)
add(*args) # 6
Q26: What is a descriptor?
An object that defines __get__, __set__, or __delete__ to customize attribute access. property, staticmethod, and classmethod are all descriptors.
class Validated:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None: return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, int) or value < 0:
raise ValueError(f"{self.name} must be a non-negative int")
obj.__dict__[self.name] = value
class Order:
quantity = Validated()
price = Validated()
Q27: How do you flatten a nested list?
# Recursive generator
def flatten(lst):
for item in lst:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
list(flatten([1, [2, [3, 4]], [5]])) # [1, 2, 3, 4, 5]
# One level with itertools
from itertools import chain
list(chain.from_iterable([[1,2],[3,4]])) # [1, 2, 3, 4]
Q28: What is the difference between shallow and deep copy?
import copy
original = [[1, 2], [3, 4]]
shallow = original.copy() # or original[:]
deep = copy.deepcopy(original)
original[0].append(99)
print(shallow[0]) # [1, 2, 99] — inner list is shared
print(deep[0]) # [1, 2] — completely independent
Q29: How do you make a class hashable?
Implement both __hash__ and __eq__. If you define __eq__ without __hash__, Python sets __hash__ = None making the class unhashable.
class Point:
def __init__(self, x, y):
self.x = x; self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
p = Point(1, 2)
{p} # works — hashable
{p: "origin"} # works as dict key
Q30: What is typing.Protocol and how is it different from ABC?
Protocol enables structural subtyping — an object matches a Protocol if it has the right attributes, regardless of inheritance.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
# Circle never inherits from Drawable
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # works — structural compatibility, no ABC needed
Use Protocol for interfaces that third-party types should satisfy. Use ABC when you own the hierarchy and want to enforce implementation.