Interviews

🎯 Interview Guides 12 guides · updated 2026

Real questions and structured answers for data, cloud, and AI engineering interviews β€” including the system-design and GenAI rounds now showing up everywhere.

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 types
x: int = 42
y: float = 3.14
z: complex = 1 + 2j
s: str = "hello" # Immutable sequence of Unicode chars
t: tuple = (1, 2, 3) # Immutable ordered sequence
f: frozenset = frozenset({1, 2, 3}) # Immutable set (hashable, usable as dict key)
b: bytes = b"data" # Immutable byte sequence
# Mutable types
lst: list = [1, 2, 3] # Ordered, mutable sequence
d: 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 caller
def modify_int(n):
n += 1 # Creates a new int object; original unchanged
return n
x = 10
modify_int(x)
print(x) # 10 β€” unchanged
# Mutable β€” list, dict, set: mutating in-place affects caller's object
def 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 mutables
def 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 memory
print(a is c) # True β€” same object (c is an alias for a)
# Interning gotcha
x = 256
y = 256
print(x is y) # True β€” CPython caches small integers (-5 to 256)
x = 257
y = 257
print(x is y) # False β€” not cached (CPython implementation detail)
# Rule: use == for value comparison; is only for None/True/False identity
if 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 functools
import 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
@timer
def slow_function(n):
"""Compute sum."""
return sum(range(n))
slow_function(1_000_000) # slow_function took 0.0312s
# Decorator with arguments
def 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()) # 15
print(counter()) # 20
print(counter()) # 25
# Each call creates an independent closure
counter_a = make_counter()
counter_b = make_counter(start=100)
counter_a() # 1
counter_b() # 101

Closures 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 yet
total = sum(squares) # Only one value in memory at a time
# Practical use: large file processing
def 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 time

Use 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
# Usage
t1 = Temperature(100)
t2 = Temperature.from_fahrenheit(212)
print(t2._value) # 100.0
print(Temperature.is_valid(-300)) # False

Q8. 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.0
print(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:

import asyncio
import aiohttp
# asyncio β€” single thread, event loop, best for many concurrent I/O ops
async 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 asyncio
results = asyncio.run(fetch_all(urls))
from multiprocessing import Pool
import 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, Union
from collections.abc import Sequence, Generator
# Basic annotations
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}!\n" * times).strip()
# Optional (T | None) β€” equivalent
def find_user(user_id: int) -> Optional[str]: # Old style
...
def find_user(user_id: int) -> str | None: # Python 3.10+ style
...
# Generic types
def first_element(items: Sequence[int]) -> int | None:
return items[0] if items else None
# TypeVar for generic functions
from typing import TypeVar
T = TypeVar('T')
def identity(value: T) -> T:
return value
# Dataclass with type hints (auto __init__, __repr__, __eq__)
from dataclasses import dataclass, field
@dataclass
class User:
name: str
age: int
tags: list[str] = field(default_factory=list)
active: bool = True

Q12. 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 contextlib
from contextlib import contextmanager
import time
@contextmanager
def 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 manager
class 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 exceptions

Q13. What are Python’s list comprehensions, dict comprehensions, and set comprehensions?

# List comprehension
squares = [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 comprehension
word_lengths = {word: len(word) for word in ['apple', 'banana', 'cherry']}
inverted = {v: k for k, v in original_dict.items()}
# Set comprehension
unique_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
def 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):

Python 3.12 (2023):

Python 3.13 (2024):