Chapter 3: Intermediate Python - Interview Preparation Notes

Table of Contents

  1. Comprehensions (List, Dict, Set)
  2. Lambdas, Closures, and Decorators (Intro)
  3. Iterators and Generators
  4. File Handling and Context Managers
  5. Exceptions and Error Handling
  6. Modules, Packages, and Environments
  7. itertools and functools Essentials
  8. Common Interview Questions
  9. Practice Problems

1. Comprehensions (List, Dict, Set)

Comprehensions provide a concise way to create collections.

# List comprehension
squares = [x * x for x in range(6)]  # [0, 1, 4, 9, 16, 25]
 
# With condition (filter)
even_squares = [x * x for x in range(10) if x % 2 == 0]
 
# Nested loops (flatten matrix)
matrix = [[1, 2, 3], [4, 5, 6]]
flattened = [n for row in matrix for n in row]  # [1,2,3,4,5,6]
 
# Dict comprehension
names = ["alice", "bob", "charlie"]
length_map = {name: len(name) for name in names}  # {'alice':5, ...}
 
# Set comprehension (unique results)
nums = [1, 2, 2, 3, 3, 4]
unique_squares = {n * n for n in nums}  # {16, 1, 4, 9}
 
# Generator expression (lazy)
# uses parentheses; iterate to realize values
lazy = (x * x for x in range(3))

Pitfalls and tips:

  • Avoid heavy work in comprehension conditions; keep expressions small.
  • For readability, prefer regular loops for complex logic.
  • Be careful with nested comprehensions; order is left-to-right like nested for-loops.

2. Lambdas, Closures, and Decorators (Intro)

# Lambda: anonymous small function
add = lambda a, b: a + b
 
# Higher-order function example
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))  # [2,4,6,8,10]
 
# Closure: function capturing outer scope
def make_multiplier(factor):
    def multiply(n):
        return n * factor  # factor is remembered
    return multiply
 
times3 = make_multiplier(3)
assert times3(4) == 12
 
# Decorator: wraps behavior around functions
from time import perf_counter
from functools import wraps
 
def timing(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = (perf_counter() - start) * 1000
            print(f"{func.__name__} took {elapsed:.2f} ms")
    return wrapper
 
@timing
def slow_add(a, b):
    total = 0
    for _ in range(1_0000):
        total += (a + b)
    return total

Key notes:

  • Use functools.wraps to preserve metadata (__name__, docstring).
  • Keep lambdas simple; use def for multi-line logic.

3. Iterators and Generators

# Iterator protocol: __iter__ returns self, __next__ returns next item or raises StopIteration
 
# Generator function with yield
 
def countdown(n):
    while n > 0:
        yield n
        n -= 1
 
for v in countdown(3):
    print(v)  # 3 2 1
 
# Generator expression (lazy evaluation)
squares = (x * x for x in range(5))
print(next(squares))  # 0
print(list(squares))  # [1, 4, 9, 16]
 
# Sending values into generators
 
def echo():
    received = None
    while True:
        received = (yield received)
 
co = echo()
next(co)              # prime
print(co.send("hi"))  # prints None, returns 'hi'

When to use:

  • Generators for large/streaming data; they save memory.
  • Iterators decouple traversal from data structure.

4. File Handling and Context Managers

# Always use context managers for files
with open("data.txt", "w", encoding="utf-8") as f:
    f.write("hello\n")
 
# Reading files
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()         # entire file
 
with open("data.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()      # list of lines (keep newlines)
 
with open("data.txt", "r", encoding="utf-8") as f:
    for line in f:             # memory-efficient iteration
        print(line.strip())
 
# JSON basics
import json
person = {"name": "Alice", "age": 30}
with open("person.json", "w", encoding="utf-8") as f:
    json.dump(person, f, ensure_ascii=False, indent=2)
 
with open("person.json", "r", encoding="utf-8") as f:
    data = json.load(f)
 
# CSV basics
import csv
rows = [["name", "age"], ["Alice", 30], ["Bob", 25]]
with open("people.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(rows)
 
with open("people.csv", "r", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
 
# Custom context manager
from contextlib import contextmanager
 
@contextmanager
def managed_resource(name):
    print(f"Acquire {name}")
    try:
        yield name
    finally:
        print(f"Release {name}")
 
with managed_resource("db-conn") as res:
    pass

5. Exceptions and Error Handling

# Try/except/else/finally
 
def parse_int(text: str) -> int | None:
    try:
        return int(text)
    except ValueError:
        return None
    finally:
        pass  # cleanup if needed
 
# Multiple except blocks
try:
    1 / 0
except ZeroDivisionError as e:
    print("Cannot divide by zero")
except Exception as e:
    print(f"Other error: {e}")
 
# Raise exceptions
 
def withdraw(balance: float, amount: float) -> float:
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount
 
# Custom exception
class ConfigError(Exception):
    """Raised when configuration is invalid."""
 
# Best practices
# - Catch specific exceptions
# - Avoid bare except
# - Use exceptions for exceptional cases, not control flow

6. Modules, Packages, and Environments

# Imports
import math
from math import sqrt as square_root
 
# Package structure example
# mypkg/
#   __init__.py
#   utils.py
#   io/
#     __init__.py
#     reader.py
 
# Virtual environment (Windows PowerShell)
# python -m venv .venv
# .venv\\Scripts\\Activate.ps1
# pip install requests

Notes:

  • Prefer absolute imports in larger projects.
  • Keep __init__.py minimal.

7. itertools and functools Essentials

from itertools import accumulate, chain, combinations, permutations, groupby
from functools import partial, lru_cache
 
# chain: flatten iterables
merged = list(chain([1, 2], [3, 4]))  # [1,2,3,4]
 
# accumulate: running totals
prefix_sums = list(accumulate([1, 2, 3, 4]))  # [1,3,6,10]
 
# combinations / permutations
pairs = list(combinations([1, 2, 3], 2))  # [(1,2),(1,3),(2,3)]
orders = list(permutations([1, 2, 3], 2))  # [(1,2),(2,1),...]
 
# groupby: group consecutive items by key
items = [
    {"type": "A", "v": 1},
    {"type": "A", "v": 2},
    {"type": "B", "v": 3},
]
for key, grp in groupby(items, key=lambda x: x["type"]):
    print(key, list(grp))
 
# partial: freeze some args
int_base2 = partial(int, base=2)
assert int_base2("1010") == 10
 
# lru_cache: memoization
@lru_cache(maxsize=128)
def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

8. Common Interview Questions

  • What is the difference between an iterator and an iterable?
  • How do decorators work and why use functools.wraps?
  • When would you use a generator over a list?
  • Explain the try/except/else/finally flow.
  • How does with work under the hood?
  • Why use lru_cache? Trade-offs?

9. Practice Problems

  1. Top K Frequent Elements
from collections import Counter
from heapq import nlargest
 
def top_k_frequent(nums: list[int], k: int) -> list[int]:
    freq = Counter(nums)
    return [num for num, _ in nlargest(k, freq.items(), key=lambda x: x[1])]
  1. File Word Count
def word_count(path: str) -> dict[str, int]:
    counts: dict[str, int] = {}
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            for word in line.strip().split():
                counts[word] = counts.get(word, 0) + 1
    return counts
  1. Flatten Nested List (Generator)
def flatten(nested: list) -> list[int]:
    return [x for sub in nested for x in sub]
 
# Generator version (lazy)
def flatten_gen(nested):
    for sub in nested:
        for x in sub:
            yield x
  1. Safe Config Loader
import json
from typing import Any
 
class ConfigError(Exception):
    pass
 
def load_config(path: str) -> dict[str, Any]:
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        if not isinstance(data, dict):
            raise ConfigError("Config root must be an object")
        return data
    except FileNotFoundError as e:
        raise ConfigError("Config file missing") from e
    except json.JSONDecodeError as e:
        raise ConfigError("Invalid JSON") from e
  1. Sliding Window Maximum (Deque)
from collections import deque
 
def max_sliding_window(nums: list[int], k: int) -> list[int]:
    if k <= 0:
        return []
    dq: deque[int] = deque()
    out: list[int] = []
    for i, n in enumerate(nums):
        while dq and dq[0] <= i - k:
            dq.popleft()
        while dq and nums[dq[-1]] <= n:
            dq.pop()
        dq.append(i)
        if i >= k - 1:
            out.append(nums[dq[0]])
    return out