Top 50 Python Interview Questions and Answers for 2026
Python interviews in 2026 test more than your ability to write a for loop. Employers want to see that you understand the language deeply, that you can reason about performance and design, and that you have practical experience with the libraries that matter in production. Whether you are interviewing for a data analyst position, a backend engineering role, or a machine learning job, the questions below represent what you will actually face.
This guide organizes 50 questions into six categories, progressing from fundamentals to advanced topics. Each question includes a concise answer and, where applicable, a code example you can run yourself. Read through them all, but spend extra time on the categories most relevant to the role you are targeting.
Basics (Questions 1-10)
1. What are Python's core data types?
Python has several built-in data types organized into categories.
Numeric types: int, float, complex
Sequence types: list, tuple, range, str
Mapping type: dict
Set types: set, frozenset
Boolean type: bool
Binary types: bytes, bytearray, memoryview
None type: NoneType
Interviewers ask this to confirm you understand the full landscape of types, not just the ones you use daily.
2. What is the difference between mutable and immutable objects?
Mutable objects can be changed after creation. Lists, dictionaries, and sets are mutable. Immutable objects cannot be changed after creation. Strings, tuples, integers, and frozensets are immutable.
# Mutable: modifying a list in place
my_list = [1, 2, 3]
my_list[0] = 99
print(my_list) # [99, 2, 3]
# Immutable: strings cannot be modified in place
my_string = "hello"
# my_string[0] = "H" # TypeError: 'str' object does not support item assignment
my_string = "H" + my_string[1:] # Creates a new string
print(my_string) # "Hello"
Understanding mutability is critical because it affects how objects behave when passed to functions and when used as dictionary keys.
3. What is the difference between a list and a tuple?
- Lists are mutable; tuples are immutable.
- Lists use square brackets
[]; tuples use parentheses(). - Tuples are hashable (if their elements are hashable) and can be used as dictionary keys; lists cannot.
- Tuples are slightly faster and use less memory than lists.
- Tuples signal intent: they represent fixed collections of heterogeneous data, while lists represent variable-length collections of homogeneous data.
coordinates = (40.7128, -74.0060) # Tuple: fixed pair of values
temperatures = [72, 75, 68, 71] # List: variable collection of similar values
4. How does dictionary comprehension work?
A dictionary comprehension creates a dictionary from an iterable using a concise one-line syntax.
# Basic dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# With a conditional filter
even_squares = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(even_squares) # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
# Transforming an existing dictionary
prices = {"apple": 1.50, "banana": 0.75, "cherry": 2.00}
discounted = {item: round(price * 0.9, 2) for item, price in prices.items()}
print(discounted) # {'apple': 1.35, 'banana': 0.68, 'cherry': 1.8}
5. What is the difference between == and is?
== checks value equality. is checks identity, meaning whether two variables point to the exact same object in memory.
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True (same values)
print(a is b) # False (different objects in memory)
print(a is c) # True (same object)
A common use of is is checking for None: always write if x is None rather than if x == None.
6. What are *args and **kwargs?
*args allows a function to accept any number of positional arguments as a tuple. **kwargs allows any number of keyword arguments as a dictionary.
def log_message(level, *args, **kwargs):
print(f"[{level}]", *args)
for key, value in kwargs.items():
print(f" {key}: {value}")
log_message("INFO", "User logged in", user="alice", ip="192.168.1.1")
# [INFO] User logged in
# user: alice
# ip: 192.168.1.1
7. How does Python's garbage collection work?
Python uses reference counting as its primary memory management mechanism. Every object maintains a count of how many references point to it. When that count drops to zero, the memory is freed immediately.
For circular references (where two or more objects reference each other), Python has a cyclic garbage collector that periodically detects and cleans up reference cycles. You can interact with it through the gc module.
import gc
# Force a garbage collection cycle
gc.collect()
# Check if garbage collection is enabled
print(gc.isenabled()) # True
8. What is a Python virtual environment and why use one?
A virtual environment is an isolated Python installation that has its own set of installed packages. It prevents dependency conflicts between projects.
# Create a virtual environment
# python -m venv myproject_env
# Activate it (Linux/Mac)
# source myproject_env/bin/activate
# Activate it (Windows)
# myproject_env\Scripts\activate
# Install packages into the isolated environment
# pip install pandas==2.2.0
Without virtual environments, installing a package for one project can break another project that depends on a different version of the same package.
9. What is the difference between deepcopy and shallow copy?
A shallow copy creates a new object but inserts references to the same nested objects. A deep copy creates a new object and recursively copies all nested objects.
import copy
original = [[1, 2, 3], [4, 5, 6]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0][0] = 99
print(shallow[0][0]) # 99 (affected because inner list is shared)
print(deep[0][0]) # 1 (unaffected because inner list was copied)
10. What are Python's string formatting methods?
Python offers three primary string formatting approaches.
name = "Alice"
age = 30
# 1. f-strings (recommended in modern Python)
print(f"{name} is {age} years old")
# 2. str.format() method
print("{} is {} years old".format(name, age))
# 3. % operator (legacy, avoid in new code)
print("%s is %d years old" % (name, age))
f-strings are the preferred approach in 2026. They are the most readable, the fastest, and they support arbitrary expressions inside the braces.
Object-Oriented Programming (Questions 11-18)
11. How do you define a class in Python?
class Employee:
company = "Acme Corp" # Class attribute (shared by all instances)
def __init__(self, name, salary):
self.name = name # Instance attribute
self.salary = salary # Instance attribute
def annual_salary(self):
return self.salary * 12
def __repr__(self):
return f"Employee(name='{self.name}', salary={self.salary})"
emp = Employee("Alice", 8000)
print(emp.annual_salary()) # 96000
print(emp) # Employee(name='Alice', salary=8000)
12. Explain inheritance and method resolution order (MRO).
Inheritance allows a class to inherit attributes and methods from a parent class. Python supports multiple inheritance, and the Method Resolution Order (MRO) determines which method is called when multiple parent classes define the same method.
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
class DogCat(Dog, Cat):
pass
pet = DogCat()
print(pet.speak()) # "Woof" (Dog comes first in MRO)
print(DogCat.__mro__)
# (DogCat, Dog, Cat, Animal, object)
Python uses the C3 linearization algorithm to compute MRO, ensuring a consistent and predictable order.
13. What are dunder (magic) methods?
Dunder methods (double underscore methods) are special methods that Python calls implicitly in response to certain operations.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __len__(self):
return int((self.x**2 + self.y**2)**0.5)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(len(v1)) # 5
print(v1 == v2) # False
Common dunder methods include __init__, __repr__, __str__, __eq__, __lt__, __len__, __getitem__, __setitem__, and __hash__.
14. How do decorators work?
A decorator is a function that takes another function as input and returns a modified version of that function. Decorators use the @ syntax.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
def slow_function(n):
total = sum(range(n))
return total
result = slow_function(10_000_000)
# slow_function took 0.2314 seconds
Decorators are used extensively in frameworks (Flask routes, Django views) and for cross-cutting concerns like logging, caching, authentication, and retry logic.
15. What is the difference between @staticmethod and @classmethod?
@staticmethoddoes not receive any implicit first argument. It behaves like a regular function that happens to live inside a class.@classmethodreceives the class itself as the first argument (cls), not an instance. It is commonly used for alternative constructors.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
year, month, day = map(int, date_string.split("-"))
return cls(year, month, day)
@staticmethod
def is_valid(date_string):
parts = date_string.split("-")
return len(parts) == 3 and all(p.isdigit() for p in parts)
print(Date.is_valid("2026-03-14")) # True
d = Date.from_string("2026-03-14")
print(d.year) # 2026
16. What are abstract base classes?
Abstract base classes (ABCs) define a contract that subclasses must fulfill. You cannot instantiate an abstract class directly.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# shape = Shape() # TypeError: Can't instantiate abstract class
rect = Rectangle(5, 3)
print(rect.area()) # 15
17. How does Python's property decorator work?
The @property decorator lets you define methods that are accessed like attributes, enabling controlled access to instance data.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
temp = Temperature(25)
print(temp.fahrenheit) # 77.0
temp.celsius = 100
print(temp.fahrenheit) # 212.0
18. What is the difference between composition and inheritance?
Inheritance models an "is-a" relationship: a Dog is an Animal. Composition models a "has-a" relationship: a Car has an Engine.
# Composition (preferred in most cases)
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine running"
class Car:
def __init__(self, make, engine):
self.make = make
self.engine = engine # Composition: Car HAS an Engine
def start(self):
return f"{self.make}: {self.engine.start()}"
engine = Engine(200)
car = Car("Toyota", engine)
print(car.start()) # Toyota: Engine running
Modern Python practice favors composition over inheritance because it produces more flexible and testable code.
Data Structures and Algorithms (Questions 19-26)
19. What is Big O notation and why does it matter?
Big O notation describes how an algorithm's runtime or memory usage scales with input size. It expresses the worst-case growth rate.
- O(1) — Constant time. Dictionary lookups, array index access.
- O(log n) — Logarithmic. Binary search.
- O(n) — Linear. Iterating through a list.
- O(n log n) — Linearithmic. Efficient sorting algorithms like merge sort and Timsort.
- O(n^2) — Quadratic. Nested loops over the same collection.
- O(2^n) — Exponential. Recursive algorithms without memoization for problems like Fibonacci.
Interviewers ask this because writing code that works is not enough. Code that works efficiently at scale is what matters in production.
20. How does Python's built-in sort work?
Python uses Timsort, a hybrid sorting algorithm derived from merge sort and insertion sort. It has:
- Best case: O(n) for nearly sorted data
- Average case: O(n log n)
- Worst case: O(n log n)
- Space complexity: O(n)
# sort() modifies the list in place, returns None
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
numbers.sort()
print(numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
# sorted() returns a new sorted list, original unchanged
data = [3, 1, 4, 1, 5]
result = sorted(data, reverse=True)
print(result) # [5, 4, 3, 1, 1]
# Custom sort with key function
words = ["banana", "apple", "cherry"]
words.sort(key=len)
print(words) # ['apple', 'banana', 'cherry']
21. Implement binary search in Python.
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1 # Not found
sorted_list = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(sorted_list, 23)) # 5
print(binary_search(sorted_list, 10)) # -1
Binary search requires a sorted input and runs in O(log n) time, making it dramatically faster than linear search for large datasets.
22. Explain recursion with an example.
Recursion is when a function calls itself. Every recursive function needs a base case to prevent infinite recursion.
def factorial(n):
if n <= 1: # Base case
return 1
return n * factorial(n - 1) # Recursive case
print(factorial(5)) # 120 (5 * 4 * 3 * 2 * 1)
# Fibonacci with memoization
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # 12586269025
Without memoization, the naive Fibonacci implementation has O(2^n) time complexity. With memoization, it drops to O(n).
23. What is the difference between a stack and a queue?
- Stack: Last In, First Out (LIFO). Think of a stack of plates.
- Queue: First In, First Out (FIFO). Think of a line at a store.
# Stack using a list
stack = []
stack.append("a")
stack.append("b")
stack.append("c")
print(stack.pop()) # "c" (last in, first out)
# Queue using collections.deque
from collections import deque
queue = deque()
queue.append("a")
queue.append("b")
queue.append("c")
print(queue.popleft()) # "a" (first in, first out)
Use collections.deque for queues because list.pop(0) is O(n), while deque.popleft() is O(1).
24. How do hash tables work and how does Python implement them?
A hash table stores key-value pairs using a hash function to compute an index into an array of buckets. Python's dict is implemented as a hash table.
# Dictionary operations are O(1) average case
lookup_table = {"alice": 85, "bob": 92, "charlie": 78}
# Lookup: O(1)
print(lookup_table["alice"]) # 85
# Insertion: O(1)
lookup_table["diana"] = 95
# Membership test: O(1)
print("bob" in lookup_table) # True
Python dictionaries handle hash collisions using open addressing with a probing strategy. Keys must be hashable (immutable), which is why you cannot use a list as a dictionary key.
25. Implement a linked list in Python.
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def display(self):
elements = []
current = self.head
while current:
elements.append(current.data)
current = current.next
return elements
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
print(ll.display()) # [10, 20, 30]
26. What is the difference between BFS and DFS?
Breadth-First Search (BFS) explores all neighbors at the current depth before moving deeper. Depth-First Search (DFS) explores as far as possible along each branch before backtracking.
from collections import deque
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["F"],
"D": [], "E": [], "F": []
}
def bfs(graph, start):
visited = set()
queue = deque([start])
order = []
while queue:
node = queue.popleft()
if node not in visited:
visited.add(node)
order.append(node)
queue.extend(graph[node])
return order
def dfs(graph, start, visited=None):
if visited is None:
visited = []
visited.append(start)
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
return visited
print(bfs(graph, "A")) # ['A', 'B', 'C', 'D', 'E', 'F']
print(dfs(graph, "A")) # ['A', 'B', 'D', 'E', 'C', 'F']
BFS uses a queue and is ideal for finding shortest paths. DFS uses a stack (or recursion) and is better for exploring all possibilities.
Pandas and NumPy (Questions 27-34)
27. How do you create a DataFrame and perform basic operations?
import pandas as pd
# Create from a dictionary
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana"],
"department": ["Engineering", "Marketing", "Engineering", "Marketing"],
"salary": [95000, 72000, 88000, 76000]
})
# Basic operations
print(df.shape) # (4, 3)
print(df.dtypes) # Column data types
print(df.describe()) # Summary statistics
print(df.head(2)) # First 2 rows
# Filtering
engineers = df[df["department"] == "Engineering"]
high_earners = df[df["salary"] > 80000]
28. Explain groupby operations in pandas.
groupby splits data into groups, applies a function to each group, and combines the results.
import pandas as pd
df = pd.DataFrame({
"department": ["Eng", "Mkt", "Eng", "Mkt", "Eng"],
"quarter": ["Q1", "Q1", "Q2", "Q2", "Q1"],
"revenue": [100, 80, 120, 90, 110]
})
# Single aggregation
print(df.groupby("department")["revenue"].mean())
# Eng 110.0
# Mkt 85.0
# Multiple aggregations
summary = df.groupby("department")["revenue"].agg(["mean", "sum", "count"])
print(summary)
# Group by multiple columns
detail = df.groupby(["department", "quarter"])["revenue"].sum()
print(detail)
29. How do you merge DataFrames?
Pandas offers multiple ways to combine DataFrames, analogous to SQL joins.
import pandas as pd
employees = pd.DataFrame({
"emp_id": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Charlie", "Diana"]
})
salaries = pd.DataFrame({
"emp_id": [1, 2, 3, 5],
"salary": [95000, 72000, 88000, 65000]
})
# Inner join (only matching rows)
inner = pd.merge(employees, salaries, on="emp_id", how="inner")
# Left join (all employees, salary where available)
left = pd.merge(employees, salaries, on="emp_id", how="left")
# Outer join (all rows from both)
outer = pd.merge(employees, salaries, on="emp_id", how="outer")
print(left)
# emp_id name salary
# 0 1 Alice 95000.0
# 1 2 Bob 72000.0
# 2 3 Charlie 88000.0
# 3 4 Diana NaN
30. How do you handle missing data in pandas?
import pandas as pd
import numpy as np
df = pd.DataFrame({
"name": ["Alice", "Bob", None, "Diana"],
"score": [85, np.nan, 72, np.nan]
})
# Detect missing values
print(df.isnull().sum())
# name 1
# score 2
# Drop rows with any missing values
cleaned = df.dropna()
# Fill missing values
filled = df.fillna({"name": "Unknown", "score": df["score"].mean()})
# Forward fill (use previous valid value)
df["score"] = df["score"].ffill()
# Interpolation
df["score"] = df["score"].interpolate(method="linear")
Interviewers ask this because real-world data is always messy. How you handle missing values directly impacts the quality of your analysis.
31. What is the difference between loc and iloc?
loc selects by label (row/column names). iloc selects by integer position.
import pandas as pd
df = pd.DataFrame(
{"A": [10, 20, 30], "B": [40, 50, 60]},
index=["x", "y", "z"]
)
print(df.loc["x", "A"]) # 10 (by label)
print(df.iloc[0, 0]) # 10 (by position)
print(df.loc["x":"y"]) # Rows x and y (inclusive)
print(df.iloc[0:2]) # Rows 0 and 1 (exclusive end)
32. How do you create and manipulate NumPy arrays?
import numpy as np
# Create arrays
arr = np.array([1, 2, 3, 4, 5])
zeros = np.zeros((3, 4))
randoms = np.random.randn(3, 3)
# Vectorized operations (no loops needed)
print(arr * 2) # [2, 4, 6, 8, 10]
print(arr ** 2) # [1, 4, 9, 16, 25]
print(np.sqrt(arr)) # [1.0, 1.414, 1.732, 2.0, 2.236]
# Reshaping
matrix = np.arange(12).reshape(3, 4)
print(matrix.shape) # (3, 4)
# Boolean indexing
print(arr[arr > 3]) # [4, 5]
NumPy is faster than pure Python lists because it stores data in contiguous memory blocks and uses optimized C implementations for operations.
33. How do you apply custom functions to a DataFrame?
import pandas as pd
df = pd.DataFrame({
"name": ["Alice Smith", "Bob Jones"],
"salary": [95000, 72000]
})
# apply() on a column (Series)
df["last_name"] = df["name"].apply(lambda x: x.split()[-1])
# apply() on a row
df["tax"] = df.apply(lambda row: row["salary"] * 0.3 if row["salary"] > 80000 else row["salary"] * 0.2, axis=1)
# map() for element-wise transformation
df["salary_grade"] = df["salary"].map(lambda x: "Senior" if x > 80000 else "Junior")
# applymap() for element-wise on entire DataFrame (use map() in pandas 2.1+)
numeric_df = df[["salary", "tax"]]
formatted = numeric_df.map(lambda x: f"${x:,.0f}")
print(formatted)
34. How do you pivot and reshape data in pandas?
import pandas as pd
df = pd.DataFrame({
"date": ["2026-01", "2026-01", "2026-02", "2026-02"],
"product": ["A", "B", "A", "B"],
"sales": [100, 150, 120, 180]
})
# Pivot table
pivot = df.pivot_table(values="sales", index="date", columns="product", aggfunc="sum")
print(pivot)
# product A B
# date
# 2026-01 100 150
# 2026-02 120 180
# Melt (unpivot): wide to long format
melted = pivot.reset_index().melt(id_vars="date", var_name="product", value_name="sales")
print(melted)
Error Handling and Testing (Questions 35-42)
35. Explain try/except/else/finally.
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero")
return None
except TypeError as e:
print(f"Invalid types: {e}")
return None
else:
print("Division successful") # Runs only if no exception
return result
finally:
print("This always runs") # Cleanup code
print(divide(10, 2)) # Division successful -> This always runs -> 5.0
print(divide(10, 0)) # Cannot divide by zero -> This always runs -> None
The else block runs only when no exception occurs. The finally block always runs, making it ideal for cleanup operations like closing files or database connections.
36. How do you create custom exceptions?
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f"Cannot withdraw ${amount}. Balance: ${balance}"
)
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(e) # Cannot withdraw $150. Balance: $100
37. What is the difference between assertions and exceptions?
Assertions check for conditions that should never occur if the code is correct. They are debugging aids. Exceptions handle expected runtime errors.
# Assertions: catch programming errors
def calculate_average(numbers):
assert len(numbers) > 0, "Cannot average an empty list"
return sum(numbers) / len(numbers)
# Exceptions: handle expected runtime issues
def read_config(filepath):
try:
with open(filepath) as f:
return f.read()
except FileNotFoundError:
return default_config()
Assertions can be disabled with python -O, so never use them for input validation or security checks.
38. How do you write tests with pytest?
# test_calculator.py
import pytest
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Basic test
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
# Testing exceptions
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
# Parametrized tests
@pytest.mark.parametrize("a,b,expected", [
(10, 2, 5),
(9, 3, 3),
(7, 2, 3.5),
])
def test_divide(a, b, expected):
assert divide(a, b) == expected
Run tests with pytest test_calculator.py -v.
39. What are fixtures in pytest?
Fixtures provide reusable setup and teardown logic for tests.
import pytest
@pytest.fixture
def sample_database():
db = {"users": [{"name": "Alice", "age": 30}]}
yield db # Test runs here
db.clear() # Teardown
@pytest.fixture
def api_client():
client = create_test_client()
yield client
client.close()
def test_user_count(sample_database):
assert len(sample_database["users"]) == 1
def test_add_user(sample_database):
sample_database["users"].append({"name": "Bob", "age": 25})
assert len(sample_database["users"]) == 2
40. How do you use mocking in Python tests?
from unittest.mock import patch, MagicMock
def get_user_data(user_id):
import requests
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
@patch("requests.get")
def test_get_user_data(mock_get):
mock_get.return_value = MagicMock(
json=lambda: {"name": "Alice", "id": 1}
)
result = get_user_data(1)
assert result["name"] == "Alice"
mock_get.assert_called_once_with("https://api.example.com/users/1")
Mocking replaces real objects with controlled substitutes, allowing you to test code in isolation without hitting external services.
41. What is test coverage and how do you measure it?
Test coverage measures what percentage of your code is executed during tests.
# Run with coverage
# pytest --cov=myproject --cov-report=html tests/
# Example output:
# Name Stmts Miss Cover
# -----------------------------------------
# myproject/core.py 50 5 90%
# myproject/utils.py 30 0 100%
# -----------------------------------------
# TOTAL 80 5 94%
Aim for 80-90% coverage in production code. 100% coverage does not guarantee correctness, but low coverage almost guarantees bugs.
42. How do you handle multiple exceptions?
# Catching multiple exception types
try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error: {e}")
# Exception chaining
try:
data = fetch_data()
except ConnectionError as e:
raise RuntimeError("Failed to load data") from e
# Exception groups (Python 3.11+)
try:
results = process_batch(items)
except* ValueError as eg:
print(f"Value errors: {eg.exceptions}")
except* TypeError as eg:
print(f"Type errors: {eg.exceptions}")
Advanced Topics (Questions 43-50)
43. What are generators and how do they work?
Generators are functions that yield values one at a time instead of returning them all at once. They are memory-efficient for large datasets.
def fibonacci_generator(limit):
a, b = 0, 1
while a < limit:
yield a
a, b = b, a + b
# Uses almost no memory regardless of limit
for num in fibonacci_generator(1_000_000):
if num > 100:
break
print(num, end=" ")
# 0 1 1 2 3 5 8 13 21 34 55 89
# Generator expression (like a list comprehension but lazy)
squares = (x**2 for x in range(1_000_000))
print(next(squares)) # 0
print(next(squares)) # 1
A generator maintains its state between yield calls, making it ideal for processing streams of data, reading large files line by line, and implementing custom iterators.
44. Explain context managers and the with statement.
Context managers guarantee that setup and cleanup code runs, even if an error occurs. They implement the __enter__ and __exit__ dunder methods.
# Using a context manager
with open("data.txt", "w") as f:
f.write("Hello, World!")
# File is automatically closed, even if an exception occurs
# Creating a custom context manager with a class
class DatabaseConnection:
def __enter__(self):
self.conn = create_connection()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
self.conn.close()
return False # Do not suppress exceptions
# Creating a context manager with contextlib
from contextlib import contextmanager
@contextmanager
def timer(label):
import time
start = time.time()
yield
print(f"{label}: {time.time() - start:.4f}s")
with timer("Data processing"):
result = sum(range(10_000_000))
45. What is the Global Interpreter Lock (GIL)?
The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time. This means that CPU-bound Python code cannot achieve true parallelism with threads.
import threading
import multiprocessing
import time
def cpu_bound_task(n):
return sum(i * i for i in range(n))
# Threading: no speedup for CPU-bound work due to GIL
# Use multiprocessing instead for CPU-bound parallelism
# multiprocessing bypasses the GIL
if __name__ == "__main__":
with multiprocessing.Pool(4) as pool:
results = pool.map(cpu_bound_task, [10_000_000] * 4)
Key points about the GIL:
- It only affects CPython (the standard Python implementation).
- I/O-bound tasks (network requests, file reads) still benefit from threading because the GIL is released during I/O operations.
- Use multiprocessing or asyncio to work around the GIL for CPU-bound work.
- Python 3.13 introduced an experimental free-threaded mode that removes the GIL.
46. What are metaclasses?
A metaclass is a class whose instances are classes. Just as a class defines how instances behave, a metaclass defines how classes behave.
class ValidatedMeta(type):
def __new__(mcs, name, bases, namespace):
# Enforce that all classes using this metaclass have a 'validate' method
if "validate" not in namespace and name != "ValidatedBase":
raise TypeError(f"{name} must implement a validate() method")
return super().__new__(mcs, name, bases, namespace)
class ValidatedBase(metaclass=ValidatedMeta):
pass
class User(ValidatedBase):
def validate(self):
return bool(self.name)
# class BadModel(ValidatedBase): # TypeError: BadModel must implement validate()
# pass
Metaclasses are rarely needed in application code but are used extensively in frameworks like Django (model definitions) and SQLAlchemy (declarative base).
47. How does asyncio work?
asyncio enables concurrent execution of I/O-bound tasks using a single thread with an event loop.
import asyncio
async def fetch_url(url):
print(f"Fetching {url}...")
await asyncio.sleep(1) # Simulates network I/O
return f"Data from {url}"
async def main():
# Run three requests concurrently (not sequentially)
tasks = [
fetch_url("https://api.example.com/users"),
fetch_url("https://api.example.com/orders"),
fetch_url("https://api.example.com/products"),
]
results = await asyncio.gather(*tasks)
for result in results:
print(result)
asyncio.run(main())
# All three complete in ~1 second, not ~3 seconds
48. What are type hints and why use them?
Type hints annotate the expected types of function parameters and return values. They do not affect runtime behavior but enable static analysis tools to catch bugs.
from typing import Optional
def calculate_discount(
price: float,
discount_percent: float,
minimum: Optional[float] = None
) -> float:
discounted = price * (1 - discount_percent / 100)
if minimum is not None:
return max(discounted, minimum)
return discounted
# Modern Python (3.10+) uses union syntax
def process(value: int | str) -> str:
return str(value).upper()
Use mypy or pyright to check type annotations: mypy myproject/.
49. What are slots and when should you use them?
__slots__ restricts the attributes an instance can have, reducing memory usage and slightly increasing attribute access speed.
class PointWithSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
class PointWithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
import sys
a = PointWithSlots(1, 2)
b = PointWithoutSlots(1, 2)
print(sys.getsizeof(a)) # ~56 bytes
print(sys.getsizeof(b)) # ~56 bytes (but b also has __dict__ overhead)
# a.z = 3 # AttributeError: 'PointWithSlots' has no attribute 'z'
b.z = 3 # Works fine
Use slots when you have many instances of a class (millions of objects) and memory matters.
50. What are dataclasses and when should you use them?
Dataclasses automatically generate boilerplate methods like __init__, __repr__, __eq__, and more.
from dataclasses import dataclass, field
@dataclass
class Employee:
name: str
department: str
salary: float
skills: list = field(default_factory=list)
@property
def is_senior(self) -> bool:
return self.salary > 100_000
# Auto-generated __init__, __repr__, __eq__
emp1 = Employee("Alice", "Engineering", 120_000, ["Python", "SQL"])
emp2 = Employee("Alice", "Engineering", 120_000, ["Python", "SQL"])
print(emp1) # Employee(name='Alice', department='Engineering', ...)
print(emp1 == emp2) # True
print(emp1.is_senior) # True
# Frozen (immutable) dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
Use dataclasses for classes that are primarily containers of data. Use regular classes when you need complex initialization logic or metaclass features.
Preparation Strategy
Knowing the answers is only half the battle. Interviewers also evaluate how you communicate your thought process. For every question, practice explaining your reasoning out loud, not just writing the code. Describe the tradeoffs, mention the edge cases, and explain why you chose a particular approach.
- Practice writing code by hand. Many interviews use shared editors without autocomplete.
- Understand time and space complexity for every solution you write.
- Prepare questions to ask the interviewer. This shows engagement and helps you evaluate the company.
- Review the job description and focus on the categories most relevant to the role.
- Build projects that demonstrate the skills covered in these questions.
Master Python with our free Python for Business Beginners textbook.