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.

Mutability, copying and default arguments: three traps
Rebinding is not mutation.
score = 10; saved = score; score += 5; print(score, saved)outputs15 10. The cause is rebindingscoreto15;savedstill reaches10. In contrast,appendchanges an existing list.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 usecopy.deepcopywhen ownership semantics justify it.A mutable default is reused.
def collect(tag, bucket=[]): bucket.append(tag); return bucketcreatesbucketat definition time. Calls with"iter", then"decorator", return['iter'], then['iter', 'decorator']. Correct it withbucket=Noneand 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.

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:
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.it = iter([5, 8]); print(next(it)); print(list(it)); print(list(it))prints5,[8],[]. Continue with DSA Interview Questions for Placements for data-structure problem patterns.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;iscompares 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.




