This guide focuses on Python's data model, reliable abstractions, asynchronous work, and evidence-based performance. Examples are original. The goal is not to memorize implementation trivia; it is to explain trade-offs and build code whose behavior can be tested.
The data model is a protocol system
Python syntax often delegates to special methods. len(value) invokes value.__len__(), iteration uses the iterator protocol, and a + b may call a.__add__(b) before considering the reflected operation on b.
A custom value object should implement the smallest protocol needed by callers. For addition, return NotImplemented for unsupported operands rather than raising TypeError immediately; Python can then try the reflected method:
class Distance:
def __init__(self, metres):
self.metres = float(metres)
def __add__(self, other):
if not isinstance(other, Distance):
return NotImplemented
return Distance(self.metres + other.metres)
If defining equality, consider hashing and ordering together. Equal values need equal hashes. Mutable objects whose equality changes over time should usually remain unhashable. Use dataclass(frozen=True) for simple immutable records; use an explicit class when validation or behavior deserves a named abstraction.
Descriptors, properties, and validation boundaries
A descriptor implements __get__, __set__, or __delete__ and can manage attribute access. property is the familiar built-in descriptor. A property is useful when it preserves a simple attribute interface while enforcing an invariant:
class Percentage:
def __init__(self, value):
self.value = value
@property
def value(self):
return self._value
@value.setter
def value(self, number):
if not 0 <= number <= 100:
raise ValueError("percentage must be from 0 to 100")
self._value = number
Do not use descriptors merely to make a field look clever. Prefer explicit validation at a boundary when the rule applies only once. A reusable descriptor can be justified when the same behavior is shared consistently across many classes.
Decorators should preserve the callable contract
A decorator wraps a callable. functools.wraps preserves metadata such as the function name and docstring, which helps introspection and tooling:
from functools import wraps
def announce(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
A cache is only correct when the function is referentially transparent for its cache key and results are safe to reuse. Caching a function that depends on mutable global state, current time, user identity, or a changing database can return stale or cross-user data. Define invalidation and memory bounds as part of the design, not as an afterthought.
Context managers encode ownership
A context manager scopes acquisition and release. contextlib.contextmanager can express a small resource lifecycle without a class:
from contextlib import contextmanager
@contextmanager
def managed_connection(connect):
connection = connect()
try:
yield connection
finally:
connection.close()
The finally block runs if the body raises. In production, prefer the library's own context manager where it exists; a database connection may need commit/rollback semantics, not merely close(). Explain ownership: who creates a resource, who closes it, and whether exceptions are propagated.
Concurrency is about the workload
Threads are useful for overlapping blocking I/O, but shared mutable state needs synchronization. Processes can use multiple CPU cores for CPU-bound Python work, at the cost of serialization and process-management overhead. asyncio is useful when many tasks spend time awaiting nonblocking I/O; it does not automatically make CPU-heavy code faster.
In Python 3.11 and later, asyncio.TaskGroup provides structured concurrency: child tasks belong to a scope, and failures are propagated as the group exits. Cancellation is part of the API contract. Coroutines should clean up in finally and re-raise CancelledError after cleanup rather than swallowing cancellation.
import asyncio
async def load_pair(fetch, left, right):
async with asyncio.TaskGroup() as group:
a = group.create_task(fetch(left))
b = group.create_task(fetch(right))
return a.result(), b.result()
Discuss limits, timeouts, retries, and partial failure. Parallel requests can overload a downstream service unless concurrency is bounded. For a CPU-bound algorithm, first measure; an executor or another implementation may be more appropriate than adding async syntax.
Measure performance before optimizing
Start with a representative input and a correctness test. Use timeit for focused microbenchmarks, cProfile for call-level profiling, and tracemalloc for Python allocation traces. A microbenchmark can mislead when it excludes I/O, warm-up, realistic data distributions, or interpreter variation.
The usual optimization order is: choose a better algorithm, avoid duplicated work, reduce unnecessary allocations, then tune a measured hot path. Replacing readable code with a lower-level trick is not an improvement if the bottleneck lies elsewhere. State the before-and-after measurement and keep a regression test.
API and architecture questions
When designing a service boundary, validate untrusted input at the edge, keep domain rules in one place, and make side effects visible. Separate pure transformations from I/O where this makes tests simpler. Use a timeout for remote calls; retries need a policy and are unsafe for non-idempotent operations without an idempotency key.
A useful failure model distinguishes invalid input, not-found state, dependency failure, and unexpected defects. Log enough context to diagnose a failure, but do not log credentials, session tokens, or sensitive personal data. An interview answer should connect the design to its operational behavior: latency, consistency, recovery, observability, and cost.
Advanced practice prompts
- A custom object is used as a key, then one of its fields changes. Explain the lookup failure and redesign the key as an immutable value.
- A decorator caches results from a function that reads a feature flag. Identify the stale-result risk and propose explicit versioning or cache invalidation.
- Three independent network calls must all finish, but one can fail. Compare
TaskGroupbehavior with returning partial results, and define cancellation and timeout policy. - A function is slow on a 10 MB input. Describe how you would profile it before choosing an optimization.
Answer framework
For each prompt, state the invariant, identify the failure mode, and name a test that would prove the fix. For concurrency and caching, mention lifecycle and invalidation. For performance, provide evidence rather than a guess. Advanced Python is most useful when its protocol behavior is made explicit and its trade-offs are measured.