Python Interview Questions for AI Engineers

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?

FoundationCoding0-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?

Public scoring signals:

  • Correctly identifies pass-by-object-reference semantics
  • Distinguishes shallow copy from deep copy
  • Proposes at least two prevention strategies with trade-offs
  • Recognises that immutability has a cost in verbosity
Q2

When Should an AI Pipeline Use a Generator Instead of a List?

FoundationCoding0-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
Q3

How Would You Design a Python Protocol for Multiple LLM Providers?

AppliedCoding2-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
  • Recognises runtime_checkable limitations
Q4

How Do Context Managers Prevent Resource Leaks During Timeouts?

AppliedCoding2-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
Q5

How Do You Test Nondeterministic LLM Outputs Reliably?

AppliedCoding2-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
Q6

When Should You Use Asyncio, Threads or Process Pools?

ProductionApplied2-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
  • Identifies the event-loop-blocking failure mode
Q7

How Would You Profile a Slow AI Pipeline?

ProductionApplied5-8 years

Competency: Profiling and bottleneck diagnosis

Interview scenario

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
Q8

How Would You Debug Intermittent Data Transformation Errors?

ProductionProject Deep Dive5-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

How to Structure a Strong Python Interview Answer

  1. Clarify the question — restate what is being asked and what assumptions you are making
  2. Decompose into stages — identify the components or pipeline stages involved
  3. Compare options — list at least two approaches and their trade-offs
  4. Decide — choose one approach and justify it with a constraint or evidence
  5. 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.

NoteCode patterns are illustrative. Run with your own environment and verify expected behaviour with tests.
Flawed: Mutable defaultdef process(docs: list = []): ... — default list shared across calls. Expected: fresh list. Actual: items accumulate.
Corrected: Default Nonedef process(docs: list | None = None): docs = docs or [] — fresh list per call. Expected: no accumulation across calls.
Flawed: Manual closeconn = open_db(); result = llm.call(); conn.close() — close() skipped on exception. Actual: connection leak on timeout.
Corrected: Context managerwith open_db() as conn: result = llm.call() — closed on any exit. Expected: pool returns to initial count.
Flawed: Blocking asyncasync def handler(): result = cpu_model(data) — blocks event loop. Expected: concurrent completion. Actual: serial execution.
Corrected: Process poolresult = 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.

Practise Python Under Interview Constraints

Need to learn the underlying skill?

FastAPI Interview Questions

If you need to learn API design and async patterns for AI services, start here.

Already know it but need interview practice and feedback?

AI Engineer Interview Preparation Course

Practise these Python questions under timed conditions with live feedback and follow-up pressure.