Chapter 3: Intermediate Python - Interview Preparation Notes
Table of Contents
- Comprehensions (List, Dict, Set)
- Lambdas, Closures, and Decorators (Intro)
- Iterators and Generators
- File Handling and Context Managers
- Exceptions and Error Handling
- Modules, Packages, and Environments
- itertools and functools Essentials
- Common Interview Questions
- 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 totalKey notes:
- Use
functools.wrapsto preserve metadata (__name__, docstring). - Keep lambdas simple; use
deffor 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:
pass5. 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 flow6. 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 requestsNotes:
- Prefer absolute imports in larger projects.
- Keep
__init__.pyminimal.
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
withwork under the hood? - Why use
lru_cache? Trade-offs?
9. Practice Problems
- 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])]- 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- 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- 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- 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