Python interview bank
Questions interviewers actually ask, each answered at the length you would say it aloud, with the topic to reread if you were not sure.
Basics
21 questions · 0 Seen01 What does assignment do in Python, and why can mutation be visible through another name? reveal ▾ hide ▴
Assignment binds a name to an object; it does not copy the object or put it inside a variable box. Two names can therefore refer to the same mutable object. If backup = records, then records.append(item) changes that shared list, so backup observes the new item too. Rebinding records = [] is different: it points only that name at a new list. Use an explicit shallow or deep copy when independent state is required, and choose the depth from the nested sharing the API intends to preserve.
02 When does a Python loop else clause run, and what is it useful for? reveal ▾ hide ▴
A loop’s else suite runs when the loop finishes normally: a for exhausts its iterator or a while condition becomes false. A break from that same loop skips the suite; continue does not. This makes the construct useful for searches: break when a matching record is found, and put the not-found path in else. A return or an unhandled exception leaves the surrounding function or suite, so it does not reach the else. In nested loops, only the break associated with that particular loop suppresses it.
03 Why is a mutable default argument shared across calls, and how do you fix it? reveal ▾ hide ▴
A default expression is evaluated when the def statement executes, and the resulting object is stored on that function object. Calls that omit the argument reuse the same object. A function such as def add(item, items=[]) therefore accumulates data across calls because each call mutates one list. The usual fix is an immutable sentinel, commonly None, followed by creating a fresh list inside the function. Deliberately shared state should instead have an explicit owner and name. A nested def can create fresh defaults only because executing it again creates a new function object.
04 What is the difference between equality and identity in Python? reveal ▾ hide ▴
== asks whether two objects represent equal values, using their equality protocol; is asks whether both references point to the exact same object. Use is for singleton sentinels such as None, so value is None is the normal test. Do not use identity for strings or numbers just because small examples sometimes share an implementation object; interning and caching are not a value contract. Custom classes can define __eq__, which may make distinct instances compare equal. For containers and domain values, choose the operation that matches the question you actually mean.
05 Why must set elements be hashable, and what contract must custom objects satisfy? reveal ▾ hide ▴
A set uses an element’s hash to select a lookup location and equality to confirm a match, so members need a stable hash while they are stored. The important rule is not simply “immutable”: any hashable object may be used, and a tuple is hashable only when all its elements are hashable. Custom objects that compare equal must produce the same hash. Mutating fields involved in __hash__ or __eq__ after insertion can make an element appear lost. Prefer immutable value objects for set members, or keep mutable records outside the set and store stable identifiers instead.
06 What does tuple immutability guarantee, and when is a tuple hashable? reveal ▾ hide ▴
Tuple immutability means its sequence of references cannot be replaced, inserted, or removed after creation. It does not recursively freeze the referenced objects. If a tuple contains a list, that list can still be mutated and observers of the tuple will see the changed contents. A tuple is hashable only when every element is hashable, so a tuple containing a list cannot be a dictionary key or set member. Use tuples for fixed-position records or multiple return values, but use a named record type when field meaning would otherwise depend on remembering numeric positions.
45 Why can a bare name in a match case behave differently from a constant? reveal ▾ hide ▴
In Python 3.14 structural pattern matching, a bare name such as case READY: is normally a capture pattern: it binds the subject rather than comparing it with an existing local or global value. A capture that matches everything can also make later cases unreachable. Use a literal or a dotted name such as case Status.READY: for value matching, place specific patterns before general ones, and include an unknown-input case. Guards run only after their pattern succeeds, so side effects in guards and case order are observable trade-offs that tests should expose.
46 How would you safely remove items while iterating in Python? reveal ▾ hide ▴
Python 3.14 does not provide one universal rule for changing a container during iteration. Resizing a dictionary or set commonly raises RuntimeError, while deleting from a list may keep running but skip elements because indices shift. Prefer a comprehension or a new result for filtering. If in-place list deletion is required, iterate over items.copy() or use a carefully reviewed reverse-index loop. For sets, calculate removals first or iterate over a copy. Tests need adjacent removable elements, no removals, and all removals; merely asserting that no exception occurred misses silent list errors.
52 When can a Python function handle an argument-binding error itself? reveal ▾ hide ▴
For a user-defined function in Python 3.14, Python evaluates the callable and argument expressions, then binds values to the signature before starting the body. Missing required arguments, duplicate values, unexpected keywords, and violations of positional-only or keyword-only rules therefore raise TypeError before body logging or an internal try can run. Recovery must sit at the caller or forwarding wrapper. Use inspect.Signature.bind() when an adapter needs to validate ahead of invocation. A trade-off is that argument expressions may already have produced side effects before later binding fails, so avoid effectful call construction.
53 What should a test assert about a function contract rather than its implementation? reveal ▾ hide ▴
In Python 3.14, a caller observes accepted argument shapes, the returned value or raised exception, and documented side effects. Tests should cover a normal input, exact boundaries such as empty or threshold values, and one failure on each side, while checking whether caller-owned objects changed. They should not depend on local variable names or helper-call order unless that order is the public behavior. Type annotations remain descriptive rather than automatic runtime validation, so malformed deserialized input needs explicit boundary tests. Inject clocks, randomness, files, or network operations when those dependencies would otherwise hide important inputs.
58 What scope does a list comprehension use, and what happens to callbacks it creates? reveal ▾ hide ▴
In Python 3.14, a comprehension’s iteration target lives in an implicit scope, so [item * 2 for item in values] does not overwrite an outer item. Functions created inside that scope still close over bindings rather than per-iteration snapshots. If the comprehension builds callbacks and they run later, they can all observe the final iteration value. Bind the value with a default parameter or use a helper factory. A comprehension that immediately computes ordinary values has no deferred timing gap. Test callbacks after the comprehension with at least two distinct inputs.
59 How do you reason about evaluation order in a nested list comprehension? reveal ▾ hide ▴
Python 3.14 evaluates comprehension for and if clauses in the same left-to-right nesting order as equivalent nested loops. The leftmost iterable is obtained first; later clauses may use names bound by earlier ones, and the result expression runs only after every filter succeeds. Rewrite a complicated comprehension as explicit nested loops to verify name availability, output order, and call counts. Repeating an expensive parser in both the filter and result performs it twice. Prefer a loop, a helper, or an assignment expression when avoiding duplicate work is clearer than preserving a one-liner.
60 When should you not use a list comprehension? reveal ▾ hide ▴
In Python 3.14, a list comprehension eagerly builds a result and is best when traversal, filtering, and transformation form one readable value expression. Do not use it mainly for file writes, network calls, logging, or mutation while discarding the produced list. If an expression fails, assignment to the result name does not occur, but external side effects from earlier elements are not rolled back. A normal loop exposes partial completion, error handling, and recovery more clearly. For a large result consumed once, a generator expression can reduce materialization, but it transfers single-pass consumption responsibility.
67 Do Python type annotations validate values at runtime? reveal ▾ hide ▴
In Python 3.14, annotations provide machine-readable contract metadata, but ordinary assignment and function calls do not automatically enforce them. A JSON field annotated as int may still arrive as a string, Boolean, or missing value unless boundary code or a framework explicitly validates it. Use static checking to catch development-time mismatches and explicit parsing for untrusted runtime data. Be careful that bool is a subclass of int, so isinstance(value, int) admits flags unless excluded. Tests should cover malformed container shapes, missing fields, false values, and numeric boundaries.
68 How do mutation and rebinding differ when a Python function receives an object? reveal ▾ hide ▴
In Python 3.14, a call binds each parameter name to the object produced by its argument expression; it does not deep-copy the object. items.append(x) mutates that list, so every alias can observe the changed object. items = new_list only rebinds the function’s local name and does not redirect the caller’s name. Assignment semantics are the same regardless of type annotations. Make an API choose clearly between transforming and updating in place, then test both identity and content. A shallow copy isolates the outer container only, so nested mutable values may remain shared.
71 How should a Python set cross an ordered API or serialization boundary? reveal ▾ hide ▴
Python 3.14 sets model unordered unique membership and do not promise iteration order. JSON has no native set type; converting directly to a list without an ordering rule produces unstable snapshots and signatures. Sort comparable elements, sort by a documented domain key, or preserve first occurrence by traversing the original sequence with a separate seen set. Choose deliberately because plain set conversion also discards counts and original positions. If duplicates indicate invalid input, validate before deduplication. Tests should vary input order and assert the exact external representation rather than one observed process order.
72 Should an API return its internal set directly, a copy, or a frozenset? reveal ▾ hide ▴
In Python 3.14, returning an internal mutable set gives the caller authority to clear, add, or update the same object the owner uses. Return set(internal) for a mutable top-level snapshot, or frozenset(internal) when the result should expose read-only, hashable membership. Neither choice recursively copies or freezes member objects, which must already be hashable and stable. A copy also costs time and memory proportional to membership, so a high-volume API may instead expose query methods or an explicit immutable view contract. Mutation-test two callers to verify the promised isolation.
76 What creates a one-item tuple, and why is tuple(value) different? reveal ▾ hide ▴
In Python 3.14, the comma creates a tuple; parentheses usually only group an expression. (42) is an integer, while (42,) is a one-item tuple. tuple(value) instead consumes value as an iterable, so tuple("ab") produces two strings and a one-shot iterator becomes exhausted. To wrap one existing object, write (value,), not tuple(value). This distinction can silently change strings and generators rather than raising immediately. Tests should include an empty string, a multi-character string, an existing tuple, and a single-pass iterator to expose both shape and consumption.
77 When is a tuple a poor return type for an evolving public API? reveal ▾ hide ▴
In Python 3.14, a tuple return makes element count, order, and meaning part of the caller contract. Fixed-length unpacking fails with ValueError when a branch or later release adds or removes a position, and bare indexes become hard to review as fields grow. A short local pair can remain clear; an evolving record usually benefits from NamedTuple, a data class, or another object with named fields. NamedTuple retains tuple behavior but its annotations do not validate runtime input. Test every return branch for one stable shape and treat field additions as compatibility changes.
78 Why can value or default be wrong when zero is valid data? reveal ▾ hide ▴
In Python 3.14, or returns its first operand when that object is truthy and otherwise returns the second operand; it is not specifically a None coalescing operator. Valid values such as 0, False, "", and empty containers therefore get replaced by default. If absence alone triggers fallback, write an explicit value is None check or use a unique sentinel when None is also valid. Custom truth testing can call __bool__() or __len__() and may even raise, so avoid treating it as free validation. Test every permitted false value.
79 What must an API decide before converting text or floats to int? reveal ▾ hide ▴
Python 3.14 gives int() different contracts by input kind: int("101", 2) parses text with a base, while int(3.9) truncates the numeric value toward zero. int("3.9") raises ValueError; none of these operations proves a domain range, unit, or precision policy. Boundary code must define accepted syntax, sign, base, whitespace, minimum and maximum, then report invalid data instead of silently substituting a default. For money, binary float is usually the wrong exact representation; use integer minor units or Decimal under an explicit rounding policy.
Functions in depth
28 questions · 0 Seen07 How do positional-only and keyword-only parameters make a Python API safer? reveal ▾ hide ▴
Parameters before / are positional-only, while parameters after a bare * or after *args are keyword-only. Positional-only parameters keep an internal name out of the public calling contract, so the implementation can rename it without breaking keyword callers. Keyword-only parameters make ambiguous options such as booleans, units, and timeouts visible at the call site and prevent accidental transposition. *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a new dictionary. Use catch-all parameters only when the API genuinely accepts an open set of arguments.
08 Why do callbacks created in a loop often read the final loop value? reveal ▾ hide ▴
A closure normally retains access to an enclosing binding, not a snapshot of its value when the function is created. Callbacks built in one loop can share the loop variable’s cell, so when they run later they all read its final binding. For example, three lambda: queue_name functions may all return the last queue name. Bind the current object with a default such as lambda queue_name=queue_name: queue_name, or call a helper factory once per iteration. A default stores an object reference too, so copy explicitly if later mutation must not be observed.
09 What does a decorator replace, and why should wrappers use functools.wraps? reveal ▾ hide ▴
A decorator runs when the decorated definition executes and rebinds the function name to the decorator’s return value. A wrapper can therefore change call behavior, but it can also hide metadata and the original signature. functools.wraps(original) copies useful attributes such as __name__, __doc__, and annotations, and sets __wrapped__ so inspect.signature and other tools can follow the chain. It does not prove that the wrapper forwards arguments or preserves semantics. A transparent wrapper should pass *args and **kwargs correctly, return the result, preserve exceptions, and handle async functions without forgetting await.
10 How does a generator differ from a reusable collection? reveal ▾ hide ▴
A generator is an iterator that computes values lazily and keeps a suspended execution frame between yields. Creating it does not run the body to completion; each next() resumes at the previous yield until the function returns and raises StopIteration. It is single-pass, so a second loop over an exhausted generator produces nothing. Laziness can reduce memory and allow streaming, but errors and side effects also occur during consumption, not creation. If several consumers need independent passes, create a fresh generator for each one or materialize the data when its size and lifetime make that safe.
11 When is functools.partial clearer than a lambda, and what does it capture? reveal ▾ hide ▴
functools.partial creates a callable that invokes an existing callable with selected positional or keyword arguments supplied in advance. It is clear when specialization is the whole operation, such as turning power(base, exponent) into a square function with exponent=2. The partial object keeps references to the original callable and bound argument objects; it does not copy mutable values. Later call arguments fill the remaining positions and may override previously bound keywords. Use a named function when you also need branching, validation, cleanup, or a domain-specific interface that deserves its own explanation.
12 Why can a function raise UnboundLocalError even when a global with that name exists? reveal ▾ hide ▴
Python classifies names for the whole function block before it runs. A binding operation anywhere in that function—assignment, an import, a loop target, or several other forms—usually makes the target local throughout the block. An earlier read then looks for that local binding instead of falling back to a same-named global; if execution has not assigned it yet, UnboundLocalError is raised. Fix the ownership rather than blindly adding global: pass the value in and return a replacement, or use nonlocal only when the nearest enclosing function deliberately owns persistent state.
40 Do *args and **kwargs copy the objects supplied by the caller? reveal ▾ hide ▴
In Python 3.14, a variadic positional parameter receives a new tuple and a variadic keyword parameter receives a new dictionary, but their elements and values are the same objects supplied by the caller. Replacing a top-level entry in the local kwargs does not alter the unpacked source mapping, while mutating a nested dictionary can affect caller-owned state. Treat packing as a shallow container boundary, not a deep copy. Document ownership, copy the nested level that must be isolated, and mutation-test both the outer mapping and a nested value.
41 How would you diagnose a duplicate keyword error in a forwarded Python call? reveal ▾ hide ▴
Python 3.14 evaluates argument expressions in source order, then binds them to the target signature before entering the function body. If an explicit keyword and **options, two unpacked mappings, or a positional value and keyword fill one parameter, binding raises TypeError; code inside the callee cannot catch its own pre-entry failure. Inspect the target with inspect.signature() and use Signature.bind() in the forwarding layer to reproduce the contract. Then choose an explicit merge policy. Function calls reject duplicate keywords even though dictionary displays let later unpacked values replace earlier ones.
42 How does __exit__ decide whether an exception from a with block propagates? reveal ▾ hide ▴
In Python 3.14, __exit__ receives the exception type, value, and traceback when the protected suite fails; all three are None after normal completion. A truthy return suppresses that exception, while False or None lets it propagate after cleanup. Suppression should therefore match only documented exception types, not return True unconditionally. Also decide what happens if cleanup itself fails, because that error may replace the block error unless both are retained through chaining. Tests should cover success, a suppressible failure, an unrelated failure, and a cleanup failure.
43 Who cleans up when a context manager fails partway through __enter__? reveal ▾ hide ▴
Python 3.14 calls __exit__ only after __enter__ succeeds. If entry acquires one resource and fails while acquiring the next, the manager’s entry logic must release the partial state itself. A robust multi-step manager can register each completed acquisition in an internal ExitStack, then use pop_all() only after the full entry succeeds. Do not acquire a batch first and register cleanup afterward. A failure-injection test should make the second or later acquisition fail and assert that every earlier resource was released exactly once.
44 What contract must a function decorated with contextmanager obey? reveal ▾ hide ▴
In Python 3.14, contextlib.contextmanager adapts a generator that must yield exactly once. Code before yield performs entry, the yielded value becomes the as target, and resumed code handles exit. Put unconditional release in finally; use explicit except, else, and raise paths for rollback, commit, or selective suppression. Calling the decorated function creates a one-shot manager rather than executing its body immediately, so create a fresh instance for each use. Test block exceptions and failures after yield, not only successful completion.
50 In what order are stacked Python decorators evaluated, applied, and called? reveal ▾ hide ▴
In Python 3.14, decorator expressions above a def are evaluated from top to bottom, then their resulting callables are applied to the function from bottom to top. Thus @outer above @inner binds the equivalent of outer(inner(func)). At call time, control enters the outer wrapper first and leaves it last. The distinction matters for authorization, retries, caching, transactions, and exception boundaries. Do not infer order from indentation alone: expand the nested calls and test recorded entry, result, exception, and exit events, including cache-hit and rejected-call paths.
51 Why can a synchronous wrapper break an async function decorator? reveal ▾ hide ▴
In Python 3.14, calling an async def returns a coroutine object; its body runs only when awaited. A synchronous wrapper that merely returns func(*args, **kwargs) surrounds coroutine creation, not its later execution, so timing logs and try blocks finish before failures occur. A transparent async decorator needs an async def wrapper and must await func(...) inside the intended logging, cleanup, retry, or exception boundary. Preserve metadata with wraps, but remember that metadata does not fix behavior. Test awaited success, exceptions, and cancellation, and never use blocking time.sleep() in retry code.
54 What problem does functools.Placeholder solve in Python 3.14 partial objects? reveal ▾ hide ▴
Before Python 3.14, partial() could pre-fill only a leading run of positional arguments; later positions usually required a wrapper or keywords. Python 3.14 adds functools.Placeholder, allowing a positional slot to be reserved and filled by a later call. Every placeholder must receive a positional argument, and it cannot be used as a keyword placeholder. This improves concise adaptation of positional APIs, but it raises the minimum runtime version and can make a call shape harder to read. Prefer a named wrapper when validation, enforced policy, or clearer parameter meaning matters.
55 Why is the initializer part of a reduce contract rather than a convenience? reveal ▾ hide ▴
In Python 3.14, functools.reduce(function, iterable, initial) treats initial as preceding the input. Empty input returns it, and a one-item input still invokes the binary function once. Without an initializer, the first item becomes the accumulator and empty input raises TypeError. The seed therefore defines the result type and empty-input semantics. Use a true identity when one exists; otherwise reject empty input instead of inventing a misleading value. An in-place method such as dict.update() returns None, so hiding it in a reducer breaks the next accumulator step.
56 Why should a generator use return instead of raising StopIteration? reveal ▾ hide ▴
In Python 3.14, StopIteration escaping from a generator body is transformed into RuntimeError: generator raised StopIteration; this prevents accidental truncation when a nested operation unexpectedly exhausts. Use return to end the generator normally. A return value becomes StopIteration.value, which yield from can receive, but an ordinary for loop does not emit it as an item. When reading another iterator optionally, call next(source, default) or catch StopIteration locally. Test empty and exhausted inputs so a normal protocol boundary is not mistaken for an application failure.
57 Does breaking out of a for loop close a generator-owned resource? reveal ▾ hide ▴
In Python 3.14, break exits the loop but does not generally call close() on its iterator. A referenced generator may remain suspended at yield, keeping a file, cursor, or lock alive until it resumes, is explicitly closed, or is collected. Put release in the generator’s finally, and make the owning consumer call close() in its own finally or use contextlib.closing(). Do not rely on immediate garbage collection, which is an implementation-dependent lifetime assumption. Test by consuming one item and stopping, then assert that cleanup happened immediately.
61 Which calls share an lru_cache entry, and why can equivalent calls miss? reveal ▾ hide ▴
In Python 3.14, lru_cache builds keys from the call pattern; it does not first bind every call to a normalized signature. Positional and keyword spellings, omitted versus explicit defaults, and even different keyword orders may occupy separate entries. With typed=True, immediate argument types are also separated, but nested contents are not recursively type-tagged. Standardize the internal calling convention when hit rate matters, then inspect cache_info() under representative traffic. Do not write correctness logic that assumes equal-looking calls merge, because cache-key details beyond the documented contract are implementation concerns.
62 Does lru_cache guarantee one computation per key under concurrency? reveal ▾ hide ▴
Python 3.14 keeps the internal lru_cache mapping coherent across threads, but that thread-safety guarantee is not single-flight behavior. Two threads can observe the same cold key before either stores a result, so the wrapped function may run more than once. The function must therefore tolerate duplicate execution, or the business layer must add keyed coordination when duplicate work or side effects are unacceptable. A warm-cache test cannot reveal this race; overlap two cold calls deliberately. Also avoid caching async def directly, because that stores a coroutine object that cannot safely be awaited repeatedly.
63 When should a function not be protected by lru_cache? reveal ▾ hide ▴
Do not cache until you can state how long a result remains valid for a complete key. In Python 3.14, lru_cache sees only arguments; database rows, permissions, configuration, time, and other hidden dependencies do not invalidate an entry. Add a stable version dimension or call cache_clear() after the owning transaction commits. Use finite maxsize for an unbounded key space, and prefer immutable results because hits return the same stored object. Caching instance methods also retains self in keys and may extend instance lifetimes. Measure hit patterns and object sizes rather than assuming reuse pays.
64 How does map(strict=True) change multi-input mapping in Python 3.14? reveal ▾ hide ▴
Multi-input map() normally stops when its shortest iterable is exhausted, silently ignoring longer tails. Python 3.14 adds strict=True; a length mismatch then raises ValueError during iteration. Construction remains lazy, so creating the map proves neither equal lengths nor successful transformation. Tests must consume it, and several outputs may already have been yielded before the mismatch appears. Strict mode detects alignment errors but provides no rollback for writes performed by the consumer. Use a transaction or prevalidation when the business operation requires all-or-nothing effects, and keep the default only when truncation is intentional.
65 Why can filter(None, values) remove valid data? reveal ▾ hide ▴
In Python 3.14, filter(None, values) applies ordinary truth-value testing and keeps only truthy elements. It removes not just None, but also 0, False, empty strings, and empty containers. Those may be valid domain values, such as a zero balance or a deliberately blank label. If only absence should be removed, use an exact predicate such as lambda value: value is not None. The result is a lazy, single-pass iterator, so tests must consume it and should include every allowed false value. Materialize only at the API boundary that actually requires a reusable collection.
66 When are a comprehension or loop clearer than map, filter, and reduce? reveal ▾ hide ▴
In Python 3.14, map(named_function, source) clearly emphasizes reuse of a named transformation, while a comprehension keeps a short expression, filter, and iteration shape together. A generator expression preserves laziness without nested lambdas. Use reduce() only when a custom accumulator and binary transition remain easy to explain; sum, any, all, joining, or an explicit loop often names the result better. A loop is preferable when steps have side effects, partial-failure policy, or several state updates. Do not choose functional forms merely for brevity, and document who owns consumption of lazy results.
69 Which Python statements create a local scope, and which only create a suite? reveal ▾ hide ▴
In Python 3.14, a function call creates a local namespace, while modules, class definitions, comprehensions, and annotation scopes have their own specific rules. Ordinary if, for, while, with, try, and match suites do not create function-like local scopes. A name bound inside one is generally visible later in the containing function, but only if execution actually reached the binding; an untaken branch can therefore lead to UnboundLocalError. Do not port block-scope assumptions from other languages. Initialize required values explicitly or structure branches so every continuing path binds them.
70 Why is assigning through locals() unreliable for changing function variables? reveal ▾ hide ▴
In Python 3.14, locals() exposes a mapping representing the current local namespace, but optimized function frames do not make mapping writes a supported interface for changing fast local variables. Reading it is useful for diagnosis or for an API that explicitly consumes a mapping; synthesizing locals()[name] = value and then reading bare name is not reliable program logic. Put dynamic fields in an explicit dictionary, data class, or regular object instead. Also avoid dumping the whole mapping in logs because it may contain credentials or large objects. Test the explicit container contract directly.
73 Which value does singledispatch inspect when selecting an implementation? reveal ▾ hide ▴
In Python 3.14, functools.singledispatch selects from the runtime type of the first argument only. Later arguments, container element types, and return annotations do not create additional dispatch dimensions. Therefore save(context, value) dispatches on context, even if registrations were written with value in mind. Put the behavior-defining object first or use a clearer conditional or multiple-dispatch design. For singledispatchmethod, the bound self or cls is skipped and the first ordinary argument is used. Test by varying each argument independently and asserting generic.dispatch(type).
74 How does singledispatch resolve subclasses and overlapping abstract base classes? reveal ▾ hide ▴
In Python 3.14, an exact registration wins first; otherwise singledispatch considers the concrete class’s MRO and applicable abstract base classes. The nearest ordinary registered base wins, but two unrelated matching ABCs can be ambiguous and raise RuntimeError rather than follow registration order. Built-in relationships also matter: bool is a subclass of int, so an integer handler accepts flags unless a more specific registration intervenes. Build a dispatch matrix covering exact types, ordinary subclasses, virtual subclasses, bool, and an unregistered type. Use dispatch(ConcreteType) to test resolution, not registry[type], which sees explicit entries only.
75 What makes singledispatch registration risky in a plugin system? reveal ▾ hide ▴
Python 3.14 registrations normally execute when their modules are imported, so a plugin that is not on the real startup path contributes no handlers. Re-registering the same exact type changes the shared generic function, and the later registration wins, making import order part of behavior. Define deterministic discovery, a duplicate policy, and startup checks using dispatch() for critical concrete types. Isolate tests with a locally created generic function or a fresh process because the public API has no unregister operation. Adding a broad ABC registration is also an API change: existing classes may select a new implementation.
Objects and classes
9 questions · 0 Seen13 How do class attributes and instance attributes interact in Python? reveal ▾ hide ▴
Attribute lookup on an ordinary instance checks mechanisms including the class hierarchy when the instance does not supply its own value. A class attribute is therefore shared as a fallback, while an assignment such as account.currency = "EUR" normally creates or replaces an instance attribute. That new value shadows Account.currency for that account without changing the class or other instances. This distinction is especially important for mutable class attributes: an inherited list can be shared accidentally across every instance. Put per-object mutable state in __init__, and change shared configuration through the class when shared ownership is intentional.
14 Why is super() about the MRO rather than simply calling a parent class? reveal ▾ hide ▴
super() delegates to the next implementation in the current class’s method resolution order, not necessarily to one hard-coded parent. That matters in diamond-shaped multiple inheritance: cooperative methods can each run once as the call moves through the C3-linearized MRO. Every participating method must accept a compatible signature, consume the arguments it owns, forward the rest, and call super() consistently. Directly invoking a base method can skip another class or execute shared ancestors twice. Inspect Class.mro() when behavior is unclear, and prefer composition when the classes cannot maintain one cooperative method contract.
15 What does an abstract base class enforce, and what does it not enforce? reveal ▾ hide ▴
An abstract base class built with ABC and @abstractmethod prevents instantiation of a subclass while required abstract methods remain unresolved. This moves an incomplete nominal implementation failure to construction time and provides a common target for isinstance and shared behavior. An abstract method may still contain a reusable implementation that subclasses call with super(). The mechanism does not validate arbitrary method signatures or guarantee semantic correctness; a subclass can implement the name badly. Virtual subclass registration affects membership checks but does not inject methods. Tests must still exercise the behavioral contract and failure cases.
16 Why should a dataclass field use default_factory for mutable defaults? reveal ▾ hide ▴
field(default_factory=list) stores a callable and invokes it for each new dataclass instance, so every object receives its own list. Writing a mutable list directly as a field default would imply one shared object, and dataclasses reject common unhashable mutable defaults with ValueError. A factory can also construct dictionaries, sets, or domain-specific defaults that require a fresh owner. default_factory takes a zero-argument callable, not the result of calling it. Use __post_init__ for validation or values derived from several fields, because that logic needs the fully initialized instance rather than a simple default.
17 How do data and non-data descriptors differ during attribute lookup? reveal ▾ hide ▴
A descriptor is a class attribute whose protocol methods customize attribute access. A data descriptor defines __set__ or __delete__ in addition to __get__, and it takes precedence over an instance’s __dict__. A non-data descriptor defines only __get__, so an instance attribute can shadow it. Functions are non-data descriptors, which is how accessing a function through an instance produces a bound method while still allowing instance shadowing. A reusable descriptor should usually store per-instance values on the instance or in weak-key storage, and return itself when __get__ receives instance=None for class-level access.
18 Where does a metaclass participate in class creation, and when is it justified? reveal ▾ hide ▴
A metaclass is the class of a class; type is the usual default. During a class statement, __prepare__ can supply the namespace, the class body populates it, and the metaclass’s __new__ and __init__ create and initialize the class object. Calling that class later goes through the metaclass’s __call__ to create instances. A custom metaclass is justified when a framework must enforce or register behavior consistently at class-definition time. For local transformations, prefer a class decorator or __init_subclass__, because metaclasses spread through inheritance and unrelated metaclasses can conflict.
47 How does deepcopy handle cycles and shared children in an object graph? reveal ▾ hide ▴
In Python 3.14, copy.deepcopy() maintains a memo mapping from source identities to copied objects during one traversal. If two source edges reach the same mutable child, both copied edges normally reach one copied child; if an edge points back to an ancestor, the memo closes the cycle instead of recursing forever. A custom __deepcopy__(memo) must register its new object before recursively copying children and pass the same memo onward. Naive recursion can duplicate shared state or overflow on cycles. Test identity relationships and back-references, not just equality of printed values.
48 How do copy, deepcopy, and copy.replace differ in Python 3.14? reveal ▾ hide ▴
copy.copy() creates a new outer object while normally retaining references to its children. copy.deepcopy() recursively follows the object graph, using a memo to preserve sharing and cycles. copy.replace(), added in Python 3.13 and present in 3.14, creates the same supported type with named fields changed; unchanged fields follow that type’s replacement semantics, so replacement is not deep copying. It supports named tuples, data classes, and classes defining __replace__(). Choose from the mutation path and ownership contract. Blind deep copies can be expensive or invalid for locks, handles, and identity-bearing objects.
49 How would you test that a copy operation provides the promised independence? reveal ▾ hide ▴
Under Python 3.14, equality alone cannot prove copy independence because distinct graphs may compare equal before mutation. Build a fixture with a mutable nested child, two fields sharing that child, a cycle when supported, and one object the policy deliberately keeps shared. Copy it, mutate each relevant path in the result, and assert which source paths change and which do not. Also verify identities and source-to-copy topology. This test distinguishes assignment, shallow copy, topology-preserving deep copy, and naive recursive duplication. Keep the fixture bounded because a full deep copy may be costly.
Concurrency
7 questions · 0 Seen19 What is the difference between a coroutine object and an asyncio Task? reveal ▾ hide ▴
Calling an async def function creates a coroutine object, but it does not independently schedule the body. Awaiting that object drives it as part of the current task. An asyncio.Task wraps a coroutine, schedules it on the running event loop, and stores its result, exception, or cancellation state. Creating a task is therefore an ownership decision, not just alternate syntax for await. The caller must retain and eventually await it, or create it inside a TaskGroup that gives the child a bounded lifetime and a defined failure path.
20 How does asyncio.TaskGroup handle a failing child task? reveal ▾ hide ▴
TaskGroup treats its child tasks as one structured operation. When a child raises a non-cancellation exception, the group cancels unfinished siblings, waits for their cleanup, and then raises the failures that still need reporting as an exception group when the context exits. Coroutines should release resources in finally or async with and normally let CancelledError propagate. This differs from default asyncio.gather, which propagates the first exception to its waiter but does not automatically cancel other awaitables for that reason. Choose the API according to whether sibling work remains meaningful after one failure.
21 How do you choose between threads and processes for concurrent Python work? reveal ▾ hide ▴
Choose from the workload and sharing boundary. Threads share one process and memory, so they are convenient for blocking I/O and for libraries that release the interpreter lock, but shared mutable state needs synchronization. Processes have separate memory and can run CPU-heavy Python work in parallel, at the cost of startup, inter-process communication, serialization, and larger resource use. The usual GIL-enabled CPython build does not make CPU-bound pure Python threads parallel, but that fact is not a substitute for measurement. Benchmark the real task, include data-transfer cost, and test shutdown and failure behavior.
22 Why can threaded Python code still have race conditions despite the GIL? reveal ▾ hide ▴
The GIL is not a transaction around an application invariant. A logical operation such as checking an account balance and then updating it spans multiple reads, decisions, and writes; another thread can interleave between them, especially around blocking calls or explicit lock releases. Do not infer thread safety from one observed bytecode sequence or from a built-in operation appearing atomic on one interpreter build. Protect the whole invariant with a Lock, move ownership to one worker and communicate through a queue, or use immutable messages. Tests should force interleavings and verify both results and shutdown behavior.
23 When would you use Executor.map instead of submit with as_completed? reveal ▾ hide ▴
Use Executor.map when applying one callable across inputs and consuming results in the same order as those inputs. A slow early item can therefore delay later results even if their work has finished. Use submit when each job needs separate metadata, cancellation, or error handling; it returns a Future. Iterating as_completed(futures) then exposes futures in completion order, so fast results can be processed immediately. In both styles, retrieving a result re-raises the worker exception. Bound the amount of submitted work, define timeouts, and close the executor with a context manager or explicit shutdown().
24 Why should a retried Celery task be idempotent? reveal ▾ hide ▴
A Celery task travels through a broker to a worker, and failures can occur after an external side effect but before the message is acknowledged or the result is recorded. A retry or redelivery may then execute the task again. The task should use a stable operation key, make writes conditional or upsert safely, and record completion at the same durable boundary as the side effect where possible. Retry only transient failures with bounded backoff and jitter; permanent validation errors should fail immediately. acks_late changes when acknowledgement occurs, but it cannot by itself make a non-idempotent payment or email safe.
37 How should an asyncio coroutine handle cancellation while owning a resource? reveal ▾ hide ▴
Cancellation is a cooperative control signal delivered as CancelledError at a suspension point. Put resource release in finally or use an async context manager so cleanup runs whether the operation succeeds, fails, or is cancelled. A coroutine may catch cancellation briefly to restore invariants, but it should normally re-raise after cleanup; suppressing it can make TaskGroup and timeout boundaries misbehave. If cleanup itself awaits, keep the ownership boundary active until it completes. Use shield only for a deliberately non-cancellable inner operation, while remembering that the caller can still be cancelled.
Standard library
6 questions · 0 Seen25 When would you choose Counter, defaultdict, or deque from collections? reveal ▾ hide ▴
Choose the container that states the operation. Counter maps hashable values to counts and provides frequency-oriented operations such as most_common. defaultdict(factory) creates and inserts a value when a missing key is accessed through __getitem__, which is useful for grouping but can mutate the mapping during a read. deque supports efficient appends and pops at both ends, making it a better FIFO queue than repeatedly removing index zero from a list. These types do not replace domain validation: define what negative counts, missing groups, maximum queue length, and serialization should mean at the API boundary.
26 Why can itertools.groupby produce several groups for the same key? reveal ▾ hide ▴
itertools.groupby groups consecutive runs with the same key; it does not collect equal keys from the entire iterable like a database GROUP BY. Input ordered as A, B, A therefore produces three groups. Sort by the same key first when global grouping is required, while accounting for the sort’s time and memory cost. Each returned group is an iterator sharing the underlying input with the outer groupby iterator. Once the outer iterator advances, an earlier group may no longer be available, so consume it immediately or materialize that group if it must outlive the current iteration.
27 What does pathlib improve, and what boundaries still need explicit handling? reveal ▾ hide ▴
pathlib.Path represents filesystem paths as objects and provides composable operations such as / joining, suffix changes, traversal, and opening files. It improves readability and portability over manual separator concatenation, but it does not make I/O infallible or secure. Relative paths still depend on the current working directory, resolve() changes the path view rather than granting access, and a file can change between a check and its later use. Specify text encodings, handle expected OSError subclasses, and validate that user-selected paths remain inside an allowed root at the moment the operation is performed.
28 What information is lost at a Python JSON boundary? reveal ▾ hide ▴
JSON carries a small language-neutral data model, not arbitrary Python object identity or behavior. Objects become mappings with string keys, arrays become lists, and custom classes, sets, bytes, tuples as a distinct type, and timezone-aware datetimes need an explicit representation. A round trip may therefore preserve business data without preserving the original Python types. Define a schema and convert at the boundary instead of relying on default=str, which can silently erase structure. Treat input as untrusted: limit size and nesting where relevant, validate required fields, and decide deliberately whether non-standard numeric values are accepted.
29 Why can one Python log call produce duplicate output? reveal ▾ hide ▴
Loggers form a dotted-name hierarchy. A record handled by service.api normally propagates to ancestor loggers, so attaching equivalent handlers to both the child and root can emit it twice. Libraries should create a named logger with logging.getLogger(__name__) and leave handler policy to the application. The application should configure handlers, levels, formatting, and destinations once at its entry point. If a child truly owns separate handling, set propagate=False deliberately. Remember that logger and handler levels both filter records, and use exception-aware logging inside an active exception handler when the traceback is required.
30 How do you run an external command safely with subprocess? reveal ▾ hide ▴
For most bounded commands, call subprocess.run with the executable and each argument as separate list elements. This avoids shell parsing and keeps untrusted text as one argument. Use check=True when a nonzero exit status should become CalledProcessError, set a timeout, and choose whether output is captured as bytes or decoded text with an explicit encoding. Avoid shell=True for user-controlled data because quoting mistakes become command injection. For streaming or long-lived interaction, use Popen and manage pipes carefully so neither side deadlocks. Also define the child environment and working directory when reproducibility matters.
Typing and tooling
6 questions · 0 Seen31 Do Python type hints enforce types at runtime? reveal ▾ hide ▴
No. Type hints describe contracts for static checkers, editors, reviewers, and libraries that deliberately inspect annotations; an ordinary function call does not validate arguments or return values against them. Runtime boundaries such as HTTP requests, JSON, environment variables, and database rows still need parsing and validation. Any is an escape hatch for the checker, not proof that a value is safe, and object is usually better when code accepts any value but must narrow before using it. Keep static checking and runtime validation separate, then test that the conversion layer reports invalid input clearly.
32 How does a typing.Protocol differ from an abstract base class? reveal ▾ hide ▴
A Protocol describes the members a value must provide for static structural subtyping. A class can satisfy it without inheriting or registering, which matches duck-typed APIs and avoids coupling implementations to one hierarchy. An abstract base class is nominal by default and can prevent instantiation until abstract methods are implemented; it can also provide shared runtime behavior. @runtime_checkable permits limited isinstance checks for a protocol, but those checks only examine attribute presence, not full signatures or semantics. Keep protocols small and consumer-owned, and validate behavior separately when crossing an untrusted runtime boundary.
33 How should you choose the scope of a pytest fixture? reveal ▾ hide ▴
Choose the narrowest scope that matches the resource’s real ownership and keeps tests isolated. Function scope creates a fresh fixture per test and is the safest default for mutable state. Class, module, package, or session scope can reduce expensive setup, but any mutation may leak between tests and make order matter. A yield fixture should acquire before yield and release in finally so cleanup runs when the test fails. Do not widen scope merely to make a suite faster; first measure setup cost, reset shared state explicitly, and test that a failed assertion still closes files, transactions, processes, or temporary services.
34 What is the difference between a wheel and a source distribution? reveal ▾ hide ▴
A source distribution, or sdist, packages source material and build metadata; installing it generally requires a build step in the target environment. A wheel is a built distribution that installers can unpack into the environment without running the project’s normal build, although wheels may be specific to Python versions, ABIs, or platforms. Publish both when downstream users may need unsupported platforms or inspect and rebuild source, and test the artifacts rather than only the repository checkout. Declare the build backend and its requirements in pyproject.toml, then build in isolation so undeclared local tools cannot hide missing dependencies.
35 What does a Python virtual environment isolate, and what does it not isolate? reveal ▾ hide ▴
A virtual environment gives a project its own interpreter configuration and package installation directory, so installing one project’s dependencies does not normally modify another project’s packages or the base environment. Activation mainly adjusts shell variables such as PATH; it is convenient, not required, because invoking the environment’s interpreter directly selects it. A venv does not isolate operating-system libraries, processes, network access, credentials, or containers, and it is not a portable artifact to copy between machines. Recreate it from declared and locked dependencies, exclude it from version control, and verify which interpreter python and pip actually target.
36 How do you choose between timeit, cProfile, and a sampling profiler? reveal ▾ hide ▴
Start with the question you need answered. timeit repeatedly measures a small, controlled operation and is useful for comparing focused alternatives after setup has been separated. cProfile instruments function calls across a representative run and reports call counts plus cumulative and per-call time, which helps locate where an application spends time but adds profiler overhead. A sampling profiler periodically observes stacks and usually disturbs a live process less, at the cost of approximate results and less detail for very short calls. Use realistic data, repeat measurements, inspect variance, and optimize only a bottleneck that matters to the end-to-end workload.
Functions
2 questions · 0 Seen38 Why do Python closures created in a loop often observe the final value, and how do you fix it? reveal ▾ hide ▴
A closure retains access to the loop variable’s binding, not a snapshot from each iteration. Calls made after the loop therefore read the final object stored in that shared cell. If each callback needs the current value, bind it at function creation with a default parameter, such as lambda item=item: item, or call a helper factory that creates a new local binding per iteration. Prefer the helper when each callback has mutable private state or richer behavior. Test callbacks after the loop and with changed outer state so an immediate invocation does not hide late binding.
39 When does a closure need nonlocal, and when can it mutate state without it? reveal ▾ hide ▴
nonlocal is required when an inner function rebinds a name from an enclosing function scope. Without it, assignment classifies that name as local, and reading it first can raise UnboundLocalError. Mutating an object through an existing binding is different: items.append(value) changes the list but does not rebind items, so it needs no nonlocal. Use the distinction deliberately. Rebinding a small immutable counter can be clear, while a dataclass or dictionary may make several related state fields and invariants easier to inspect. Avoid nonlocal when an explicit object would give ownership a clearer boundary.
No questions match this filter.