This lesson covers five practical Python interview topics: the buffer/iterator protocol and how to implement efficient custom iterators; common design patterns and their Pythonic alternatives; structural typing with Protocols versus nominal typing; typing generics, TypeVar and variance; and runtime vs static type checking with tips for CI integration. Each question has a beginner-friendly explanation, real-world analogies, and runnable examples with expected output. Read slowly, run the examples in your editor, and use the small lists to anchor the main ideas.
The iterator protocol is how Python iterates over objects: an object must implement __iter__() to return an iterator, and that iterator must implement __next__() which raises StopIteration when finished. The buffer protocol is a lower-level interface that allows objects to expose raw memory (bytes) to other objects without copying; common uses include memoryview, numpy arrays, and bytes-like objects. For interview purposes focus on iterators for iteration tasks and use memoryview for efficient byte access when working with binary data.
Example 1: a custom iterator implemented efficiently using a class (manual) and then the generator equivalent (recommended for brevity).
# Manual iterator class (explicit)
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
# The iterator is the object itself
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Generator-based iterator (simpler and efficient)
def countdown_gen(start):
while start > 0:
yield start
start -= 1
# Usage
for n in Countdown(3):
print("class:", n)
for n in countdown_gen(3):
print("gen: ", n)
Expected output:
# Output:
class: 3
class: 2
class: 1
gen: 3
gen: 2
gen: 1 Example 2: using memoryview to avoid copying when slicing bytes - think of memoryview as giving someone a window to your buffer without moving furniture around.
data = bytearray(b"abcdefghijklmnopqrstuvwxyz")
view = memoryview(data)
# slice via memoryview: no copy of the underlying bytes
part = view[2:10]
print(bytes(part)) # convert to bytes just to display
# modify underlying buffer through view
part[0] = ord("Z")
print(data[:12]) Expected output:
# Output:
b'cdefghij'
bytearray(b'abZdefghijkl') Tip: For most iterator needs, prefer generator functions for clarity and efficiency. Use memoryview when working with large binary data to avoid copies.
Classic design patterns solve recurring problems. In Python, you can implement them but often there's a simpler or more idiomatic (Pythonic) way that leverages the language features: modules as singletons, first-class functions for factories, dataclasses for builders, and composition over inheritance.
Examples: module-singleton, factory via classmethod, and a dataclass builder pattern.
# Singleton via module: myconfig.py
# DATABASE_URL = "postgres://..."
# LOG_LEVEL = "info"
# Import myconfig elsewhere and use constants: it's a singleton-like pattern.
# Factory: choose subclass at runtime
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r * self.r
class Square(Shape):
def __init__(self, a):
self.a = a
def area(self):
return self.a * self.a
def shape_factory(kind, size):
if kind == "circle":
return Circle(size)
return Square(size)
s = shape_factory("circle", 2)
print("area:", s.area())
# Builder via dataclass
from dataclasses import dataclass, replace
@dataclass
class Query:
table: str
columns: tuple = ("*",)
where: str | None = None
def with_where(self, clause):
return replace(self, where=clause)
q = Query("users").with_where("age > 18")
print(q)
Expected output (formatted):
# Output:
area: 12.56636
Query(table='users', columns=('*',), where='age > 18') Tip: In interviews, mention Pythonic alternatives (modules, functions, dataclasses) and show you choose readability and built-in language features before complex patterns.
Nominal typing means types are distinct by name (class identity). Structural typing means compatibility is determined by an object's shape: the attributes and methods it provides. Python's typing.Protocol enables structural typing: if an object has the required attributes/methods, it matches the Protocol without explicit inheritance.
Example: a simple Protocol that requires a .read() method for file-like objects.
from typing import Protocol
class Reader(Protocol):
def read(self, n: int = -1) -> bytes:
...
def process(r: Reader) -> int:
data = r.read(10)
return len(data)
# Works with a real file object and with any object that implements read(n)
with open("file", "rb") as f:
print("bytes read:", process(f))
class Fake:
def read(self, n: int = -1) -> bytes:
return b"hello"
print("fake:", process(Fake()))Expected output:
# Output (example):
bytes read: 10
fake: 5 Tip: Emphasize that Protocols are static-checker helpers (mypy/pyright). At runtime, Python still uses duck typing; Protocols describe expected shape for tooling and documentation.
Generics let you write code parameterized by types. TypeVar defines a type variable you can use in function signatures and classes. Variance (covariant, contravariant, invariant) describes how subtyping of parameterized types relates to subtyping of their type arguments. Most simple cases use invariant TypeVar for safety.
Example: a generic Pair class and a generic identity function.
from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")
class Pair(Generic[T, S]):
def __init__(self, a: T, b: S):
self.a = a
self.b = b
def swap(self) -> "Pair[S, T]":
return Pair(self.b, self.a)
def identity(x: T) -> T:
return x
p = Pair(1, "one")
print(p.a, p.b)
q = p.swap()
print(q.a, q.b)
print(identity(3))Python 3.12+ has dedicated syntax for this (PEP 695)
The TypeVar form above still works and you will see it everywhere, but since Python 3.12 generics have real syntax. There is no separate TypeVar declaration and no Generic[...] base class - the type parameters are scoped to the thing that declares them.
# Python 3.12+ : type parameters are part of the syntax
class Pair[T, S]:
def __init__(self, a: T, b: S):
self.a = a
self.b = b
def swap(self) -> "Pair[S, T]":
return Pair(self.b, self.a)
def identity[T](x: T) -> T:
return x
# The 'type' statement declares a type alias (also PEP 695)
type Vector = list[float]
type Pairs[T] = list[tuple[T, T]]
def scale(v: Vector, k: float) -> Vector:
return [x * k for x in v]
print(identity(3), scale([1.0, 2.0], 3))Expected output:
3 [3.0, 6.0]Two more small typing additions worth knowing:
@override (3.12, PEP 698) - marks a method as intentionally overriding a base-class method, so a type checker catches it if the parent renames or removes it.itertools.batched() (3.12) - chunks an iterable into fixed-size tuples, replacing the hand-rolled zip(*[iter(it)]*n) trick most codebases carry.from itertools import batched
from typing import override
class Base:
def run(self) -> None: ...
class Child(Base):
@override
def run(self) -> None: # type checker errors if Base.run disappears
print("running")
print(list(batched("ABCDEFG", 3)))Expected output:
[('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)]Expected output:
# Output:
1 one
one 1
3 Tip: In interviews, show you can express relationships with TypeVar and prefer Generic for reusable container types. Mention variance only if asked for deeper type theory examples.
Runtime type checking validates types while the program runs (e.g., using isinstance or libraries like pydantic). Static type checking uses tools like mypy or pyright to analyze code without running it and catch potential type errors earlier. Static checks are lightweight and scale well in CI; runtime checks add safety for external input and when types must be enforced at boundaries.
Example: a simple static-check-style annotation and a runtime check using isinstance.
def greet(name: str) -> str:
return "Hello " + name
# Static tools will warn if you call greet(123)
print(greet("Alice"))
# Runtime check
def greet_checked(name):
if not isinstance(name, str):
raise TypeError("name must be a string")
return "Hello " + name
print(greet_checked("Bob"))
# Uncommenting next line would raise at runtime:
# print(greet_checked(123))Expected output:
# Output:
Hello Alice
Hello Bob Closing notes: For interviews, explain trade-offs and demonstrate small runnable examples. Show you prefer simple, readable solutions (generators, dataclasses, Protocols) and that you understand how static typing supports maintainability while runtime checks protect real-world inputs.
2 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.