Python Fundamentals
1. List vs Tuple — when do you use each?
List is mutable, tuple is immutable. Use tuples for fixed-size records (returning multiple values, dict keys, hashable sets). Lists for growable sequences.
2. is vs == ?
is checks identity (same object in memory). == checks equality (same value). Use == for value comparisons; reserve is for singletons like None, True, False.
3. Mutable default argument — what's the pitfall?
def add(x, lst=[]): # BAD — list shared across calls
lst.append(x); return lst
Default args are evaluated once at definition time. Fix:
def add(x, lst=None):
if lst is None: lst = []
...
4. What is the GIL?
The Global Interpreter Lock ensures only one thread executes Python bytecode at a time inside CPython. It simplifies the C implementation but limits CPU-bound multithreading. Workarounds: multiprocessing, asyncio (I/O-bound), or native extensions that release the GIL (NumPy).
5. *args and **kwargs?
*args collects positional args into a tuple; **kwargs collects keyword args into a dict. Spread the other way with func(*lst, **dct).
OOP
6. Difference between class method, static method, instance method?
- Instance (
def m(self)) — receives the instance. - Class method (
@classmethod) — receives the class. Used for alternative constructors. - Static method (
@staticmethod) — no implicit first arg. Pure helper that just lives in the class namespace.
7. What is duck typing?
"If it walks like a duck and quacks like a duck, it's a duck." Python doesn't care about declared types — only whether an object supports the operations you use on it. This is being replaced/augmented by structural typing via Protocol in modern type hints.
8. What is __slots__ and why use it?
A class attribute that restricts the allowed instance attributes and replaces __dict__ with a static struct — 30-50% less memory per instance. Useful when you'll create millions of small objects.
Data Structures
9. Dict vs defaultdict vs Counter?
dict— plain key → value.defaultdict(list)— auto-creates a default when a missing key is accessed.Counter—defaultdict(int)plusmost_common(), arithmetic, set ops.
10. List comprehension vs generator expression?
[x*x for x in nums] # list — eager, allocates memory
(x*x for x in nums) # generator — lazy, one item at a time
For large data, prefer generators to keep memory flat.
11. Implement an LRU cache.
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)
Behind the scenes: ordered dict that evicts the least recently used key when the cap is hit.
Async and Concurrency
12. asyncio vs threads vs multiprocessing?
asyncio— single-threaded cooperative concurrency. Best for I/O-bound (HTTP, DB).- Threads — concurrent I/O, but the GIL serialises Python bytecode. Useful for blocking C libraries.
- Multiprocessing — true parallelism by spawning subprocesses. Best for CPU-bound work.
13. async/await — when does the event loop yield?
Only at await points. A long synchronous block inside an async function blocks the whole loop. Move CPU work into run_in_executor or a separate process.
14. What is a coroutine?
A function that can pause itself with await and resume later. async def f(): … defines one. Calling f() returns a coroutine object; you await it to actually run it.
Type Hints and Modern Python
15. Why use type hints if Python is dynamic?
- Tools (mypy, pyright) catch bugs before runtime.
- IDEs offer better autocomplete and refactoring.
- Hints serve as inline documentation that stays in sync with the code.
16. Protocol vs ABC?
ABC— nominal subtyping (must explicitly inherit).Protocol— structural subtyping (duck-typed; matches if shape matches). Closer to Go interfaces / TypeScript.
17. What does @dataclass do?
Generates __init__, __repr__, __eq__ (and optionally __hash__) from declared fields. Use frozen=True for immutable value objects, slots=True for memory wins.
Performance
18. How do you profile slow Python code?
cProfilefor function-level timing.line_profilerfor line-by-line.py-spyfor production sampling profiler (no code changes).memory_profiler/tracemallocfor memory.
19. NumPy vs pure Python for arithmetic?
NumPy operates on contiguous C arrays and offloads loops to optimised C/BLAS — 10-100× faster than equivalent pure-Python loops. Always reach for NumPy when you're doing math on large arrays.
20. What is the "Python is slow" trade-off?
Pure CPython is ~50× slower than C for tight loops. You pay this for productivity. Mitigations: numpy, numba, cython, mypyc, PyPy (alternative JIT-ing interpreter), or rewrite hot paths in Rust/Go via FFI.
Real-World
21. Walk me through a Django request lifecycle.
- WSGI / ASGI server accepts the request.
- URL resolver matches the path to a view.
- Middleware chain runs (auth, sessions, CSRF, GZip).
- View function executes — queries the DB via the ORM, returns a
HttpResponse. - Middleware reverses on the way out.
- Response is serialised + sent.
22. Decorator — write @retry(times=3) from memory.
import functools, time
def retry(times=3, delay=1):
def deco(fn):
@functools.wraps(fn)
def wrap(*a, **kw):
for i in range(times):
try: return fn(*a, **kw)
except Exception:
if i == times - 1: raise
time.sleep(delay)
return wrap
return deco
23. Tell me about a Python bug you debugged in production.
Strong-answer template: (1) symptom in customer terms, (2) hypothesis you tested first, (3) data you collected (logs, profile, tracebacks), (4) actual root cause, (5) the fix + tests + monitoring to prevent recurrence.
24. Pytest fixtures — what are they?
Reusable setup/teardown helpers. Use @pytest.fixture to declare; tests pull them in by adding the fixture name as a function arg.
25. What is __main__ and if __name__ == "__main__": ?
When Python runs a file directly, it sets __name__ == "__main__". The guard lets the same file be importable as a module without executing the script behaviour.
