🐍
Question Bank

Python Interview Questions

All 20 questions — full access

✓ Full Access 5 questions shown
← Study Guide
1 What is the difference between a list and a tuple?

Question: Explain the key differences between lists and tuples in Python. When would you use each?

Quick Answer: Lists are mutable (changeable), tuples are immutable (fixed). Tuples are faster, use less memory, and can be used as dictionary keys.

python — editable
# Example 1: Mutability difference
my_list = [1, 2, 3]
my_list[0] = 99        # Works fine — lists are mutable
print(my_list)
# Output: [99, 2, 3]

my_tuple = (1, 2, 3)
# my_tuple[0] = 99     # Raises: TypeError: 'tuple' object does not support item assignment
print(my_tuple)
# Output: (1, 2, 3)
python — editable
# Example 2: Tuples as dictionary keys (lists cannot do this)
# Tuples are hashable because they are immutable
coords = {(10, 20): "New York", (40, 50): "London"}
print(coords[(10, 20)])
# Output: New York

# Lists are NOT hashable — this would fail:
# bad_dict = {[10, 20]: "New York"}
# Raises: TypeError: unhashable type: 'list'
python — editable
120 bytes
print(f"Tuple size: {sys.getsizeof(my_tuple)} bytes")
# Output: Tuple size: 80 bytes
# Tuples use less memory because they don't need resize overhead"># Example 3: Memory and performance comparison
import sys

my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)

print(f"List size:  {sys.getsizeof(my_list)} bytes")
# Output: List size:  120 bytes
print(f"Tuple size: {sys.getsizeof(my_tuple)} bytes")
# Output: Tuple size: 80 bytes
# Tuples use less memory because they don't need resize overhead

🎯 Tip: "Tuples are hashable so they can be dict keys. Lists cannot because they're mutable. I use tuples for fixed data like DB row results or coordinates."

2 What are Python's mutable and immutable types?

Question: Name Python's mutable and immutable types. What happens when you modify an immutable object?

Quick Answer: Immutable types (int, float, str, tuple, frozenset, bytes) create a new object on modification. Mutable types (list, dict, set, bytearray) change in place.

python — editable
# Example 1: Immutable strings — "modification" creates a new object
a = "hello"
b = a                # b points to the same object as a
print(id(a) == id(b))
# Output: True

a = a + " world"     # a now points to a NEW string object
print(a)
# Output: hello world
print(b)
# Output: hello
print(id(a) == id(b))
# Output: False
# b still points to the original "hello" — it was never modified
python — editable
# Example 2: Mutable lists — modification changes the SAME object
x = [1, 2, 3]
y = x                # y points to the same list object
x.append(4)          # Modifies the list in place
print(x)
# Output: [1, 2, 3, 4]
print(y)
# Output: [1, 2, 3, 4]
# Both x and y see the change because they share the same object
print(id(x) == id(y))
# Output: True
python — editable
# Example 3: Immutable integers — reassignment creates a new object
a = 10
print(id(a))
# Output: 4344024144  (some memory address)
a = a + 5            # Creates a brand new int object 15
print(a)
# Output: 15
# The integer 10 still exists (until garbage collected)
# Python caches small integers (-5 to 256), so id() for those may be reused

🎯 Tip: "Understanding mutability prevents aliasing bugs. In data pipelines, I'm careful with mutable default arguments — def f(lst=[]) is a classic trap because the default list is shared across calls."

3 Explain list comprehension vs generator expression

Question: What is the difference between [x for x in range(n)] and (x for x in range(n))? When would you use each?

Quick Answer: List comprehension creates the full list in memory. Generator expression produces values lazily, one at a time. For large data, generators save memory.

python — editable
# Example 1: List comprehension — entire list stored in memory
squares_list = [x ** 2 for x in range(6)]
print(squares_list)
# Output: [0, 1, 4, 9, 16, 25]
print(type(squares_list))
# Output: <class 'list'>
python — editable
# Example 2: Generator expression — lazy, one value at a time
squares_gen = (x ** 2 for x in range(6))
print(type(squares_gen))
# Output: <class 'generator'>

# Must iterate or convert to see values
print(next(squares_gen))
# Output: 0
print(next(squares_gen))
# Output: 1
print(list(squares_gen))   # Remaining values
# Output: [4, 9, 16, 25]
python — editable
87624 bytes
print(f"Generator memory: {sys.getsizeof(gen_expr)} bytes")
# Output: Generator memory: 200 bytes
# Generator uses constant memory regardless of data size"># Example 3: Memory comparison — generators win for large data
import sys

list_comp = [x for x in range(10000)]
gen_expr = (x for x in range(10000))

print(f"List memory:      {sys.getsizeof(list_comp)} bytes")
# Output: List memory:      87624 bytes
print(f"Generator memory: {sys.getsizeof(gen_expr)} bytes")
# Output: Generator memory: 200 bytes
# Generator uses constant memory regardless of data size

🎯 Tip: "In data pipelines, I use generators to avoid OOM on large datasets. If I need to iterate only once, a generator is always better than a list."

4 What is the difference between `deepcopy` and `shallow copy`?

Question: Explain shallow copy vs deep copy. When does each matter? Provide an example where shallow copy causes a bug.

Quick Answer: Shallow copy creates a new outer object but shares inner objects. Deep copy creates new copies of everything recursively. Matters when you have nested structures.

python — editable
# Example 1: Shallow copy — inner lists are shared
import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)       # Shallow copy
a[0].append(999)       # Modify an inner list

print(a)
# Output: [[1, 2, 999], [3, 4]]
print(b)
# Output: [[1, 2, 999], [3, 4]]
# BUG: b was affected because inner lists are shared references
python — editable
# Example 2: Deep copy — completely independent
import copy

a = [[1, 2], [3, 4]]
c = copy.deepcopy(a)   # Deep copy — all nested objects are cloned
a[0].append(999)

print(a)
# Output: [[1, 2, 999], [3, 4]]
print(c)
# Output: [[1, 2], [3, 4]]
# c is fully independent — no shared references
python — editable
# Example 3: Multiple ways to shallow copy a list
original = [1, 2, 3]

# Method 1: copy module
copy1 = copy.copy(original)

# Method 2: list slicing
copy2 = original[:]

# Method 3: list() constructor
copy3 = list(original)

# All produce independent shallow copies for flat lists
original.append(4)
print(original)
# Output: [1, 2, 3, 4]
print(copy1)
# Output: [1, 2, 3]
print(copy2)
# Output: [1, 2, 3]
print(copy3)
# Output: [1, 2, 3]
# For flat lists (no nesting), shallow copy is safe and sufficient

🎯 Tip: "If your data has nested structures (list of lists, dict of dicts), always use deepcopy. For flat structures, shallow copy or slicing is fine and faster."

5 What does `if __name__ == '__main__'` do?

Question: What is the purpose of if __name__ == '__main__' in Python? Why is it important?

Quick Answer: It checks if the file is being run directly (not imported). Code inside this block only executes when the file is the entry point.

python — editable
# Example 1: Basic usage in a module file
# File: utils.py
def helper():
    return "I'm a helper function"

def add(a, b):
    return a + b

if __name__ == '__main__':
    # Only runs when: python utils.py
    # Does NOT run when: from utils import helper
    print(helper())
    # Output: I'm a helper function
    print(add(3, 4))
    # Output: 7
python — editable
# Example 2: Understanding __name__ value
# When run directly:  __name__ == '__main__'
# When imported:      __name__ == 'utils' (the module name)

# File: demo.py
print(f"__name__ is: {__name__}")

# Running: python demo.py
# Output: __name__ is: __main__

# Importing: import demo
# Output: __name__ is: demo
python — editable
# Example 3: Real-world pattern — ETL script with reusable functions
# File: etl_pipeline.py
def extract(source):
    """Extract data from source — reusable when imported"""
    return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

def transform(data):
    """Transform data — reusable when imported"""
    return [row for row in data if row["id"] > 0]

def load(data):
    """Load data — reusable when imported"""
    print(f"Loaded {len(data)} rows")

if __name__ == '__main__':
    # Orchestration only runs when script is executed directly
    raw = extract("db://source")
    clean = transform(raw)
    load(clean)
    # Output: Loaded 2 rows

🎯 Tip: "This pattern makes ETL code both runnable as a script AND importable as a module. Without it, import side effects would trigger pipeline runs unintentionally."

🔒 5 of 20 questions shown

Unlock All 20 Questions

Get full access to every question, flash card, and guide across all 5 topics.

Get Full Access — from ₹299/month

₹499 quarterly · ₹799 for 6 months · ₹3,999 lifetime.