Python Interview Questions: Data Model, Iterators and Concurrency with Traced Answers

Trace the Python behaviour that interviews probe: object sharing, shallow copies, exhausted iterators, generator state, decorated calls, exceptions and concurrency choices.

KnowledgeGate Team

Exam prep & CS education

Updated 29 Aug 20266 min read

You may write working Python yet struggle when asked, “Was this object copied?”, “Why is this iterator empty now?”, or “Will threads make this loop faster?” Definitions alone rarely settle these questions. The useful answer is an observable trace through names and objects, iterator state, decorator calls, exception flow, and concurrency choices.

Python's data model starts with names, objects and identity

A name refers to an object. That object has a type, a value and an identity. Assignment normally rebinds a name; it does not clone the object.

first = [4, 7]
second = [4, 7]
alias = first

print(first == second, first is second, first is alias)
alias.append(9)
print(first, second)

The output is:

True False True
[4, 7, 9] [4, 7]

== compares values; is compares identity. first and second are equal but distinct. alias shares first, so append changes both views. Use is None for the singleton check, but not is to compare numbers or strings. This precision matters across Placement Preparation.

Two panels showing that first and alias share one list while second is a separate equal list, before and after an append.

Mutability, copying and default arguments: three traps

  1. Rebinding is not mutation. score = 10; saved = score; score += 5; print(score, saved) outputs 15 10. The cause is rebinding score to 15; saved still reaches 10. In contrast, append changes an existing list.

  2. A shallow copy separates only the outer container.

    profile = {"name": "Asha", "scores": [10, 20]}
    copied = profile.copy()
    copied["scores"].append(30)
    copied["name"] = "Ravi"
    
    print(profile)
    print(copied)

    The outputs are {'name': 'Asha', 'scores': [10, 20, 30]} and {'name': 'Ravi', 'scores': [10, 20, 30]}. The outer dictionaries differ but share the nested list. Copy it intentionally, or use copy.deepcopy when ownership semantics justify it.

  3. A mutable default is reused. def collect(tag, bucket=[]): bucket.append(tag); return bucket creates bucket at definition time. Calls with "iter", then "decorator", return ['iter'], then ['iter', 'decorator']. Correct it with bucket=None and a new list inside. This is object sharing and local name binding, not “pass by reference”.

Iterable versus iterator: protocol and exhaustion

An iterable is an object from which iter(obj) can obtain an iterator. An iterator is the stateful object consumed by next(). Its __next__() returns a value or raises StopIteration; a for loop manages both operations and the stop signal.

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current == 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

it = Countdown(3)
print(iter(it) is it)
print(next(it))
print(list(it))
print(list(it))

The four outputs are True, 3, [2, 1], and []. State moves 3 -> 2 -> 1 -> 0: next consumes 3, the first list consumes the remainder, and the last sees exhaustion. Exhaustion is not deletion. Lists normally produce fresh iterators; this Countdown is its own one-shot iterator. Do not catch StopIteration in ordinary for loops.

Generators package iterator state around yield

Calling a generator function creates a generator iterator without running its body to completion. Each next() resumes from the suspended point. yield returns one value and preserves local state.

def running_totals(values):
    total = 0
    for value in values:
        total += value
        yield total

totals = running_totals([4, 7, 2])
print(next(totals))
print(list(totals))
print(list(totals))

The output is 4, then [11, 13], then [], as preserved total becomes 4, 11, and 13. Python supplies the iterator methods, but the same consumption rules apply. Similarly, g = (n * n for n in [2, 3, 4]); print(next(g), sum(g)) prints 4 25: sum receives only 9 and 16. Laziness does not make a generator faster or reusable. Immediately materialising it as a list defeats incremental consumption.

Decorators and exceptions: trace the call boundary

@trace is rebinding equivalent to divide = trace(divide). The decorator receives a function and returns a wrapper, which runs on every later call. functools.wraps preserves basic metadata, but does not turn the wrapper into the original function object.

from functools import wraps

def trace(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        print(f"calling {fn.__name__}")
        result = fn(*args, **kwargs)
        print(f"returned {result}")
        return result
    return wrapper

@trace
def divide(total, count):
    if count == 0:
        raise ValueError("count must be non-zero")
    return total / count

print(divide(18, 3))

try:
    divide(18, 0)
except ValueError as exc:
    print(type(exc).__name__, str(exc))

The lines are calling divide, returned 6.0, 6.0, calling divide, and ValueError count must be non-zero. The failing call has no returned line because control leaves the wrapper when fn raises, then the matching outer except handles it.

Raise a narrow, meaningful exception for an expected domain failure. Catch only what you can handle, use bare raise to preserve the original traceback, and reserve finally for cleanup required on success or failure. Never hide defects with except Exception: pass.

Concurrency limits: choose by the wait

Concurrency overlaps progress; parallelism runs work simultaneously. In conventional GIL-enabled CPython, one thread normally executes Python bytecode at a time. Threads still help during blocking I/O; some native code releases the lock. Implementations and builds may differ. The GIL does not make compound operations race-free.

from concurrent.futures import ThreadPoolExecutor
from time import sleep

def wait_and_label(label):
    sleep(0.01)
    return f"{label}:done"

with ThreadPoolExecutor(max_workers=2) as pool:
    results = list(pool.map(wait_and_label, ["A", "B", "C"]))

print(results)

The output is ['A:done', 'B:done', 'C:done'] because map preserves input order, not task completion order. This trace establishes result ordering; compare elapsed time against a serial run before claiming a speedup.

For substantial CPU-bound pure-Python work, measure a process-based design:

from concurrent.futures import ProcessPoolExecutor

def subtotal(start, stop):
    return sum(n * n for n in range(start, stop))

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=2) as pool:
        parts = list(pool.map(subtotal, [0, 4], [4, 7]))
    print(parts, sum(parts))

This prints [14, 77] 91: 0 + 1 + 4 + 9 = 14, 16 + 25 + 36 = 77, then 14 + 77 = 91. Separate processes can run beyond one process's conventional GIL; measure startup, serialisation and communication costs.

Async fits cooperative waits with awaitable APIs; a blocking CPU loop stalls it. Use locks or message passing for shared state; handle cancellation and errors.

Three lanes comparing threads for blocking I/O, async cooperative waits, and processes for CPU-bound Python work.

How technical interviews test these ideas

Common questions ask you to predict aliasing, explain shallow-copy leakage, implement an iterator, trace generator exhaustion, expand a decorator, place an exception boundary, or justify concurrency. A strong answer states its assumptions, predicts the observable result and then names the rule that caused it.

Try three answer-first checks:

  1. box = (1, [2, 3]); box[1].append(4); print(box) prints (1, [2, 3, 4]). The tuple's slots stay fixed, but its contained list mutates.

  2. it = iter([5, 8]); print(next(it)); print(list(it)); print(list(it)) prints 5, [8], []. Continue with DSA Interview Questions for Placements for data-structure problem patterns.

  3. Ten network waits suggest threads or async. Two substantial pure-Python CPU chunks in conventional GIL-enabled CPython suggest measuring processes and transfer overhead. State assumptions, not “GIL means no threading”.

Use Technical Interview Prep: OS, DBMS, CN and OOP for wider CS revision. For structured technical and HR practice, use the Interview & Resume Preparation Course, where you can rehearse explaining trade-offs aloud.

Python interview answers: the short version and next step

  • Assignment binds names.

  • Mutation changes an object.

  • == compares value; is compares identity.

  • An iterator carries consumable state.

  • A generator is an iterator suspended at yield.

  • Decorators wrap calls, exceptions transfer control, and concurrency depends on waits, CPU work and state ownership.

Final drill: records = [2, 4]; alias = records; totals = running_totals(alias); alias.append(6); print(next(totals), list(totals), records is alias) prints 2 [6, 12] True. It starts after the append and consumes 2, 4, 6 through the shared list.

Build the underlying concepts and practise further with the Python course.