Python Interview Preparation
How to use this guide?
Interviews test three things: conceptual knowledge, problem solving, and communication. For Python roles you will be evaluated on language fundamentals, standard library knowledge, algorithms/data structures in Python, design patterns and idiomatic usage, and practical system-level concerns (performance, memory, concurrency, testing). Use this guide to revise high-density topics, practice code snippets until you can write and explain them, and rehearse concise explanations of tricky concepts.
Practical: keep a copy of key snippets in a single file; run them in a REPL to verify outputs. Prepare to explain why a solution works and discuss complexity/alternatives.
Most frequently asked Python interview questions
Below are common questions with short theory and runnable examples or snippets.
Q1: What are Python’s core data types? When to use which?
Theory: Built-in core types include int, float, bool, str, list, tuple, set, dict, bytes. Use list for ordered, mutable collections; tuple for ordered, immutable collections; set for unique elements and membership tests; dict for key→value mapping. Use str for text (immutable). Use bytes for binary data.
Practical:
# list
l = [1, 2, 3]
# tuple
t = (1, 2, 3)
# set
s = {1, 2, 3}
# dict
d = {'a': 1, 'b': 2}
Q2: Difference between list and tuple? Why use tuple?
Theory: Tuples are immutable and can be used as dict keys if they contain hashable items. They are slightly faster and indicate an invariant collection. Use tuple for fixed-size heterogeneous data (like function return values).
Practical:
coords = (10.0, 20.0) # tuple is good here
# trying to modify will raise
# coords[0] = 5 -> TypeError
Q3: How does Python manage memory? What is reference counting and the garbage collector?
Theory: CPython uses reference counting (each object tracks how many references point to it). When refcount hits zero the object is deallocated. To handle cycles, CPython also has a cyclic garbage collector that periodically detects reference cycles of unreachable objects and frees them.
Practical:
import sys
a = []
b = a
print(sys.getrefcount(a)) # >= 2 (one from 'a', one from getrefcount arg)
del b
print(sys.getrefcount(a))
Q4: Explain mutable vs immutable. Why does list.copy() matter?
Theory: Mutable objects (list, dict, set) can change in-place; immutable objects (tuple, str, int) don’t. Copying avoids unintentional shared-state bugs. Use shallow copy (list.copy() or list[:]) to duplicate top-level structure. Use copy.deepcopy() for nested structures.
Practical:
import copy
a = [1, [2, 3]]
b = a.copy() # shallow
b[1].append(4)
print(a) # nested list mutated
c = copy.deepcopy(a) # deep copy
c[1].append(5)
print(a, c)
Q5: Explain *args and **kwargs.
Theory: *args collects positional arguments into a tuple. **kwargs collects keyword arguments into a dict. They allow flexible argument lists and are used for wrapper functions or APIs.
Practical:
def f(*args, **kwargs):
print(args, kwargs)
f(1,2, x=10, y=20)
# pass-through
def wrapper(*a, **kw):
return f(*a, **kw)
Q6: What is list comprehension? Why use it?
Theory: List comprehensions provide a concise, often faster, and more readable way to build lists from iterables with optional filtering. They are syntactic sugar over loops.
Practical:
nums = [1,2,3,4,5]
squares = [n*n for n in nums if n % 2 == 1]
Q7: Generator vs list — when to use?
Theory: Generators produce items lazily, memory efficient for large sequences, and can represent infinite sequences. Lists are materialized in memory. Use generators when you stream large data.
Practical:
def gen():
for i in range(10**6):
yield i
g = gen()
next(g) # produces one item without building entire list
Q8: Explain yield and generator comprehension.
Theory: yield suspends function execution and returns a value to the caller while maintaining state for resumption. Generator comprehensions use parentheses: (x*x for x in range(10)).
Practical:
def fibonacci(n):
a,b = 0,1
for _ in range(n):
yield a
a,b = b, a+b
for x in fibonacci(5):
print(x)
Q9: Decorators — what are they and how to preserve metadata?
Theory: Decorators are callables that take and return functions (or callables). They wrap behavior. Use functools.wraps to preserve __name__, __doc__, and metadata.
Practical:
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*a, **kw):
print("calling", func.__name__)
return func(*a, **kw)
return wrapper
@log
def add(a,b):
return a+b
Q10: Explain lambda and when not to use it.
Theory: lambda creates anonymous single-expression functions. Prefer named functions for complex logic or when reusability/readability matters.
Practical:
squared = lambda x: x*x
sorted_list = sorted([3,1,2], key=lambda x: -x)
Q11: What are __init__, __new__, and __repr__?
Theory: __init__ initializes instance after creation. __new__ creates/returns a new instance (rarely overridden). __repr__ returns unambiguous string representation for debugging; implement __str__ for user-facing representation.
Practical:
class C:
def __new__(cls, *a, **k):
return super().__new__(cls)
def __init__(self, x):
self.x = x
def __repr__(self):
return f"C({self.x!r})"
Q12: Context manager and with statement — how to write one?
Theory: Context managers manage setup/cleanup (e.g., file handling). Implement __enter__ and __exit__ or use contextlib.contextmanager.
Practical:
class MyCtx:
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc, tb):
print("exit")
with MyCtx():
print("inside")
Or:
from contextlib import contextmanager
@contextmanager
def my_ctx():
print("enter")
try:
yield
finally:
print("exit")
Q13: Explain exceptions and custom exceptions.
Theory: Use exceptions for error handling. Create custom exceptions by subclassing Exception to signal domain-specific errors. Prefer specific exceptions to avoid catching everything.
Practical:
class MyError(Exception):
pass
def f(x):
if x < 0:
raise MyError("x must be >= 0")
Q14: What is the Global Interpreter Lock (GIL)?
Theory: In CPython, GIL ensures only one native thread executes Python bytecode at a time. It simplifies memory management but limits multi-thread CPU-bound parallelism. I/O-bound tasks still benefit from threads; CPU-bound tasks need multiprocessing or native extensions releasing GIL.
Practical (demo of threads not speeding up CPU-bound task):
import threading, time
def busy(n):
s = 0
for i in range(n):
s += i*i
t1 = threading.Thread(target=busy, args=(10**7,))
t2 = threading.Thread(target=busy, args=(10**7,))
start = time.time()
t1.start(); t2.start(); t1.join(); t2.join()
print("time:", time.time() - start)
(Compare with multiprocessing for real parallelism.)
Q15: Explain map, filter, reduce, and idiomatic Python alternatives.
Theory: map/filter return iterators (in Py3), reduce in functools. List comprehensions/generator expressions are usually clearer.
Practical:
from functools import reduce
nums = [1,2,3,4]
doubled = list(map(lambda x:x*2, nums))
odd = list(filter(lambda x: x%2, nums))
sum_all = reduce(lambda a,b: a+b, nums)
# better:
doubled = [x*2 for x in nums]
Q16: Difference between is and ==?
Theory: is checks identity (same object), == checks equality via __eq__. Use is for None checks (x is None) and singletons.
Practical:
a = [1,2]
b = a
c = [1,2]
print(a is b) # True
print(a == c) # True
print(a is c) # False
Q17: Explain comprehensions for dicts and sets.
Theory: Similar to list comprehensions; compact and efficient.
Practical:
squares = {x: x*x for x in range(5)}
unique = {x%3 for x in range(10)}
Q18: How to profile Python code and measure performance?
Theory: Use timeit for microbenchmarks, cProfile and py-spy or perf for CPU profiling, tracemalloc for memory. Optimize after measuring; premature optimization is bad.
Practical:
import timeit
print(timeit.timeit("sum(range(1000))", number=1000))
Q19: Explain immutability pitfalls (mutable default arguments).
Theory: Default args are evaluated once at function definition. Using mutable defaults leads to state shared across calls.
Practical:
def f(a, lst=[]):
lst.append(a)
return lst
print(f(1)) # [1]
print(f(2)) # [1, 2] <- surprising
# Right way
def f(a, lst=None):
if lst is None:
lst = []
lst.append(a)
return lst
Q20: What is __slots__?
Theory: __slots__ restricts allowable attributes and prevents creating __dict__, reducing memory usage for many instances. Use for performance-critical, memory-constrained classes.
Practical:
class Point:
__slots__ = ('x','y')
def __init__(self,x,y):
self.x, self.y = x, y
Q21: Iterators vs Iterables — how to implement custom iterator?
Theory: Iterable implements __iter__ returning an iterator. Iterator implements __next__ and __iter__ (returns self). StopIteration signals end.
Practical:
class Count:
def __init__(self, n):
self.n = n
def __iter__(self):
self.i = 0
return self
def __next__(self):
if self.i < self.n:
v = self.i
self.i += 1
return v
raise StopIteration
for x in Count(3):
print(x)
Q22: How to do concurrency — threads, processes, async?
Theory: Threads: good for I/O-bound. Multiprocessing: use separate processes for CPU-bound. asyncio: cooperative concurrency using async/await for high-concurrency I/O without threads. Choose based on workload and libraries’ async compatibility.
Practical (async example):
import asyncio, aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as s:
async with s.get(url) as r:
return await r.text()
async def main():
html = await fetch('https://example.com')
print(len(html))
asyncio.run(main())
Q23: Explain Python’s sort() vs sorted() and key usage.
Theory: list.sort() sorts in place and returns None. sorted() returns a new sorted list and accepts any iterable. Use key for customizing order; use reverse=True for descending.
Practical:
data = [{'name':'b','age':30},{'name':'a','age':25}]
sorted_by_age = sorted(data, key=lambda x: x['age'])
data.sort(key=lambda x: x['name'])
Q24: Explain pickling and its security implications.
Theory: pickle serializes Python objects, but unpickling untrusted data is unsafe (code execution risk). Use JSON for simple types; use pickle only for trusted environments.
Practical:
import pickle
d = {'a':1}
s = pickle.dumps(d)
obj = pickle.loads(s)
Q25: Explain namedtuple and dataclass.
Theory: namedtuple provides lightweight immutable tuples with named fields. dataclass (Python 3.7+) simplifies writing classes with boilerplate (init, repr, eq). Dataclasses can be frozen (immutable) and support default factories.
Practical:
from collections import namedtuple
P = namedtuple('P', ['x','y'])
p = P(1,2)
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int = 0
Q26: How to handle dependencies and virtual environments?
Theory: Use venv (or virtualenv) to isolate dependencies. Keep requirements.txt (pip freeze) or pyproject.toml/poetry.lock for reproducible builds.
Practical:
python -m venv venv
source venv/bin/activate
pip install requests
pip freeze > requirements.txt
Q27: Explain asyncio event loop basics
Theory: asyncio runs tasks on an event loop; coroutines (async def) yield control using await. IO-bound code with async-aware libraries can scale to thousands of concurrent tasks.
Practical:
import asyncio
async def say(n):
await asyncio.sleep(1)
print(n)
async def main():
await asyncio.gather(*(say(i) for i in range(5)))
asyncio.run(main())
Tricky concepts and deep explanations
Below are commonly probed tricky areas. Understand them well and practice explaining with examples.
A. Mutable default arguments (revisited)
Theory: See earlier — defaults evaluated once. Always use None + factory.
Practical (quick demo shown earlier).
B. Late binding closures in loops
Theory: A classic gotcha: closures capture variables by reference (not value) leading to surprising results when creating functions in loops.
Practical:
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2,2,2] - all use final i
# Fix: bind current value
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0,1,2]
C. Method resolution order (MRO) and multiple inheritance
Theory: Python uses C3 linearization for MRO. Explain with diamond hierarchy examples. super() follows MRO.
Practical:
class A:
def m(self): print('A')
class B(A):
def m(self): print('B'); super().m()
class C(A):
def m(self): print('C'); super().m()
class D(B,C):
def m(self): print('D'); super().m()
D().m()
# Output order follows MRO: D B C A
D. Descriptor protocol (__get__, __set__)
Theory: Descriptors control attribute access; used to implement properties, methods, and attributes on classes. Understand data vs non-data descriptors.
Practical:
class Reveal:
def __get__(self, obj, objtype=None):
return "revealed"
class Obj:
x = Reveal()
o = Obj()
print(o.x) # "revealed"
E. Metaclasses (theory)
Theory: Metaclasses create classes; used rarely but useful for frameworks. type is the default metaclass. Use only when you need to modify class creation.
Practical (basic):
class Meta(type):
def __new__(mcls, name, bases, dct):
dct['added'] = 42
return super().__new__(mcls, name, bases, dct)
class C(metaclass=Meta):
pass
print(C.added) # 42
F. __slots__ vs __dict__
Theory: __slots__ reduces memory; prevents dynamic attribute creation unless __dict__ included.
Practical: Shown earlier.
G. Thread-safety and CPython internals
Theory: GIL simplifies single-interpreter thread-safety but not for shared mutable data—use locks or thread-safe queues.
Practical:
from threading import Thread, Lock
counter = 0
lock = Lock()
def inc():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = [Thread(target=inc) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)
H. Coroutines vs Generators — subtle difference
Theory: Generators produce data (yield), coroutines consume using send(); async/await are built on top of coroutines but are different building blocks.
Practical: Advanced topic — show yield vs await usage if asked.
Best practices interviewers expect
A. Code style & readability
Theory: Follow PEP8 naming, spacing, line length, docstrings. Prefer explicit code over clever one-liners.
Practical:
# Bad
def f(x): return x+1
# Better
def increment(x):
"""Return x + 1"""
return x + 1
B. Type hints for clarity
Theory: Use typing to document intent and enable static checks.
Practical:
from typing import List, Dict
def average(values: List[float]) -> float:
return sum(values) / len(values)
C. Write tests
Theory: Unit tests and small testable functions show professionalism.
Practical:
def add(a,b): return a+b
def test_add():
assert add(2,3) == 5
D. Logging vs printing
Theory: Use logging for production-level traceability with levels and handlers.
Practical:
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
logger.info("starting")
E. Avoid premature optimization
Theory: Profile, measure, then optimize. Keep code correct and clear first.
F. Use standard library
Theory: Prefer built-in modules (itertools, functools, collections, pathlib) over reinventing solutions.
Practical:
from collections import Counter
Counter(['a','b','a'])
G. Secure coding practices
Theory: Avoid exec/eval on untrusted input, sanitize inputs, secure secrets, and validate user data. For web work use established frameworks and libraries.
Common mistakes (theory + how to avoid)
Mistake 1: Mutable default argument — fix with None.
(Explained earlier.)
Mistake 2: Shadowing builtins (e.g., using list, str, id as variable names)
Avoidance: Choose variable names that don't shadow builtins. If you accidentally shadow, reassigning can break standard functions.
Practical:
list = [1,2] # avoid — now built-in list is shadowed
del list
Mistake 3: Confusing equality vs identity (== vs is) — use is None pattern
Avoidance: Use is only for singletons.
Mistake 4: Catching broad exceptions (except Exception: or except:)
Avoidance: Catch specific exceptions and re-raise or log with context.
Practical:
try:
x = int(s)
except ValueError:
handle_bad_input()
Mistake 5: Inefficient loops instead of comprehensions or vectorized ops
Avoidance: For numeric data prefer NumPy/Pandas vectorized ops.
Mistake 6: Not closing files — use with
Avoid with:
with open('f') as f:
data = f.read()
Mistake 7: Using pickle for untrusted sources (security risk)
Avoidance: Use JSON or safe serializers.
Mistake 8: Overuse of global variables — leads to hard-to-debug state
Avoidance: Pass arguments, use classes, or modules encapsulation.
Interview strategy & closing tips
- Explain your thinking: Narrate tradeoffs, complexity, and assumptions. Interviewers value reasoning more than a single correct line.
- State time & space complexity: Always give Big-O.
- Start with examples: Manual dry run with a small example to validate approach.
- Write simple correct code first: Then optimize or generalize.
- Handle edge cases: Null/empty inputs, extremes, invalid inputs.
- Test your code aloud: Show sample runs or walk through the code path.
- Know standard libs and idioms: itertools, collections, functools, heapq.
- Practice common patterns: sliding window, two pointers, hashing, sorting-based, recursion with memoization.
- Prepare quick cheat-sheets: For list/dict methods, slicing, comprehension, and iterator protocols.
- Know how to use REPL and write readable variable names during whiteboard or shared-editor interviews.
Quick reference: bite-sized snippets to memorize
- Swap variables:
a, b = b, a
- Reverse list in place:
lst.reverse()
- Slicing:
sub = lst[1:5:2] # start:stop:step
- Dictionary comprehension:
{k:v for k,v in pairs}
- Default dict:
from collections import defaultdict
d = defaultdict(list)
d['a'].append(1)
- Counter:
from collections import Counter
Counter(items).most_common(3)
- Groupby (from itertools):
from itertools import groupby
groups = {k: list(v) for k,v in groupby(sorted(data), key=lambda x: x.key)}
- LRU cache:
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n): ...
Final checklist before interview (practical)
- Know basic algorithms: sorting, binary search, BFS/DFS, two-sum, sliding window.
- Know Python specifics: list vs tuple, comprehension, generator, decorator, context manager, iterators.
- Practice 20–30 coding problems on a whiteboard/editor and explain them.
- Prepare 6–8 good questions for the interviewer about team, codebase, tooling.
- Sleep well and rehearse clear explanations.
