Tricky questions
This folder has no README. Everything inside it is listed below.
Notes
- Mutable default arguments 3 Default argument values are evaluated once, at function definition time, and the same object is reused across calls.
- Late-binding closures 3 A closure is a nested function that captures and remembers variables from its enclosing scope, even after that scope has finished executing.
- is vs == and string/int interning is compares identity (same object in memory). == compares value.
- bool is a subclass of int True and False are real ints. isinstance(True, int) is True.
- Class variable vs instance variable A class-level attribute is shared by all instances.
- List multiplication and aliasing [[]] * 3 creates a list containing three references to the same inner list, not three independent lists.
- Chained comparisons a < b < c is not (a < b) < c — it's (a < b) and (b < c), with b evaluated only once.
- Generator exhaustion Generators (and most iterators) are single-pass.
- async def returns a coroutine, doesn't run Calling an async def function returns a coroutine object without running its body.
- super() and MRO with multiple inheritance In multiple inheritance, super().method() doesn't necessarily call the parent class you wrote next to the class — it calls the next class in the MRO (Method…
- Walrus operator (:=) gotchas The walrus operator (PEP 572, Python 3.8+) is an assignment expression — it returns the assigned value.
- Comprehension scope In Python 3, comprehensions (list, set, dict, generator) have their own scope — the loop variable doesn't leak.
- += on an immutable inside a dict d[k] += x looks like one operation but is actually d[k] = d[k] + x.
- id() reuse after garbage collection id() returns a number unique to an object for the lifetime of that object.
- Implicit string concatenation 3 Adjacent string literals are concatenated at parse time.
- dict.get(k, default) and falsy values get(k, default) is a regular method call.
- Floats and equality 0.1 + 0.2 != 0.3. IEEE 754 double-precision floats can't represent most decimal fractions exactly.
- Dataclass mutable defaults Same root issue as 01_mutable_default_arguments.md, but specific to @dataclass: trying to use field: list = [] raises a ValueError at class construction time.
- Starred unpacking ` and *` in assignment, function calls, and (since 3.5) literals do different things in different positions.
- Exception swallowing in finally A return, break, or continue inside finally silently suppresses the active exception.
- Circular imports When module A imports B and B imports A, one of them sees a partially initialized version of the other.
- Descriptor invocation order When you write obj.attr, Python doesn't just look in the instance dict.
- Empty class as a dict key 4 An instance of a class with no methods at all works fine as a dictionary key.
- Metaclass vs class decorator Both can modify a class, but they fire at different times and have different scopes.
- pickle security Unpickling executes arbitrary code. A malicious pickle payload can run any code on your machine.
- nonlocal vs global Assigning to a name inside a function creates a local variable, even if a same-named variable exists in an enclosing or global scope.
- yield from semantics yield from gen looks like a shortcut for for x in gen: yield x.
- Blocking calls in async def Putting a synchronous blocking call (time.sleep, requests.get, open().read() on a slow disk) inside an async def function blocks the entire event loop.
- Decorator with arguments A decorator that takes its own arguments needs three levels of nesting, not two.
- functools.wraps A decorator that doesn't use @functools.wraps(func) replaces the function's metadata with the wrapper's. __name__, __doc__, __module__, __qualname__,…
- Collection complexity (list / tuple / set / dict) Picking the wrong container turns linear work into quadratic.
- Truthiness of containers and falsy values bool([]) is False. bool([0]) is True. bool([False]) is True.
- Positional-only and keyword-only parameters (/ and *) def f(a, , b) and def f(a, /, b) look similar but mean opposite things. ` makes everything after keyword-only; /` makes everything before positional-only.
- Mutating a dict key after insertion If a key's hash depends on mutable state and you mutate it after insertion, the dict still holds the entry — but lookups land in the wrong bucket and find…
- True, 1, and 1.0 as the same dict key True, 1, and 1.0 collapse to a single dict entry.
- float('nan') as a dict key NaN can be a dict key — but you can't look it up by another NaN, because NaN is not equal to anything (including itself).
- Self-referential dicts (and lists) A dict (or list) can contain itself as a value.
- Mutating a dict during iteration Adding or removing keys while iterating raises RuntimeError: dictionary changed size during iteration.
- setdefault always evaluates the default d.setdefault(key, default) — the default is a regular function argument, evaluated every time, even when the key already exists.
- __missing__ — and why .get() doesn't trigger it A dict subclass can define __missing__(self, key) to handle missing keys for d[key].
- Merging dicts: {a, b} vs a | b vs update vs ChainMap a | b — modern, readable. Best for "give me a merged copy."
- Duplicate keys in dict comprehensions {f(x): x for x in iter} silently keeps only the last value when f(x) collides.
- Inverting a dict — what could go wrong {v: k for k, v in d.items()} looks innocent.
- dict.fromkeys shares a single default object dict.fromkeys(keys, default) evaluates default once and assigns the same object to every key.
- frozenset vs tuple as a dict key Both are hashable, both work as dict keys — but they answer different questions. frozenset({1, 2}) == frozenset({2, 1}), while (1, 2) != (2, 1).
- Dict insertion order — when did it become a guarantee? Before 3.6, iterating a dict could return keys in any order.
- d[k] vs d.get(k) vs d.setdefault(k, v) vs defaultdict Only the last two change the dict, and only on a miss.
- Dict shallow copy trap — nested mutables are shared d.copy() (or dict(d), or {**d}) creates a new outer dict but copies references for the values.
- Two awaits for one HTTP request 4 Why two awaits, when the request was already sent by the first one?