460 Python Developer Interview Questions & Answers

154 top • 31 Amazon • 49 Google • 44 Netflix • 48 Meta • 41 NVIDIA • 47 Apple • 46 Microsoft

Python Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

121. Which Python profiling tools would you use to investigate CPU, memory, and latency problems?PerformanceHard

Question Details

Explain when to use cProfile, pstats, timeit, py-spy, tracemalloc, memory profilers, application metrics, and distributed tracing, and how you would avoid drawing conclusions from unrealistic microbenchmarks.

Short Interview Answer (30-60 seconds)

I would start with production symptoms and metrics, not a profiler. First I would check p95 latency, error rate, CPU use, memory growth, queue delay, and dependency timing. Then I would choose the tool based on the suspected bottleneck. I would use cProfile and pstats for function level CPU work, py spy for low overhead sampling, tracemalloc and memory profilers for allocations, and tracing for service latency. I would use timeit only for small isolated checks, not as proof of production speed.

Detailed Explanation

I would treat profiling as an evidence gathering process. The first step is to define the visible problem. For example, the problem may be high p95 latency, growing memory, high CPU use, or slow background jobs. Then I would set the measurement boundary. That means deciding whether I am measuring one function, one request, one worker job, or the whole service.

Useful Questions to Ask the Interviewer
  1. What user-visible symptom and measurable performance target define success?
  2. What workload, environment, data size, and concurrency level should I assume?
  3. What profiling evidence is available, and which tradeoffs or system changes are allowed?

For latency, I would start with application metrics and distributed tracing. Application metrics show trends such as request rate, p95 latency, p99 latency, error rate, CPU use, and memory use. Distributed tracing follows one request across services, databases, queues, and network calls. This helps separate time spent doing Python work from time spent waiting.

For CPU problems, I would use cProfile in a controlled run. It is deterministic, which means it records function calls and time during the run. Then I would use pstats to sort the output by total time and cumulative time. Total time shows time inside one function. Cumulative time includes time spent in functions it calls. If I need to observe a running process with less overhead, I would use py spy. It is a sampling profiler, which means it checks stack frames at intervals instead of recording every call.

For memory problems, I would use tracemalloc first when Python allocations are suspected. It can compare snapshots and show where memory was allocated. If memory grows line by line in a script or worker, I would use a memory profiler. I would remember that tracemalloc does not see every native allocation from C extensions.

For tiny code experiments, I would use timeit. It is useful for checking a small function in isolation. I would not use it to prove that a web endpoint is faster. A microbenchmark may ignore database time, network delay, serialization, cache behavior, and concurrency.

After finding evidence, I would make one targeted change. Then I would retest with the same representative workload. I would compare before and after latency, CPU, memory, errors, and dependency time. I would also verify that results stay correct. The goal is to reduce the measured bottleneck without moving the problem somewhere else.

Which Python profiling tools would you use to investigate CPU, memory, and latency problems? diagram
Technical Approach

First, define the symptom. Use a concrete metric such as p95 latency, CPU use, memory growth, or queue delay. Second, capture a baseline from metrics, logs, and traces. Third, reproduce the problem with representative requests, data sizes, and dependency behavior. Fourth, classify the bottleneck using evidence. It may be CPU work, memory allocation, database time, network wait, queue delay, locks, or event loop blocking. Fifth, choose the tool that matches the suspected problem. Use cProfile and pstats for controlled CPU runs. Use py spy for sampling a running process. Use tracemalloc and memory profilers for memory growth. Use distributed tracing for end to end latency. Sixth, make one targeted change. Seventh, retest with the same workload and verify correctness.

Practical Insights

The cost depends on the tool. Application metrics and traces are useful in production, but they can add small overhead and sampling bias. cProfile gives detailed CPU data, but it can slow the program during a controlled run. py spy has lower overhead, but it may miss very short events. tracemalloc helps with Python allocations, but it may miss memory held by native libraries. timeit is cheap for small code, but it does not include real service behavior. The main cost is running realistic tests and comparing results carefully.

Why Interviewers Ask This

Interviewers ask this to check whether you measure before optimizing. They want to see if you can pick the right tool for CPU, memory, and latency symptoms. They also want to know if you understand tool limits. A strong candidate does not trust one small benchmark. They compare real metrics, traces, and profiler evidence.

Common interview mistakes

A common mistake is optimizing code before measuring the real symptom. Another mistake is using average latency and ignoring p95 or p99 latency. Some candidates use timeit on a tiny function and treat it as proof that the full service is faster. That is risky because production includes databases, network calls, queues, payload sizes, and concurrency. Another mistake is using cProfile once and assuming it explains memory or network delay. Some people confuse CPU time with waiting time. Others ignore memory growth, allocation churn, connection pool waits, lock contention, or event loop blocking. A final mistake is changing the workload between before and after tests.

Interview tip

Start with the symptom and metric. Then name the tool that answers that exact question. Explain one limitation for each tool. Say that timeit is useful for small isolated checks, but real performance needs representative load, metrics, traces, and before and after comparison.

Interviewer may ask next
What if timeit shows that a function is faster, but the endpoint is still slow in production?

I would not treat the timeit result as the final answer. timeit only measures a small isolated piece of code. The endpoint may still be slow because it waits on the database, network calls, serialization, middleware, or a queue. I would go back to production metrics and distributed tracing. The measurement boundary should be the full request, not just the small function. If traces show that most time is spent in a database call, optimizing Python loop speed will not help much. The tradeoff is that full service measurement takes more setup than a microbenchmark. But it gives evidence that matches real user latency.

What if memory keeps growing but cProfile does not show the cause?

I would switch tools because cProfile is mainly for CPU profiling. It tells me where function time is spent, not where memory is being retained. For Python allocation growth, I would use tracemalloc snapshots. I would compare snapshots before and after the workload to find allocation sources. If I need line level growth, I would use a memory profiler. I would also check whether native libraries hold memory, because tracemalloc may not see every native allocation. The tradeoff is that memory tools add overhead and need representative input sizes. The result is still stronger than guessing from CPU data.

122. How do you decide between threading, multiprocessing, and asyncio for a Python workload?PerformanceHard

Question Details

Compare I/O-bound and CPU-bound work, the effect of the GIL, process overhead, event-loop behavior, cancellation, shared state, and how you would choose a concurrency model for a production service.

Short Interview Answer (30-60 seconds)

I would first confirm the Python runtime and measure whether the workload is mostly waiting for input and output or spending time on CPU work. I would use threading when I must run blocking input and output libraries concurrently and the shared state is manageable. I would use multiprocessing for substantial pure Python CPU work when parallel execution is worth the process startup, serialization, communication, memory, and cancellation costs. I would use asyncio for high concurrency input and output when the libraries support async behavior and the service can handle cancellation, timeouts, backpressure, and event loop discipline. On the standard GIL enabled CPython build, threads usually do not run pure Python bytecode in parallel, but they can still help while waiting, and native extensions may release the GIL. Optional free threaded Python builds can run Python threads in parallel, so I would confirm the interpreter build and extension compatibility before relying on that behavior. I would validate the choice with representative load, latency, throughput, CPU, memory, queue growth, and failure behavior rather than assuming one model is always fastest.

Detailed Explanation

I would begin by confirming the Python runtime and measuring the workload instead of deciding from the task name. I would check whether the service uses the standard GIL enabled CPython build or an optional free threaded build, because that changes how Python threads can use CPU cores. I would then use application metrics, traces, profiling, queue measurements, event loop lag, and resource utilization to determine whether the workload is mainly CPU work, blocking input and output, or high concurrency async input and output under representative load.

Useful Questions to Ask the Interviewer
  1. What user-visible symptom and measurable performance target define success?
  2. What workload, environment, data size, and concurrency level should I assume?
  3. What profiling evidence is available, and which tradeoffs or system changes are allowed?

A workload is CPU bound when most time is spent performing Python or native computation. It is input and output bound when most time is spent waiting for a database, network service, disk, queue, or another external resource. I would also check whether the important libraries are synchronous, async capable, or native extensions that may release the GIL. This matters because the same business task can behave differently depending on the runtime, library implementation, task size, and data movement.

Threading is usually suitable when the code uses blocking input and output libraries and several operations need to wait concurrently. While one thread waits for network, database, or disk input and output, another thread can run. On the standard GIL enabled CPython build, only one thread normally executes Python bytecode at a time, so threading is usually not the first choice for heavy pure Python CPU work. However, the GIL does not make threading useless. Native extensions may release it, and optional free threaded Python builds can allow Python threads to execute in parallel across CPU cores. I would confirm interpreter support and extension compatibility before relying on free threading because it is optional and some extensions may not behave the same way. Threading also has costs such as context switching, thread stack memory, lock contention, race conditions, limited cancellation, and difficult shared state management. I would use a bounded thread pool rather than creating unlimited threads.

Multiprocessing is usually suitable for substantial pure Python CPU work that can be divided into independent units. Separate processes have separate Python interpreters and can execute Python code in parallel across CPU cores. The costs are process startup, serialization, data copying, interprocess communication, larger memory use, result collection, and more complex failure handling. Small tasks may become slower because the overhead is larger than the useful work. Shared in memory state is not automatically available between processes, so I would prefer immutable inputs, coarse task sizes, and clear message based boundaries. I would also remember that cancellation is limited after work has started in another process, so shutdown and task termination need explicit design.

Asyncio is usually suitable for services with many concurrent input and output operations when the libraries support nonblocking async interfaces. Many tasks share one event loop and cooperate by yielding control at await points. This can provide high concurrency without one operating system thread per task. It does not automatically make code faster, and one blocking synchronous call or CPU heavy loop can delay every task on the event loop. Blocking work should be moved to a controlled executor, process, or separate worker when appropriate.

Asyncio also requires careful timeout, cancellation, and cleanup behavior. Cancellation is cooperative, so code must reach await points and handle cleanup correctly. Background tasks must not leak after a request ends. The service also needs bounded concurrency and backpressure so that it does not create unlimited tasks, fill queues, exhaust connection pools, or overload downstream systems. Shared mutable state can still create races because tasks may interleave at await points.

For a production service, I would use a clear decision order. First, confirm the runtime and whether the GIL is enabled. Second, measure CPU time versus waiting time. Third, check whether required libraries are blocking, async capable, or native code that releases the GIL. Fourth, estimate concurrency, task size, serialization cost, and memory use. Fifth, evaluate shared state, cancellation, backpressure, graceful shutdown, observability, deployment, and team maintenance cost. Only then would I select the simplest model that satisfies the measured workload.

If the database driver and HTTP client are synchronous, a bounded thread pool may be simpler than converting the whole service to asyncio. If the service already uses async libraries and must handle many concurrent network requests, asyncio may be the clearest choice. If a request contains expensive pure Python computation on a standard GIL enabled build, I may keep the service input and output path async or threaded but move that CPU work to a process pool or separate worker. If a native library releases the GIL or the runtime is a compatible free threaded build, threads may also be worth benchmarking for CPU work.

The choice can be mixed, but each boundary must be explicit. For example, an async service may use a bounded process pool for CPU heavy work. A threaded service may send large computation to worker processes. I would avoid mixing models without a measured need because the combination increases cancellation, shutdown, debugging, and deployment complexity.

I would validate the chosen model with the same representative workload and compare latency percentiles, throughput, CPU use, memory use, queue growth, connection pool pressure, event loop lag, errors, timeouts, cancellation behavior, and graceful shutdown. The correct model is the one that improves the measured workload while keeping correctness, resource use, reliability, and maintenance cost acceptable.

How do you decide between threading, multiprocessing, and asyncio for a Python workload? diagram
Technical Approach
  1. Confirm the Python runtime and whether the GIL is enabled.
  2. Measure where the workload spends time.
  3. Classify it as mainly CPU work, blocking input and output, or high concurrency async input and output.
  4. Check whether the libraries are synchronous, async capable, or native code that may release the GIL.
  5. Choose threading for bounded concurrent blocking input and output when shared state is manageable.
  6. Benchmark threads for CPU work only when native code releases the GIL or a compatible free threaded build is in use.
  7. Choose multiprocessing for large enough pure Python CPU tasks that justify process and serialization overhead.
  8. Choose asyncio for high concurrency input and output when the full path can cooperate with the event loop.
  9. Define limits for threads, processes, tasks, queues, and connection pools.
  10. Design cancellation, timeouts, backpressure, shutdown, and failure handling.
  11. Test the model with representative traffic and data.
  12. Compare latency, throughput, CPU, memory, queue growth, errors, and operational complexity before adopting it.
Practical Insights

The main cost is not one simple algorithmic complexity. Threading adds thread memory, context switching, lock management, and shared state risk. Multiprocessing adds process startup, serialization, communication, duplicated memory, and worker management. Asyncio can support many waiting tasks with lower per task overhead, but it adds event loop rules, cancellation handling, and the risk that one blocking call delays all tasks. Every model also uses database connections, network sockets, queues, and downstream capacity. The best choice is the model whose useful concurrency is greater than its coordination and operational cost for the measured workload.

Why Interviewers Ask This

Interviewers ask this question to see whether the candidate can classify a workload using evidence instead of choosing a concurrency model by habit. They want to know whether the candidate understands the GIL, blocking input and output, process overhead, event loop behavior, cancellation, shared state, backpressure, and production operations. They are also checking whether the candidate validates the choice with realistic load and resource measurements.

Common interview mistakes

A common mistake is choosing a model from intuition without measuring whether the work is CPU bound or waiting. Another is describing the GIL as universal without checking whether the service uses the standard GIL enabled build or an optional free threaded build. Candidates may also claim that threads are useless, even though they can help blocking input and output, native extensions may release the GIL, and compatible free threaded builds can run Python threads in parallel. Other mistakes include using threads for heavy pure Python CPU work on the standard build, ignoring process startup and serialization cost, assuming process work can always be cancelled after it starts, placing blocking calls inside the event loop, creating unbounded threads or async tasks, sharing mutable state without synchronization, and assuming async cancellation stops work immediately. Candidates also forget backpressure, connection pool limits, graceful shutdown, task cleanup, process failure handling, extension compatibility, and the need to test under realistic concurrency.

Interview tip

Start by confirming the Python runtime and classifying the measured workload. Then explain threading for blocking input and output, multiprocessing for substantial pure Python CPU work, and asyncio for high concurrency async input and output. Mention the standard GIL behavior, optional free threaded builds, native extensions that may release the GIL, one major cost of each model, cancellation and backpressure, and finish by saying that the choice must be verified under representative production load.

Interviewer may ask next
What would you do if an asyncio service contains a CPU heavy Python function?

I would not run that function directly on the event loop because it would delay unrelated tasks. The exact boundary is the CPU heavy function, while the surrounding network and database path can remain async. On a standard GIL enabled build, I would first reduce or optimize the computation, then move the remaining substantial pure Python CPU work to a bounded process pool or separate worker and await its result. If the work is inside a native extension that releases the GIL, or the runtime is a compatible free threaded build, I would also benchmark a bounded thread pool. This matters because the event loop must remain responsive. The tradeoff is process or thread overhead, serialization, memory use, cancellation complexity, and the need to limit queued work.

How would you choose between a thread pool and asyncio for a high concurrency network service?

I would choose based on library support, concurrency level, cancellation needs, and measured operational cost. The boundary is the network input and output path and its database or HTTP clients. If the required libraries are blocking and the expected concurrency is moderate, a bounded thread pool may be simpler and safer. If the full path supports async interfaces and the service needs many concurrent connections, asyncio may use fewer operating system threads and provide clearer timeout and cancellation control. The tradeoff is simpler synchronous code with thread and lock costs versus event loop discipline, async library requirements, and blocking call risk.

123. Tell me how you would diagnose and fix a slow Python endpoint or background job.PerformanceHard

Question Details

Explain how you would measure latency, profile CPU and memory, inspect database queries and network calls, find blocking work, test changes under load, and verify that an optimization improves the real bottleneck.

Short Interview Answer (30-60 seconds)

I would start by defining what slow means with a measurable target, such as p95 latency for an endpoint or total processing time and queue delay for a background job. Then I would capture a baseline using application metrics, logs, and distributed traces so I can see whether time is spent in Python code, database queries, network calls, queue waits, locks, or resource pools. After narrowing the problem, I would use the right profiler, such as py spy or cProfile for CPU work and tracemalloc for Python memory allocations. I would make one change that targets the measured bottleneck, run the same representative load again, compare the same metrics, verify correctness, and check that the bottleneck did not move to another dependency. The main tradeoff is that profiling and tracing can add overhead, so I would use controlled runs, sampling, or a limited production rollout.

Detailed Explanation

I would begin by defining the symptom and the success metric. For an endpoint, I would usually inspect p50, p95, and p99 latency, throughput, error rate, request queue time, and resource saturation. For a background job, I would separate queue wait time from worker execution time and also inspect retry rate, failure rate, throughput, and completion time. This creates a baseline and prevents me from optimizing code that is not responsible for the real delay.

Useful Questions to Ask the Interviewer
  1. What user-visible symptom and measurable performance target define success?
  2. What workload, environment, data size, and concurrency level should I assume?
  3. What profiling evidence is available, and which tradeoffs or system changes are allowed?

Next, I would reproduce the problem with representative requests, job payloads, data sizes, dependency behavior, and concurrency. A single fast local request is not enough because production delays may appear only with a large database, multiple workers, connection pool pressure, or many concurrent requests. I would keep the workload stable so that the before and after results are comparable. I would also keep worker count, data state, cache state, warmup period, test duration, and dependency conditions consistent between runs.

I would then break down the execution path using metrics, logs, and distributed tracing. For an endpoint, I would inspect request queue time, middleware, application code, database calls, external network calls, serialization, and response time. For a background job, I would inspect queue delay, worker startup, task code, retries, database calls, external services, locks, batching, and result publication. This helps separate active CPU time from time spent waiting.

If the evidence points to CPU work, I would normally prefer low overhead metrics, tracing, and a sampling profiler such as py spy for a running production process. I would use cProfile and pstats in a controlled environment because deterministic profiling can materially change execution timing. I would look for hot functions, repeated work, inefficient loops, expensive serialization, and algorithms whose cost grows badly with input size. A sampling profiler can miss very short events, so I would treat every profiler result as evidence rather than absolute proof.

If memory is the problem, I would inspect process memory trends and use tracemalloc snapshots to compare Python allocations over time. I would look for retained objects, large temporary objects, allocation churn, unbounded caches, growing queues, and data loaded fully into memory. I would compare tracemalloc results with process level resident memory because native extensions, allocator behavior, and child processes may increase memory without appearing fully in Python allocation snapshots.

If database time is high, I would inspect query logs, query count, execution plans, indexes, result size, transaction contention, and connection pool wait time. Common issues include repeated queries, missing indexes, selecting too much data, long transactions, and a pool that is too small or already saturated. If network time is high, I would inspect timeout values, connection reuse, payload size, retries, remote service latency, and whether independent calls can be safely combined or run concurrently.

I would also look for blocking work. In synchronous code, this may appear as thread pool starvation, lock contention, or long blocking calls. In asyncio code, a synchronous database driver, blocking file operation, CPU heavy loop, or time.sleep call can block the event loop and delay unrelated requests. An awaited asyncio.sleep normally yields control and does not block the event loop. Event loop lag and task timing can help confirm this. The fix may be an async compatible library, moving CPU work to a process, using an executor for controlled blocking work, or reducing the work itself.

After identifying the bottleneck, I would make one evidence based change. Examples include improving a query, removing repeated calls, batching operations, reducing serialization, streaming large results, changing an algorithm, adding a bounded cache, tuning a connection pool, or moving CPU intensive work to a process. I would not add more threads, workers, caching, or concurrency without checking resource limits and failure behavior because those changes can increase memory use, create contention, or overload a dependency.

I would verify the change by running the same representative workload and comparing the same baseline metrics. I would check latency percentiles, throughput, CPU, memory, queue delay, errors, pool waits, and dependency timing as relevant. I would also run correctness tests because a faster result is not useful if it is stale, incomplete, duplicated, reordered, or incorrect. Finally, I would check whether the original bottleneck was reduced or simply moved to the database, network, queue, or another worker.

For production validation, I would prefer a staged rollout, canary release, or feature flag when the risk justifies it. I would monitor the same metrics after deployment and be ready to roll back if errors, saturation, memory use, or tail latency become worse. The key idea is to measure first, optimize the proven bottleneck, and verify with the same workload.

Tell me how you would diagnose and fix a slow Python endpoint or background job. diagram
Technical Approach
  1. Define the symptom and success metric. Measure endpoint latency or job queue and execution time.
  2. Capture a baseline with percentiles, throughput, errors, CPU, memory, saturation, and dependency timing.
  3. Reproduce the issue with representative requests, data, dependency behavior, and concurrency.
  4. Use traces, logs, metrics, and query information to divide total time into application work and waiting time.
  5. Classify the bottleneck as CPU, memory, database, network, disk, queue, lock, pool, or event loop related.
  6. Use a focused profiler or diagnostic tool that matches the suspected problem.
  7. Make one evidence based change that directly addresses the measured bottleneck.
  8. Repeat the same load test and compare the same metrics.
  9. Verify functional correctness and check whether the bottleneck moved elsewhere.
  10. Release carefully and monitor the original success metric in production.
Practical Insights

There is no single algorithmic complexity for this investigation because the cost depends on the endpoint, job, data size, and dependency behavior. Profiling adds some CPU and timing overhead. Distributed tracing adds instrumentation and storage cost. Load testing uses compute, database connections, network capacity, and engineering time. Some fixes also introduce new costs. Caching uses memory and can serve stale data. More concurrency uses additional connections and can overload downstream systems. Multiprocessing increases process startup, serialization, and memory costs. The correct choice is the smallest change that improves the measured bottleneck without creating an unacceptable reliability or maintenance cost.

Why Interviewers Ask This

Interviewers ask this question to see whether the candidate measures a real production symptom before changing code. They want evidence that the candidate can separate CPU work from waiting on databases, networks, queues, locks, and other dependencies. They also want to know whether the candidate can choose suitable profiling tools, test a change under realistic load, preserve correctness, and verify that the original bottleneck was actually reduced.

Common interview mistakes

A common mistake is changing code before defining the symptom and baseline. Another is using average latency only and missing slow p95 or p99 requests. Candidates may profile unrealistic input, treat a timeit result as proof of service performance, or confuse CPU time with time spent waiting for a database or network call. Other mistakes include ignoring query count and connection pool waits, blocking the asyncio event loop with synchronous input and output, CPU work, or time.sleep, adding unbounded threads or tasks, comparing different workloads before and after the change, and assuming more workers will fix every problem. It is also a mistake to trust one profiler as complete proof, ignore profiler overhead, skip correctness checks, or improve one component while moving the bottleneck to another dependency.

Interview tip

Explain the investigation as a measured sequence. Start with the symptom and baseline, show how you divide total time into CPU and waiting, name the tool that confirms the suspected bottleneck, describe one targeted fix, and finish with the same load test, correctness checks, and production monitoring.

Interviewer may ask next
What would you do if the endpoint is fast in local profiling but slow in production?

I would treat that as evidence that the local workload or environment does not reproduce the production boundary. I would compare production traces, queue time, database latency, connection pool waits, external calls, payload sizes, data volume, concurrency, and resource limits. The exact boundary is the complete request path, not only the Python function measured locally. I would build a representative test using production like data and dependency timing, then use low overhead production metrics or sampling profiles where safe. The tradeoff is that production observation provides realistic evidence but must be limited to avoid excessive overhead and exposure of sensitive data.

How would you verify that adding more workers is the correct fix?

I would add workers only when measurements show available downstream capacity and a workload that can benefit from more parallel processing. The boundary includes the worker pool, queue, CPU, memory, database connections, network dependencies, and any shared locks. I would run the same representative load with controlled worker counts and compare queue delay, throughput, task time, CPU, memory, errors, retries, and dependency saturation. It matters because more workers can reduce queue delay but can also exhaust memory, increase lock contention, or overload the database. The main tradeoff is higher parallel capacity versus greater resource use and pressure on shared dependencies.

124. What is a unit test?NEWTestingEasy

Question Details

Define a unit test as a fast, focused, repeatable check of one small behavior in isolation from slow or uncontrolled external systems. Explain arrange-act-assert, observable outcomes, deterministic inputs, boundary and failure cases, test doubles, and why unit tests do not replace integration, contract, or end-to-end tests.

Short Interview Answer (30-60 seconds)

I use a unit test to check one small behavior quickly and repeatedly. I keep slow or uncontrolled systems such as a real database or network outside the test boundary. I arrange known inputs, act by calling the function or method, and assert an observable result. If the code depends on an external system, I can use a stub, mock, or fake so the test stays isolated. The tradeoff is that this gives fast feedback, but it does not prove that real components work together.

Detailed Explanation

See the Code while reading this explanation.

A unit test checks one small behavior by itself. It uses known input, runs one action, and checks a result that we can see. The same input should give the same result each time. Slow or changing outside systems, such as a real database or network, stay outside this test. Good unit tests are fast and easy to repeat. They also cover normal cases, boundary cases, and failure cases. They help find small mistakes early, but they do not prove that the whole system works together.

Useful Questions to Ask the Interviewer
  1. Should I explain this with the simple add example shown in the diagram?
  2. Do you want me to compare unit tests with integration, contract, and end to end tests?
What is a unit test? diagram
How to Explain It in an Interview

Start with the boundary. A unit test checks one small behavior. In the main diagram example, the system under test is add. The test sets a to 2 and b to 3, calls add, and checks that the observable return value is 5. A real database, file system, network service, current time, or other uncontrolled dependency stays outside this boundary.

Use arrange, act, assert. Arrange means prepare deterministic inputs and any needed test double. Act means call the function or method under test. Assert means check the observable outcome. The diagram shows this flow directly: prepare the inputs, run the code, then verify the result. The test should check behavior a caller can observe rather than private implementation details.

Keep inputs deterministic. The same controlled input should lead to the same result every time. Control time, randomness, environment values, and external calls when they could change between runs. If an external dependency must be replaced, use the right test double. A stub returns fixed controlled data. A mock can return controlled data and verify expected calls. A fake is a lightweight working replacement. In Python, monkeypatch or unittest.mock can replace a dependency where the code under test looks it up.

Cover the happy path, boundary cases, and failure cases. The add example shows the happy path, where 2 plus 3 returns 5. The diagram also uses division to illustrate that a small unit can have a failure rule, such as rejecting division by zero. Each defined behavior should have its own focused test. Do not invent failure behavior that the real function does not define.

For a pure function such as add, no fixture or cleanup is needed because the test creates no shared state. When reusable setup is needed, use a small fixture with the narrowest useful scope. Do not hide mutable shared state inside a fixture or make tests depend on execution order. Cleanup should remove only temporary state created by the test.

In continuous integration, unit tests should run on every change because they are fast, focused, repeatable, and isolated. They give quick feedback when one behavior breaks. They do not replace other test levels. Integration tests check selected real parts working together. Contract tests check agreements between systems. End to end tests check a complete flow. All of these layers work together to give stronger confidence.

Key Insight / Why This Solution Works
  1. Define one small behavior to test.
  2. Choose the unit test level because the behavior can be checked without real external systems.
  3. Arrange deterministic inputs and replace any slow or uncontrolled dependency with the right test double.
  4. Act by calling the function or method once for the behavior under test.
  5. Assert the observable result and only the important interaction when an interaction is part of the contract.
  6. Add separate tests for normal, boundary, and failure cases that the behavior actually defines.
  7. Keep state isolated, clean up temporary resources when needed, and run the tests independently in continuous integration.
Example

The main executable example follows the add flow shown in the diagram. The system under test is add. The test boundary contains only that function and two deterministic integer inputs. There are no fixtures, external dependencies, or cleanup steps because the function is pure and creates no shared state. Arrange sets a to 2 and b to 3. Act calls add. Assert checks the observable return value and expects 5. The diagram also shows division as a separate teaching example for a possible failure rule, but the executable code below stays focused on the add behavior. This unit test does not prove database, network, contract, or end to end integration because those systems are outside its boundary.

Code
def add(a, b):
    return a + b


def test_add_positive_numbers():
    a = 2
    b = 3
    result = add(a, b)
    assert result == 5
Where it is used

Unit tests are used for small Python behaviors such as calculations, validation rules, formatting, parsing, state changes, and service logic that can be isolated from external systems. Teams often run them on every code change in continuous integration because they are fast and repeatable. They are especially useful when a developer wants quick feedback before running slower integration or end to end tests.

Why Interviewers Ask This

Interviewers ask this to see whether I understand the right boundary for a unit test. They want to know if I can test one small behavior, isolate slow or uncontrolled dependencies, use deterministic inputs, and make focused assertions. They also want to see whether I know the limit of unit tests and when integration, contract, or end to end tests are still needed.

Common interview mistakes

Common mistakes are testing many behaviors in one test, depending on a real network or database for a unit test, using random or time based input without control, and checking private implementation details instead of observable behavior. Another mistake is using the wrong test double. A stub only returns controlled data, while a mock can also verify expected calls. When patching in Python, patch where the code under test looks up the dependency. Other mistakes include shared mutable fixtures, tests that depend on order, weak assertions, ignoring boundary or failure cases, and treating coverage as proof that the behavior is correct.

Interview tip

Give a short definition first, then explain arrange, act, assert with one small Python example. State the test boundary clearly and mention that unit tests are fast because real external systems stay outside it. Finish by saying that unit tests complement integration, contract, and end to end tests rather than replacing them.

Interviewer may ask next
What would you do if a unit test sometimes fails because the code reads the current time or calls a network service?

I would keep the same unit test boundary and control those dependencies. I would replace the time source or network call at the lookup location used by the code under test, then give the test fixed data. That matters because a unit test should be deterministic and isolated. The tradeoff is that the test becomes faster and more reliable, but it still does not prove that the real network service works correctly with my code.

When should this stop being a unit test and become an integration test?

It should become an integration test when the behavior I need to verify depends on real collaboration between selected components, such as application code and a real test database. The boundary changes from one isolated unit to those real components working together. This matters because mocks cannot prove the real integration. The tradeoff is slower setup and execution, but the test gives stronger confidence that the selected components work together correctly.

125. What is a mock in Python testing?NEWTestingEasy

Question Details

Define a mock as a configurable test double that can provide controlled behavior and record interactions. Explain stubs, fakes, spies, unittest.mock Mock, patching where a dependency is looked up, return values, side effects, autospec, and the tradeoff between isolating a unit and creating a test coupled to implementation details.

Short Interview Answer (30-60 seconds)

For a unit test, I use a mock when I want to replace a real dependency with controlled behavior and also record how my code uses that dependency. In Python, unittest.mock.Mock can return chosen values, raise errors with side_effect, and record calls. I patch the dependency where the code under test looks it up. This makes the test fast and focused, but checking too many internal calls can make the test fragile and too dependent on implementation details.

Detailed Explanation

See the Code while reading this explanation.

The practical choice is to use a mock when one small unit should be tested without calling a real dependency. A mock acts as a controlled stand in. The test decides what it should return or raise, runs the real code under test, and then checks the result and any important call. This keeps the test fast and repeatable. The main risk is checking too many internal calls, because a harmless refactor can break the test even when the visible behavior is still correct.

Useful Questions to Ask the Interviewer
  1. Should I focus on unit tests, or also compare mocks with stubs, fakes, and spies?
  2. Would you like a concrete unittest.mock example showing where to patch a dependency?
What is a mock in Python testing? diagram
How to Explain It in an Interview

A mock is a configurable test double. A test double is an object used in a test instead of a real dependency. For this example, module_a.get_data is the system under test. The real Client object is outside the unit test boundary, so the test replaces Client with a mock.

The common test doubles have different purposes. A stub mainly returns controlled data. A fake is a small working implementation, such as an in memory store. A spy records calls and can still preserve real behavior. A mock provides controlled behavior and records interactions so the test can verify important calls.

Python provides unittest.mock.Mock and related helpers. return_value controls what a mock returns. side_effect can raise an exception, call another function, or provide a sequence of results. A mock also records information such as whether it was called, how many times it was called, and which arguments were passed.

Patching must happen where the code under test looks up the dependency. If module_a contains from module_b import Client, then get_data uses module_a.Client. The test should therefore patch module_a.Client, not module_b.Client. Patching the original definition may not replace the name that module_a already imported.

The setup creates the patch and configures the mock instance. In the example, fetch returns "fake data". The execution step calls the real module_a.get_data function. The assertions first check the observable result. Then the test verifies the important interaction by checking that Client was created once and fetch was called once.

The patch is temporary. When the patch context ends, the original Client object is restored. No real network or external service state is created, so there is no external cleanup for this unit test. Each test should still create its own mock state and should not depend on execution order.

autospec can make a mock follow the interface of the real object more closely. For example, create_autospec can reject an attribute that does not exist on the real class and can catch some incorrect call signatures. This reduces the risk that a loose mock silently accepts an invalid API.

A mocked unit test should cover the normal result and useful failure paths. For a failure case, the test can set side_effect on fetch to raise the expected exception and then check the behavior required from get_data. This stays deterministic because the real dependency is not contacted.

These tests are usually fast in CI because they do not start a real external service. However, mocks do not prove that module_a and the real Client work together correctly. That requires an integration test with the real selected components.

The main tradeoff is isolation versus coupling. Mocks make a unit test focused, fast, and deterministic. But if the test checks every private call or internal step, a refactor can break the test even when the public behavior is unchanged. A strong test checks the result first and verifies only interactions that matter to the behavior.

Key Insight / Why This Solution Works
  1. Define the behavior that module_a.get_data must provide.
  2. Choose a unit test because the real Client dependency is outside the test boundary.
  3. Patch module_a.Client because that is where get_data looks up Client.
  4. Configure the mock instance with the needed return_value or side_effect.
  5. Call the real module_a.get_data function.
  6. Assert the observable result.
  7. Verify only important interactions, such as Client being created once and fetch being called once.
  8. Let the patch context end so the original Client is restored.
  9. Run the test independently in CI without contacting the real dependency.
Example

The example keeps module_a.get_data as the system under test and treats Client as the dependency outside the unit boundary. module_a imports Client from module_b, so the test patches module_a.Client because that is the name get_data looks up. The mock Client instance is configured so fetch returns "fake data". The test calls get_data, checks the returned value, checks that Client was created once, and checks that fetch was called once. When the patch context ends, the original Client is restored automatically.

Code
# module_b.py
class Client:
    def fetch(self):
        return "real data"


# module_a.py
from module_b import Client


def get_data():
    client = Client()
    return client.fetch()


# test_module_a.py
from unittest.mock import patch
import module_a


def test_get_data():
    with patch("module_a.Client") as mock_client:
        mock_client.return_value.fetch.return_value = "fake data"

        result = module_a.get_data()

        assert result == "fake data"
        mock_client.assert_called_once_with()
        mock_client.return_value.fetch.assert_called_once_with()
Where it is used

Mocks are commonly used in unit tests when Python code depends on a network client, email sender, payment client, clock, random source, file service, or another component that should not run during a focused unit test. They are also useful for forcing controlled failure paths with side_effect. A mock should not replace an integration test when the goal is to prove that real components communicate correctly.

Why Interviewers Ask This

Interviewers ask this to see whether you understand how to isolate one unit of Python code from a real dependency. They want to know whether you can choose the right test double, configure controlled behavior, verify meaningful interactions, patch the correct lookup location, and avoid tests that depend too heavily on internal implementation details. They are also checking whether you understand that a mocked unit test does not prove that the real integration works.

Common interview mistakes

A common mistake is patching module_b.Client when module_a already imported Client and looks up module_a.Client. Another mistake is using a mock when the real collaboration is the behavior that needs testing. Tests also become fragile when they verify every internal call instead of the result and a few meaningful interactions. Other mistakes include using a loose mock that accepts invalid attributes, ignoring failure paths, sharing mutable state between tests, depending on test order, and treating a mocked test as proof that the real integration works.

Interview tip

Start by naming the unit test boundary and the real dependency you want to replace. Then explain controlled behavior, recorded interactions, and the rule to patch the lookup location. Mention return_value, side_effect, and autospec. Finish with the tradeoff: mocks give fast isolation, but too many interaction assertions can make tests fragile.

Interviewer may ask next
How would you test the same unit when Client.fetch raises an error?

I would keep the same unit test boundary and still patch module_a.Client because that is the lookup location. I would set mock_client.return_value.fetch.side_effect to the expected exception, call module_a.get_data, and assert the failure behavior that get_data is required to provide. This matters because it tests the failure path without contacting the real dependency. The main tradeoff is still isolation versus coupling, so I would verify only interactions that are important to the required behavior.

When would you use an integration test instead of this mocked unit test?

I would change the test boundary when I need proof that module_a and the real Client work together. In that integration test, Client would remain real inside the selected boundary instead of being replaced by a mock. This matters because a mock only proves how the unit behaves against the interface that the test configured. It does not prove that the real integration is correct. The tradeoff is that an integration test needs more setup and is usually slower, but it gives stronger confidence about real component collaboration.

126. How would you mock external dependencies in Python tests without making the tests misleading?TestingHard

Question Details

Explain unittest.mock or monkeypatch, where to patch an imported dependency, how to mock HTTP clients, clocks, queues, and databases, and how over-mocking can hide integration problems.

Short Interview Answer (30-60 seconds)

I mock only the dependency that is outside the unit test boundary, and I patch the name where the code under test looks it up. For HTTP clients, clocks, queues, and database gateways, I return controlled results and assert only important calls and visible behavior. I keep the fake response close to the real contract, cover errors and timeouts, and add separate integration or contract tests with real components. The main tradeoff is speed and isolation versus confidence in the real integration, so I avoid mocking every internal method.

Detailed Explanation

See the Code while reading this explanation.

The first decision is the test boundary. In a unit test, I keep the business function real and replace only dependencies that leave that boundary, such as an HTTP client, the current clock, a queue publisher, or a database gateway. The unit test should prove the behavior of the business function, not prove that the external service or database works.

Useful Questions to Ask the Interviewer
  1. What behavior and test boundary should I cover?
  2. Which dependencies, environments, and test tools should I assume?
  3. Which failures, edge cases, and quality risks are most important?

I patch where the code under test looks up the dependency. Suppose service.py contains from payments import charge and then calls charge. The test should patch service.charge because that is the name used by service.py. Patching payments.charge may leave the already imported name unchanged, which can cause the test to call the real dependency or fail for the wrong reason.

For HTTP calls, I normally replace the HTTP client object or the small client wrapper used by the application. The test returns a realistic response object with the status and body shape that the application actually reads. I test success, timeout, connection failure, invalid data, and relevant error responses. I also assert the important request contract, such as the URL, payload, headers, and timeout, but I avoid asserting every private helper call.

For clocks, I replace the clock function or inject a clock object so the test uses a fixed instant. This keeps expiry, retry, and scheduling tests deterministic. For queues, I replace the publisher at the application boundary and assert the message topic and payload. A lightweight fake queue can be better when several operations must work together because it behaves more like the real interface. For databases, I may mock a repository in a small service unit test, but I use a real controlled database for queries, constraints, transactions, mappings, and migrations. Mocking a database cannot prove that SQL or schema behavior is correct.

The setup should be small and explicit. A pytest fixture can create the mock and restore the original attribute automatically after the test. The test arranges controlled dependency results, runs one public action, and asserts the returned value, state change, or raised error. It may also verify one important interaction, such as publishing one message, but it should not mirror the complete internal call sequence.

Over mocking makes tests misleading when every collaborator is replaced, return values are invented without matching the real contract, or assertions depend on private implementation details. Such tests can pass even when the real API changed, the queue rejects the payload, or the database constraint fails. To prevent this, I keep unit tests focused, add contract tests for external request and response shapes, add integration tests for selected real components, and run those tests in CI. Mocked unit tests provide fast feedback, while integration and contract tests provide confidence that the boundaries still work.

Cleanup should be automatic. unittest.mock.patch, pytest monkeypatch, and scoped fixtures restore replaced names after each test. Tests should not depend on order, shared mutable mocks, live network access, production queues, or production databases. This keeps the suite repeatable on a developer machine and in CI.

How would you mock external dependencies in Python tests without making the tests misleading? diagram
Key Insight / Why This Solution Works
  1. Define the public behavior being tested and name the exact unit boundary.
  2. List dependencies that leave that boundary, such as the HTTP client, clock, queue publisher, or repository.
  3. Patch each dependency where the code under test looks it up.
  4. Configure realistic controlled results for success, failure, and edge cases.
  5. Run the public function or method once.
  6. Assert the visible result, state change, or error.
  7. Verify only important interactions and contracts.
  8. Let the fixture or patch context restore the original dependency.
  9. Add contract or integration tests for behavior that mocks cannot prove.
  10. Run unit, contract, and integration tests at suitable stages in CI.
Example

The example tests a checkout service. The service uses a payment client, a queue publisher, and a clock that are imported into the service module. The test patches those names in the service module because that is where the code looks them up. It fixes the time, returns a realistic payment result, runs checkout, asserts the returned order data, and verifies the important payment and queue contracts. A second test makes the payment client raise a timeout and confirms that no queue message is published. The patches are scoped to each test and are restored automatically.

Code
from datetime import datetime, timezone
from unittest.mock import Mock, patch

import pytest

import checkout_service


def test_checkout_charges_customer_and_publishes_event():
    fixed_time = datetime(2026, 7, 20, 12, 0, tzinfo=timezone.utc)
    payment_result = {"payment_id": "pay_123", "status": "approved"}

    with (
        patch("checkout_service.utc_now", return_value=fixed_time),
        patch("checkout_service.payment_client.charge", return_value=payment_result) as charge_mock,
        patch("checkout_service.event_publisher.publish") as publish_mock,
    ):
        result = checkout_service.checkout(
            order_id="order_100",
            customer_id="customer_7",
            amount_cents=2500,
        )

    assert result == {
        "order_id": "order_100",
        "payment_id": "pay_123",
        "status": "paid",
        "paid_at": fixed_time,
    }
    charge_mock.assert_called_once_with(
        customer_id="customer_7",
        amount_cents=2500,
        idempotency_key="order_100",
    )
    publish_mock.assert_called_once_with(
        "order.paid",
        {
            "order_id": "order_100",
            "payment_id": "pay_123",
            "paid_at": fixed_time.isoformat(),
        },
    )


def test_checkout_does_not_publish_when_payment_times_out():
    with (
        patch(
            "checkout_service.payment_client.charge",
            side_effect=TimeoutError("payment provider timeout"),
        ),
        patch("checkout_service.event_publisher.publish") as publish_mock,
    ):
        with pytest.raises(TimeoutError, match="payment provider timeout"):
            checkout_service.checkout(
                order_id="order_100",
                customer_id="customer_7",
                amount_cents=2500,
            )

    publish_mock.assert_not_called()
Where it is used

This approach is used when a Python service calls payment providers, email services, cloud APIs, message brokers, clocks, or repositories. For example, an order service unit test can replace the payment client and queue publisher while keeping order rules real. Separate contract tests can verify the payment request shape, and integration tests can verify the real database transaction and queue adapter.

Why Interviewers Ask This

Interviewers ask this to see whether the candidate can isolate a small unit of Python code while still preserving confidence in the real system. They are evaluating whether the candidate knows where to patch, how to choose between mocks, stubs, and fakes, how to make tests deterministic, and when a real integration test is required because a mocked test cannot prove that an external contract still works.

Common interview mistakes

A common mistake is patching the module that originally defined a function instead of the name used by the module under test. Another mistake is inventing mock responses that do not match the real external contract. Tests also become fragile when they assert every internal call, call order, or private method. Over mocking can make the test pass while the real HTTP API, queue, or database integration is broken. Other mistakes include sharing mutable mocks between tests, allowing accidental live network calls, using fixed sleep calls, ignoring timeout and failure paths, mocking database behavior that should be checked against a real controlled database, and treating unit test coverage as proof that integrations work.

Interview tip

Start by naming the unit boundary. Then say that you patch at the lookup location, keep mock data faithful to the real contract, assert visible behavior, and use separate contract or integration tests for confidence that the real dependency works.

Interviewer may ask next
How would you stop a mocked HTTP test from passing after the provider changes its response?

I would keep the unit test boundary around the application logic, but add a contract test for the HTTP client wrapper. The contract test would validate the real or provider supplied response shape that the wrapper expects, including required fields and error forms. The unit mock would reuse a fixture based on that contract. This matters because a unit mock cannot detect an external schema change. The tradeoff is that contract tests are slower and may require a sandbox or recorded provider response, but they protect against misleading mock data.

When would you replace a database mock with a real test database?

I would change the boundary when the behavior depends on real queries, constraints, transactions, mappings, or migrations. The service unit test can still mock the repository, but the repository itself should be tested against a controlled test database with migrations and isolated cleanup. This matters because a mock cannot reproduce database rules reliably. The tradeoff is longer setup and CI runtime in exchange for confidence in actual database behavior.

127. What is pytest?NEWTestingEasy

Question Details

Define pytest as a Python testing framework and test runner. Explain test discovery, plain assert statements and assertion introspection, fixtures, parametrization, marks, exception assertions, plugins, configuration, and command-line execution. Distinguish pytest from the standard-library unittest framework and from a mocking library.

Short Interview Answer (30-60 seconds)

I would use pytest when I want a simple Python testing framework and test runner that can automatically find and run tests. It lets me use normal assert statements and gives useful details when an assertion fails. It also supports fixtures for reusable setup, parametrization for running one test with several inputs, marks for grouping tests, pytest.raises for expected exceptions, configuration files, and plugins. unittest is another testing framework in the Python standard library. A mocking library is different because it creates test doubles rather than finding and running tests.

Detailed Explanation

See the Code while reading this explanation.

I would choose pytest when I want a simple way to write, find, and run checks for Python code. It can find matching test files by itself, run each check, and show a useful result when something is wrong. It also helps reuse preparation work and repeat the same check with different values. Some checks can be grouped or selected. Projects can store common settings in a file. Extra tools can add more abilities. This keeps small tests easy to read while still supporting larger test suites.

Useful Questions to Ask the Interviewer
  1. Would you like a short definition, or should I also explain the main pytest features?
  2. Would you like me to compare pytest with unittest and mocking libraries?
What is pytest? diagram
How to Explain It in an Interview

pytest is both a Python testing framework and a test runner. The framework gives us tools for writing and organizing tests. The runner collects matching tests, prepares their setup, executes them, and reports which tests passed, failed, or were skipped.

The test boundary depends on the behavior we want to verify. A small unit test can call one function directly. An integration test can keep selected real components together. pytest can run both kinds of tests, so pytest itself does not mean that every test is a unit test.

Test discovery means pytest searches for tests by naming rules. By default, test files commonly match names such as test_math.py or math_test.py. Test functions commonly start with test_. This lets me open a terminal in the project folder and run pytest without listing every test manually.

pytest uses normal Python assert statements. For example, assert add(2, 3) == 5 checks that the result is correct. pytest rewrites assertions so that, when an assertion fails, it can display useful values from the expression. This behavior is commonly called assertion introspection.

Fixtures provide reusable setup and cleanup. A fixture can create test data, prepare a temporary resource, or supply another object before a test runs. The fixture scope controls how long that value lives. For isolated mutable data, function scope is a good default because each test gets fresh setup. A fixture can also perform teardown after the test when cleanup is needed.

Parametrization runs the same test with several input sets. In the diagram example, test_add receives different values for a, b, and expected. This avoids copying the whole test for every case and makes success cases and edge cases easier to read.

Marks attach extra meaning to tests. Teams can use marks such as slow or integration to select groups of tests. pytest also provides marks for behavior such as skipping a test or marking an expected failure.

Expected exceptions are tested with pytest.raises. In the diagram example, dividing ten by zero is placed inside pytest.raises with ZeroDivisionError. The test succeeds only when that expected exception is raised.

Plugins extend pytest with extra features. A project can also store settings in pytest.ini, pyproject.toml, or tox.ini. Configuration can define test paths, default command options, and registered marks. pytest.ini is optional because many options can also be supplied when pytest is run from the command line.

The execution flow is simple. First pytest collects matching tests. Next fixtures prepare the required setup. Then pytest runs the tests. Finally it reports results such as passed, failed, and skipped tests.

pytest is different from unittest. unittest is another testing framework and test runner included in the Python standard library. Its common style uses TestCase classes and methods such as self.assertEqual, although unittest can support other patterns too. pytest commonly uses simple test functions and plain assert statements.

A mocking library has a different job. A library such as unittest.mock can create mocks, stubs, or other test doubles that replace dependencies during a test. It is not a test runner. A mocked unit test also does not prove that the real dependency works correctly in an integration test.

For reliable tests, I avoid hidden shared mutable state and test order dependencies. I keep test data deterministic. If time, randomness, network access, environment values, or external services can change a result, I control them at the correct test boundary. Cleanup should remove temporary state after a test when required.

In continuous integration, the project can run the same pytest command after each code change. Small isolated tests usually run quickly. Tests that start databases, processes, or external services take more time, so I use those only when the real collaboration is part of the behavior being tested.

The main tradeoff is flexibility versus discipline. pytest makes tests easy to write and extend, but large fixtures, too much hidden setup, unnecessary plugins, or weak assertions can make a suite harder to understand. I prefer small fixtures, focused assertions, deterministic data, and the smallest test level that proves the behavior I care about.

Key Insight / Why This Solution Works
  1. Define the behavior that the test must prove.
  2. Choose the correct test level for that behavior.
  3. Let pytest collect matching test files and test functions.
  4. Arrange deterministic inputs and create only the fixtures needed for setup.
  5. Keep dependencies real when their collaboration is part of the test boundary. Replace them only when isolation is the goal.
  6. Run the test action.
  7. Use plain assert statements or pytest.raises to check the expected result or expected exception.
  8. Let fixture teardown clean temporary state when cleanup is required.
  9. Read the pytest report for passed, failed, and skipped tests.
  10. Run the same tests independently and in continuous integration.
Example

The example follows the same ideas shown in the diagram. The add function is the small function under test. The sample_list fixture provides reusable test data for test_max. The parametrized test_add runs the same assertion with three input sets. test_max shows a fixture being passed into a test by name. test_division_by_zero uses pytest.raises to verify the expected ZeroDivisionError. These examples have no external dependency or persistent state, so they do not need a mock or cleanup step.

Code
import pytest


def add(a, b):
    return a + b


@pytest.fixture
def sample_list():
    return [1, 2, 3]


@pytest.mark.parametrize(
    "a,b,expected",
    [
        (1, 1, 2),
        (2, 3, 5),
        (0, 0, 0),
    ],
)
def test_add(a, b, expected):
    assert add(a, b) == expected


def test_max(sample_list):
    assert max(sample_list) == 3


def test_division_by_zero():
    with pytest.raises(ZeroDivisionError):
        10 / 0
Where it is used

pytest is widely used in Python projects for unit tests, integration tests, API tests, database tests, and other automated checks. Developers can run it locally while writing code and teams can run the same tests in continuous integration before code is merged or released. Fixtures are useful for repeatable setup, parametrization is useful when one behavior needs several inputs, marks help select groups of tests, and plugins add capabilities when the basic framework is not enough.

Why Interviewers Ask This

Interviewers ask this to check whether I understand what pytest does in a Python project and how its main features work together. They want to see whether I understand discovery, assertions, fixtures, parametrization, marks, expected exceptions, plugins, configuration, and command line execution. They also want to know whether I can distinguish pytest from unittest and from a mocking library, because these tools have different jobs in a test suite.

Common interview mistakes

Common mistakes include thinking pytest is only a test runner, assuming every pytest test is a unit test, depending on test execution order, and sharing mutable fixture state between tests. Other mistakes include using very large fixtures with hidden setup, over mocking dependencies, patching a dependency where it is defined instead of where the code under test looks it up, and writing weak assertions that do not prove useful behavior. A mocked test should not be treated as proof that a real integration works. Temporary resources should also be cleaned up when a test creates them.

Interview tip

Start by saying that pytest is a Python testing framework and test runner. Then explain its features in a clear order: discovery, plain assert statements, assertion introspection, fixtures, parametrization, marks, expected exceptions, plugins, configuration, and command line execution. Finish by explaining that unittest is another testing framework, while a mocking library only helps replace dependencies inside tests.

Interviewer may ask next
How would you keep pytest tests isolated if a fixture creates mutable state?

I would keep that test boundary isolated by giving each test fresh mutable state. A function scoped fixture is a good default because pytest creates a new fixture value for every test call. If the fixture creates a temporary resource, I would also add teardown so that resource is removed after the test. This matters because shared mutable state can make results depend on test order and create flaky failures. The tradeoff is that fresh setup can take more time than shared setup, but the tests are more reliable.

When would you move from a small pytest unit test to an integration test in continuous integration?

I would change the test boundary when the behavior I need to prove depends on real collaboration between selected components. A mocked unit test can prove the logic inside one function, but it cannot prove that a real database mapping or service adapter works. An integration test keeps the required collaborating components real and runs them with controlled test setup in continuous integration. This matters because it catches boundary problems that mocks can hide. The tradeoff is slower setup and longer continuous integration time, so I use integration tests where real collaboration must be verified.

128. What is a pytest fixture?NEWTestingEasy

Question Details

Define a pytest fixture as a function that supplies a reliable test context, data, dependency, or setup and cleanup behavior. Explain fixture declaration, dependency injection through test parameters, scopes, yield teardown, fixture composition, parametrization, conftest.py visibility, and why mutable shared state can make tests order-dependent.

Short Interview Answer (30-60 seconds)

I use a pytest fixture when tests need reliable setup, data, a dependency, or cleanup. I declare it with @pytest.fixture, and a test requests it by using the fixture name as a function parameter. Pytest creates or reuses the fixture according to its scope and injects the returned or yielded value into the test. With yield, code after yield runs as cleanup when that fixture scope ends. The main tradeoff is that wider scopes can reduce repeated setup, but mutable shared state can make tests order dependent.

Detailed Explanation

See the Code while reading this explanation.

A fixture is a helper that prepares something a test needs. It can create sample data, open a resource, or prepare a known starting state. The test asks for that helper by name, and the test runner gives the prepared value to the test automatically. The helper can also close or remove what it created after the work is finished. You can choose how long the prepared value is kept. A short lifetime gives stronger separation between tests. A longer lifetime can save setup work, but shared changing data can let one test affect another.

Useful Questions to Ask the Interviewer
  1. Do you want only the basic fixture idea, or should I also explain scope, cleanup, composition, and parametrization?
  2. Should I show a small pytest example with yield and a fixture that depends on another fixture?
What is a pytest fixture? diagram
How to Explain It in an Interview

The practical goal is to keep test setup explicit, reusable, and isolated. A pytest fixture is a Python function marked with @pytest.fixture. A test requests the fixture by putting the fixture name in the test function parameters. Pytest resolves that name, runs the fixture when needed, and passes the fixture value into the test. This is dependency injection. In simple words, the test asks for a dependency and pytest supplies it.

A fixture can return a value directly. It can also use yield when setup and cleanup belong together. Code before yield performs setup. The value at yield is given to the test. Code after yield performs cleanup when the fixture scope ends. A finalizer can also register cleanup behavior.

Fixture scope controls lifetime and reuse. Function scope is the default and creates a new fixture instance for each requesting test. Class scope reuses one instance for the requesting class. Module scope reuses one instance for the requesting module. Package scope reuses one instance for the requesting package. Session scope reuses one instance for the whole test session. A smaller scope usually gives better isolation. A wider scope can reduce repeated setup, but it also increases the risk of shared state.

Fixtures can depend on other fixtures. In the example, the user fixture requests db_connection as a parameter. Pytest resolves that dependency first and then gives the connection value to user. Fixtures can also be parametrized with params. In the example, number provides 10, 20, and 30. Pytest runs test_positive once for each value, and request.param gives the current value.

Common fixtures can live in conftest.py. Tests in that folder and its subfolders can discover them without importing them directly. This is useful for shared setup, but fixtures should stay small and easy to understand.

The important reliability rule is to avoid hidden mutable shared state. If a wide scope fixture returns a list, dictionary, set, or other mutable object and one test changes it, another test can see that change. Then results may depend on execution order. Prefer fresh data for each test when state can change, or copy and reset state carefully when a wider scope is justified.

The fixture is setup support, not the behavior being asserted. The test should still run the real behavior inside the chosen test boundary and make focused assertions on the result. Fixtures can support unit tests, integration tests, API tests, database tests, and other test levels. A fixture itself does not prove that a real integration works.

In CI, pytest creates and reuses fixtures according to their scopes, runs the tests, and runs registered cleanup after the fixture finishes. Reliable tests use deterministic data, avoid order dependencies, and leave no state that can leak into later tests.

Key Insight / Why This Solution Works
  1. Identify the setup, data, resource, or dependency that tests need.
  2. Put that preparation in a small function and mark it with @pytest.fixture.
  3. Choose the smallest useful scope. Use function scope by default when tests may change the data.
  4. Request the fixture by adding its name to the test function parameters.
  5. Let pytest resolve any fixture dependencies and inject the value into the test.
  6. Use yield when the fixture must release a resource or undo setup. Put cleanup after yield.
  7. Use params when the same fixture should provide several deterministic values.
  8. Put reusable fixtures in conftest.py when tests in a folder tree should share them.
  9. Keep mutable state isolated so one test cannot change the starting state of another test.
  10. Run tests independently in CI and verify that cleanup leaves no state behind.
Example

The example defines db_connection with module scope. It creates a simple connection value before yield and runs cleanup code after yield when the module scope ends. The user fixture depends on db_connection, which demonstrates fixture composition. Two tests request user by parameter name and assert its name and id. The number fixture is parametrized with 10, 20, and 30, so test_positive runs once for each value. The code is self contained and shows the same declaration, dependency injection, scope, yield cleanup, composition, and parametrization shown in the diagram.

Code
import pytest


@pytest.fixture(scope="module")
def db_connection():
    print("open connection")
    connection = {"db": "demo"}
    yield connection
    print("close connection")


@pytest.fixture
def user(db_connection):
    return {"id": 1, "name": "Alice", "db": db_connection}


def test_user_name(user):
    assert user["name"] == "Alice"


def test_user_id(user):
    assert user["id"] == 1


@pytest.fixture(params=[10, 20, 30])
def number(request):
    return request.param


def test_positive(number):
    assert number > 0
Where it is used

Pytest fixtures are used whenever tests need repeatable context. Common examples include sample objects, temporary files, test clients, configuration, database connections, prepared records, fake services, and reusable test data. Function scope is common for state that tests may change. Wider scopes are useful for expensive resources that can be safely reused. conftest.py is useful when many tests in the same folder tree need the same fixtures.

Why Interviewers Ask This

Interviewers ask this to check whether you understand how pytest prepares reliable test context, injects dependencies, controls fixture lifetime, performs cleanup, and keeps tests isolated. They also want to see whether you can choose a suitable fixture scope and avoid shared mutable state that can make results depend on test order.

Common interview mistakes

A common mistake is using a wide scope fixture that returns mutable data and then letting tests change that data. Later tests can see the changed state, which creates flaky and order dependent results. Another mistake is using fixtures as hidden global setup, which makes tests hard to understand. It is also easy to choose session or module scope only for speed without checking whether the resource is safe to reuse. Some developers forget cleanup after creating files, connections, or other resources. Others confuse test parametrization with fixture parametrization. Keep fixtures small, choose scope deliberately, and make each test independent.

Interview tip

Start with one sentence: a fixture prepares reliable test context and pytest injects it by parameter name. Then explain scope, yield cleanup, composition, parametrization, conftest.py, and the shared mutable state warning in that order. Use one small example and say why function scope is the safest default when tests change state.

Interviewer may ask next
What can go wrong if a session scope fixture returns a mutable dictionary that tests change?

The session scope fixture is shared across the whole test session, so a change made by one test can be visible to later tests. The boundary here is the shared fixture state, not the behavior under test. This matters because tests can become order dependent and flaky. The safest change is to use function scope for mutable data that each test can change, or return a fresh copy when a wider scope resource must be reused. The tradeoff is more setup work in exchange for stronger isolation.

When would you choose module or session scope instead of the default function scope?

I would choose a wider scope when fixture setup is expensive and the shared resource can be reused safely without leaking mutable state between tests. The boundary change is the fixture lifetime. Module scope shares one instance inside a requesting module, while session scope shares one instance across the whole test session. This matters in CI because wider scopes can reduce repeated setup time. The tradeoff is weaker isolation, so cleanup, reset behavior, and state ownership must be very clear.

129. How would you use pytest parametrization to test many inputs without duplicating test code?TestingMedium

Question Details

Explain @pytest.mark.parametrize, multiple parameters, readable case identifiers, expected exceptions, edge cases, and when separate tests are clearer than one large parameterized test.

Short Interview Answer (30-60 seconds)

I use pytest.mark.parametrize when the same behavior should be checked with several inputs and expected results. I keep one clear test body, pass multiple parameters when the case needs them, and give important cases readable ids. For expected exceptions, I include the expected exception in the case data and use pytest.raises only for those cases. The tradeoff is that a large parameter table can hide intent, so I use separate tests when cases need different setup, assertions, or business explanations.

Detailed Explanation

See the Code while reading this explanation.

The practical decision is to parametrize only cases that follow the same test flow. The unit under test stays the same, the setup is similar, the action is the same, and the assertion shape is the same. Only the inputs and expected outcomes change.

Useful Questions to Ask the Interviewer
  1. What behavior and test boundary should I cover?
  2. Which dependencies, environments, and test tools should I assume?
  3. Which failures, edge cases, and quality risks are most important?

With pytest.mark.parametrize, I define parameter names and provide a list of cases. Pytest creates one independent test case for each row. This removes repeated test functions while still reporting each case separately. For example, a discount function can be tested with an amount, a customer type, and an expected total. The test body calls the function once and compares the result with the expected value.

Multiple parameters are useful when the behavior depends on more than one value. The parameter names should describe the business meaning, such as amount, customer_type, and expected_total. I avoid vague names such as a, b, and result because they make failures harder to understand.

Readable case identifiers help when the raw parameter values are not clear. I can use ids with short labels such as regular_customer, vip_customer, zero_amount, and boundary_amount. Another option is pytest.param with id for each case. The id should explain why the case exists, not repeat every input value.

Expected exceptions need a clear pattern. If only a few cases should fail, I can store the expected exception type with each case. The test uses pytest.raises for exception cases and a normal equality assertion for success cases. I keep success and failure handling explicit so the reader can see the intended behavior. When success and exception cases need very different setup or assertions, separate tests are usually clearer than one complicated parameterized test.

Edge cases should come from the real contract of the function. Typical examples include zero, empty input, minimum and maximum allowed values, invalid types when the function validates them, and values directly around a business boundary. I do not add random cases without a reason. Each row should represent a distinct behavior or risk.

Parametrized cases should remain deterministic and independent. The test should not mutate shared input objects or depend on execution order. If a case uses a list, dictionary, or custom object that the function may modify, I create fresh data for each case with a factory or fixture. Pytest reports each parameter set separately in local runs and CI, which makes failures easier to locate.

A large parameterized test becomes misleading when rows test different behaviors, require many optional fields, or use branching logic to choose different assertions. At that point, I split the cases into smaller parameterized tests or separate named tests. Parametrization should reduce duplication without hiding the purpose of the test.

How would you use pytest parametrization to test many inputs without duplicating test code? diagram
Key Insight / Why This Solution Works
  1. Identify one behavior that should work for several inputs.
  2. Confirm that every case uses the same setup, action, and assertion shape.
  3. Choose clear parameter names.
  4. Create cases with inputs and expected outcomes.
  5. Add readable ids for important or non obvious cases.
  6. Use pytest.raises for cases that expect an exception.
  7. Add boundary and edge cases from the real function contract.
  8. Keep each case independent and deterministic.
  9. Split the test when cases require different setup, actions, or assertions.
  10. Run the cases in CI and use the case ids to diagnose failures.
Example

The example tests a calculate_shipping function with several order totals and customer types. The first test uses multiple parameters and readable ids for successful cases. The second test parametrizes invalid inputs and verifies the expected exception with pytest.raises. Each case follows one clear test flow, and success and exception cases are separated because their assertions are different.

Code
import pytest


def calculate_shipping(order_total: int, customer_type: str) -> int:
    if order_total < 0:
        raise ValueError("order_total must be zero or greater")
    if customer_type not in {"regular", "vip"}:
        raise ValueError("unsupported customer type")
    if customer_type == "vip" or order_total >= 5000:
        return 0
    return 500


@pytest.mark.parametrize(
    ("order_total", "customer_type", "expected_shipping"),
    [
        pytest.param(0, "regular", 500, id="zero-total"),
        pytest.param(4999, "regular", 500, id="below-free-shipping"),
        pytest.param(5000, "regular", 0, id="free-shipping-boundary"),
        pytest.param(1000, "vip", 0, id="vip-customer"),
    ],
)
def test_calculate_shipping(
    order_total: int,
    customer_type: str,
    expected_shipping: int,
) -> None:
    result = calculate_shipping(order_total, customer_type)
    assert result == expected_shipping


@pytest.mark.parametrize(
    ("order_total", "customer_type", "expected_message"),
    [
        pytest.param(
            -1,
            "regular",
            "order_total must be zero or greater",
            id="negative-total",
        ),
        pytest.param(
            1000,
            "unknown",
            "unsupported customer type",
            id="unsupported-customer-type",
        ),
    ],
)
def test_calculate_shipping_rejects_invalid_input(
    order_total: int,
    customer_type: str,
    expected_message: str,
) -> None:
    with pytest.raises(ValueError, match=expected_message):
        calculate_shipping(order_total, customer_type)
Where it is used

Pytest parametrization is used for validation rules, parsing functions, pricing logic, permission checks, date calculations, API input validation, and algorithms with many boundary cases. It is especially useful when the same public behavior must be checked against a table of inputs and expected outputs.

Why Interviewers Ask This

Interviewers ask this to see whether the candidate can organize many input cases without copying the same test body. They are evaluating knowledge of pytest parametrization, readable case design, exception testing, edge case coverage, and the judgment to split tests when one parameter table becomes difficult to understand.

Common interview mistakes

A common mistake is placing unrelated behaviors in one parameter table. Another is adding branching logic inside the test so every row follows a different path. Other mistakes include unclear parameter names, missing case ids, sharing mutable data between cases, repeating the same case with different values but no new behavior, mixing success and exception assertions in a confusing way, and creating a very large table that is harder to understand than several focused tests.

Interview tip

Explain that parametrization is best when the setup, action, and assertion shape stay the same. Mention multiple parameters, readable ids, explicit exception cases, real edge cases, and the point where separate tests become clearer.

Interviewer may ask next
How would you parametrize cases that expect different exception types?

I would include the expected exception type and message in each failure case, then pass them to pytest.raises. The boundary remains one failure behavior with the same setup and action. This matters because each row stays explicit and pytest reports the failing case by id. If the exception cases need different setup or very different assertions, I would split them into separate tests.

When would you stop adding rows and create separate tests?

I would create separate tests when cases no longer share the same setup, action, and assertion shape. The boundary changes from one repeated behavior to several distinct behaviors. This matters because branches, optional parameters, and unrelated expectations make a parameter table hard to read. The tradeoff is a small amount of repeated structure in exchange for clearer intent and easier failure diagnosis.

130. When you are in a leadership role, how do you motivate team members?BehavioralHard

Question Details

Describe a real situation where you led or influenced Python developers without relying only on authority. Explain how you understood individual needs, clarified the goal, removed obstacles, encouraged ownership, handled low motivation, and measured the team's progress.

I motivate team members by first understanding what is making the work difficult for them. Then I connect each person’s work to a clear goal, give them useful ownership, remove blockers, and make progress visible without using pressure as the main tool.

Interview tip:

Use the STAR method. Explain how you understood each person, clarified the shared goal, removed obstacles, encouraged ownership, addressed low motivation, and tracked progress.

Situation

During a previous Python project, I helped lead a small development team that was improving a data processing service. The work had become repetitive, several technical issues were slowing us down, and one developer had become less engaged because tasks were being assigned without enough context. The team was still completing work, but discussions were quiet and progress was becoming less predictable.

Task

My responsibility was to help the team complete the planned improvements while rebuilding energy and ownership. I did not want to motivate people only by pushing deadlines. I needed to understand what each developer needed, explain why the work mattered, remove avoidable friction, and create a simple way to see whether the team was moving forward.

Action

I started with short individual conversations. I asked each developer what was slowing them down, which tasks they felt confident owning, and what type of support would help. I learned that one person wanted more challenging backend work, another needed clearer acceptance criteria, and the less engaged developer felt that decisions were being made before the team could contribute. I then explained the shared goal in practical terms. We were not only changing Python code. We were making the service easier to maintain and reducing failures during data processing. I divided the work into clear outcomes and invited developers to choose ownership where their interests and skills matched the need. I gave the less engaged developer ownership of reviewing the processing flow and proposing a safer error handling approach. This mattered because it gave that person a real decision to make instead of another isolated coding task. I also removed obstacles. I clarified unclear requirements, arranged a focused review for a difficult dependency, and created small examples that made expected behavior easier to test. During team check ins, I asked about progress and blockers instead of asking only whether tasks were finished. We tracked completed outcomes, open risks, and the next useful step. When motivation dropped, I addressed it privately and directly. I listened first, adjusted the task when the concern was reasonable, and explained any constraint that could not change. I also recognized useful contributions during team discussions, especially when someone prevented a defect or helped another developer, because those actions supported the whole team even when they did not produce a large visible feature.

Result

The team became more active in planning and review discussions, and work moved with fewer repeated questions. The developer who had been less engaged presented the new error handling approach and helped the team adopt it. We completed the planned improvements with clearer ownership and a more reliable development process. I learned that motivation is usually stronger when people understand the purpose, have a meaningful area to own, and can see that their concerns lead to practical action.

Why Interviewers Ask This

Interviewers ask this question to learn whether a candidate can lead through trust, clarity, and support instead of relying only on authority. A strong answer shows that the candidate understands individual needs, creates ownership, removes obstacles, handles low motivation respectfully, and uses visible progress to keep a team aligned.

Interviewer may ask next
How did you handle the developer who had become less engaged?

I spoke with the developer privately and asked what was causing the low engagement. I learned that the person felt excluded from technical decisions and was receiving tasks without enough context. I gave the developer ownership of reviewing the processing flow and proposing the error handling approach. I still set clear expectations, but I also made sure the person had a meaningful decision to own and regular support when blockers appeared.

How did you know your approach was improving the team's motivation?

I looked for changes in behavior and delivery rather than relying only on a general feeling. The team raised blockers earlier, contributed more during planning and code reviews, and needed fewer repeated clarifications. Ownership also became clearer because developers could explain their next step and the reason behind it. Those signs showed that the team was more engaged and that progress was becoming more predictable.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.