Key Technical Python Interview Questions for Mid-Level Roles
Python is an interpreted, dynamically typed language with first-class functions and objects, focusing on readability and expressiveness over raw speed—performance-critical sections can be offloaded to C extensions like NumPy to mitigate this gap. It lacks C++-style access specifiers under the philosophy of "consenting adults," supports OOP via composition and inheritance, and excels as both a standalone tool and a "glue" between disparate systems.
import os
from pathlib import Path
def traverse_and_list_paths(root_dir):
"""Yields absolute paths to all files within a directory and its subdirectories."""
root = Path(root_dir).resolve()
for entry in os.scandir(root):
if entry.is_dir(follow_symlinks=False):
yield from traverse_and_list_paths(entry.path)
else:
yield entry.path
Key notes for this solution include using pathlib for clean, cross-platform path handling and os.scandir for faster directory iteration than os.listdir, plus yield from to delegate generator behavior recursively.
# Initial variable assignments
X0 = dict(zip(('w','x','y','z','v'), (7,8,9,10,11)))
X1 = range(15)
X2 = [num for num in X1 if num in X0]
X3 = [X0[key] for key in sorted(X0.keys())]
X4 = [num for num in X1 if num in X3]
X5 = {num: num**3 for num in X1 if num % 2 == 1}
X6 = [(num, num**2) for num in X1 if num % 3 == 0]
The final values are:
X0 = {'w':7, 'x':8, 'y':9, 'z':10, 'v':11}X1 = [0,1,2,...,14]X2 = []X3 = [11,7,8,9,10]X4 = [7,8,9,10,11]X5 = {1:1, 3:27, 5:125, 7:343, 9:729, 11:1331, 13:2197}X6 = [(0,0), (3,9), (6,36), (9,81), (12,144)]
Python’s Global Interpreter Lock (GIL) limits true parallel CPU-bound execution within a single process by ensuring only one thread executes bytecode at a time. For I/O-bound tasks, threading remains efficient due to GIL releases during waits; for CPU-bound work, use multiprocessing, concurrent.futures.ProcessPoolExecutor, or external parallel frameworks like Dask to bypass the GIL entirely.
Distributed version control systems (DVCS) such as Git are industry standard for code management. Git tracks changes locally, supports branching/merging workflows, and facilitates collaboration via platforms like GitHub or GitLab. Critical features include commit history, blame, rebase (for linear history), and pull requests for code review.
def compute_values(n, accumulator=None):
if accumulator is None:
accumulator = []
for i in range(n):
accumulator.append(i**3)
print(accumulator)
compute_values(2)
compute_values(3, [5,6,7])
compute_values(3)
The output is:
[0, 1]
[5, 6, 7, 0, 1, 8]
[0, 1, 0, 1, 8]
Mutable default arguments are initialized once when the function is defined, not each time it is called, leading to unexpected shared state. The workaround is to use None as a sentinel value and create a new mutable object inside the function if needed.
Monkey patching modifies a class, function, or module’s behavior at runtime after its initial definition. While useful for mocking dependencies in unit tests (via libraries like unittest.mock), it should be avoided in production code as it breaks encapsulation, makes debugging difficult, and can cause unpredictable side effects when multiple patches interact.
*args captures arbitrary positional arguments as a tuple, while **kwargs captures arbitrary keyword arguments as a dictionary. These are convention-based names—any valid identifier prefixed with * or ** works, but args and kwargs improve readability. They are useful for wrapper functions, variadic APIs, and passing arguments between functions dynamically.
def log_arguments(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
class SampleClass:
_internal_counter = 0
def __init__(self, value):
self._value = value
@log_arguments
def instance_method(self, multiplier):
return self._value * multiplier
@classmethod
def increment_counter(cls):
cls._internal_counter += 1
return cls._internal_counter
@staticmethod
def add_two_numbers(a, b):
return a + b
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
if not isinstance(new_value, (int, float)):
raise TypeError("Value must be a number")
self._value = new_value
Decorators wrap a target object (function or class) to modify its behavior without changing its source code directly. @classmethod receives the class itself as its first argument (cls by convention), @staticmethod receives no implicit first argument, and @property defines a getter (with optional @<property>.setter and @<property>.deleter decorators) that acts like a regular attribute but triggers custom logic.
class Base:
def move(self):
print("Base moving")
def halt(self):
print("Base halting")
def reset(self):
raise NotImplementedError("Subclasses must implement reset()")
class Left(Base):
def move(self):
super().move()
print("Left moving")
class Right(Base):
def move(self):
super().move()
print("Right moving")
def halt(self):
super().halt()
print("Right halting")
class Hybrid(Left, Right):
def move(self):
super().move()
print("Hybrid moving")
def reset(self):
print("Hybrid resetting")
class Basic(Left, Right):
pass
b = Base()
l = Left()
r = Right()
h = Hybrid()
ba = Basic()
The method resolution order (MRO) for Python new-style classes uses the C3 linearization algorithm to avoid ambiguity in multiple inheritance. The outputs are:
b.move() → Base moving
l.move() → Base moving → Left moving
r.move() → Base moving → Right moving
h.move() → Base moving → Right moving → Left moving → Hybrid moving
ba.move() → Base moving → Right moving → Left moving
b.halt() → Base halting
l.halt() → Base halting
r.halt() → Base halting → Right halting
h.halt() → Base halting → Right halting
ba.halt() → Base halting
b.reset() → NotImplementedError
l.reset() → NotImplementedError
r.reset() → NotImplementedError
h.reset() → Hybrid resetting
ba.reset() → NotImplementedError
class TreeNode:
def __init__(self, label):
self.label = label
self.children = []
def __repr__(self):
return f"<TreeNode '{self.label}'>"
def add_child(self, child_node):
self.children.append(child_node)
def dfs_print(self):
print(self)
for child in self.children:
child.dfs_print()
def bfs_print(self):
queue = [self]
while queue:
current = queue.pop(0)
print(current)
queue.extend(current.children)
# Build sample tree
root = TreeNode("core")
branch1 = TreeNode("branch1")
branch2 = TreeNode("branch2")
branch3 = TreeNode("branch3")
leaf1a = TreeNode("leaf1a")
leaf1b = TreeNode("leaf1b")
leaf2a = TreeNode("leaf2a")
leaf3a = TreeNode("leaf3a")
leaf3b = TreeNode("leaf3b")
leaf1a1 = TreeNode("leaf1a1")
root.add_child(branch1)
root.add_child(branch2)
root.add_child(branch3)
branch1.add_child(leaf1a)
branch1.add_child(leaf1b)
branch2.add_child(leaf2a)
leaf1a.add_child(leaf1a1)
branch3.add_child(leaf3a)
branch3.add_child(leaf3b)
root.dfs_print() performs a depth-first traversal, printing nodes in corre → branch1 → leaf1a → leaf1a1 → leaf1b → branch2 → leaf2a → branch3 → leaf3a → leaf3b order. root.bfs_print() performs a breadth-first traversal, printing nodes in core → branch1 → branch2 → branch3 → leaf1a → leaf1b → leaf2a → leaf3a → leaf3b → leaf1a1 order. Generators can replace the queue/list in BFS to reduce memory usage for large trees.
Python’s garbage collection (GC) primarily uses reference counting: every object tracks how many references point to it, and when the count reaches zero, the object is immediately deallocated. To handle reference cycles (e.g., two objects referencing each other with no external pointers), a cyclic GC runs periodically, using generation-based heuristics to prioritize scanning younger, more recently created objects which are statistically more likely to be garbage.
import random
import cProfile
def g1(input_list):
sorted_full = sorted(input_list)
filtered = [x for x in sorted_full if x < 0.7]
return [x**2 for x in filtered]
def g2(input_list):
filtered = [x for x in input_list if x < 0.7]
sorted_filtered = sorted(filtered)
return [x**2 for x in sorted_filtered]
def g3(input_list):
squared = [x**2 for x in input_list]
sorted_squared = sorted(squared)
threshold = 0.7**2
return [x for x in sorted_squared if x < threshold]
# Generate test data
test_data = [random.random() for _ in range(200000)]
Efficiency from highest to lowest is g2, g1, g3. g2 reduces the size of the list before sorting, which dominates the runtime. g3 squares all elements first and uses a slightly more complex threshold, adding minor overhead. Use cProfile or timeit to benchmark and validate performance claims objectively.