Python Interview Questions
Covering language fundamentals, memory model, OOP, async, and best practices.
1. Explain the GIL. What are its implications?
The Global Interpreter Lock (CPython) prevents multiple native threads from executing Python bytecode simultaneously. For CPU-bound work, threads don't help — use multiprocessing (separate processes) or asyncio. For I/O-bound work (network, disk), threading works because the GIL is released during I/O waits. NumPy/SciPy release the GIL for their C-level computations, enabling real parallelism.
2. is vs ==
a = [1, 2, 3]; b = [1, 2, 3]; c = a
print(a == b) # True — same value (__eq__)
print(a is b) # False — different objects
print(a is c) # True — same object
# CPython caches small ints (-5 to 256) and some strings
x = 256; y = 256; print(x is y) # True (cached)
x = 257; y = 257; print(x is y) # False (not cached)3. The mutable default argument trap
def bad(item, lst=[]): # list created ONCE at definition
lst.append(item)
return lst
bad(1) # [1]
bad(2) # [1, 2] — unexpected shared state!
def good(item, lst=None): # correct pattern
if lst is None:
lst = []
lst.append(item)
return lst4. How do decorators work?
import functools
def log(func):
@functools.wraps(func) # preserves __name__, __doc__
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Returned {result}")
return result
return wrapper
@log # equivalent to: add = log(add)
def add(a, b):
return a + b
add(2, 3) # Calling add → Returned 55. Generator vs list comprehension
# List: eager, O(n) memory, re-iterable
squares_list = [x**2 for x in range(1000)]
# Generator: lazy, O(1) memory, single-pass
squares_gen = (x**2 for x in range(1000))
# Use generators for large sequences or pipelines
total = sum(x**2 for x in range(1_000_000)) # efficient6. Python memory management
CPython uses reference counting — each object tracks the number of references pointing to it
When reference count reaches 0, memory is freed immediately (deterministic destruction)
Cyclic garbage collector handles circular references, running periodically in generations
__slots__ reduces memory ~40-50% for classes with many instances by avoiding per-instance __dict__
7. asyncio vs threading
Threading: preemptive, OS-scheduled, one thread per OS thread — limited by GIL for CPU-bound
asyncio: cooperative, event-loop-scheduled, single-threaded — no GIL contention, zero thread overhead
Blocking code in asyncio freezes the event loop — use asyncio.run_in_executor() to offload to threads
asyncio shines at high-concurrency I/O (thousands of simultaneous requests with minimal memory)
8. @classmethod vs @staticmethod vs instance method
class User:
def __init__(self, name: str):
self.name = name
def greet(self) -> str: # instance method — has self
return f"Hi, {self.name}"
@classmethod
def from_dict(cls, data: dict) -> 'User': # factory / alternative constructor
return cls(data['name'])
@staticmethod
def validate_name(name: str) -> bool: # utility — no self/cls
return len(name) >= 29. What are Python protocols?
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle: # doesn't inherit from Drawable
def draw(self) -> None:
print("Drawing circle")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # works — structural typing (duck typing + type safety)10. What is __slots__?
class Point:
__slots__ = ('x', 'y') # fixed attributes, no __dict__
def __init__(self, x, y):
self.x = x; self.y = y
# ~40-50% less memory per instance, faster attribute access
# Trade-off: cannot add arbitrary attributes dynamically
# Best for: millions of instances (data processing, simulations)Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free