Eight public interview questions that test Python reasoning used in AI services, pipelines, tests, and debugging — from mutability and iterators to async boundaries, resource safety, and production profiling. Each question includes an answer framework, trade-offs, failure modes, and a common weak answer.
Audience: Software and backend engineers preparing for AI service interviews, AI/ML engineers who need to demonstrate Python implementation depth, Full-stack developers moving into AI application rolesPrerequisites: Working familiarity with Python syntax and data structures, Basic understanding of functions, classes, and imports
Last technically reviewed: 2026-09-01
Python Skills Covered in These Interview Questions
These eight public interview questions test Python reasoning that AI engineers use daily: mutability and identity, iterators and memory, typing and protocol design, exception and resource safety, testing nondeterministic code, async versus threads and processes, profiling bottlenecks, and debugging production-style data transformations. They are not trivia — they test whether you can reason about Python behaviour under real service constraints.
What Interviewers Evaluate in Python Answers
What the interviewer is evaluating: Whether you can move beyond syntax recall to reason about Python's data model, memory behaviour, concurrency boundaries, and error handling — the areas that distinguish an engineer who writes production AI services from one who runs notebooks.
Can you explain why a list mutation affects callers but a tuple reassignment does not
Do you understand when generators save memory versus when they add complexity
Can you design a typed interface that catches errors at development time without over-constraining
Do you reason about resource cleanup with context managers rather than hoping garbage collection handles it
Can you diagnose whether a bottleneck is CPU-bound, I/O-bound, or memory-bound before choosing a solution
8 Python Interview Questions for AI Engineers
Q1
Why Does Mutating a List Change the Caller’s Data?
Foundation·Coding·0-2 years
Competency: Python data model — mutability and identity
Interview scenario
A function appends an item to a list passed as an argument. The caller sees the modification. Explain why this happens and how you would prevent unintended mutations in a shared-data AI pipeline component.
Approach: Explain that Python passes object references, not copies — so a mutable list shared between caller and function is the same object. Then describe defensive copying, immutability, and documentation as prevention strategies.
Python's argument passing is 'pass-by-object-reference' — the function receives a reference to the same list object, not a copy.
list.append() mutates the object in place, so the caller's reference now sees the new item.
In a pipeline component that processes data for multiple downstream consumers, unintended mutation can cause data corruption, race conditions, or silent bugs.
Prevention options: (a) copy.deepcopy for nested structures, (b) return a new list instead of mutating in place, (c) use tuple or frozen dataclass for data that should not change, (d) document mutation behaviour explicitly in the function contract.
Trade-offs:
Defensive copying is safe but costs memory and time — unacceptable for large ML tensors or big document collections
Immutability via tuples or frozen dataclasses prevents mutation but makes transformations more verbose (create new objects instead of modifying)
Documentation is zero-cost but relies on caller discipline — fine for small teams, risky for shared libraries
Failure modes:
Shallow copy (list.copy()) does not protect nested mutable objects — a dict inside a copied list is still shared
Using tuple() on a list of dicts makes the outer container immutable but the dicts inside are still mutable
Assuming 'pass-by-value' semantics — this is a fundamental Python misconception
Validation:
Write a test that checks the original list is unchanged after calling the function
Use id() to demonstrate that the function's parameter and the caller's variable point to the same object
Show that copy.deepcopy produces a different id()
def safe_process(items: list[dict]) -> list[dict]:
# Return new list — do not mutate caller's data
return [{**item, "processed": True} for item in items]
# Caller's list is untouched
original = [{"id": 1}, {"id": 2}]
result = safe_process(original)
assert original[0] == {"id": 1} # No mutation
Common weak answer: Saying 'Python passes by value for immutables and by reference for mutables' — this is incorrect. Python always passes object references. The difference is whether the object itself is mutable or immutable.
Safe follow-up: What if the list contains numpy arrays — does copy.deepcopy work correctly, and what would you use instead for large tensor pipelines?
When Should an AI Pipeline Use a Generator Instead of a List?
Foundation·Coding·0-2 years
Competency: Iterators, generators and memory
Interview scenario
You are processing 500,000 documents for a RAG ingestion pipeline. A colleague suggests loading all documents into a list and iterating over it. Explain when a generator is the better choice and when it creates more problems than it solves.
Approach: Explain that generators produce values lazily, one at a time, so they avoid holding all 500K documents in memory simultaneously. Then identify the trade-off: generators cannot be indexed, replayed, or parallelised without materialisation.
A list of 500K document dicts could consume gigabytes of RAM — a generator yielding one document at a time uses constant memory.
Generators are ideal for streaming pipelines: read document → chunk → embed → store → yield next.
However, generators are single-pass — once consumed, they are exhausted. If you need to retry failed documents or run multiple passes (e.g., re-embed after a model change), you need a list or a re-creatable generator.
Generators cannot be sliced, indexed, or passed to multiprocessing.Pool.map directly without being converted to a list first.
Trade-offs:
Memory: generator wins (O(1) vs O(n) memory)
Replayability: list wins (can iterate multiple times, slice, index)
Parallelism: list wins (can chunk for multiprocessing; a generator must be materialised first)
Error recovery: list wins (can retry specific indices; a generator that raises mid-iteration loses position)
Failure modes:
Using a generator for a pipeline that needs retry — when an exception occurs at item 300K, the generator is exhausted and you cannot resume
Passing a generator to multiprocessing.Pool.map — it materialises the entire generator into a list internally, defeating the memory saving
Assuming a generator is always faster — for small datasets, the overhead of generator creation and next() calls can be slower than a simple list comprehension
Validation:
Measure peak memory with sys.getsizeof or tracemalloc for list vs generator on a sample dataset
Time both approaches — for small datasets, list comprehension may be faster
Test error recovery: raise an exception mid-iteration and verify whether the generator can resume
Common weak answer: Saying 'generators are always better for large data' without acknowledging the replayability, parallelism, and error recovery trade-offs.
Safe follow-up: If you need both low memory and retry capability, what pattern would you use instead of a plain generator?
Public scoring signals:
Correctly explains lazy evaluation and constant memory
Identifies at least two generator limitations (no index, single-pass, no parallelism)
Proposes a concrete alternative for the retry use case
Measures or proposes measuring the trade-off rather than asserting
How Would You Design a Python Protocol for Multiple LLM Providers?
Applied·Coding·2-5 years
Competency: Typing and protocol design
Interview scenario
Design a Python Protocol that defines a common interface for LLM providers (OpenAI, Anthropic, a local Ollama wrapper). The interface must support streaming responses and structured output. Explain how Protocol differs from ABC and when each is appropriate.
Approach: Define a Protocol with chat() and stream_chat() methods, explain that Protocol is structural typing (duck typing with static checks) while ABC is nominal typing (explicit inheritance), and state when each fits.
Protocol (PEP 544) enables structural subtyping — any class that has the right methods satisfies the Protocol, without inheriting from it.
This is ideal for LLM providers: OpenAI's SDK, Anthropic's SDK, and a custom Ollama wrapper do not share a base class, but they can all implement the same method signatures.
ABC (abstract base class) requires explicit inheritance — the provider class must inherit from the ABC. This is better when you control all implementations and want to enforce a contract at runtime.
For a provider-agnostic AI service, Protocol is better because you cannot make OpenAI's SDK inherit from your ABC.
Trade-offs:
Protocol: no runtime enforcement by default (use @runtime_checkable for isinstance checks, but it only checks method existence, not signatures)
ABC: runtime enforcement via abstractmethod, but requires inheritance from your base class
Protocol: works with third-party classes you cannot modify
ABC: better for internal hierarchies where you want shared logic in the base class
Failure modes:
Using @runtime_checkable and assuming it validates method signatures — it only checks method names exist, not parameter types
Making the Protocol too narrow (only OpenAI's method shape) so Anthropic cannot satisfy it without adapters
Forgetting that Protocol classes are not instantiated — they are type hints, not base classes
Validation:
Run mypy or pyright with strict mode to verify that each provider class satisfies the Protocol
Write a test that calls chat() on each provider through a function typed to accept the Protocol
Verify that removing a required method from a provider causes a type error
from typing import Protocol, Iterator
class LLMProvider(Protocol):
def chat(self, messages: list[dict], **kwargs) -> str: ...
def stream_chat(self, messages: list[dict], **kwargs) -> Iterator[str]: ...
def structured_output(self, messages: list[dict], schema: type, **kwargs) -> dict: ...
# Any class with these methods satisfies LLMProvider
class OpenAIWrapper:
def chat(self, messages, **kwargs) -> str: ...
def stream_chat(self, messages, **kwargs) -> Iterator[str]: ...
def structured_output(self, messages, schema, **kwargs) -> dict: ...
def call_provider(p: LLMProvider, msgs: list[dict]) -> str:
return p.chat(msgs) # mypy verifies p has chat()
Common weak answer: Defining an ABC and trying to make OpenAI's SDK inherit from it — you cannot modify third-party classes. Or using Union[OpenAIClient, AnthropicClient, OllamaClient] instead of a Protocol, which does not scale when adding providers.
Safe follow-up: How would you handle the case where one provider supports tool calling but another does not — would you add it to the Protocol or use a separate Protocol?
Public scoring signals:
Correctly distinguishes structural (Protocol) from nominal (ABC) subtyping
Designs a Protocol with at least chat and streaming methods
Identifies that Protocol works with third-party classes without inheritance
How Do Context Managers Prevent Resource Leaks During Timeouts?
Applied·Coding·2-5 years
Competency: Exception and resource safety
Interview scenario
An AI service opens a database connection, calls an LLM API, and writes the result to the database. The LLM call can raise a timeout. Write the code using context managers and explain how you ensure the database connection is always closed, even on timeout or cancellation.
Approach: Use a context manager (with statement) for the database connection so it is closed on any exit path. Wrap the LLM call in a try/except for timeout, log the failure, and let the context manager handle cleanup.
The 'with' statement guarantees __exit__ is called on the connection even if an exception propagates — this is the standard Python resource safety pattern.
If the LLM call raises a TimeoutError, the exception propagates up through the 'with' block, which triggers the connection's __exit__ (close) before the exception continues.
For async services (FastAPI), use 'async with' for async database drivers (asyncpg, databases) — the same guarantee applies.
If you need to clean up multiple resources (connection + HTTP client + file), nest context managers or use ExitStack for dynamic resource management.
Cancellation (asyncio.CancelledError) is distinct from synchronous TimeoutError. In async code, a cancelled task propagates CancelledError through the async with block, which still triggers __aexit__. The cleanup path is the same, but you should not catch CancelledError as if it were a timeout — let it propagate so the caller knows the task was cancelled, not timed out.
Never log raw prompts or records that may contain PII. Log only metadata (prompt length, model name, latency, status code) or use a redaction filter before logging.
Trade-offs:
Nesting context managers creates indentation — use contextlib.ExitStack or async-exit-stack for many resources
Catching the timeout inside the with-block lets you log and return a fallback, but masks the error from upstream — decide based on whether the caller can handle it
For connection pooling (SQLAlchemy, asyncpg pool), 'with' returns a connection to the pool rather than closing it — the pool itself manages lifecycle
try/finally is a valid alternative that guarantees cleanup. Context managers provide a cleaner abstraction because they encapsulate the acquire/release pair, reduce boilerplate, and are harder to get wrong with multiple return paths.
Failure modes:
Opening the connection outside the with-block and closing it manually — if the LLM call raises, the close() line is never reached
Using try/finally instead of a context manager — correct but more verbose and error-prone (forgetting finally on one of multiple return paths)
In async code, using 'with' instead of 'async with' for an async resource — the resource is never properly closed
Catching asyncio.CancelledError and treating it as a timeout — this prevents proper task cancellation propagation and can leave the caller waiting indefinitely.
Validation:
Test with a mock LLM that raises TimeoutError and verify the connection is closed using a connection pool metric
Use tracemalloc or a connection counter to confirm no connection leak after 1000 iterations with random failures
In async tests, use pytest-asyncio and assert the pool returns to its initial connection count
def process_with_llm(db_pool, llm_client, prompt: str) -> str:
with db_pool.get_conn() as conn: # Closed on any exit
try:
result = llm_client.complete(prompt, timeout=30)
except TimeoutError:
log.warning("LLM timeout for prompt: %s", prompt[:100])
return "[timeout]"
conn.execute(
"INSERT INTO results (prompt, output) VALUES (?, ?)",
prompt, result
)
return result
Common weak answer: Using try/finally to close the connection manually, with the close() call at the end of the function — if any line between the open and the close raises, the connection leaks.
Safe follow-up: What if the database write itself fails after the LLM call succeeds — how do you handle partial completion in a service without a distributed transaction?
Public scoring signals:
Uses context manager (with statement) for resource cleanup
Explains that __exit__ is called even on exception propagation
Identifies async with for async resources
Proposes testing resource leak with mocks or pool metrics
How Do You Test Nondeterministic LLM Outputs Reliably?
Applied·Coding·2-5 years
Competency: Testing nondeterministic dependencies
Interview scenario
You are testing an AI service that calls an LLM API. The LLM's output is nondeterministic — the same prompt can produce different responses. How do you write tests that are reliable, fast, and do not call the real API in CI?
Approach: Use dependency injection to separate the LLM call from the business logic, mock the LLM client in tests with deterministic fixtures, and test the real API integration separately with a smoke test that is skipped in CI.
The service should accept an LLM client as a parameter (dependency injection), not create it internally — this allows tests to inject a mock.
Unit tests mock the LLM client to return fixed responses — they test the service's logic (prompt construction, response parsing, error handling, database writes) without calling the API.
The mock should return realistic response shapes (not just 'hello') so parsing logic is tested — use fixtures stored as JSON files.
For the real API integration, write a separate smoke test marked with @pytest.mark.integration that is skipped in CI via a marker or environment variable.
Trade-offs:
Mock tests are fast and deterministic but do not catch API changes (e.g., response format changes, new error types)
Recording real API responses (VCR.py) captures actual formats but recordings go stale when the API changes
Calling the real API in tests gives the most realistic coverage but is slow, costly, and nondeterministic — flaky tests erode trust
Failure modes:
Mocking at too high a level — mocking the entire service function rather than just the LLM client, so the test does not exercise internal logic
Using a fixed seed or temperature=0 in tests — this reduces randomness but is still nondeterministic across model versions
Storing API keys in test files or CI environment without rotation — a security risk
Validation:
Run unit tests 100 times in CI and confirm zero flaky failures
Periodically run the integration smoke test manually against the real API to catch format changes
Use a contract test that validates the mock response shape against the real API response shape
Common weak answer: Calling the real LLM API in tests and asserting that the response 'contains' certain keywords — this is flaky, slow, costs money, and breaks when the model changes.
Safe follow-up: How would you test the service's retry logic without making the test wait for actual timeouts?
Public scoring signals:
Uses dependency injection to separate LLM client from service logic
Mocks the LLM client with deterministic fixtures, not real API calls
Separates unit tests from integration smoke tests
Identifies the trade-off between mock realism and maintenance cost
When Should You Use Asyncio, Threads or Process Pools?
Production·Applied·2-5 years
Competency: Async versus threads and processes
Interview scenario
Your RAG service receives concurrent requests, each calling an embedding API (I/O-bound) and then running a local reranking model (CPU-bound). A colleague suggests using asyncio for both. Explain why this is partially incorrect and what you would use instead.
Approach: Use asyncio for the embedding API call (I/O-bound — awaiting network response) and a process pool or separate worker for the reranking model (CPU-bound — asyncio does not help with CPU work). Explain that async does not increase CPU parallelism.
asyncio uses a single-threaded event loop — it improves I/O concurrency by switching tasks while one waits for network, but it cannot run CPU-bound work in parallel.
The embedding API call is I/O-bound: the coroutine awaits an HTTP response, freeing the event loop to handle other requests. asyncio is correct here.
The reranking model is CPU-bound: running it inside the event loop blocks all other requests until it finishes. This is the classic 'blocking the event loop' problem.
Solution: run the reranking model in a process pool (concurrent.futures.ProcessPoolExecutor or multiprocessing) and await its result from the async context using loop.run_in_executor().
Generators can be parallelised with careful design (e.g., chunking into batches and submitting each batch to a pool), but a single-pass generator cannot be split across workers without materialising it first.
Trade-offs:
Process pool: true CPU parallelism but process creation overhead and data serialisation cost (pickle the model input/output)
Thread pool: no serialisation cost but Python's GIL means only one thread runs Python at a time — useful if the reranker releases the GIL (e.g., numpy/PyTorch C extensions)
GPU inference: if the reranker runs on GPU, use a dedicated inference server (vLLM, TGI) and call it via async HTTP — the GPU handles parallelism, and the async call is I/O-bound from Python's perspective
Failure modes:
Running a CPU-bound reranker inside the event loop — all concurrent requests stall until the reranker finishes, causing p99 latency spikes
Using threading for a pure-Python CPU workload — the GIL prevents true parallelism, so threads do not help
Forgetting that loop.run_in_executor() with a process pool pickles arguments — large tensors or complex objects may fail to pickle
Validation:
Benchmark: send 10 concurrent requests and measure p99 latency — if the reranker blocks the event loop, p99 will be ~10x the single-request latency
After moving to a process pool, p99 should drop to ~1-2x the single-request latency (depending on pool size)
Use asyncio.get_event_loop().run_in_executor(None, fn, args) for a quick test — None uses the default thread pool, then switch to ProcessPoolExecutor for CPU-bound work
# Create pool ONCE at app startup, not per request
_executor = ProcessPoolExecutor(max_workers=4)
async def rag_handler(query, embed_client, reranker):
embeddings = await embed_client.embed(query) # I/O-bound: async
loop = asyncio.get_event_loop()
ranked = await loop.run_in_executor(_executor, reranker.rerank, embeddings)
return ranked
Common weak answer: Saying 'asyncio makes everything faster' — it does not. It improves I/O concurrency on a single thread but does nothing for CPU-bound work. Running a CPU-bound model inside the event loop is an anti-pattern that degrades all concurrent request latency.
Safe follow-up: What if the reranker is a PyTorch model on GPU — does the GIL analysis change, and what architecture would you use for serving it?
Public scoring signals:
Correctly identifies the embedding call as I/O-bound and the reranker as CPU-bound
Explains that asyncio does not provide CPU parallelism
Proposes process pool or dedicated inference server for CPU-bound work
A RAG pipeline takes 4 seconds per query. The team suspects the embedding model is the bottleneck. Describe how you would profile the pipeline to confirm or refute this hypothesis, and what you would do if the bottleneck is somewhere else entirely.
Approach: Use structured timing around each pipeline stage (not just the total) to measure where time is actually spent. Profile with cProfile for CPU time and identify whether the bottleneck is embedding, retrieval, LLM generation, or serialisation.
Start with stage-level timing: wrap each pipeline stage (embed, retrieve, rerank, generate) with time.perf_counter() and log the duration. This gives a breakdown without profiler overhead.
If stage timing shows embedding is 0.2s and LLM generation is 3.2s, the hypothesis is refuted — the bottleneck is the LLM, not the embedding model.
Use cProfile for deeper analysis if stage timing is insufficient — it shows per-function call counts and cumulative time, revealing hidden costs (e.g., JSON serialisation, regex parsing, database connection overhead).
For async services, use asyncio debug mode and tracing (e.g., OpenTelemetry spans) to measure time spent awaiting each I/O operation.
Trade-offs:
Stage-level timing: minimal overhead, easy to add, but does not show intra-stage breakdowns
cProfile: detailed per-function data but adds 10-30% overhead and is not suitable for production traffic
py-spy (sampling profiler): low overhead, works on production, but less detailed than cProfile
OpenTelemetry tracing: production-safe, distributed, but requires instrumentation setup
Failure modes:
Optimising a component without profiling first — if it is a small fraction of total time, even a large improvement saves little.
Using time.time() instead of time.perf_counter() — time.time() can go backwards on NTP adjustments and has lower resolution
Profiling in development with warm caches and concluding the bottleneck is small — the production bottleneck may be different (cold cache, network latency, database load)
Validation:
Run the profiler on production-like data and traffic patterns, not just a single test query
Measure p50 and p95 separately — the p95 bottleneck may differ from the p50 bottleneck
After optimisation, re-profile to confirm the bottleneck moved — do not assume the fix worked without measurement
Common weak answer: Optimising the embedding model based on the team's guess without measuring — if embedding is 5% of the total time, even a 100x improvement saves less than 0.2 seconds. Always measure before optimising.
Safe follow-up: If profiling reveals the bottleneck is JSON serialisation of large retrieval results, what would you change — and what would you measure to confirm the improvement?
Public scoring signals:
Uses stage-level timing before deep profiling
Distinguishes CPU profiling from I/O tracing
Identifies at least two profiling tools with trade-offs
States that profiling must happen on production-like data
How Would You Debug Intermittent Data Transformation Errors?
Production·Project Deep Dive·5-8 years
Competency: Python data transformation debugging in production
Interview scenario
A Python data pipeline transforms JSON records from an external API into a normalized format for a downstream ML feature store. Intermittently, the pipeline produces records with missing fields or incorrect types — but only for about 2% of inputs. The API documentation says all fields are always present. Describe your debugging approach from symptom to root cause to fix.
Approach: Move from symptom to instrumented isolation: log the failing records, reproduce locally with the same inputs, inspect the transformation at each step, identify whether the issue is in parsing, type coercion, or missing-key handling, and write a regression test before fixing.
Step 1 — Capture: Log the failing records with their input JSON and output. This tells you whether the input is malformed or the transformation is wrong.
Step 2 — Reproduce: Take one failing input and run the transformation locally. Inspect the output at each step: parse, validate, transform, serialize.
Step 3 — Isolate: If the input JSON has a field that is null instead of a string, the issue is in the API response. If the input is correct but the output has missing fields, the issue is in the transformation logic — a dict.get() that silently returns None for unexpected keys.
Step 4 — Fix: The 2% failure rate suggests an edge case — the API occasionally returns a field as null or a different type. Fix the transformation to handle these cases explicitly with type checking, default values, or rejection with logging.
Step 5 — Regression test: Write a test with the failing input shape (null field, missing key, wrong type) and verify the transformation handles it correctly.
Trade-offs:
Silent defaults (dict.get(key, default)) hide data quality issues — the pipeline appears to work but downstream ML features get incorrect values
Strict validation (raising on unexpected types) catches issues early but may reject valid-but-unusual API responses
Logging all transformations is expensive — sample or log only failures
Failure modes:
Assuming the API always returns the documented schema — real APIs have bugs, version skew, and edge cases
Using dict.get() without checking the result — None propagates silently through the pipeline
Not logging the original input alongside the output — without both, you cannot reproduce the failure
Validation:
Run the regression test with the failing input shape and confirm it passes
Add a schema validation step (pydantic or jsonschema) at the pipeline entry to catch malformed inputs early
Monitor the failure rate after the fix and confirm it drops to zero
# Bad: silent None propagation
record = {"feature": data.get("value")} # None if missing
# Good: explicit handling
value = data.get("value")
if value is None:
log.warning("Missing value in record %s", record_id)
continue
record = {"feature": float(value)}
Common weak answer: Blaming the ML feature store for incorrect features without checking whether the Python transformation pipeline silently produced None values from missing API fields.
Safe follow-up: How would you add schema validation at the pipeline entry to catch malformed API responses before they reach the transformation logic?
Public scoring signals:
Follows a structured debugging approach (capture, reproduce, isolate, fix, test)
Identifies silent None propagation as a likely cause for intermittent failures
Proposes schema validation at pipeline entry
Writes a regression test with the failing input shape
Clarify the question — restate what is being asked and what assumptions you are making
Decompose into stages — identify the components or pipeline stages involved
Compare options — list at least two approaches and their trade-offs
Decide — choose one approach and justify it with a constraint or evidence
Validate — describe how you would test or measure that your answer is correct
Common Weak Patterns in Python Interview Answers
Mistake: Asserting 'Python is pass-by-value for immutables and pass-by-reference for mutables'
Why it fails: This is incorrect — Python is always pass-by-object-reference. The difference is object mutability, not calling convention.
Fix: Say 'Python passes object references. Immutable objects cannot be modified through the reference, so they appear pass-by-value. Mutable objects can be modified, so they appear pass-by-reference.'
Mistake: Saying 'asyncio makes everything faster'
Why it fails: asyncio improves I/O concurrency on a single thread but does not provide CPU parallelism. Running CPU-bound work in the event loop blocks all concurrent requests.
Fix: Distinguish I/O-bound (async helps) from CPU-bound (use process pool or dedicated worker) and explain the GIL's role.
Mistake: Optimising based on guesses without profiling
Why it fails: If the suspected bottleneck is 5% of total time, even a 10x improvement saves 4.5% — not worth the engineering cost.
Fix: Always measure stage-level timing before optimising. Use time.perf_counter() around each stage, then profile deeper if needed.
Mistake: Using try/finally instead of context managers for resource cleanup
Why it fails: Manual close() calls in finally blocks are error-prone — if there are multiple return paths or exceptions, it is easy to miss one.
Fix: Use 'with' or 'async with' for any resource that has a close/cleanup method. It guarantees __exit__ is called on all exit paths.
Failure-Oriented Python Code Lab
A side-by-side comparison of a flawed and corrected Python pipeline component. Environment: Python 3.12. Expected output verified with pytest. Limitations: does not cover async resource cleanup or multiprocessing edge cases.
Note
Code patterns are illustrative. Run with your own environment and verify expected behaviour with tests.
Flawed: Mutable default
def process(docs: list = []): ... — default list shared across calls. Expected: fresh list. Actual: items accumulate.
Corrected: Default None
def process(docs: list | None = None): docs = docs or [] — fresh list per call. Expected: no accumulation across calls.
Flawed: Manual close
conn = open_db(); result = llm.call(); conn.close() — close() skipped on exception. Actual: connection leak on timeout.
Corrected: Context manager
with open_db() as conn: result = llm.call() — closed on any exit. Expected: pool returns to initial count.
Flawed: Blocking async
async def handler(): result = cpu_model(data) — blocks event loop. Expected: concurrent completion. Actual: serial execution.
Corrected: Process pool
result = await loop.run_in_executor(pool, cpu_model, data) — non-blocking. Expected: concurrent completion without blocking. Limitation: pickling overhead for large tensors.
Python Sources and Technical Review
All Python language claims reference the official Python 3 documentation. The async vs CPU-bound analysis additionally references FastAPI documentation for service architecture context. No competitor blogs are used as technical sources.