Landing a senior or experienced Python role requires more than just knowing syntax—it demands a deep understanding of the language’s architecture, memory management, and advanced features.
Here are five technical interview questions that dive deep into Python’s core mechanics, complete with the answers and code examples you need to ace your next interview.
1. Generators vs. Lists: The Power of yield
The Question:
Explain the difference between returning a list and using a generator with yield for large datasets. Give a practical scenario where one is strictly preferred over the other.
The Answer (Technical):
| Feature | List Comprehension ([…]) | Generator Expression ((…) or yield) |
| Memory | Eager Evaluation: Stores the entire sequence in memory. High memory usage for large datasets. | Lazy Evaluation: Generates and yields one item at a time. Minimal memory footprint. |
| Iteration | Fast iteration since all elements are readily available. | Slower iteration setup, but memory-efficient for long or infinite sequences. |
| Reusability | Can be iterated over multiple times. | Can only be iterated over once (exhausted after use). |
Strict Preference: Use a generator (with yield) when processing infinite streams of data (like log files) or datasets that exceed available memory.
Code Example:
Python
# List (Eagerly loads all 1 million integers into RAM)
list_data = [i for i in range(1_000_000)]
print(f"List Size: {list_data.__sizeof__()} bytes")
# Generator (Generates on demand, small memory footprint)
def million_counter():
for i in range(1_000_000):
yield i
generator_obj = million_counter()
print(f"Generator Size: {generator_obj.__sizeof__()} bytes")
# Output will show the generator object is vastly smaller
2. The Global Interpreter Lock (GIL) and Concurrency
The Question:
What is the Python Global Interpreter Lock (GIL), and how does it affect the performance of CPU-bound versus I/O-bound tasks in a multi-threaded Python program?
The Answer (Technical):
The GIL is a mutex (mutual exclusion lock) that protects access to Python objects, preventing multiple native threads from executing Python bytecodes simultaneously within a single process. It ensures thread safety by only allowing one thread to control the interpreter at any given time.
- CPU-Bound Tasks: The GIL is detrimental. It forces threads to run sequentially, negating the benefit of multi-core CPUs. For true parallel CPU work, you must use the
multiprocessingmodule (processes, not threads). - I/O-Bound Tasks: The GIL has minimal impact. When a thread performs an I/O operation (like reading a file or waiting for a network response), the GIL is released, allowing another thread to run. This mechanism effectively allows I/O tasks to run concurrently.
3. Decorators: Syntax and Purpose
The Question:
Write a simple Python decorator that logs the time a function was called and the time it finished executing, and briefly explain the @ syntax.
The Answer (Technical):
A decorator is syntactical sugar for passing a function to another function (the decorator function) and replacing the original function with the result. It allows you to wrap or modify the behavior of another function without explicitly changing its code.
The @log_time syntax is equivalent to writing: say_hello = log_time(say_hello).
Code Example:
Python
import time
def log_time(func):
"""The decorator function"""
def wrapper(*args, **kwargs):
start_time = time.time()
print(f"[{func.__name__}] started at {time.ctime(start_time)}")
# Call the original function
result = func(*args, **kwargs)
end_time = time.time()
print(f"[{func.__name__}] finished at {time.ctime(end_time)}. Took {end_time - start_time:.4f}s")
return result
return wrapper
@log_time
def complex_calculation(a, b):
# Simulate a CPU-bound task
time.sleep(0.5)
return a * b
complex_calculation(5, 10)
# Output:
# [complex_calculation] started at ...
# [complex_calculation] finished at ...
4. Mutable vs. Immutable Defaults in Functions
The Question:
Explain the danger of using a mutable default argument (like a list or dictionary) in a function definition, and provide a corrected, Pythonic way to handle mutable defaults.
The Answer (Technical):
Mutable default arguments are initialized only once—when the function is first defined. If the function modifies the default value, that modification is persisted across all future calls to the function, leading to unexpected behavior.
Code Example (The Danger):
Python
def add_to_list(item, data=[]): # 🚫 Bad: default=[] is mutable
data.append(item)
return data
print(add_to_list('a')) # Output: ['a']
print(add_to_list('b')) # Output: ['a', 'b'] - INCORRECT!
Corrected Code (Pythonic Solution):
The Pythonic solution is to set the default value to None (which is immutable) and then check for it inside the function.
Python
def add_to_list_safe(item, data=None): # ✅ Good: default=None is immutable
if data is None:
data = []
data.append(item)
return data
print(add_to_list_safe('a')) # Output: ['a']
print(add_to_list_safe('b')) # Output: ['b'] - CORRECT!
5. Shallow vs. Deep Copy
The Question:
In the context of complex objects (objects containing other objects, like a list of lists), what is the difference between a shallow copy and a deep copy, and when is the copy module required?
The Answer (Technical):
- Shallow Copy (e.g.,
list.copy()orcopy.copy()): Creates a new top-level container, but populates it with references to the same sub-objects found in the original. Changes to a sub-object in the copy will affect the original. - Deep Copy (e.g.,
copy.deepcopy()): Creates a completely independent new container and recursively creates a copy of all sub-objects within it. Changes to any part of the deep copy will not affect the original object.
The built-in shallow copy methods (like slicing [:] or .copy()) are often enough for simple lists, but for nested mutable structures (lists of lists, dictionaries of lists, etc.), the copy.deepcopy() function is required to ensure total isolation.
Code Example:
Python
import copy
original = [[1, 2], [3, 4]]
# 1. Shallow Copy
shallow_copy = original.copy()
shallow_copy[0][0] = 99 # Mutates the *sub-list* in both!
print(f"Original: {original}") # Output: [[99, 2], [3, 4]]
print(f"Shallow Copy: {shallow_copy}") # Output: [[99, 2], [3, 4]]
# 2. Deep Copy
original_2 = [[1, 2], [3, 4]]
deep_copy = copy.deepcopy(original_2)
deep_copy[0][0] = 55 # Mutates the *sub-list* only in the copy
print(f"Original 2: {original_2}") # Output: [[1, 2], [3, 4]]
print(f"Deep Copy: {deep_copy}") # Output: [[55, 2], [3, 4]]
Conclusion: Beyond the Basics 🎯
Mastering the five concepts above—from the memory efficiency of generators to the concurrency limitations of the GIL and the subtle dangers of mutable defaults—is what elevates a competent coder to a true Python expert.
In a technical interview, the goal isn’t just to write code; it’s to demonstrate that you understand the “why” behind Python’s design choices. By deeply grasping concepts like the object model, memory management (shallow vs. deep copy), and function enhancement (decorators), you prove you can write clean, efficient, and bug-free production-level code.
Go beyond rote memorization. Practice explaining these concepts clearly, using the appropriate terminology, and be ready to illustrate your points with concrete, efficient code examples. This technical fluency is your key to unlocking the next level of your software engineering career.


Leave a Reply