Python Interview Questions and Answers
These questions span Python fundamentals through advanced patterns β whatβs tested in backend engineer, data engineer, ML engineer, and software developer interviews.
Language Fundamentals
Q1. What are Pythonβs key data types and when do you choose each?
# Immutable typesx: int = 42y: float = 3.14z: complex = 1 + 2js: str = "hello" # Immutable sequence of Unicode charst: tuple = (1, 2, 3) # Immutable ordered sequencef: frozenset = frozenset({1, 2, 3}) # Immutable set (hashable, usable as dict key)b: bytes = b"data" # Immutable byte sequence
# Mutable typeslst: list = [1, 2, 3] # Ordered, mutable sequenced: dict = {"key": "val"} # Hash map, insertion-ordered (Python 3.7+)se: set = {1, 2, 3} # Unordered, unique elements, O(1) membership
# Choose:# list β ordered sequence that changes (append, remove)# tuple β fixed structure, function return values, dict keys# dict β key-based lookup, JSON-like data# set β membership test, deduplication, set operations# frozenset β immutable set (e.g., allowed values as a constant)Q2. Explain the difference between mutable and immutable objects. What is the impact on function arguments?
# Immutable β int, str, tuple: rebinding inside function doesn't affect callerdef modify_int(n): n += 1 # Creates a new int object; original unchanged return n
x = 10modify_int(x)print(x) # 10 β unchanged
# Mutable β list, dict, set: mutating in-place affects caller's objectdef modify_list(lst): lst.append(99) # Mutates the same object in memory
items = [1, 2, 3]modify_list(items)print(items) # [1, 2, 3, 99] β caller sees the change!
# Rebinding (not mutating) doesn't affect caller even for mutablesdef rebind_list(lst): lst = [10, 20] # Creates a new list; local name rebound, caller's unchanged
items = [1, 2, 3]rebind_list(items)print(items) # [1, 2, 3]Python is βpass by object referenceβ β the function receives a reference to the same object.
Q3. What is the difference between == and is in Python?
a = [1, 2, 3]b = [1, 2, 3]c = a
print(a == b) # True β same value (equality check)print(a is b) # False β different objects in memoryprint(a is c) # True β same object (c is an alias for a)
# Interning gotchax = 256y = 256print(x is y) # True β CPython caches small integers (-5 to 256)
x = 257y = 257print(x is y) # False β not cached (CPython implementation detail)
# Rule: use == for value comparison; is only for None/True/False identityif result is None: # Correct idiom print("no result")Functions & Closures
Q4. What are decorators in Python and how do you write one?
A decorator wraps a function to extend or modify its behavior without changing its source:
import functoolsimport time
def timer(func): @functools.wraps(func) # Preserves __name__, __doc__ of wrapped function 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
@timerdef slow_function(n): """Compute sum.""" return sum(range(n))
slow_function(1_000_000) # slow_function took 0.0312s
# Decorator with argumentsdef retry(max_attempts=3, exceptions=(Exception,)): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except exceptions as e: if attempt == max_attempts: raise print(f"Attempt {attempt} failed: {e}. Retrying...") return wrapper return decorator
@retry(max_attempts=3, exceptions=(ConnectionError,))def fetch_data(url): ...Q5. What is a closure and how do you use it?
A closure is a function that retains access to variables from its enclosing scope even after that scope has returned:
def make_counter(start=0, step=1): count = start # Enclosed variable
def counter(): nonlocal count # nonlocal required to rebind (not just read) count += step return count
return counter
counter = make_counter(start=10, step=5)print(counter()) # 15print(counter()) # 20print(counter()) # 25
# Each call creates an independent closurecounter_a = make_counter()counter_b = make_counter(start=100)counter_a() # 1counter_b() # 101Closures are the mechanism behind decorators, factory functions, and callbacks with captured state.
Q6. What are generators and when should you use them over lists?
A generator is a function that yields values one at a time, suspending execution between yields:
def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b
fib = fibonacci()print([next(fib) for _ in range(10)]) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Generator expression (lazy list comprehension)squares = (x**2 for x in range(1_000_000)) # No memory allocated yettotal = sum(squares) # Only one value in memory at a time
# Practical use: large file processingdef read_chunks(filepath, size=8192): with open(filepath, 'rb') as f: while chunk := f.read(size): yield chunk
for chunk in read_chunks('large_video.mp4'): process(chunk) # Memory-efficient β only one chunk loaded at a timeUse generators when: data is large (doesnβt fit in RAM), data is infinite (streams), you only need to iterate once.
OOP & Design
Q7. Explain Pythonβs class system: __init__, __new__, class methods, and static methods.
class Temperature: _unit = 'celsius' # Class variable (shared across all instances)
def __new__(cls, *args, **kwargs): # Called before __init__; creates the instance # Override only for metaclass magic or immutable types (float subclassing) return super().__new__(cls)
def __init__(self, value: float): self._value = value # Instance variable
@property def fahrenheit(self): return self._value * 9/5 + 32
@classmethod def from_fahrenheit(cls, f_value: float) -> 'Temperature': # cls refers to the class (or subclass if called on subclass) return cls((f_value - 32) * 5/9)
@staticmethod def is_valid(value: float) -> bool: # No access to class or instance; pure utility return value >= -273.15
# Usaget1 = Temperature(100)t2 = Temperature.from_fahrenheit(212)print(t2._value) # 100.0print(Temperature.is_valid(-300)) # FalseQ8. What are dunder (magic) methods and what are the most commonly used ones?
class Vector: def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): # Unambiguous string for developers return f"Vector({self.x}, {self.y})"
def __str__(self): # Human-readable string return f"({self.x}, {self.y})"
def __add__(self, other): # v1 + v2 return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar): # v * 3 return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # 3 * v return self.__mul__(scalar)
def __abs__(self): # abs(v) return (self.x**2 + self.y**2)**0.5
def __eq__(self, other): # v1 == v2 return self.x == other.x and self.y == other.y
def __hash__(self): # Needed if __eq__ is defined; enables dict keys/sets return hash((self.x, self.y))
def __len__(self): # len(v) return 2
def __iter__(self): # for coord in v: yield self.x yield self.y
def __getitem__(self, idx): # v[0] return (self.x, self.y)[idx]
v = Vector(3, 4)print(abs(v)) # 5.0print(3 * v) # (9, 12)Concurrency
Q9. Explain Pythonβs GIL and when to use threading vs multiprocessing vs async.
The GIL (Global Interpreter Lock) prevents multiple Python threads from executing Python bytecode simultaneously in CPython. Only one thread runs at a time.
Practical impact:
- CPU-bound tasks (number crunching, image processing): threading gives NO speedup (GIL blocks parallelism). Use
multiprocessing. - I/O-bound tasks (HTTP requests, file reads, DB queries): threading works because the GIL is released during I/O waits. Use
threadingorasyncio.
import asyncioimport aiohttp
# asyncio β single thread, event loop, best for many concurrent I/O opsasync def fetch(session, url): async with session.get(url) as resp: return await resp.json()
async def fetch_all(urls): async with aiohttp.ClientSession() as session: tasks = [fetch(session, url) for url in urls] return await asyncio.gather(*tasks)
# Fetch 100 URLs concurrently in ~same time as 1 with asyncioresults = asyncio.run(fetch_all(urls))from multiprocessing import Poolimport numpy as np
def heavy_compute(array): return np.sum(array ** 2)
# True parallelism β each process has its own Python interpreter (no GIL)with Pool(processes=4) as pool: results = pool.map(heavy_compute, [np.random.rand(10**6) for _ in range(4)])Note: Python 3.13 (2024) is experimenting with a no-GIL build β watch this space.
Q10. What is asyncio and how does the event loop work?
import asyncio
async def fetch_user(user_id: int) -> dict: await asyncio.sleep(0.1) # Simulates I/O β suspends this coroutine return {"id": user_id, "name": f"User {user_id}"}
async def main(): # Sequential β 0.3 seconds u1 = await fetch_user(1) u2 = await fetch_user(2) u3 = await fetch_user(3)
# Concurrent β 0.1 seconds (all three I/O ops overlap) users = await asyncio.gather( fetch_user(1), fetch_user(2), fetch_user(3) )
# TaskGroup (Python 3.11+, preferred over gather) async with asyncio.TaskGroup() as tg: t1 = tg.create_task(fetch_user(1)) t2 = tg.create_task(fetch_user(2)) results = [t1.result(), t2.result()]
asyncio.run(main())The event loop runs in a single thread. When a coroutine hits await, it suspends and control returns to the loop, which runs other ready coroutines. No thread synchronization needed β coroutines yield cooperatively.
Modern Python
Q11. What are Python type hints and how do you use them?
Type hints (PEP 484+) are annotations that improve readability and enable static analysis with mypy or Pyright:
from typing import Optional, Unionfrom collections.abc import Sequence, Generator
# Basic annotationsdef greet(name: str, times: int = 1) -> str: return (f"Hello, {name}!\n" * times).strip()
# Optional (T | None) β equivalentdef find_user(user_id: int) -> Optional[str]: # Old style ...
def find_user(user_id: int) -> str | None: # Python 3.10+ style ...
# Generic typesdef first_element(items: Sequence[int]) -> int | None: return items[0] if items else None
# TypeVar for generic functionsfrom typing import TypeVarT = TypeVar('T')
def identity(value: T) -> T: return value
# Dataclass with type hints (auto __init__, __repr__, __eq__)from dataclasses import dataclass, field
@dataclassclass User: name: str age: int tags: list[str] = field(default_factory=list) active: bool = TrueQ12. Explain Pythonβs with statement and context managers.
Context managers handle setup/teardown (resource acquisition/release) even when exceptions occur:
# __enter__ called on entry; __exit__ called on exit (even on exception)with open('data.txt', 'r') as f: content = f.read()# File is closed here regardless of exception
# Writing a context manager with contextlibfrom contextlib import contextmanagerimport time
@contextmanagerdef timer(label: str): start = time.perf_counter() try: yield # Code in the `with` block runs here finally: elapsed = time.perf_counter() - start print(f"{label}: {elapsed:.3f}s")
with timer("processing"): result = expensive_computation()
# Class-based context managerclass DatabaseConnection: def __init__(self, url): self.url = url self.conn = None
def __enter__(self): self.conn = connect(self.url) return self.conn
def __exit__(self, exc_type, exc_val, exc_tb): self.conn.close() return False # Don't suppress exceptionsQ13. What are Pythonβs list comprehensions, dict comprehensions, and set comprehensions?
# List comprehensionsquares = [x**2 for x in range(10)]evens = [x for x in range(20) if x % 2 == 0]flat = [item for sublist in nested for item in sublist]
# Dict comprehensionword_lengths = {word: len(word) for word in ['apple', 'banana', 'cherry']}inverted = {v: k for k, v in original_dict.items()}
# Set comprehensionunique_lengths = {len(word) for word in words}
# Generator expression (lazy, parentheses)total = sum(x**2 for x in range(1_000_000)) # Memory-efficient
# Nested comprehension (matrix transpose)matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]transposed = [[row[i] for row in matrix] for i in range(3)]Q14. What are the new features in Python 3.10β3.13?
Python 3.10 (2021):
- Structural pattern matching (
match/case) - Union types with
|syntax:int | strinstead ofUnion[int, str] - Better error messages
# Structural pattern matchingdef describe(point): match point: case (0, 0): return "Origin" case (x, 0): return f"On x-axis at {x}" case (0, y): return f"On y-axis at {y}" case (x, y): return f"Point at ({x}, {y})"Python 3.11 (2022):
- Exception Groups (
ExceptionGroup) andexcept*syntax TaskGroupfor asyncio (safer thangather)tomllibin standard library- 10β60% faster than Python 3.10
Python 3.12 (2023):
- f-string improvements (nested expressions, reuse of same quote type)
@overridedecorator for type checking- Type parameter syntax (
type X = list[int]) pathlib.Path.is_junction()
Python 3.13 (2024):
- Interactive REPL improvements
- Experimental free-threaded (no-GIL) build
- JIT compiler (experimental)
- Incremental garbage collector