Python Full-Stack Interview Questions 51–55 (Buffer/Iterator Protocol, Design Patterns, Protocols, Generics, Type Checking)
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.
51. Explain the buffer/iterator protocol and how to implement custom iterators efficiently.
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.
- Iterator protocol: implement __iter__ and __next__ (or use generator functions to avoid boilerplate).
- Buffer protocol: implement methods to expose memory view (rare to implement by hand; prefer using bytes/bytearray or numpy).
- Efficiency: avoid building intermediate lists; use generators, iterators, and memoryview to avoid copies.
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.
52. What are Python design patterns commonly used (Singleton, Factory, Builder) and the “Pythonic” alternatives?
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.
- Singleton: in Python, a module is effectively a singleton. If you do need a single-instance class, use a simple module-level instance or dependency injection rather than complex metaclasses.
- Factory: use functions or classmethods that return instances. Simple and readable.
- Builder: use dataclasses with default values and replace methods, or classmethods that accept configuration dictionaries.
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.
53. Explain structural typing (Protocols) vs nominal typing in Python’s typing system.
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.
- Nominal: class A and class B are different unless B subclasses A.
- Structural: if an object implements the methods required by a Protocol, a static type checker treats it as compatible.
- Protocols are very useful for duck-typed code with static checks: you describe the expected behavior, not the exact class lineage.
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.
54. What are typing generics, TypeVar, variance, and how to write generic functions/classes?
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.
- TypeVar lets you express relationships: e.g., T = TypeVar("T") and def id(x: T) -> T.
- Use Generic[...] on classes to make them generic containers.
- Be conservative with variance; only declare covariant/contravariant when you understand substitutability rules.
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))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.
55. Explain runtime type checking vs static type checking (mypy, pyright), and how to integrate type checks into CI.
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.
- Static checking: mypy/pyright — catches mismatches early, improves editor tooling, no runtime cost.
- Runtime checking: use selectively (input validation, API boundaries, pydantic for models).
- CI: run static type checker as part of lint/test pipeline; fail the build on errors or enforce a baseline to gradually adopt types.
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 - Run mypy/pyright in a dedicated CI job. Decide whether failures block merges or are reported only.
- Use a config file (mypy.ini/pyproject) to set strictness, ignore third-party stubs, and gradually tighten rules.
- Combine static typing with runtime validation at public boundaries (APIs) using libraries like pydantic or explicit checks.
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.