[{"data":1,"prerenderedAt":336},["ShallowReactive",2],{"topic-python-fundamentals":3},{"framework":4,"topic":15,"subtopics":23},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":8,"stem":12,"tier":13,"__hash__":14},"frameworks\u002Fframeworks\u002Fpython.yml","Python interview questions across language fundamentals, data structures and common gotchas for backend, data and automation roles.","yml","python",{},"Python",3,"frameworks\u002Fpython",1,"QsijsotyAr-3rnhJDWWZmc7hE6HAhylS5t1dKpigMOA",{"id":16,"description":17,"extension":7,"frameworkSlug":8,"meta":18,"name":19,"order":13,"slug":20,"stem":21,"__hash__":22},"topics\u002Ftopics\u002Fpython-fundamentals.yml","Mutability, data types and common Python gotchas that come up in nearly every Python interview.",{},"Fundamentals","fundamentals","topics\u002Fpython-fundamentals","s0g44vxzPWFDbmltZbaw52-Rnfw29-sEc6vjnipDrEk",[24,181,217,256,296],{"id":25,"title":26,"body":27,"description":31,"difficulty":34,"extension":35,"framework":10,"frameworkSlug":8,"meta":36,"navigation":37,"order":13,"path":38,"questions":39,"related":174,"seo":175,"seoDescription":176,"stem":177,"subtopic":178,"topic":19,"topicSlug":20,"updated":179,"__hash__":180},"qa\u002Fpython\u002Ffundamentals\u002Fmutability.md","Mutability",{"type":28,"value":29,"toc":30},"minimark",[],{"title":31,"searchDepth":32,"depth":32,"links":33},"",2,[],"medium","md",{},true,"\u002Fpython\u002Ffundamentals\u002Fmutability",[40,45,50,54,58,62,66,70,74,78,82,86,90,94,98,102,106,110,114,118,122,126,130,134,138,142,146,150,154,158,162,166,170],{"id":41,"difficulty":42,"q":43,"a":44},"mutable-immutable","easy","Which Python types are mutable and which are immutable?","**Immutable** (the value can never change in place — any \"change\" makes a new\nobject): `int`, `float`, `complex`, `bool`, `str`, `tuple`, `frozenset`,\n`bytes`, and `None`.\n\n**Mutable** (can be modified in place): `list`, `dict`, `set`, `bytearray`,\nand most custom class instances.\n\n```python\ns = \"hello\"\nprint(id(s))\ns += \" world\"        # looks like mutation...\nprint(id(s))         # ...but id() changed — a NEW string was created\n\nnums = [1, 2, 3]\nprint(id(nums))\nnums.append(4)       # genuine in-place mutation\nprint(id(nums))      # same id — same object\n```\n\nWhy it matters: mutability drives how **assignment, function arguments, and\n`==`\u002F`is` behave**. Immutable objects are also **hashable** (usable as dict\nkeys \u002F set members), while mutable ones generally aren't.\n",{"id":46,"difficulty":47,"q":48,"a":49},"default-arg","hard","What is the mutable default argument trap?","A function's default argument is **evaluated once, when the `def` statement\nruns** — not on each call. So a mutable default (like `[]` or `{}`) is created\na single time and **shared across every call**, accumulating state between\ninvocations.\n\n```python\ndef add(item, bucket=[]):   # the same list object on every call\n    bucket.append(item)\n    return bucket\n\nadd(1)   # [1]\nadd(2)   # [1, 2]  \u003C- surprise! the list persisted\nadd(3)   # [1, 2, 3]\n```\n\nThe fix is the standard `None` sentinel — use `None` as the default and create\na fresh object inside the body:\n\n```python\ndef add(item, bucket=None):\n    if bucket is None:      # new list per call\n        bucket = []\n    bucket.append(item)\n    return bucket\n```\n",{"id":51,"difficulty":34,"q":52,"a":53},"is-vs-eq","What is the difference between `is` and `==`?","`==` tests **value equality** — \"do these represent the same data?\" — by\ncalling the object's `__eq__`. `is` tests **identity** — \"are these the *exact\nsame object* in memory?\" — comparing `id()`s. They often agree, but not always.\n\n```python\na = [1, 2, 3]\nb = [1, 2, 3]\na == b   # True  — equal contents\na is b   # False — two distinct list objects\n\nc = a\nc is a   # True  — same object\n```\n\n**Rule: use `is` only for singletons** — `None`, `True`, `False` (e.g.\n`if x is None:`). Don't use `is` for numbers or strings: small ints and some\nstrings *appear* to work because CPython caches\u002Finterns them\n(`256 is 256` -> `True`, but `1000 is 1000` can be `False`), which is an\nimplementation detail you shouldn't rely on.\n",{"id":55,"difficulty":34,"q":56,"a":57},"shallow-deep-copy","What is the difference between a shallow and a deep copy?","A **shallow copy** (`copy.copy`, `list(x)`, `x[:]`, `dict(x)`) creates a new\nouter container but **copies references** to the nested objects — so the inner\nobjects are still **shared**. A **deep copy** (`copy.deepcopy`) recursively\ncopies everything, producing a fully **independent** structure.\n\n```python\nimport copy\noriginal = [[1, 2], [3, 4]]\n\nshallow = copy.copy(original)\nshallow[0].append(99)\nprint(original)   # [[1, 2, 99], [3, 4]]  \u003C- inner list was shared!\n\ndeep = copy.deepcopy(original)\ndeep[0].append(99)\nprint(original)   # unchanged — fully independent\n```\n\nUse a shallow copy when the elements are immutable (or sharing is fine); reach\nfor `deepcopy` when you have nested mutable structures and need true isolation\n(note it's slower and handles cycles).\n",{"id":59,"difficulty":34,"q":60,"a":61},"tuple-mutable","Can a tuple contain mutable objects?","Yes. A tuple's **immutability is shallow**: you can't reassign or resize its\nslots, but each slot is just a reference, and if that reference points to a\nmutable object, that object can still be changed in place.\n\n```python\nt = (1, [2, 3])\nt[1].append(4)     # allowed — mutating the list inside\nprint(t)           # (1, [2, 3, 4])\n\nt[1] = [9]         # TypeError — can't reassign a tuple slot\n```\n\nA consequence interviewers love: a tuple containing a list is **not\nhashable**, because hashability requires all contents to be immutable — so\n`hash((1, [2]))` raises `TypeError`, and such a tuple can't be a dict key.\n",{"id":63,"difficulty":47,"q":64,"a":65},"hashable","Why can't you use a list as a dictionary key?","Dictionary keys (and set members) must be **hashable**. Hashing requires that an\nobject's hash value **stays constant for its lifetime**, which in practice means\nit must be **immutable**. Lists are mutable, so Python deliberately makes them\n**unhashable** — they have no `__hash__`.\n\n```python\nd = {}\nd[[1, 2]] = 'x'   # TypeError: unhashable type: 'list'\nd[(1, 2)] = 'x'   # tuples are immutable -> hashable\n```\n\nThe reason is correctness: a dict places a key in a bucket based on its hash. If\na key could mutate after insertion, its hash would change and you'd never be\nable to find it again. Use an immutable equivalent — a **tuple** instead of a\nlist, a `frozenset` instead of a set.\n",{"id":67,"difficulty":34,"q":68,"a":69},"id-function","What does the id() function tell you?","`id(obj)` returns a unique integer **identity** for an object — in CPython, its\nmemory address. Two names with the same `id` refer to the **same object**; `is`\nis essentially an `id` comparison.\n\n```python\na = [1, 2]\nb = a\nid(a) == id(b)   # True  — same object\na is b           # True\nc = [1, 2]\nid(a) == id(c)   # False — equal value, different object\n```\n\n`id` is useful for understanding aliasing and why mutation through one name is\nvisible through another. The actual value is implementation-specific (don't rely\non it being an address).\n",{"id":71,"difficulty":47,"q":72,"a":73},"small-int-cache","Why does `is` sometimes work for equal integers?","CPython **pre-caches small integers** from **−5 to 256** as singletons, so equal\nvalues in that range share one object and `is` returns `True`. Outside that range,\nequal integers are usually distinct objects.\n\n```python\na = 256\nb = 256\na is b      # True  — cached\n\nc = 257\nd = 257\nc is d      # False — separate objects (in a REPL)\nc == d      # True  — always compare values with ==\n```\n\nThis is a CPython implementation detail, **not** a language guarantee. Never use\n`is` to compare numbers — use `==`. `is` is only for singletons like `None`.\n",{"id":75,"difficulty":47,"q":76,"a":77},"string-interning","What is string interning in Python?","CPython **interns** some strings — storing one shared copy — so identical\nstring literals can be the same object. Short, identifier-like strings are\ninterned automatically; others may not be.\n\n```python\na = \"hello\"\nb = \"hello\"\na is b           # True  — interned literal\n\nc = \"hello world!\"\nd = \"hello world!\"\nc is d           # often False (not auto-interned)\n\nimport sys\ne = sys.intern(\"hello world!\")  # force interning\n```\n\nLike int caching, this is an optimization detail. Always compare string **values**\nwith `==`, not identity with `is`.\n",{"id":79,"difficulty":47,"q":80,"a":81},"augmented-tuple","What happens when you use += on a list inside a tuple?","You get a surprising result: the list **is** mutated, **and** a `TypeError` is\nraised. `t[0] += [3]` does `t[0] = t[0] + [3]` — the `+=` extends the list in place\n(succeeds), then tries to reassign the tuple slot (fails, since tuples are\nimmutable).\n\n```python\nt = ([1, 2], 'x')\nt[0] += [3]      # TypeError: 'tuple' object does not support item assignment\nprint(t)         # ([1, 2, 3], 'x')  — the list WAS extended!\n```\n\nSo the mutation happens before the assignment error. Use `t[0].extend([3])` if you\nwant to mutate the inner list without the confusing error.\n",{"id":83,"difficulty":34,"q":84,"a":85},"list-slicing-copy","Does slicing a list create a copy?","Yes — slicing produces a **new (shallow) list** containing the same element\nreferences. `lst[:]` is a common idiom for a shallow copy. But the **elements**\nthemselves are shared, so nested mutables are still linked.\n\n```python\na = [1, 2, 3]\nb = a[:]          # shallow copy\nb.append(4)\na                 # [1, 2, 3] — unaffected\n\nnested = [[1], [2]]\ncopy = nested[:]\ncopy[0].append(9)\nnested            # [[1, 9], [2]] — inner list shared\n```\n\nSlicing copies the outer list only; use `copy.deepcopy` for full independence of\nnested structures.\n",{"id":87,"difficulty":47,"q":88,"a":89},"pass-by-object","Is Python pass-by-value or pass-by-reference?","Neither exactly — Python is **pass-by-object-reference** (a.k.a. \"pass by\nassignment\"). The function receives a reference to the same object, so it can\n**mutate** a mutable argument in place, but **rebinding** the parameter doesn't\naffect the caller.\n\n```python\ndef mutate(lst): lst.append(4)     # caller sees this\ndef rebind(lst): lst = [0]         # caller does NOT see this\n\ndata = [1, 2, 3]\nmutate(data); print(data)  # [1, 2, 3, 4]\nrebind(data); print(data)  # [1, 2, 3, 4] (unchanged)\n```\n\nImmutable args (ints, strings, tuples) can't be mutated, so they *appear*\npass-by-value. The key is mutate-in-place vs reassign-the-name.\n",{"id":91,"difficulty":47,"q":92,"a":93},"deepcopy-cycles","How does deepcopy handle circular references?","`copy.deepcopy` tracks already-copied objects in a **memo dictionary**, so it\nhandles **circular references** without infinite recursion — each object is copied\nonce and reused.\n\n```python\nimport copy\na = [1, 2]\na.append(a)            # a contains itself\nb = copy.deepcopy(a)   # works — no infinite loop\nb[2] is b              # True — the cycle is preserved in the copy\n```\n\nA naive recursive copy would loop forever; `deepcopy`'s memo makes it safe. You\ncan customize copying via `__deepcopy__`\u002F`__copy__` methods on your classes.\n",{"id":95,"difficulty":34,"q":96,"a":97},"is-pitfall","When is using `is` instead of `==` a bug?","Using `is` to compare **values** is a bug — it tests identity, which only\ncoincidentally matches for cached singletons (small ints, interned strings,\n`None`). It fails unpredictably for other values.\n\n```python\nx = 1000\nx is 1000        # may be False (and raises a SyntaxWarning in 3.8+)\nx == 1000        # True\n\na = \"long string value\"\na is \"long string value\"  # often False\n```\n\nRule: use `==` for value equality; reserve `is` for `None`, `True`, `False`, and\nsentinel objects. Linters flag `is` comparisons with literals.\n",{"id":99,"difficulty":47,"q":100,"a":101},"custom-hashable","How do you make a custom class hashable?","Implement both `__eq__` and `__hash__`, keeping them **consistent**: equal objects\nmust have equal hashes. Base the hash on the same immutable fields used for\nequality.\n\n```python\nclass Point:\n    def __init__(self, x, y):\n        self.x, self.y = x, y\n    def __eq__(self, other):\n        return (self.x, self.y) == (other.x, other.y)\n    def __hash__(self):\n        return hash((self.x, self.y))\n\n{Point(1, 2), Point(1, 2)}   # one element — treated as equal\n```\n\nDefining `__eq__` **without** `__hash__` makes the class **unhashable** (Python\nsets `__hash__ = None`). Only hash on fields that don't change after creation.\n",{"id":103,"difficulty":34,"q":104,"a":105},"frozen-dataclass","What is a frozen dataclass?","A `@dataclass(frozen=True)` makes instances **immutable** — attempting to set an\nattribute raises `FrozenInstanceError`. Frozen dataclasses also get a `__hash__`\nautomatically, so they're usable as dict keys \u002F set members.\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass Point:\n    x: int\n    y: int\n\np = Point(1, 2)\np.x = 9          # FrozenInstanceError\n{p: \"origin\"}    # hashable\n```\n\nIt's the concise modern way to define immutable value objects with auto-generated\n`__init__`, `__eq__`, `__repr__`, and `__hash__`.\n",{"id":107,"difficulty":42,"q":108,"a":109},"string-immutability","Why are strings immutable in Python?","Once created, a `str` can't be changed — any \"modification\" returns a **new**\nstring. Immutability enables interning, safe use as dict keys (cached hash), thread\nsafety, and predictable behavior.\n\n```python\ns = \"hello\"\ns[0] = \"H\"           # TypeError: 'str' does not support item assignment\ns = s.replace(\"h\", \"H\")  # new string, rebind\ns += \" world\"        # new string each time\n```\n\nRepeated concatenation in a loop creates many throwaway strings — prefer\n`\"\".join(parts)` for efficiency, the Python analog of `StringBuilder`.\n",{"id":111,"difficulty":34,"q":112,"a":113},"tuple-vs-list","What is the difference between a tuple and a list?","- **`list`** — mutable, variable-length, for **homogeneous, changing** sequences.\n- **`tuple`** — immutable, fixed, for **heterogeneous, fixed records** (and\n  hashable, so usable as dict keys).\n\n```python\npoint = (3, 4)        # fixed record — tuple\nscores = [90, 85]     # changing collection — list\nscores.append(70)     #\npoint[0] = 9          # tuples are immutable\n\nd = {(0, 0): \"origin\"}  # tuple key ; list key would fail\n```\n\nTuples are slightly faster and more memory-efficient, and signal \"this shouldn't\nchange.\" Use a list when you need to add\u002Fremove\u002Freorder.\n",{"id":115,"difficulty":47,"q":116,"a":117},"mutate-during-iteration","What happens if you modify a list while iterating over it?","Modifying a list's size during iteration **skips or repeats elements** because the\ninternal index shifts under you — a classic bug (Python doesn't always raise, it\nsilently misbehaves; dicts\u002Fsets *do* raise `RuntimeError`).\n\n```python\nnums = [1, 2, 3, 4]\nfor n in nums:\n    if n % 2 == 0:\n        nums.remove(n)   # skips elements\nprint(nums)              # [1, 3] sometimes wrong for other inputs\n\nnums = [n for n in nums if n % 2]      # build a new list\n```\n\nIterate over a **copy** (`for n in nums[:]`) or, better, build a new list with a\ncomprehension\u002F`filter`.\n",{"id":119,"difficulty":34,"q":120,"a":121},"del-statement","What does the del statement do?","`del` **unbinds a name** (or removes an item\u002Fslice\u002Fattribute) — it doesn't directly\n\"delete\" the object. The object is garbage-collected only when its **reference\ncount hits zero**.\n\n```python\na = [1, 2, 3]\nb = a\ndel a            # unbinds 'a'; the list still lives (b references it)\nprint(b)         # [1, 2, 3]\n\ndel b[0]         # removes an item -> [2, 3]\n```\n\nSo `del a` removes the *name*, not necessarily the value. For container items it\nmutates the container. After `del a`, referencing `a` raises `NameError`.\n",{"id":123,"difficulty":34,"q":124,"a":125},"rebind-vs-mutate","What is the difference between rebinding and mutating?","**Rebinding** points a name at a **new** object (`x = [...]`); it doesn't affect\nother names pointing at the old object. **Mutating** changes an object **in\nplace** (`x.append(...)`); all names referencing it see the change.\n\n```python\na = [1, 2]\nb = a\na.append(3)   # mutate -> b sees it; b == [1, 2, 3]\na = [9]       # rebind -> b unchanged; b == [1, 2, 3]\n```\n\nThis distinction explains most \"why did my other variable change?\" confusion.\nAliases share mutations but not rebinding.\n",{"id":127,"difficulty":47,"q":128,"a":129},"class-mutable-attr","Why is a mutable class attribute shared across instances?","A mutable value assigned at **class level** (not in `__init__`) is **one object\nshared by every instance**. Mutating it through one instance affects all of them —\na common bug.\n\n```python\nclass Cart:\n    items = []          # shared by ALL instances\n    def add(self, x): self.items.append(x)\n\na, b = Cart(), Cart()\na.add(\"apple\")\nb.items                 # ['apple'] — leaked into b!\n\nclass Cart:\n    def __init__(self):\n        self.items = []  # per-instance\n```\n\nInitialize mutable attributes in `__init__` so each instance gets its own.\n",{"id":131,"difficulty":47,"q":132,"a":133},"list-mult-trap","What is the trap with [[]] * 3?","List multiplication copies the **references**, not the objects — so `[[]] * 3`\ncreates three references to the **same** inner list. Mutating one mutates all.\n\n```python\ngrid = [[]] * 3\ngrid[0].append(1)\nprint(grid)          # [[1], [1], [1]] — all share one list!\n\ngrid = [[] for _ in range(3)]  # three distinct lists\ngrid[0].append(1)\nprint(grid)          # [[1], [], []]\n```\n\nThe same applies to `[0] * 3` (fine for immutable ints) vs `[[]] * 3` (broken for\nmutables). Use a comprehension to get independent inner objects.\n",{"id":135,"difficulty":34,"q":136,"a":137},"namedtuple","What is a namedtuple and when do you use it?","`collections.namedtuple` (or `typing.NamedTuple`) creates an **immutable**\ntuple subclass with **named fields** — readable, hashable, lightweight records.\n\n```python\nfrom collections import namedtuple\nPoint = namedtuple(\"Point\", [\"x\", \"y\"])\np = Point(3, 4)\np.x          # 3  — named access\np[0]         # 3  — still index-accessible\np.x = 9      # immutable\n```\n\nIt's great for returning multiple values with clear names while keeping tuple\nsemantics. For more features (defaults, methods, mutability options), a\n`@dataclass` is the modern alternative.\n",{"id":139,"difficulty":47,"q":140,"a":141},"global-nonlocal","What do the global and nonlocal keywords do?","They let you **rebind** a name from an outer scope. `global` targets module-level\nnames; `nonlocal` targets the nearest enclosing function scope. Without them,\nassignment inside a function creates a **new local** instead.\n\n```python\ncount = 0\ndef inc():\n    global count\n    count += 1        # rebinds the module-level count\n\ndef outer():\n    x = 1\n    def inner():\n        nonlocal x\n        x = 2          # rebinds outer's x\n    inner()\n    return x           # 2\n```\n\nNote you only need them to **reassign** — you can *mutate* an outer mutable object\n(e.g. `list.append`) without `global`\u002F`nonlocal`.\n",{"id":143,"difficulty":42,"q":144,"a":145},"copy-methods","What are the ways to copy a list or dict?","Several produce a **shallow** copy; `copy.deepcopy` is the only deep one.\n\n```python\na = [1, 2, 3]\na[:]            # slice copy\na.copy()        # method\nlist(a)         # constructor\n\nd = {\"x\": 1}\nd.copy()        # method\ndict(d)         # constructor\n{**d}           # unpacking\n\nimport copy\ncopy.deepcopy(a)  # deep — independent nested objects\n```\n\nAll the shallow methods share nested mutable elements; choose `deepcopy` when you\nneed full independence (at a performance cost).\n",{"id":147,"difficulty":42,"q":148,"a":149},"none-identity","Why check for None with `is` rather than ==?","`None` is a **singleton** — there's exactly one `None` object — so `is None` is the\ncorrect, fast, and idiomatic identity check. `== None` can be **overridden** by a\ncustom `__eq__`, giving wrong or surprising results.\n\n```python\nif x is None:        # idiomatic, can't be fooled\n    ...\n\nclass Weird:\n    def __eq__(self, other): return True\nWeird() == None      # True  misleading\nWeird() is None      # False\n```\n\nPEP 8 explicitly recommends `is`\u002F`is not` for `None` comparisons.\n",{"id":151,"difficulty":34,"q":152,"a":153},"set-hashable-elements","Why must set elements be hashable?","Like dict keys, set members are stored by **hash** for O(1) membership tests, so\nthey must be **hashable** (and thus effectively immutable). Lists, dicts, and sets\ncan't be set elements; tuples and frozensets can.\n\n```python\n{1, 2, 3}            #\n{[1], [2]}           # TypeError: unhashable type: 'list'\n{(1, 2), (3, 4)}     # tuples are hashable\n{frozenset({1, 2})}  #\n```\n\nIf you need a set of sets, use `frozenset` for the inner ones. The hashability\nrequirement is the same reason lists can't be dict keys.\n",{"id":155,"difficulty":34,"q":156,"a":157},"comprehension-scope","Do comprehensions leak their loop variable?","In **Python 3**, comprehensions have their **own scope**, so the loop variable does\n**not** leak into the surrounding scope (unlike a regular `for` loop, and unlike\nPython 2).\n\n```python\nsquares = [i * i for i in range(5)]\nprint(i)        # NameError — i is local to the comprehension\n\nfor j in range(5):\n    pass\nprint(j)        # 4 — a normal for loop DOES leak\n```\n\nThis avoids accidental variable clobbering. The same isolation applies to set,\ndict, and generator comprehensions.\n",{"id":159,"difficulty":42,"q":160,"a":161},"tuple-unpacking-swap","How does tuple unpacking enable swapping variables?","Python evaluates the **right side first** into a tuple, then unpacks it into the\nleft-side names — so you can swap without a temporary variable.\n\n```python\na, b = 1, 2\na, b = b, a        # builds (2, 1), then unpacks -> a=2, b=1\n\n# also works for multiple\u002Fextended unpacking:\nfirst, *rest = [1, 2, 3, 4]   # first=1, rest=[2, 3, 4]\n```\n\nThe right-hand tuple is fully created before any assignment, which is why the swap\nis atomic and needs no temp. This is immutability of the intermediate tuple at\nwork.\n",{"id":163,"difficulty":34,"q":164,"a":165},"shallow-nested-trap","What is the trap with shallow-copying nested structures?","A shallow copy duplicates the outer container but **shares the nested objects**, so\nmutating a nested element changes both the original and the copy — a subtle\naliasing bug.\n\n```python\nimport copy\noriginal = {\"users\": [\"ada\"]}\nshallow = copy.copy(original)\nshallow[\"users\"].append(\"grace\")\noriginal[\"users\"]    # ['ada', 'grace'] — shared nested list!\n\ndeep = copy.deepcopy(original)\ndeep[\"users\"].append(\"hopper\")\noriginal[\"users\"]    # unchanged\n```\n\nUse `deepcopy` whenever you copy a structure with nested mutables you intend to\nmodify independently.\n",{"id":167,"difficulty":34,"q":168,"a":169},"frozenset","What is a frozenset?","A `frozenset` is an **immutable** version of `set` — same operations\n(union, intersection, membership) but no `add`\u002F`remove`. Because it's immutable,\nit's **hashable**, so it can be a dict key or an element of another set.\n\n```python\nfs = frozenset([1, 2, 3])\nfs.add(4)              # AttributeError — immutable\n{fs: \"a set\"}          # hashable key\n{frozenset({1}), frozenset({2})}  # set of sets\n```\n\nUse it for constant sets and whenever you need a set-like value that must be\nhashable.\n",{"id":171,"difficulty":47,"q":172,"a":173},"eq-hash-contract","What is the relationship between __eq__ and __hash__?","They must stay **consistent**: if `a == b`, then `hash(a) == hash(b)`. Otherwise\nhash-based containers (dict, set) misbehave — an object you stored becomes\nunfindable.\n\n```python\nclass Money:\n    def __init__(self, cents): self.cents = cents\n    def __eq__(self, o): return self.cents == o.cents\n    # defined __eq__ but not __hash__ -> unhashable\n{Money(100)}   # TypeError: unhashable type: 'Money'\n```\n\nDefining `__eq__` sets `__hash__` to `None` automatically (making instances\nunhashable) unless you also define `__hash__`. Hash only on fields that never\nchange after creation, or make the object immutable.\n",null,{"description":31},"Python interview questions on mutable vs immutable types, the mutable default argument trap, is vs ==, and shallow vs deep copy.","python\u002Ffundamentals\u002Fmutability","Mutability & Data Types","2026-06-17","lfA_zY1n_f1sWG-yTVHSPtktZt0M8Eoy085LnkCD_To",{"id":182,"title":183,"body":184,"description":31,"difficulty":34,"extension":35,"framework":10,"frameworkSlug":8,"meta":188,"navigation":37,"order":32,"path":189,"questions":190,"related":174,"seo":211,"seoDescription":212,"stem":213,"subtopic":214,"topic":19,"topicSlug":20,"updated":215,"__hash__":216},"qa\u002Fpython\u002Ffundamentals\u002Fscope-legb.md","Scope Legb",{"type":28,"value":185,"toc":186},[],{"title":31,"searchDepth":32,"depth":32,"links":187},[],{},"\u002Fpython\u002Ffundamentals\u002Fscope-legb",[191,195,199,203,207],{"id":192,"difficulty":34,"q":193,"a":194},"legb-rule","What is the LEGB rule?","**LEGB** describes the order Python searches for a name: **Local** (inside the\ncurrent function), **Enclosing** (any outer functions), **Global** (the module's\ntop level), then **Built-in** (names like `len`, `print`). The first match wins,\nand the search stops there.\n\n```python\nx = \"global\"\ndef outer():\n    x = \"enclosing\"\n    def inner():\n        x = \"local\"\n        print(x)     # \"local\"  — Local found first\n    inner()\nouter()\n```\n\nWhy it matters: nearly every \"why is this variable that value?\" question reduces\nto walking **L -> E -> G -> B** until a name is found.\n",{"id":196,"difficulty":34,"q":197,"a":198},"global-vs-nonlocal","What is the difference between `global` and `nonlocal`?","Both let you **rebind** a name from an outer scope instead of creating a new local.\n`global` targets the **module-level** name; `nonlocal` targets the **nearest\nenclosing function** scope (and that name must already exist there).\n\n```python\ncount = 0\ndef inc():\n    global count\n    count += 1        # rebinds module-level count\n\ndef outer():\n    x = 1\n    def inner():\n        nonlocal x\n        x = 2          # rebinds outer's x, not a new local\n    inner()\n    return x           # 2\n```\n\nRule of thumb: you only need these keywords to **reassign** an outer name — you can\nalways *mutate* an outer mutable object (e.g. `list.append`) without them.\n",{"id":200,"difficulty":47,"q":201,"a":202},"unbound-local-error","Why does assigning to a name make it local and cause UnboundLocalError?","Python decides a name's scope **at compile time** by scanning the whole function\nbody. If a name is **assigned anywhere** in a function, it is treated as **local\nfor the entire function** — even on lines before the assignment. Reading it before\nit's bound raises **UnboundLocalError**.\n\n```python\nx = 10\ndef f():\n    print(x)      # UnboundLocalError: x is local because of the line below\n    x = 20        # this assignment makes x local everywhere in f\n```\n\nThe fix is to declare `global x` (or `nonlocal x`) if you meant the outer name, or\nsimply read a different name. Rule of thumb: **an assignment anywhere makes the\nname local everywhere** in that function.\n",{"id":204,"difficulty":47,"q":205,"a":206},"late-binding-closures","Why do closures in a loop all capture the same value?","Closures capture **variables, not values** — this is **late binding**. The inner\nfunction looks up the loop variable **when it is called**, by which time the loop\nhas finished and the variable holds its final value.\n\n```python\nfuncs = [lambda: i for i in range(3)]\n[f() for f in funcs]      # [2, 2, 2]  — all see the final i\n\n# Fix: bind the current value via a default argument\nfuncs = [lambda i=i: i for i in range(3)]\n[f() for f in funcs]      # [0, 1, 2]\n```\n\nThe default-argument trick captures `i`'s value at definition time. Rule of thumb:\nif loop-created closures behave strangely, you're hitting late binding — bind the\nvalue explicitly.\n",{"id":208,"difficulty":34,"q":209,"a":210},"module-vs-function-shadowing","How does name shadowing work between module and function scope?","A local name **shadows** (hides) an outer name of the same identity for the\nduration of the scope. Assigning to it inside a function creates a separate local\nthat leaves the **module-level** name untouched.\n\n```python\nvalue = \"module\"\ndef f():\n    value = \"function\"   # new local — shadows the global\n    print(value)         # \"function\"\nf()\nprint(value)             # \"module\"  — unchanged\n\nlist = [1, 2]            # shadows the built-in list() in this scope!\n```\n\nWatch out for shadowing **built-ins** (`list`, `id`, `sum`, `type`) — it silently\nbreaks later calls. Rule of thumb: keep names distinct from outer scopes and\nbuilt-ins to avoid surprising lookups.\n",{"description":31},"Python interview questions on the LEGB scope rule, global vs nonlocal, UnboundLocalError, late binding in loop closures, and name shadowing.","python\u002Ffundamentals\u002Fscope-legb","Variables, Scope & the LEGB Rule","2026-06-18","Dpt_Q0OFjMvd7AoVKH6T5OWFzpQa5ln0EU8zsuvK55I",{"id":218,"title":219,"body":220,"description":31,"difficulty":42,"extension":35,"framework":10,"frameworkSlug":8,"meta":224,"navigation":37,"order":11,"path":225,"questions":226,"related":174,"seo":251,"seoDescription":252,"stem":253,"subtopic":254,"topic":19,"topicSlug":20,"updated":215,"__hash__":255},"qa\u002Fpython\u002Ffundamentals\u002Fnumbers-operators.md","Numbers Operators",{"type":28,"value":221,"toc":222},[],{"title":31,"searchDepth":32,"depth":32,"links":223},[],{},"\u002Fpython\u002Ffundamentals\u002Fnumbers-operators",[227,231,235,239,243,247],{"id":228,"difficulty":42,"q":229,"a":230},"numeric-types","What are Python's built-in numeric types?","Three: **`int`** (whole numbers, unlimited size), **`float`** (double-precision\nbinary floating point), and **`complex`** (a real + imaginary part written with\n`j`). `bool` is technically a subclass of `int` (`True == 1`).\n\n```python\na = 42          # int\nb = 3.14        # float\nc = 2 + 3j      # complex\nc.real, c.imag  # (2.0, 3.0)\nTrue + True     # 2  — bool is an int subclass\n```\n\nWhy it matters: mixing types **promotes** to the wider one (`int + float -> float`),\nand knowing the three types explains conversion and precision behavior.\n",{"id":232,"difficulty":34,"q":233,"a":234},"division-floor-modulo","How do `\u002F`, `\u002F\u002F`, and `%` behave, especially with negatives?","`\u002F` is **true division** and always returns a `float`. `\u002F\u002F` is **floor division**\n— it rounds **toward negative infinity**, not toward zero. `%` is the matching\nmodulo, and in Python its **result takes the sign of the divisor**.\n\n```python\n7 \u002F 2       # 3.5    — always float\n7 \u002F\u002F 2      # 3\n-7 \u002F\u002F 2     # -4     — floors toward -infinity, not -3\n-7 % 2      # 1      — sign follows the divisor (2)\n7 % -2      # -1     — sign follows the divisor (-2)\n```\n\nThe identity always holds: `(a \u002F\u002F b) * b + (a % b) == a`. Rule of thumb: Python's\nfloor\u002Fmodulo differ from C\u002FJava for negatives — expect non-negative `%` when the\ndivisor is positive.\n",{"id":236,"difficulty":42,"q":237,"a":238},"int-arbitrary-precision","Why don't Python integers overflow?","Python `int` has **arbitrary precision** — it grows to hold any value, limited only\nby available memory. There is no fixed 32\u002F64-bit width, so computations never\nsilently wrap around like in C or Java.\n\n```python\n2 ** 100        # 1267650600228229401496703205376\nx = 10 ** 1000  # a 1001-digit integer — no overflow\nimport sys\nsys.maxsize     # largest \"native\" int, but ints can exceed it freely\n```\n\nWhy it matters: you can compute huge factorials or cryptographic numbers directly,\nbut very large ints cost more memory and arithmetic gets slower. Rule of thumb:\ninteger overflow is simply not a concern in Python.\n",{"id":240,"difficulty":34,"q":241,"a":242},"float-precision-decimal","Why is 0.1 + 0.2 not exactly 0.3?","Floats are stored in **binary (IEEE 754)**, and values like 0.1 and 0.2 have no\nexact binary representation — so tiny rounding errors accumulate. This is inherent\nto binary floating point, not a Python bug.\n\n```python\n0.1 + 0.2            # 0.30000000000000004\n0.1 + 0.2 == 0.3     # False\nround(0.1 + 0.2, 2)  # 0.3   — round for display\nimport math\nmath.isclose(0.1 + 0.2, 0.3)   # True — tolerant comparison\n\nfrom decimal import Decimal\nDecimal(\"0.1\") + Decimal(\"0.2\")   # Decimal('0.3')  — exact\n```\n\nRule of thumb: never compare floats with `==`; use `math.isclose` or round, and\nreach for **`Decimal`** when you need exact decimal arithmetic (e.g. money).\n",{"id":244,"difficulty":34,"q":245,"a":246},"bitwise-operators","What are Python's bitwise operators?","Bitwise operators work on the binary representation of integers: **`&`** (and),\n**`|`** (or), **`^`** (xor), **`~`** (not\u002Finvert), **`\u003C\u003C`** (left shift), and\n**`>>`** (right shift). Shifting left by `n` multiplies by `2**n`.\n\n```python\n5 & 3    # 1   (0b101 & 0b011)\n5 | 3    # 7   (0b111)\n5 ^ 3    # 6   (0b110)\n~5       # -6  (~x == -(x+1))\n1 \u003C\u003C 4   # 16  (1 * 2**4)\n20 >> 2  # 5   (20 \u002F\u002F 4)\n```\n\nWhy it matters: bitwise ops power flags\u002Fbitmasks, fast power-of-two math, and\nlow-level protocols. Rule of thumb: `~x` equals `-(x + 1)` because of two's\ncomplement.\n",{"id":248,"difficulty":42,"q":249,"a":250},"divmod-and-power","What do `divmod` and `**` do?","**`divmod(a, b)`** returns the quotient and remainder as a single tuple\n`(a \u002F\u002F b, a % b)` in one call. **`**`** is exponentiation; with a third argument,\nthe built-in `pow(base, exp, mod)` does efficient modular exponentiation.\n\n```python\ndivmod(17, 5)     # (3, 2)  — quotient and remainder together\n2 ** 10           # 1024\npow(2, 10)        # 1024  — same as **\npow(2, 10, 1000)  # 24    — (2**10) % 1000, computed efficiently\n```\n\nRule of thumb: use `divmod` when you need both results (e.g. converting seconds to\nminutes\u002Fseconds), and `pow(a, b, m)` for modular math instead of `(a ** b) % m`.\n",{"description":31},"Python interview questions on int\u002Ffloat\u002Fcomplex, floor division and modulo with negatives, arbitrary precision, float precision, bitwise operators, and divmod.","python\u002Ffundamentals\u002Fnumbers-operators","Numbers & Operators","y_gRkCXYAeOaIuz3yr39UOW1-0X0sEQJiimG2onF8sA",{"id":257,"title":258,"body":259,"description":31,"difficulty":34,"extension":35,"framework":10,"frameworkSlug":8,"meta":263,"navigation":37,"order":264,"path":265,"questions":266,"related":174,"seo":291,"seoDescription":292,"stem":293,"subtopic":294,"topic":19,"topicSlug":20,"updated":215,"__hash__":295},"qa\u002Fpython\u002Ffundamentals\u002Fstrings-formatting.md","Strings Formatting",{"type":28,"value":260,"toc":261},[],{"title":31,"searchDepth":32,"depth":32,"links":262},[],{},4,"\u002Fpython\u002Ffundamentals\u002Fstrings-formatting",[267,271,275,279,283,287],{"id":268,"difficulty":34,"q":269,"a":270},"fstring-vs-format-vs-percent","What is the difference between f-strings, .format(), and % formatting?","All three interpolate values into strings. **`%`** is the oldest C-style syntax.\n**`str.format()`** uses `{}` placeholders and is more flexible. **f-strings**\n(Python 3.6+) embed expressions **inline** and are the fastest and most readable.\n\n```python\nname, n = \"Ada\", 3\n\"Hi %s, %d items\" % (name, n)        # old % style\n\"Hi {}, {} items\".format(name, n)    # str.format\nf\"Hi {name}, {n} items\"              # f-string — preferred\nf\"{n * 2 = }\"                        # \"n * 2 = 6\"  — self-documenting\n```\n\nRule of thumb: prefer **f-strings** for new code — they evaluate expressions\ndirectly and avoid the argument-ordering errors of `%` and `.format()`.\n",{"id":272,"difficulty":34,"q":273,"a":274},"str-vs-bytes","What is the difference between str and bytes?","A **`str`** is a sequence of **Unicode code points** (text); **`bytes`** is a\nsequence of **raw bytes** (0-255). You convert between them with **`encode`**\n(str -> bytes) and **`decode`** (bytes -> str), specifying an encoding like UTF-8.\n\n```python\ns = \"café\"\nb = s.encode(\"utf-8\")    # b'caf\\xc3\\xa9'  — 5 bytes (é is 2)\nb.decode(\"utf-8\")        # \"café\"  — back to text\nlen(s), len(b)           # (4, 5)\ns + b                    # TypeError — can't mix str and bytes\n```\n\nWhy it matters: files and network sockets deal in **bytes**, your program logic in\n**str**. Rule of thumb: decode bytes to str as early as possible and encode back to\nbytes only at the I\u002FO boundary.\n",{"id":276,"difficulty":42,"q":277,"a":278},"common-string-methods","What are the common string methods like split, strip, and join?","**`split`** breaks a string into a list on a separator; **`strip`** removes\nleading\u002Ftrailing whitespace (or given characters); **`join`** glues an iterable of\nstrings together with a separator. All return **new** strings since `str` is\nimmutable.\n\n```python\n\"  a,b,c  \".strip()            # \"a,b,c\"\n\"a,b,c\".split(\",\")            # [\"a\", \"b\", \"c\"]\n\",\".join([\"a\", \"b\", \"c\"])     # \"a,b,c\"\n\"Hello\".lower(), \"Hi\".upper() # (\"hello\", \"HI\")\n\"hello\".replace(\"l\", \"L\")     # \"heLLo\"\n```\n\nRule of thumb: `sep.join(list)` is the inverse of `text.split(sep)`, and chaining\nthese covers most everyday text wrangling.\n",{"id":280,"difficulty":34,"q":281,"a":282},"join-vs-concat-loop","Why use join instead of += to build a string in a loop?","Strings are **immutable**, so each `+=` creates a **brand-new string** and copies\neverything so far — turning a loop into O(n^2) work and lots of garbage. **`join`**\nallocates the result **once**, making it O(n).\n\n```python\n# Slow — new string every iteration\nresult = \"\"\nfor word in words:\n    result += word\n\n# Fast — single allocation\nresult = \"\".join(words)\n```\n\nRule of thumb: collect pieces in a list (or generator) and call `\"\".join(...)` —\nit's the Python equivalent of a `StringBuilder`.\n",{"id":284,"difficulty":42,"q":285,"a":286},"raw-strings","What is a raw string and when do you use it?","A **raw string** (`r\"...\"`) tells Python **not to process backslash escapes**, so\n`\\n`, `\\t`, etc. stay as literal backslash-plus-character. They're ideal for\n**regular expressions** and **Windows file paths**.\n\n```python\nprint(\"a\\tb\")    # a    b   — \\t is a tab\nprint(r\"a\\tb\")   # a\\tb     — backslash kept literally\n\nimport re\nre.findall(r\"\\d+\", \"x12y3\")   # ['12', '3']  — no double-backslashing\npath = r\"C:\\Users\\name\"       # backslashes stay intact\n```\n\nRule of thumb: reach for `r\"...\"` whenever your string is full of backslashes —\nit avoids the noise and bugs of escaping every one.\n",{"id":288,"difficulty":34,"q":289,"a":290},"format-spec-mini-language","How does the format spec mini-language work (padding, :.2f)?","Inside `{}` (or after `:` in `format`), a **format spec** controls alignment,\nwidth, and precision: `{value:[fill][align][width][,][.precision][type]}`. It\nworks in f-strings and `str.format` alike.\n\n```python\nf\"{42:5}\"        # \"   42\"    — width 5, right-aligned (default for numbers)\nf\"{'hi':\u003C5}|\"    # \"hi   |\"   — left-align in width 5\nf\"{'hi':^5}|\"    # \" hi  |\"   — center\nf\"{42:05}\"       # \"00042\"    — zero-padded\nf\"{3.14159:.2f}\" # \"3.14\"     — 2 decimal places\nf\"{1234567:,}\"   # \"1,234,567\"  — thousands separator\nf\"{0.25:.1%}\"    # \"25.0%\"    — percentage\n```\n\nRule of thumb: `.2f` controls decimals, a number sets width, and `\u003C`\u002F`>`\u002F`^` set\nalignment — combine them for clean tabular output.\n",{"description":31},"Python interview questions on f-strings vs .format vs %, str vs bytes, common string methods, join vs +=, raw strings, and the format spec mini-language.","python\u002Ffundamentals\u002Fstrings-formatting","Strings & String Formatting","Fly7brM0I0JbkO0F5-ZVq4BC9z9GcKpnaukrHV5K3lA",{"id":297,"title":298,"body":299,"description":31,"difficulty":42,"extension":35,"framework":10,"frameworkSlug":8,"meta":303,"navigation":37,"order":304,"path":305,"questions":306,"related":174,"seo":331,"seoDescription":332,"stem":333,"subtopic":334,"topic":19,"topicSlug":20,"updated":215,"__hash__":335},"qa\u002Fpython\u002Ffundamentals\u002Ftruthiness-conversion.md","Truthiness Conversion",{"type":28,"value":300,"toc":301},[],{"title":31,"searchDepth":32,"depth":32,"links":302},[],{},5,"\u002Fpython\u002Ffundamentals\u002Ftruthiness-conversion",[307,311,315,319,323,327],{"id":308,"difficulty":42,"q":309,"a":310},"falsy-values","Which values are falsy in Python?","A handful of values are **falsy** (treated as `False` in a boolean context):\n**`None`**, **`False`**, **zero** of any numeric type (`0`, `0.0`, `0j`), and\n**empty containers\u002Fsequences** (`\"\"`, `[]`, `{}`, `()`, `set()`, `range(0)`).\nAlmost everything else is **truthy**.\n\n```python\nbool(0), bool(\"\"), bool([]), bool(None)   # all False\nbool(1), bool(\"x\"), bool([0]), bool(\" \")  # all True\nif not items:        # idiomatic empty check\n    print(\"empty\")\n```\n\nWhy it matters: idiomatic Python uses `if items:` rather than `if len(items) > 0:`.\nRule of thumb: \"empty or zero or None\" is falsy; everything else is truthy.\n",{"id":312,"difficulty":34,"q":313,"a":314},"bool-len-protocol","How does Python decide whether a custom object is truthy?","Python calls **`__bool__`** first; if it's not defined, it falls back to\n**`__len__`** (zero length is falsy). If neither exists, the object is **always\ntruthy** — the default for plain instances.\n\n```python\nclass Box:\n    def __init__(self, items): self.items = items\n    def __len__(self): return len(self.items)   # used for truthiness\n\nbool(Box([]))     # False — __len__ is 0\nbool(Box([1]))    # True\n\nclass Always:\n    def __bool__(self): return False   # __bool__ wins over __len__\nbool(Always())    # False\n```\n\nRule of thumb: define `__bool__` (or `__len__`) so `if obj:` makes sense for your\ntype; otherwise every instance is truthy.\n",{"id":316,"difficulty":42,"q":317,"a":318},"explicit-conversion","How do explicit type conversions like int(), str(), and list() work?","Python provides **constructor functions** that convert between types: `int()`,\n`float()`, `str()`, `bool()`, `list()`, `tuple()`, `set()`, `dict()`. They build a\n**new** object and raise `ValueError` if the input can't be converted.\n\n```python\nint(\"42\")        # 42\nint(\"3.9\")       # ValueError — not a valid int literal\nint(3.9)         # 3   — truncates toward zero\nfloat(\"3.14\")    # 3.14\nstr(255)         # \"255\"\nlist(\"abc\")      # ['a', 'b', 'c']\nlist({1: \"a\"})   # [1]  — iterates keys\n```\n\nRule of thumb: these are **explicit** conversions you call yourself — Python rarely\nconverts types implicitly, so reach for the constructor you need.\n",{"id":320,"difficulty":42,"q":321,"a":322},"empty-container-truthiness","Are empty containers and None falsy?","Yes. Every **empty** built-in container is falsy — `[]`, `{}`, `()`, `set()`,\n`\"\"` — and so is **`None`**. A non-empty container is truthy regardless of what it\ncontains, even `[0]` or `[False]`.\n\n```python\nbool([]), bool({}), bool(set()), bool(\"\")   # all False\nbool([0]), bool([False]), bool({None})       # all True — non-empty!\nbool(None)                                   # False\n```\n\nWatch the trap: `[0]` is **truthy** because it has one element, even though that\nelement is falsy. Rule of thumb: container truthiness depends on **length**, not on\nthe elements' values.\n",{"id":324,"difficulty":34,"q":325,"a":326},"and-or-short-circuit","What do `and` and `or` actually return?","`and`\u002F`or` **short-circuit** and return one of their **operands**, not a strict\n`True`\u002F`False`. `and` returns the **first falsy** operand (or the last if all are\ntruthy); `or` returns the **first truthy** operand (or the last if all are falsy).\n\n```python\n0 and 5         # 0   — first falsy, second never evaluated\n2 and 5         # 5   — both truthy, returns last\n0 or \"default\"  # \"default\"  — first truthy\nNone or 0 or [] # []  — all falsy, returns the last\nname = user_input or \"guest\"   # common default idiom\n```\n\nRule of thumb: `x or default` supplies a fallback, and short-circuiting means the\nright side is skipped when the result is already decided.\n",{"id":328,"difficulty":42,"q":329,"a":330},"is-none-vs-eq-none","Why use `is None` instead of `== None`?","`None` is a **singleton** — there is exactly one `None` object — so `is None`\nchecks **identity**, which is fast and can't be fooled. `== None` calls `__eq__`,\nwhich a class can **override** to return a misleading result.\n\n```python\nif x is None:        # idiomatic, reliable\n    ...\n\nclass Weird:\n    def __eq__(self, other): return True\nWeird() == None      # True   — misleading!\nWeird() is None      # False  — correct\n```\n\nRule of thumb: always compare against `None`, `True`, and `False` with **`is`\u002F\n`is not`** — PEP 8 explicitly recommends it.\n",{"description":31},"Python interview questions on falsy values, __bool__\u002F__len__, explicit type conversion, truthiness of empty containers, short-circuiting and\u002For, and is None vs == None.","python\u002Ffundamentals\u002Ftruthiness-conversion","Truthiness & Type Conversion","H0Pq-9nSUmc-J53irQW_l_BUHoaEHBvAnRvB87cpL4I",1781808674651]