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: September 3, 2026)

101. Minimum Rotations to Type a String with Multiple Circular DialsCodingHard

Question Details

You are given k circular dials containing the letters A through Z and a target string. Every dial initially points to A. In one move, rotate one dial one step clockwise or counterclockwise. A target character can be typed when at least one dial points to that character. Find the minimum total number of rotations required to type the target string in order, and explain the state representation, transition choices, edge cases, and complexity.

Short Interview Answer (30-60 seconds)

I would use dynamic programming over the dial positions. A state is a sorted tuple of the current positions of all k dials. I start with every dial at A. For each target character, I try moving every dial, add the shorter circular distance, sort the new positions, and keep the lowest cost for each state. This preserves all meaningful choices and avoids an unsafe greedy decision. The shown code takes O(n · S · k² log k) time and O(S · k) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We need to type the target string in order using k circular dials. Every dial starts at A. Moving one dial by one letter costs one rotation. A greedy choice is not always safe because the cheapest move now can create a worse dial arrangement for later characters. The diagram uses dynamic programming to keep the minimum cost for every reachable canonical dial configuration.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Minimum Rotations to Type a String with Multiple Circular Dials diagram
How to Explain It in an Interview
1. Understand the input and required output

The input contains an integer k and a target string made from uppercase letters A through Z.

Each of the k dials starts at A, which has index 0. The remaining letters use indices 1 through 25.

For each target character, at least one dial must point to that character. Typing the character itself has no extra cost. We return the minimum total number of rotations needed to type the complete target string in order.

In the diagram example, k = 2 and target = "CBC". The minimum result is 3.

2. Define the dynamic programming state

A state is a sorted tuple containing the current positions of all k dials.

For example, the state (0, 2) means that one dial points to A and one dial points to C.

The tuple is sorted because the physical names of identical dials do not affect future costs. The configurations (0, 2) and (2, 0) represent the same useful arrangement, so they should share one state.

The dictionary dp maps each canonical state to the minimum cost needed to type the target prefix processed so far and finish in that state.

The central invariant is: after each processed character, dp[state] stores the lowest cost for typing that prefix and ending with exactly that canonical dial configuration.

3. Initialize the state

Every dial starts at A, so every position is 0.

For k = 2, the initial state is (0, 0).

The initial dictionary is {(0, 0): 0}. The cost is 0 because no dial has moved and no target character has been processed.

4. Process each target character

For each target character, we convert it to an index from 0 through 25.

For every current state, we try moving every tuple entry to the target position. Tuple-entry indices are temporary positions inside the sorted state. They are not permanent physical dial identities.

The circular movement cost from position a to position b is:

min(|a - b|, 26 - |a - b|)

This chooses the shorter movement direction around the circular alphabet.

After moving one dial, we sort the resulting k positions. This creates the canonical next state. If different transitions reach the same state, we keep only the smallest total cost.

5. Walk through the example

The input is k = 2 and target = "CBC".

We start with dp = {(0, 0): 0}.

The first character is C, which has index 2. Moving either dial from A to C costs 2. Both choices become the same sorted state (0, 2). The new dictionary is {(0, 2): 2}.

The second character is B, which has index 1. From state (0, 2), moving the tuple entry at position 0 from A to B costs 1 and creates canonical state (1, 2) with total cost 3. Moving the tuple entry at position 1 from C to B also costs 1 and creates canonical state (0, 1) with total cost 3. The new dictionary is {(0, 1): 3, (1, 2): 3}.

The final character is C, which has index 2.

From state (0, 1) with cost 3, moving A to C costs 2 and produces (1, 2) with total cost 5. Moving B to C costs 1 and produces (0, 2) with total cost 4.

From state (1, 2) with cost 3, moving B to C costs 1 and produces (2, 2) with total cost 4. One dial already points to C, so selecting that tuple entry costs 0 and keeps canonical state (1, 2) with total cost 3.

The final states include (1, 2) with cost 3, (0, 2) with cost 4, and (2, 2) with cost 4. The minimum is 3.

One concrete optimal physical sequence before canonical sorting is to move Dial 1 from A to C for cost 2, move Dial 2 from A to B for cost 1, and reuse Dial 1 at C for cost 0. The final answer is 3.

6. Explain why the result is correct

At every target character, the algorithm tries every possible dial move from every reachable state.

It discards a path only when another path reaches the same canonical state with a lower cost. Therefore, no cheaper meaningful configuration is lost.

By induction, after every processed prefix, each stored state has its minimum possible cost. After the complete target is processed, the minimum value in dp is the globally minimum number of rotations.

7. Explain the Python implementation, complexity, and edge cases

The code uses one dictionary for the current target prefix and another dictionary for the next prefix. Each dictionary key is a sorted tuple of dial positions. Each value is the minimum cost for that state.

Let n be the target length and S be the number of reachable canonical states during one target step. Each of the S states tries k dial moves. For each move, the code copies k positions and sorts the resulting tuple in O(k log k) time. The shown code therefore takes O(n · S · k² log k) time.

The dictionaries can store up to S tuples, and each tuple contains k positions. The auxiliary space is O(S · k). Across all possible configurations, S is at most C(k + 25, k), although the reachable set for one target prefix may be smaller.

Important edge cases are an empty target, a character already covered by a dial, clockwise and counterclockwise wrap-around, repeated characters, and k = 1.

Key Insight / Why This Solution Works

The key insight is that the cheapest immediate move is not always part of the cheapest complete sequence. Each move changes the dial arrangement, and that arrangement affects later characters. Dynamic programming preserves these future choices. The state is a sorted tuple of all dial positions. Sorting removes unnecessary identity from identical dials, so equivalent arrangements share one state. For each target character, the algorithm tries moving every dial from every current state, adds the circular distance, and retains the minimum cost for each resulting canonical state. The invariant is that dp[state] is the minimum cost for typing the processed prefix and ending in that state.

Code
from typing import Dict, Tuple


def min_rotations(k: int, target: str) -> int:
    initial = tuple([0] * k)
    dp: Dict[Tuple[int, ...], int] = {initial: 0}

    for ch in target:
        target_pos = ord(ch) - ord("A")
        next_dp: Dict[Tuple[int, ...], int] = {}

        for state, current_cost in dp.items():
            for dial_index in range(k):
                current_pos = state[dial_index]
                diff = abs(current_pos - target_pos)
                move_cost = min(diff, 26 - diff)

                positions = list(state)
                positions[dial_index] = target_pos
                next_state = tuple(sorted(positions))
                candidate = current_cost + move_cost

                if candidate < next_dp.get(next_state, float("inf")):
                    next_dp[next_state] = candidate

        dp = next_dp

    return min(dp.values(), default=0)


if __name__ == "__main__":
    k = 2
    target = "CBC"
    print(min_rotations(k, target))  # 3
Time & Space Complexity

Let n be the number of characters in the target. Let S be the number of reachable canonical dial configurations during one target step. For each character, the code processes up to S states. From each state, it tries k dial entries. For each choice, it copies k positions and sorts them in O(k log k) time. Therefore, the shown code takes O(n · S · k² log k) time. Python dictionary lookup and update are O(1) on average. The dictionaries store up to S tuples of length k, so the auxiliary space is O(S · k). Across all configurations, S is at most C(k + 25, k).

Where it is used

This pattern is useful when several interchangeable resources can handle an ordered sequence of requests and each choice changes the cost of later choices. Examples include movable cursors, robotic arms, machine heads, or identical workers assigned to ordered tasks. Canonical states are useful when resource names do not affect future decisions and equivalent arrangements can be merged.

Why Interviewers Ask This

This question tests whether the candidate can recognize when a greedy choice is unsafe and replace it with dynamic programming. It also checks state design, transition reasoning, circular-distance calculation, and the ability to remove unnecessary identity through canonical sorting. The interviewer can evaluate whether the candidate preserves all meaningful choices, maintains a clear invariant, writes correct dictionary updates, handles wrap-around and repeated characters, and includes tuple copying and sorting in the complexity analysis.

Common interview mistakes

A common mistake is choosing the dial with the smallest immediate rotation cost. That greedy choice can produce a worse arrangement for later characters. Another mistake is keeping permanent physical dial identities in the canonical dynamic programming state. Equivalent arrangements should be sorted into one tuple. Candidates reaching the same state must keep only the lowest cost. It is also easy to forget circular wrap-around and use only the direct distance. Finally, the complexity must include copying and sorting each k-position tuple.

Interview tip

Define dp[state] before writing code. Say that state is a sorted tuple and dp[state] is the minimum cost after the processed prefix. Then explain one transition and why canonical sorting safely merges equivalent dial arrangements.

Interviewer may ask next
How could you reduce the sorting work in each transition?

Because the state is already sorted and only one position changes, we could remove the selected entry and insert the target position into the correct sorted location instead of sorting all k values again. A straightforward Python-list implementation would take O(k) work for each transition. Since each state tries k transitions, the total time would become O(n · S · k²). The auxiliary space would remain O(S · k). The tradeoff is more complicated transition code.

How would the solution change if each dial had a different rotation cost per step?

Physical dial identity would then matter because moving different dials could have different costs. We could no longer sort the positions and merge permutations. The state would keep positions in fixed dial order. For each target character, we would try every dial and multiply its circular distance by that dial's step cost. The invariant would remain the minimum cost for each ordered state. If S is the number of reachable ordered states, the time would be O(n · S · k²) with tuple copying, and the auxiliary space would be O(S · k). The main tradeoff is a larger state space.

102. Median of Two Sorted ArraysCodingHard

Question Details

Given two sorted arrays, return their combined median while meeting logarithmic-time expectations. Explain the partition conditions, handling of unequal sizes, and important boundary cases.

Short Interview Answer (30-60 seconds)

I would binary-search a partition in the shorter array. The two partition positions must place half of the combined elements on the left. I compare the largest values on the left with the smallest values on the opposite right sides. If the partition is invalid, I move it left or right. When both conditions hold, I calculate the median from the boundary values. This takes O(log(min(m, n))) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to find the median of two sorted arrays without fully merging them. Merging would take linear time. Instead, we binary-search a partition in the shorter array. The partition divides the combined values into left and right halves. When every value on the left is less than or equal to every value on the right, the median can be calculated from the partition boundaries.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Median of Two Sorted Arrays diagram
How to Explain It in an Interview
1. Understand the input and required output

The inputs are two sorted integer arrays. The output is their combined median as a floating-point value.

The original example is A = [1, 3] and B = [2]. The combined sorted order would be [1, 2, 3]. The middle value is 2, so the expected answer is 2.0.

We do not actually build the combined array. It is shown only to verify the answer.

2. Binary-search the shorter array

We make nums1 the shorter array. This keeps the binary-search range as small as possible.

For the example, nums1 becomes [2] and nums2 becomes [1, 3]. Their lengths are m = 1 and n = 2.

The total length is 3. We calculate half = (m + n + 1) // 2 = 2. This means the left side of the combined partition must contain two elements.

The binary-search interval is inclusive. It starts with left = 0 and right = m.

3. Define the partition and boundary values

For each binary-search step, i is the partition position in nums1.

We calculate the matching partition in nums2 with j = half - i.

Four values describe the partition boundaries:

maxLeft1 is the largest nums1 value on the left side.

minRight1 is the smallest nums1 value on the right side.

maxLeft2 is the largest nums2 value on the left side.

minRight2 is the smallest nums2 value on the right side.

If a partition is at an array boundary, the code uses negative infinity or positive infinity. This lets the same comparisons work without accessing an invalid index.

4. Walk through the exact example

In iteration 1, left = 0 and right = 1.

We calculate i = (0 + 1) // 2 = 0.

We then calculate j = half - i = 2 - 0 = 2.

The nums1 partition is before the value 2. The nums2 partition is after the value 3.

The boundary values are:

maxLeft1 = negative infinity

minRight1 = 2

maxLeft2 = 3

minRight2 = positive infinity

We check the two partition conditions.

The first condition is true because negative infinity is less than or equal to positive infinity.

The second condition is false because 3 is greater than 2.

This means the nums1 partition is too far left. We move it right by setting left = i + 1 = 1.

In iteration 2, left = 1 and right = 1.

We calculate i = (1 + 1) // 2 = 1.

We then calculate j = half - i = 2 - 1 = 1.

The nums1 partition is after the value 2. The nums2 partition is between 1 and 3.

The boundary values are:

maxLeft1 = 2

minRight1 = positive infinity

maxLeft2 = 1

minRight2 = 3

Now both conditions are true. We have 2 <= 3 and 1 <= positive infinity. The correct partition has been found, so the binary search stops.

5. Calculate the median

The combined length is 3, which is odd.

For an odd combined length, the median is the largest value on the left side of the valid partition.

The left maximum is max(maxLeft1, maxLeft2) = max(2, 1) = 2.

Therefore, the returned median is 2.0.

For an even combined length, the median is the average of the largest left-side value and the smallest right-side value.

6. Explain why the method is correct

The central invariant is that the two partitions place exactly half of the combined elements on the left side.

The partition is valid when maxLeft1 <= minRight2 and maxLeft2 <= minRight1.

These conditions guarantee that every value on the combined left side is less than or equal to every value on the combined right side. The median must therefore be one of the values directly beside the partitions.

If maxLeft1 is greater than minRight2, the nums1 partition is too far right, so we move right to i - 1.

Otherwise, maxLeft2 is greater than minRight1. The nums1 partition is too far left, so we move left to i + 1.

Each update reduces the binary-search interval.

7. Explain complexity and boundary cases

Binary search is performed only on the shorter array. The time complexity is O(log(min(m, n))).

The algorithm uses only a fixed number of variables. Its auxiliary space complexity is O(1).

Important cases include one empty array, arrays with very different lengths, an odd or even combined length, duplicate values, and all values in one array being smaller than all values in the other array.

Both arrays cannot be empty because a median would not exist.

Key Insight / Why This Solution Works

The key insight is that we do not need to merge the arrays. We only need to find a partition that divides the combined sorted values into a left half and a right half. We binary-search partition i in the shorter array and calculate partition j in the other array. The invariant is that the left side contains half of the combined elements. The partition is correct when maxLeft1 <= minRight2 and maxLeft2 <= minRight1. These conditions guarantee that all left-side values come before all right-side values, so the median can be read directly from the four boundary values.

Code
from typing import List


def find_median_sorted_arrays(nums1: List[int], nums2: List[int]) -> float:
    if not nums1 and not nums2:
        raise ValueError("At least one input array must contain a value.")

    # Always binary-search the shorter array.
    if len(nums1) > len(nums2):
        nums1, nums2 = nums2, nums1

    m, n = len(nums1), len(nums2)
    total = m + n
    half = (total + 1) // 2

    left, right = 0, m

    while left <= right:
        i = (left + right) // 2
        j = half - i

        max_left1 = nums1[i - 1] if i > 0 else float("-inf")
        min_right1 = nums1[i] if i < m else float("inf")
        max_left2 = nums2[j - 1] if j > 0 else float("-inf")
        min_right2 = nums2[j] if j < n else float("inf")

        if max_left1 <= min_right2 and max_left2 <= min_right1:
            if total % 2 == 1:
                return float(max(max_left1, max_left2))

            largest_left = max(max_left1, max_left2)
            smallest_right = min(min_right1, min_right2)
            return (largest_left + smallest_right) / 2.0

        if max_left1 > min_right2:
            right = i - 1
        else:
            left = i + 1

    raise ValueError("The input arrays must be sorted.")


if __name__ == "__main__":
    a = [1, 3]
    b = [2]
    median = find_median_sorted_arrays(a, b)
    print(median)  # 2.0
Time & Space Complexity

Let m and n be the lengths of the two arrays. We binary-search only the shorter array. Each iteration removes about half of the remaining partition positions. The time complexity is therefore O(log(min(m, n))). The algorithm does not merge or copy the arrays. It stores only indices, lengths, and four boundary values. The auxiliary space complexity is O(1).

Where it is used

This partition-based binary-search pattern is useful when sorted data is stored in separate collections and combining all records would be expensive. It can appear in analytics systems, database operations, distributed data processing, and services that need a median or another middle-ranked value from already sorted sources.

Why Interviewers Ask This

This question tests whether the candidate can apply binary search to partition positions instead of searching for a specific value. It also checks whether the candidate can maintain an invariant across two arrays, reason carefully about unequal sizes, handle virtual boundary values, choose the correct search direction, distinguish odd and even totals, and justify the required O(log(min(m, n))) time with O(1) auxiliary space.

Common interview mistakes

A common mistake is binary-searching the longer array instead of the shorter one. Another is calculating j incorrectly instead of using j = half - i. Candidates may compare the wrong boundary values or move the wrong binary-search boundary. They may also access i - 1, i, j - 1, or j without handling array boundaries. Other mistakes include using the odd-length formula for an even total, merging the arrays and missing the logarithmic-time requirement, or forgetting that both empty arrays have no valid median.

Interview tip

Before writing code, draw both partition lines and name maxLeft1, minRight1, maxLeft2, and minRight2. Then state the two valid-partition conditions. This makes the binary-search direction and median formula much easier to derive correctly.

Interviewer may ask next
What changes when the combined number of elements is even?

The partition search and validity conditions stay the same. After finding the valid partition, calculate the largest value on the left and the smallest value on the right. Return their average. The time complexity remains O(log(min(m, n))), and the auxiliary space remains O(1).

How does the solution work when one array is empty?

The empty array becomes nums1 because it is the shorter array. Its partition is at index 0. The code uses negative infinity for its missing left value and positive infinity for its missing right value. The median is then read entirely from the non-empty array. The method still uses O(1) auxiliary space. With an empty shorter array, the binary search completes in constant time.

103. Merge K Sorted ListsCodingHard

Question Details

Given multiple sorted linked lists, merge them into one sorted linked list. Explain how a priority queue or divide-and-conquer approach works and analyze time and auxiliary space.

Short Interview Answer (30-60 seconds)

I would use a min heap to always select the smallest current node among the k sorted lists. I first push the head of every non-empty list as a tuple containing its value, list index, and node reference. Then I repeatedly pop the smallest node, attach it to the merged list, and push its next node when one exists. This works because every list is already sorted. The time complexity is O(N log k), and the auxiliary space is O(k).

Detailed Explanation

See the Code while reading this explanation.

The problem gives several sorted linked lists and asks us to combine their existing nodes into one sorted linked list. The key idea is to compare only the current head node from each list. A min heap keeps the smallest available head at the top, so we can build the result in sorted order without scanning all k lists for every node.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Merge K Sorted Lists diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a collection of sorted linked lists. Some lists may be empty. The output is the head of one merged linked list containing all input nodes in non-decreasing order.

The diagram uses these lists:

List 1: 1 → 4 → 5 List 2: 1 → 3 → 4 List 3: 2 → 6

The expected merged result is:

1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

There are k = 3 lists and N = 8 total nodes.

2. Choose the algorithm and data structure

I use a min heap. A min heap is a priority queue that keeps its smallest item at the top.

Each heap entry stores:

(node value, list index, node reference)

The node value controls the heap order. The list index safely breaks ties when two nodes have the same value. The node reference lets us attach the actual node and access its next node.

The central invariant is that the heap contains the first unmerged node from every list that still has nodes. Therefore, the top of the heap is the smallest node that can be added next.

3. Initialize the state

First, create an empty min heap. Push the head of every non-empty list into it.

The initial heap entries represent:

(1, 0, List 1 head) (1, 1, List 2 head) (2, 2, List 3 head)

Next, create a dummy node and let current point to it. The dummy node makes it easy to attach the first real node without writing a special case.

4. Walk through the example

Step 1: The available values are 1, 1, and 2. Pop 1 from List 1. Attach it to the merged list. Push its next node, which has value 4. The merged list is now 1.

Step 2: Pop 1 from List

  1. Attach it. Push its next node, which has value
  2. The merged list is now 1 → 1.

Step 3: Pop 2 from List 3. Attach it. Push its next node, which has value 6. The merged list is now 1 → 1 → 2.

Step 4: Pop 3 from List 2. Attach it. Push its next node, which has value 4. The merged list is now 1 → 1 → 2 → 3.

Step 5: Pop 4 from List 1. Attach it. Push its next node, which has value 5. The merged list is now 1 → 1 → 2 → 3 → 4.

Step 6: Pop 4 from List 2. Attach it. This node has no next node, so nothing is pushed. The merged list is now 1 → 1 → 2 → 3 → 4 → 4.

Step 7: Pop 5 from List 1. Attach it. This node has no next node. The merged list is now 1 → 1 → 2 → 3 → 4 → 4 → 5.

Step 8: Pop 6 from List 3. Attach it. This node has no next node. The merged list is now 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6.

The heap is empty, so processing stops.

5. Explain why the result is correct

Each input list is sorted. The heap contains the first unmerged node from every remaining list. Any later node in one of those lists cannot be smaller than that list's current heap node.

Therefore, the smallest heap item is the smallest unmerged node across all lists. Appending it keeps the merged list sorted. Pushing its next node restores the same invariant. When the heap is empty, every input node has been merged.

6. Explain the Python implementation

The code first pushes every non-empty list head into Python's heapq min heap. It then uses a dummy node and a current pointer to build the merged list.

Each loop iteration pops one node, connects current.next to that node, moves current forward, and pushes the popped node's next node when it exists. Returning dummy.next skips the temporary dummy node and returns the real merged head.

7. Explain complexity and edge cases

Let N be the total number of nodes and k be the number of lists. Every node is pushed into the heap once and popped once. The heap contains at most one node from each list, so its size is at most k. The time complexity is O(N log k). The auxiliary space is O(k), not counting the returned linked list.

Important edge cases include an empty collection of lists, empty lists inside the collection, all lists being empty, only one list, duplicate values, and lists with different lengths.

Key Insight / Why This Solution Works

The key insight is that we do not need to compare every remaining node. Because each linked list is already sorted, only its first unmerged node can be the next result node.

We place one current node from each non-empty list into a min heap. The invariant is that the heap contains the first unmerged node from every list that still has data. The heap therefore exposes the globally smallest available node. After removing that node, we add its next node from the same list.

Scanning all k current nodes for each of the N output nodes would take O(Nk) time. The min heap reduces each selection to O(log k), giving O(N log k) total time.

Code
import heapq
from typing import List, Optional


class ListNode:
    def __init__(
        self,
        val: int = 0,
        next: Optional["ListNode"] = None,
    ) -> None:
        self.val = val
        self.next = next


def mergeKLists(
    lists: List[Optional[ListNode]],
) -> Optional[ListNode]:
    # Each heap item is:
    # (node value, list index, node reference)
    min_heap = []

    # Add the head of every non-empty list.
    for list_index, node in enumerate(lists):
        if node is not None:
            heapq.heappush(
                min_heap,
                (node.val, list_index, node),
            )

    dummy = ListNode()
    current = dummy

    while min_heap:
        _, list_index, node = heapq.heappop(min_heap)

        # Attach the smallest available node.
        current.next = node
        current = current.next

        # Add the next node from the same list.
        if node.next is not None:
            heapq.heappush(
                min_heap,
                (node.next.val, list_index, node.next),
            )

    return dummy.next


def build_linked_list(
    values: List[int],
) -> Optional[ListNode]:
    dummy = ListNode()
    current = dummy

    for value in values:
        current.next = ListNode(value)
        current = current.next

    return dummy.next


def linked_list_to_list(
    head: Optional[ListNode],
) -> List[int]:
    values = []

    while head is not None:
        values.append(head.val)
        head = head.next

    return values


if __name__ == "__main__":
    lists = [
        build_linked_list([1, 4, 5]),
        build_linked_list([1, 3, 4]),
        build_linked_list([2, 6]),
    ]

    merged_head = mergeKLists(lists)
    print(linked_list_to_list(merged_head))
    # Output: [1, 1, 2, 3, 4, 4, 5, 6]
Time & Space Complexity

Let N be the total number of nodes across all lists. Let k be the number of linked lists.

Each node enters the heap once and leaves the heap once. A heap push or pop takes O(log k) time because the heap contains at most one node from each list. The total time complexity is O(N log k).

The heap stores at most k nodes, so the auxiliary space is O(k). Auxiliary space means extra memory used by the algorithm. The merged output is not counted as auxiliary space. The implementation also reuses the original linked-list nodes instead of creating a new node for every value.

Where it is used

This k-way merge pattern is useful when several sorted sources must be combined in order. Examples include merging sorted database results, combining time-ordered event feeds, merging log streams, and combining sorted files during external sorting.

Why Interviewers Ask This

This problem tests whether the candidate recognizes the k-way merge pattern and selects an efficient priority queue. It also checks whether they can preserve linked-list node references, handle duplicate values safely in Python heap tuples, maintain a clear invariant, and explain why the heap contains at most k nodes. The interviewer is also evaluating complexity analysis, pointer handling, executable Python code, and important empty-input edge cases.

Common interview mistakes

A common mistake is pushing only the initial list heads and forgetting to push the next node after a pop. This causes the remaining nodes in that list to be lost.

Another mistake is storing only (value, node) in the Python heap. When two values are equal, Python may try to compare ListNode objects and raise a TypeError. The list index provides a safe tie breaker.

Candidates may also lose the remaining part of a linked list by changing pointers before saving or using the next reference. Another mistake is returning the dummy node instead of dummy.next. It is also incorrect to claim that the heap contains N nodes. Its size is at most k, which is why each heap operation costs O(log k).

Interview tip

State the heap invariant before writing code: the heap contains the first unmerged node from every non-empty remaining list. Then show how every pop and push preserves that invariant.

Interviewer may ask next
Could you solve this with divide and conquer instead of a heap?

Yes. Merge the lists in pairs. After one round, merge the resulting lists in pairs again. Continue until only one list remains. Each round processes all N nodes, and there are O(log k) rounds, so the time complexity is O(N log k). An iterative implementation normally uses O(k) auxiliary space for the working collection of list heads. A recursive implementation may also use O(log k) recursion-stack space. The main tradeoff is that divide and conquer performs full pairwise merges, while the heap produces the result one smallest node at a time.

How would you handle a very large number of lists that cannot all stay open at once?

Merge the lists in manageable batches. First merge each batch into a temporary sorted result. Then merge those temporary results in later rounds. The merge logic stays correct because every temporary result is sorted. The total time remains about O(N log k), while memory and open-file usage can be limited by the batch size. The tradeoff is additional temporary storage and more input-output operations.

104. Serialize and Deserialize Binary TreeCodingHard

Question Details

Design methods to convert a binary tree into a string and reconstruct the original tree from that string. Preserve structure and values, handle empty children, and explain complexity.

Short Interview Answer (30-60 seconds)

I would use preorder depth first traversal with a null marker. During serialization, I record the current node, then its left subtree, then its right subtree. I write # whenever a child is empty. During deserialization, I read the tokens in the same order with one shared index. A value creates a node, while # returns None. This preserves both values and structure. Both operations take O(n) time. The stored tokens use O(n) space, and recursion uses O(h) stack space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to convert a binary tree into a string and later rebuild the same tree. We must preserve the node values and the exact positions of empty children. We use preorder depth first traversal because it processes the root before the left and right subtrees. We also store # for every missing child, so the serialized data contains the full tree structure.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Serialize and Deserialize Binary Tree diagram
How to Explain It in an Interview
1. Understand the input and required output

The serialization method receives the root of a binary tree and returns a string.

The deserialization method receives that string and returns the root of a reconstructed tree.

The reconstructed tree must have the same node values and the same left and right child structure as the original tree. The tree is not assumed to be a binary search tree.

The example tree has root 1. Node 1 has left child 2 and right child 3. Node 2 has left child 4 and right child 5. Node 3 has no left child and has right child 6.

2. Use preorder traversal and null markers

The traversal order is root, left subtree, then right subtree.

When the current node exists, we store its value. When the current position is empty, we store #.

The null markers are required because node values alone do not show the exact shape of a tree. A # token tells us that a specific left or right child is missing.

For the example, the complete serialized string is:

1,2,4,#,#,5,#,#,3,#,6,#,#

3. Serialize the example tree

Start at node 1 and add 1.

Move to the left child, node 2, and add 2.

Move to node 4 and add 4. Its left child is empty, so add #. Its right child is also empty, so add another #.

Return to node 2 and visit node 5. Add 5. Both children of node 5 are empty, so add # and #.

Return to node 1 and visit node 3. Add 3. Its left child is empty, so add #.

Visit the right child, node 6, and add 6. Both children of node 6 are empty, so add # and #.

The 13 tokens are processed in this exact order:

1, 2, 4, #, #, 5, #, #, 3, #, 6, #, #

4. Deserialize the token sequence

First, split the string by commas. Keep one shared index starting at 0.

At index 0, read 1 and create node 1.

At index 1, read 2 and create the left child of node 1.

At index 2, read 4 and create the left child of node 2.

At indices 3 and 4, read # and return None for the left and right children of node 4.

At index 5, read 5 and create the right child of node 2.

At indices 6 and 7, read # and return None for both children of node 5.

At index 8, read 3 and create the right child of node 1.

At index 9, read # and return None for the left child of node 3.

At index 10, read 6 and create the right child of node 3.

At indices 11 and 12, read # and return None for both children of node 6.

Each recursive call reads exactly one token. A value creates a node. A # token returns None. For every created node, the algorithm builds the left subtree before the right subtree.

5. Explain why the result is correct

Preorder traversal fixes the order of the nodes. Every # marker records one missing child position.

The central invariant is that each serialization call writes exactly one token for its current tree position. Each deserialization call consumes exactly one token and returns either one complete subtree or None.

Because serialization and deserialization use the same root, left, right order, the reconstructed tree has the same values and structure as the original tree.

deserialize(serialize(root)) therefore reconstructs the same tree.

6. Explain the Python implementation

The serialize method creates a list named tokens. Its nested dfs function processes one tree position at a time.

If the node is None, dfs appends # and returns. Otherwise, it appends the node value, processes the left child, and then processes the right child. The tokens are joined with commas at the end.

The deserialize method splits the string into a list of tokens. It keeps an index shared by every recursive call.

Each dfs call reads tokens[index] and then increases the index by one. If the token is #, it returns None. Otherwise, it creates a TreeNode, recursively builds its left child, recursively builds its right child, and returns the completed node.

7. Explain complexity and edge cases

Let n be the number of real nodes and h be the height of the tree.

Serialization takes O(n) time. It visits every real node and records every missing child position. A binary tree with n nodes has n + 1 null child positions, so the total work is still linear.

Deserialization also takes O(n) time because it consumes every token once.

The serialized output contains O(n) tokens. Splitting the string during deserialization creates another O(n) token list. The recursive call stack uses O(h) space.

For a balanced tree, h is O(log n). For a fully skewed tree, h is O(n).

An empty tree serializes to # and deserializes to None. A single node contains its value followed by two null markers. Skewed trees work because every missing child is stored. Negative and multi-digit values work because each value is stored as its own comma-separated token.

Key Insight / Why This Solution Works

The key insight is that preorder traversal alone is not enough unless missing children are also recorded. The algorithm therefore writes node values in root, left, right order and writes # for every empty child. The central invariant is that one recursive call represents one tree position. During serialization, that call writes exactly one token. During deserialization, that call consumes exactly one token and returns either a complete subtree or None. Since both operations follow the same order, the original values and structure are preserved.

Code
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional


@dataclass
class TreeNode:
    val: int
    left: Optional[TreeNode] = None
    right: Optional[TreeNode] = None


class Codec:
    def serialize(self, root: Optional[TreeNode]) -> str:
        tokens: list[str] = []

        def dfs(node: Optional[TreeNode]) -> None:
            if node is None:
                tokens.append("#")
                return

            tokens.append(str(node.val))
            dfs(node.left)
            dfs(node.right)

        dfs(root)
        return ",".join(tokens)

    def deserialize(self, data: str) -> Optional[TreeNode]:
        tokens = data.split(",")
        index = 0

        def dfs() -> Optional[TreeNode]:
            nonlocal index

            token = tokens[index]
            index += 1

            if token == "#":
                return None

            node = TreeNode(int(token))
            node.left = dfs()
            node.right = dfs()
            return node

        return dfs()


if __name__ == "__main__":
    root = TreeNode(
        1,
        left=TreeNode(
            2,
            left=TreeNode(4),
            right=TreeNode(5),
        ),
        right=TreeNode(
            3,
            right=TreeNode(6),
        ),
    )

    codec = Codec()

    serialized = codec.serialize(root)
    print("Serialized:", serialized)

    rebuilt_root = codec.deserialize(serialized)
    rebuilt_serialized = codec.serialize(rebuilt_root)
    print("Rebuilt:   ", rebuilt_serialized)

    expected = "1,2,4,#,#,5,#,#,3,#,6,#,#"
    assert serialized == expected
    assert rebuilt_serialized == expected

    print("The rebuilt tree has the same values and structure.")
Time & Space Complexity

Let n be the number of real nodes and h be the height of the tree. Serialization takes O(n) time because it visits every node and every missing child position once. Deserialization takes O(n) time because it reads every token once. The serialized output uses O(n) space. Splitting the serialized string also creates an O(n) token list. The recursive call stack uses O(h) space. For a balanced tree, h is O(log n). For a skewed tree, h can be O(n).

Where it is used

This pattern is useful when a tree must be saved to a file, stored in a database or cache, sent between services, copied across a network, or restored after a program restarts. It is also useful in testing when a program needs to save and rebuild the exact same tree structure.

Why Interviewers Ask This

The interviewer is checking whether you can flatten a recursive data structure without losing information. They want to see correct traversal order, clear recursion base cases, and careful handling of empty children. They are also evaluating whether you can manage shared recursive state, rebuild left and right subtrees in the correct order, write executable Python, and explain output space and recursion stack space accurately.

Common interview mistakes

A common mistake is storing only node values and not storing null markers. That loses the exact tree structure. Another mistake is using a different traversal order during deserialization. Candidates may also forget the None base case, build the right subtree before the left subtree, reset the token index inside each recursive call, or forget to advance the index after reading a token. It is also incorrect to claim O(1) extra space while ignoring the token list and recursion stack.

Interview tip

Explain the one-token-per-tree-position invariant before writing code. Say that a value creates a node and # creates an empty child. Then keep the traversal order root, left, right identical in both methods.

Interviewer may ask next
How would you handle a very deep skewed tree without risking Python's recursion limit?

Use an iterative preorder traversal with an explicit stack. For serialization, push the right child before the left child so the left side is processed first. For deserialization, use a stack of frames that records whether each created node still needs its left or right child. The time complexity remains O(n). The serialized data and explicit stack use O(n) space. The main tradeoff is more complex state management.

How could you reduce the size of the serialized data?

Use a binary format, variable-length integer encoding, or a compact bitmap for null child positions. The same preorder order and structural information must still be preserved. Serialization and deserialization remain O(n), and total storage remains O(n), but the number of bytes per node can be smaller. The tradeoff is that the format becomes harder to read and the encoding code becomes more complex.

105. Trapping Rain WaterCodingHard

Question Details

Given nonnegative bar heights, calculate how much rainwater can be trapped after raining. Explain a correct two-pointer, prefix-maximum, or stack-based approach and its complexity.

Short Interview Answer (30-60 seconds)

I would use two pointers, one at each end of the height array. I also keep the highest bar seen from the left and from the right. At each step, I process the side with the smaller current height because that side already has a safe boundary. I add the trapped water at that position, update the pointer, and continue until the pointers cross. This runs in O(n) time and uses O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to calculate the total amount of rainwater trapped between nonnegative bars. The diagram uses the two-pointer method. This method fits well because we can decide the trapped water at one side without building extra prefix and suffix arrays.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Trapping Rain Water diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a list of nonnegative bar heights.

For the example:

height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]

The output is one integer. It is the total number of trapped water units.

For this example, the answer is 6.

2. Choose the two-pointer method

I place one pointer at the start and one pointer at the end.

The left pointer starts at index 0. The right pointer starts at index 11.

I also keep two values:

left_max is the highest bar seen from the left.

right_max is the highest bar seen from the right.

The key rule is simple. I process the side with the smaller current height. The taller opposite side gives a boundary, so the water on the smaller side depends only on that side's maximum.

3. Initialize the state

The initial values are:

left = 0

right = 11

left_max = 0

right_max = 0

water = 0

The loop continues while left is less than or equal to right.

4. Walk through the example

Step 1: left is 0 and right is 11. The heights are 0 and 1. The left side is smaller, so we process index 0. left_max stays 0. We add 0 water. Then left becomes 1.

Step 2: the heights at indices 1 and 11 are both 1. The code processes the left side. left_max becomes 1. We add 0 water. Then left becomes 2.

Step 3: the heights are 0 and 1. We process index 2. left_max is 1, so trapped water is 1 - 0 = 1. Total water becomes 1. Then left becomes 3.

Step 4: the heights are 2 and 1. The right side is smaller, so we process index 11. right_max becomes 1. We add 0 water. Then right becomes 10.

Step 5: the heights at indices 3 and 10 are both 2. The code processes the left side. left_max becomes 2. We add 0 water. Then left becomes 4.

Step 6: the heights are 1 and 2. We process index 4. Trapped water is 2 - 1 = 1. Total becomes 2. Then left becomes 5.

Step 7: the heights are 0 and 2. We process index 5. Trapped water is 2 - 0 = 2. Total becomes 4. Then left becomes 6.

Step 8: the heights are 1 and 2. We process index 6. Trapped water is 2 - 1 = 1. Total becomes 5. Then left becomes 7.

Step 9: the heights are 3 and 2. We process index 10 from the right. right_max becomes 2. We add 0 water. Then right becomes 9.

Step 10: the heights are 3 and 1. We process index 9. Trapped water is 2 - 1 = 1. Total becomes 6. Then right becomes 8.

Step 11: the heights are 3 and 2. We process index 8. right_max stays 2. We add 0 water. Then right becomes 7.

Step 12: both pointers are at index 7, where the height is 3. The code processes the left side. left_max becomes 3. We add 0 water. Then left becomes 8.

Now left is greater than right, so the loop stops.

The trapped water at each index is:

[0, 0, 1, 0, 1, 2, 1, 0, 0, 1, 0, 0]

The total is 6.

5. Explain why the result is correct

At every step, the algorithm processes the side with the smaller current boundary.

Suppose the left height is smaller than or equal to the right height. The right side is already tall enough to act as a boundary. Therefore, the trapped water at the left position depends only on left_max.

The same reasoning applies when the right height is smaller.

Each position is finalized once. Its water value never needs to be changed later.

6. Explain the Python implementation

The code creates left and right pointers. It also creates left_max, right_max, and water.

Inside the loop, it compares height[left] with height[right].

When the left side is smaller, it updates left_max, adds left_max minus the current height, and moves left.

Otherwise, it updates right_max, adds right_max minus the current height, and moves right.

When the pointers cross, the function returns the total water.

7. Explain complexity and edge cases

The time complexity is O(n) because each index is processed exactly once.

The auxiliary space complexity is O(1) because the algorithm uses only a few variables.

Important edge cases include an empty list, fewer than three bars, all zero heights, strictly increasing heights, and strictly decreasing heights. All of these produce 0 trapped water.

Key Insight / Why This Solution Works

The key insight is that trapped water at an index is limited by the smaller of the highest bar on its left and the highest bar on its right. The two-pointer method avoids storing all left and right maximum values. It keeps only left_max and right_max. The central invariant is that the side with the smaller current boundary can be finalized because the taller opposite side guarantees that the trapped water on the smaller side depends only on that side's maximum. This lets the algorithm process every position once with constant extra memory.

Code
from typing import List


class Solution:
    def trap(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        left_max, right_max = 0, 0
        water = 0

        while left <= right:
            if height[left] <= height[right]:
                left_max = max(left_max, height[left])
                water += left_max - height[left]
                left += 1
            else:
                right_max = max(right_max, height[right])
                water += right_max - height[right]
                right -= 1

        return water


if __name__ == "__main__":
    heights = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
    result = Solution().trap(heights)
    print(result)  # 6
Time & Space Complexity

The time complexity is O(n). Each index is processed exactly once as either the left pointer or the right pointer moves inward. The auxiliary space complexity is O(1). Auxiliary space means extra memory used by the algorithm. We only store the two pointers, two maximum values, and the running total. The amount of extra memory does not grow with the input size.

Where it is used

This two-pointer pattern is useful when a result depends on information from both ends of an array and one side can be safely finalized at each step. Similar reasoning appears in container problems, sorted-array searches, partitioning tasks, and other problems where left and right boundaries move toward each other.

Why Interviewers Ask This

Interviewers use this problem to check whether a candidate can recognize a two-pointer pattern and maintain a correct invariant. They also want to see whether the candidate understands why one side can be finalized safely. The question tests careful pointer movement, correct state updates, accurate complexity analysis, and the ability to compare a constant-space solution with approaches that use extra arrays or a stack.

Common interview mistakes

A common mistake is moving the pointer on the taller side. The algorithm must process the side with the smaller current height. Another mistake is calculating water before updating left_max or right_max. This can produce a negative or incorrect value. Candidates also sometimes use only the current left and right heights instead of the maximum height seen from each side. Another mistake is claiming O(n) extra space even though this version uses only constant auxiliary space. An off-by-one error in the loop condition can also skip the final position or process it incorrectly.

Interview tip

State the invariant before writing the loop: process the smaller side because the opposite side is already high enough to guarantee a boundary. Then make the code follow that sentence exactly.

Interviewer may ask next
Can we solve this using prefix and suffix maximum arrays instead?

Yes. Build a left_max array where left_max[i] stores the highest bar from index 0 to i. Build a right_max array where right_max[i] stores the highest bar from i to the end. Then water at index i is min(left_max[i], right_max[i]) - height[i]. This is O(n) time and O(n) auxiliary space. It is easier to explain, but it uses more memory than the two-pointer method.

What happens when the input has fewer than three bars?

The answer is 0 because at least three bars are needed to create a space between two boundaries. The current code already returns 0 for an empty list, one bar, or two bars. It still runs in O(n) time and uses O(1) auxiliary space.

106. Word Ladder IICodingHard

Question Details

Given a start word, an end word, and a dictionary, return every shortest valid transformation sequence where each step changes one character. Explain how breadth-first search and path reconstruction are combined without producing longer paths.

Short Interview Answer (30-60 seconds)

I would use breadth-first search to find the minimum distance from the start word to every reachable word. During BFS, I store every parent that reaches a word at that same minimum distance. When the end word is first reached, I finish processing the current BFS level but do not expand deeper levels. Then I use DFS from the end word through the parent map to build every shortest sequence. The expected BFS time is O(NL²), and the auxiliary BFS space is O(N + P), excluding the returned paths.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to return every shortest transformation sequence from beginWord to endWord. Each step must change exactly one character, and every intermediate word must appear in the dictionary. BFS is the right choice because the graph is unweighted. It explores words in increasing distance order. A parent map keeps every predecessor that reaches a word at its shortest distance. DFS then reconstructs all shortest paths.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Word Ladder II diagram
How to Explain It in an Interview
1. Understand the input and required output

The input contains beginWord, endWord, and wordList. The words used in a valid transformation must have the same length. Two words are connected when they differ in exactly one character.

The output is a list of paths. Every path must begin with beginWord and end with endWord. Every path must use the minimum possible number of transformations.

For the example:

beginWord = "hit" endWord = "cog" wordList = ["hot", "dot", "dog", "lot", "log", "cog"]

One valid returned order is:

[["hit", "hot", "dot", "dog", "cog"], ["hit", "hot", "lot", "log", "cog"]]

Both paths contain five words and four transformations.

2. Choose BFS and a parent map

I treat each word as a node in an implicit unweighted graph. An edge exists between two words when they differ in exactly one character.

BFS processes this graph level by level. This means it first reaches each word using the fewest transformations.

The distance dictionary stores the minimum distance from beginWord to each discovered word.

The parent map stores each child word and all parents that reach it at that minimum distance. For example, cog maps to both dog and log.

The central invariant is this: distance[word] is the minimum number of transformations from beginWord to word, and parents[word] contains every predecessor that reaches word at that minimum distance.

3. Initialize the state

The queue starts with "hit".

The initial distance map is {"hit": 0}.

The parent map is empty.

The dictionary set contains hot, dot, dog, lot, log, and cog. A set gives O(1) membership checks on average in Python.

The search begins at distance 0.

4. Walk through the example

At Level 0, the current word is hit. By changing one character at a time, BFS finds hot. We store distance["hot"] = 1 and parents["hot"] = {"hit"}.

At Level 1, the current word is hot. It reaches dot and lot. We store distance 2 for both words. Their parent sets are {"hot"}.

At Level 2, dot reaches dog and lot reaches log. Both new words receive distance 3. We store dog to dot and log to lot in the parent map.

At Level 3, dog and log are processed in the same BFS level. Dog reaches cog first. We set distance["cog"] = 4 and add dog as a parent. Log also reaches cog with the same next distance, so we add log as another parent.

We must finish processing the whole Level 3 after cog is first found. If we stop immediately after dog finds cog, we lose the second shortest path through log.

After Level 3 is complete, BFS stops expanding. It does not expand cog or any other node at distance 4 or greater.

5. Reconstruct every shortest path

The parent map contains reverse edges from each child to its shortest parents.

Starting from cog, DFS follows both branches:

cog to dog to dot to hot to hit

cog to log to lot to hot to hit

Each path is built in reverse order. When DFS reaches hit, the path is reversed before it is added to the result.

The recursion uses one shared path list. After exploring one parent, it removes that parent with path.pop(). This restoration step is backtracking.

6. Explain why the result is correct

BFS processes words in nondecreasing distance from hit. A parent is recorded only when it reaches a child at the child’s minimum distance.

If another word reaches the same child with the same minimum distance, that parent is also stored. A parent that would produce a longer distance is ignored.

Therefore, every edge in the parent graph belongs to a shortest route. DFS only follows those edges, so it cannot produce a longer path.

Finishing the first successful BFS level keeps all shortest parents of cog. Stopping before deeper levels prevents longer paths from entering the result.

7. Explain complexity and edge cases

Let N be the number of dictionary words and L be the word length.

For each processed word, the code tries 26 letters at each of L positions. Creating each candidate word with Python slicing costs O(L). Therefore, the expected BFS time is O(NL²). Set and dictionary operations are O(1) on average.

The BFS data structures use O(N + P) auxiliary space, where P is the number of stored shortest-parent links. The recursive reconstruction also uses a call stack and path list proportional to the number of words in one ladder. Path reconstruction is output-sensitive because it must create every returned sequence.

Important edge cases are endWord not being in wordList, beginWord already equaling endWord, no valid transformation existing, repeated words in wordList, and a word having multiple shortest parents.

Key Insight / Why This Solution Works

The key idea is to separate finding shortest distances from building the final paths. BFS finds the minimum distance to each reachable word because every transformation has equal cost. During BFS, the parent map stores every predecessor that reaches a child at that same minimum distance. The queue processes states in increasing distance order. Once endWord is reached, the algorithm completes that BFS level so no shortest parent is lost, then stops expanding before deeper levels. DFS follows the recorded reverse edges from endWord to beginWord and reverses each completed path. Because the parent graph contains only shortest-distance edges, every reconstructed path is shortest.

Code
from collections import defaultdict, deque
from typing import DefaultDict, Deque, Dict, List, Optional, Set


def find_ladders(
    begin_word: str,
    end_word: str,
    word_list: List[str],
) -> List[List[str]]:
    if begin_word == end_word:
        return [[begin_word]]

    word_set: Set[str] = set(word_list)
    if end_word not in word_set:
        return []

    distance: Dict[str, int] = {begin_word: 0}
    parents: DefaultDict[str, Set[str]] = defaultdict(set)
    queue: Deque[str] = deque([begin_word])
    found_distance: Optional[int] = None

    while queue:
        word = queue.popleft()
        current_distance = distance[word]

        if found_distance is not None and current_distance >= found_distance:
            continue

        for index in range(len(word)):
            for letter in "abcdefghijklmnopqrstuvwxyz":
                if letter == word[index]:
                    continue

                neighbor = word[:index] + letter + word[index + 1 :]

                if neighbor not in word_set:
                    continue

                next_distance = current_distance + 1

                if neighbor not in distance:
                    distance[neighbor] = next_distance
                    parents[neighbor].add(word)
                    queue.append(neighbor)

                elif distance[neighbor] == next_distance:
                    parents[neighbor].add(word)

                if neighbor == end_word:
                    found_distance = next_distance

    if end_word not in distance:
        return []

    results: List[List[str]] = []
    path: List[str] = [end_word]

    def build_paths(word: str) -> None:
        if word == begin_word:
            results.append(path[::-1])
            return

        for parent in sorted(parents[word]):
            path.append(parent)
            build_paths(parent)
            path.pop()

    build_paths(end_word)
    return results


if __name__ == "__main__":
    begin_word = "hit"
    end_word = "cog"
    word_list = ["hot", "dot", "dog", "lot", "log", "cog"]

    answer = find_ladders(begin_word, end_word, word_list)
    print(answer)
Time & Space Complexity

Let N be the number of words in wordList, L be the word length, and P be the number of stored shortest-parent links. For every processed word, the code tries 26 replacement letters at each of L positions. Building each candidate word with Python string slicing takes O(L), so the expected BFS time is O(NL²). Python set and dictionary operations are O(1) on average. The BFS data structures use O(N + P) auxiliary space. The DFS call stack and current path use space proportional to one ladder’s length. Reconstructing the answers takes output-sensitive time and output space proportional to the total size of all returned paths.

Where it is used

This pattern is useful when software must return every shortest route in an unweighted state space. Examples include word transformation tools, puzzle solvers, workflow transition analysis, dependency path exploration, and game-state searches. BFS finds the minimum number of steps. A parent graph preserves all equally short choices. A second traversal then rebuilds every shortest route without exploring longer routes.

Why Interviewers Ask This

This question tests whether the candidate can combine two graph techniques correctly. The interviewer wants to see if the candidate recognizes BFS for shortest distance, preserves multiple shortest parents, and avoids carrying complete paths inside the BFS queue. It also tests careful stopping logic. Stopping too early loses valid paths, while searching too deeply creates unnecessary work. The candidate must also write correct recursive reconstruction, restore path state, and explain the output-sensitive cost accurately.

Common interview mistakes

A common mistake is stopping as soon as cog is first generated. The algorithm must finish processing every word from the current BFS level, or it may miss another shortest parent such as log. Another mistake is using visited logic that prevents multiple words from the same level from reaching the same child. Candidates may also store only one parent instead of a set of parents. During DFS, forgetting path.pop() corrupts later paths. It is also incorrect to claim O(NL) time for this slicing-based Python code. Creating each candidate string adds another O(L) factor.

Interview tip

State the main invariant before writing code: distance stores the shortest distance, and parents stores every predecessor that reaches a word at that distance. Then clearly explain why you finish the successful BFS level before starting DFS reconstruction.

Interviewer may ask next
How would you handle a very large dictionary more efficiently?

The BFS and parent-map design can stay the same, but neighbor discovery can use wildcard patterns. For example, hot creates *ot, h*t, and ho*. A map from each pattern to matching words lets BFS find candidate neighbors without trying all 26 letters at every position. Building the pattern index takes O(NL²) time with Python slicing and O(NL) stored references. Traversal is often faster in practice, but the index uses more memory and highly shared patterns can still contain many neighbors.

Why can we not stop immediately when cog is first found?

The first discovery proves the shortest distance, but another word in the same BFS level may also reach cog at that distance. In the example, dog and log are both at Level 3. If the search stops after dog finds cog, the path through log is lost. We record the shortest end distance, finish processing the current level, and then stop before deeper levels. This preserves every shortest parent without changing the expected O(NL²) BFS time or O(N + P) BFS auxiliary space.

107. What is a unit test?TestingEasy

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.

108. What is pytest?TestingEasy

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.

109. What is a pytest fixture?TestingEasy

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.

110. What is a mock in Python testing?TestingEasy

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.

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.

Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.