🐍 Interview Like a Pro: 5 Technical Python Questions That Test Core Expertise

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):

FeatureList Comprehension ([…])Generator Expression ((…) or yield)
MemoryEager 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.
IterationFast iteration since all elements are readily available.Slower iteration setup, but memory-efficient for long or infinite sequences.
ReusabilityCan 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 multiprocessing module (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() or copy.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

Your email address will not be published. Required fields are marked *