[{"data":1,"prerenderedAt":152},["ShallowReactive",2],{"topic-python-internals":3},{"framework":4,"topic":15,"subtopics":24},{"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":20,"slug":21,"stem":22,"__hash__":23},"topics\u002Ftopics\u002Fpython-internals.yml","Reference counting, garbage collection, object identity and the CPython execution model — how Python manages memory and runs your code.",{},"Memory & Internals",10,"internals","topics\u002Fpython-internals","U2yHsQaj-JeXNyACZ2urxbwgXTTPHaZg1F16EMvjKow",[25,73,113],{"id":26,"title":27,"body":28,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":8,"meta":37,"navigation":38,"order":13,"path":39,"questions":40,"related":66,"seo":67,"seoDescription":68,"stem":69,"subtopic":70,"topic":19,"topicSlug":21,"updated":71,"__hash__":72},"qa\u002Fpython\u002Finternals\u002Fgarbage-collection.md","Garbage Collection",{"type":29,"value":30,"toc":31},"minimark",[],{"title":32,"searchDepth":33,"depth":33,"links":34},"",2,[],"hard","md",{},true,"\u002Fpython\u002Finternals\u002Fgarbage-collection",[41,46,50,54,58,62],{"id":42,"difficulty":43,"q":44,"a":45},"reference-counting","medium","How does reference counting work in Python?","Every CPython object carries a **reference count** — the number of references\npointing at it. The count goes **up** when you bind a new name, append it to a\ncontainer, or pass it to a function, and **down** when a name is reassigned, goes\nout of scope, or is `del`'d. When the count hits **zero**, the object is\n**immediately deallocated**.\n\n```python\na = [1, 2, 3]    # the list's refcount is 1\nb = a            # 2 — another reference\ndel a            # 1 — still alive (b references it)\nb = None         # 0 — list is freed right away\n```\n\nThis is **deterministic** and prompt — memory is reclaimed the instant the last\nreference disappears, not at some later sweep.\n\nWhy it matters: reference counting handles the vast majority of garbage, but it\n**can't free reference cycles** on its own, which is why CPython adds a second\ncollector.\n",{"id":47,"difficulty":35,"q":48,"a":49},"cyclic-gc","Why does Python need a cyclic garbage collector?","Reference counting fails on **reference cycles** — objects that refer to each other,\nso their counts never reach zero even when nothing outside the cycle reaches them.\nWithout help they'd leak forever. CPython's **cyclic garbage collector** (the `gc`\nmodule) periodically finds and frees these unreachable cycles.\n\n```python\nimport gc\na, b = {}, {}\na['b'] = b        # a -> b\nb['a'] = a        # b -> a   (a cycle)\ndel a, b          # both refcounts stay at 1 — NOT freed by refcounting\ngc.collect()      # the cyclic collector reclaims the unreachable cycle\n```\n\nThe collector works by tracking **container** objects (lists, dicts, instances) and\ndetecting groups whose only references are internal to the group.\n\nRule of thumb: you rarely call `gc.collect()` manually — it runs automatically —\nbut it's worth knowing cycles are collected **later**, not immediately like refcounts.\n",{"id":51,"difficulty":43,"q":52,"a":53},"sys-getrefcount","What does sys.getrefcount tell you and why is it always higher than expected?","`sys.getrefcount(obj)` returns the object's current **reference count**. It almost\nalways reads **one higher** than you expect because **passing the object as the\nargument** creates a temporary reference for the duration of the call.\n\n```python\nimport sys\nx = []\nsys.getrefcount(x)   # 2 — one for `x`, one for the argument itself\n\ny = x\nsys.getrefcount(x)   # 3 — `x`, `y`, and the argument\n```\n\nSmall ints and interned strings show huge counts because they're **cached\nsingletons** shared across the whole interpreter.\n\nWhy it matters: it's a debugging\u002Fteaching tool for understanding aliasing and leaks\n— just remember to **subtract one** for the call's own reference, and don't rely on\nexact values across implementations.\n",{"id":55,"difficulty":35,"q":56,"a":57},"weak-references","What is a weak reference and when do you use one?","A **weak reference** (`weakref` module) points to an object **without increasing its\nreference count**, so it does **not** keep the object alive. When the last *strong*\nreference is gone, the object is collected and the weak reference returns `None`.\n\n```python\nimport weakref\nclass Node: pass\n\nn = Node()\nr = weakref.ref(n)   # weak — doesn't count toward n's lifetime\nr()                  # \u003CNode object> — still alive\ndel n                # last strong ref gone -> object freed\nr()                  # None — referent is dead\n```\n\nThey're ideal for **caches** and **observer\u002Fparent-child back-references** where you\nwant to reference an object but not prevent its cleanup — `weakref.WeakValueDictionary`\nis a common cache type.\n\nRule of thumb: use a weak reference to **break a cycle** or avoid a memory leak when\none object should not own the lifetime of another.\n",{"id":59,"difficulty":35,"q":60,"a":61},"del-pitfalls","What are the pitfalls of the __del__ method?","`__del__` is a **finalizer** called when an object is about to be destroyed — but\nits timing is **not guaranteed**. It runs only when the refcount hits zero (or later,\nvia the cyclic collector), so you can't rely on **when**, or even **whether**, it\nruns. Note `del obj` only decrements the refcount; it doesn't directly call `__del__`.\n\n```python\nclass Resource:\n    def __del__(self):\n        print(\"cleaning up\")   # may run late, or not at all on interpreter exit\n\nr = Resource()\nr2 = r\ndel r                          # nothing happens — r2 still references it\ndel r2                         # NOW refcount is 0 -> __del__ runs\n```\n\nObjects in a reference cycle that define `__del__` historically couldn't be\ncollected at all (improved in Python 3.4+ via PEP 442), and exceptions raised inside\n`__del__` are **ignored**.\n\nRule of thumb: don't use `__del__` for important cleanup — use **context managers\n(`with`)** or `try\u002Ffinally`, which give deterministic, explicit release.\n",{"id":63,"difficulty":35,"q":64,"a":65},"generational-gc","What is generational garbage collection?","The cyclic collector is **generational**: it sorts tracked objects into **three\ngenerations (0, 1, 2)** based on how many collections they've survived. New objects\nstart in **generation 0**, which is scanned **most frequently**; survivors are\n**promoted** to older generations that are scanned **less often**.\n\nThe idea is the **weak generational hypothesis**: most objects die young, so it's\nefficient to focus collection effort on the youngest generation and rarely re-scan\nlong-lived objects.\n\n```python\nimport gc\ngc.get_count()        # (gen0, gen1, gen2) allocation counters\ngc.get_threshold()    # (700, 10, 10) — triggers per generation\ngc.collect(0)         # collect only generation 0 (cheap, frequent)\n```\n\nA generation-0 collection is fast and common; full (generation-2) collections are\nrarer and more expensive.\n\nRule of thumb: this is mostly automatic, but for latency-sensitive code you can tune\nthresholds, call `gc.collect()` strategically, or `gc.disable()` it if you manage\nlifetimes carefully.\n",null,{"description":32},"Python interview questions on memory management — reference counting, the cyclic garbage collector and reference cycles, sys.getrefcount, weak references, __del__ pitfalls, and generational GC.","python\u002Finternals\u002Fgarbage-collection","Garbage Collection & Reference Counting","2026-06-18","FHx3siFM4NKe-jUoUL6tXG3myt5m8Tb5BnZ4L36YsOU",{"id":74,"title":75,"body":76,"description":32,"difficulty":43,"extension":36,"framework":10,"frameworkSlug":8,"meta":80,"navigation":38,"order":33,"path":81,"questions":82,"related":66,"seo":108,"seoDescription":109,"stem":110,"subtopic":111,"topic":19,"topicSlug":21,"updated":71,"__hash__":112},"qa\u002Fpython\u002Finternals\u002Fidentity-interning.md","Identity Interning",{"type":29,"value":77,"toc":78},[],{"title":32,"searchDepth":33,"depth":33,"links":79},[],{},"\u002Fpython\u002Finternals\u002Fidentity-interning",[83,87,91,95,99,103],{"id":84,"difficulty":43,"q":85,"a":86},"is-vs-equals","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\n*exact same object* in memory?\" — which is effectively an `id()` comparison. They\nfrequently agree, but conceptually they ask completely different questions.\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, just another name\n```\n\nUse `==` whenever you care about the **value**, which is almost always. Reserve\n`is` for **identity** checks against singletons. Rule of thumb: if you're\ncomparing data, use `==`; if you're checking \"is this literally that object,\" use\n`is`.\n",{"id":88,"difficulty":43,"q":89,"a":90},"id-function","What does the id() function tell you?","**`id(obj)`** returns a unique integer **identity** for an object that's constant\nfor the object's **lifetime**. In **CPython** it's the object's **memory\naddress**, though that's an implementation detail. Two names with the same `id`\nrefer to the **same object**, and `is` is essentially an `id` comparison.\n\n```python\na = [1, 2]\nb = a\nid(a) == id(b)   # True  — same object\na is b           # True  — equivalent check\n\nc = [1, 2]\nid(a) == id(c)   # False — equal value, different object\n```\n\n`id` is handy for understanding **aliasing** — why mutating through one name\nshows up through another. Don't rely on the actual numeric value (it can be\nreused after an object is garbage-collected); use it only to reason about\nsameness.\n",{"id":92,"difficulty":43,"q":93,"a":94},"small-int-cache","What is the small-int cache?","CPython **pre-creates and caches integers from −5 to 256** as singletons at\nstartup. Any time a value in that range is needed, the **same cached object** is\nreused — so equal small ints share identity and `is` returns `True`. Outside that\nwindow, equal integers are typically **distinct** objects.\n\n```python\na = 256\nb = 256\na is b      # True  — both point at the cached 256\n\nc = 257\nd = 257\nc is d      # often False — separate objects\nc == d      # True  — always compare values with ==\n```\n\nThis is purely a **memory\u002Fperformance optimization** and a CPython\nimplementation detail — not a language guarantee. It's the single biggest reason\n`is` \"appears\" to work on numbers. Rule of thumb: never use `is` to compare ints,\nuse `==`.\n",{"id":96,"difficulty":43,"q":97,"a":98},"string-interning","What is string interning and sys.intern?","**Interning** stores a **single shared copy** of a string so identical strings\ncan be the **same object**, saving memory and making equality checks a fast\npointer comparison. CPython **auto-interns** short, identifier-like string\nliterals (and compile-time constants); other strings may not be. You can force it\nwith **`sys.intern`**.\n\n```python\na = \"hello\"\nb = \"hello\"\na is b           # True  — auto-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!\")\nf = sys.intern(\"hello world!\")\ne is f           # True — explicitly interned\n```\n\n`sys.intern` is useful when you compare the **same strings repeatedly** (parsing,\ntokenizing, dict keys) and want the speed\u002Fmemory win. Like int caching, which\nstrings auto-intern is an **implementation detail** — never rely on it for\ncorrectness.\n",{"id":100,"difficulty":35,"q":101,"a":102},"is-coincidental","Why does is sometimes \"work\" coincidentally?","`is` sometimes returns `True` for equal values **only because CPython caches or\ninterns those particular objects** — small ints (−5..256) and many short string\nliterals share one object. It's an accident of optimization, not equality, so it\nbreaks the moment you leave the cached range.\n\n```python\nx = 100\nx is 100         # True  — cached small int (deceiving!)\n\ny = 1000\ny is 1000        # often False — outside the cache\ny == 1000        # True  — the correct comparison\n```\n\nThe danger is that code passes during testing with small values and then fails in\nproduction with larger ones. Treat any `is`-on-a-value that works as a **lucky\ncoincidence**. Rule of thumb: if swapping `is` for `==` would change behavior on\n*some* input, you should have used `==`.\n",{"id":104,"difficulty":105,"q":106,"a":107},"correct-use-of-is","easy","What is the correct use of is?","Use `is` to test **identity against singletons** — objects of which there is\nexactly **one** — most commonly **`None`**, but also `True`, `False`, and your own\n**sentinel** objects. For singletons `is` is correct, fast, and can't be fooled by\na custom `__eq__`.\n\n```python\nif x is None:          # idiomatic, recommended by PEP 8\n    ...\n\n_MISSING = object()    # unique sentinel\ndef get(d, key, default=_MISSING):\n    val = d.get(key, _MISSING)\n    if val is _MISSING:    # distinguishes \"absent\" from \"value is None\"\n        return default\n    return val\n```\n\nA sentinel like `object()` is ideal precisely because `is` checks identity — no\nother object can ever match it. Rule of thumb: `is`\u002F`is not` for `None` and\nsentinels; `==`\u002F`!=` for everything else.\n",{"description":32},"Python interview questions on object identity: is vs ==, the id() function, the small-int cache, string interning, and the correct use of is for None and sentinels.","python\u002Finternals\u002Fidentity-interning","Identity, is vs ==, & Interning","Bxm0c6IuOw0YEDIc513jbfs-3-is_vtYIBOUhOkjzzY",{"id":114,"title":115,"body":116,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":8,"meta":120,"navigation":38,"order":11,"path":121,"questions":122,"related":66,"seo":147,"seoDescription":148,"stem":149,"subtopic":150,"topic":19,"topicSlug":21,"updated":71,"__hash__":151},"qa\u002Fpython\u002Finternals\u002Fcpython-model.md","Cpython Model",{"type":29,"value":117,"toc":118},[],{"title":32,"searchDepth":33,"depth":33,"links":119},[],{},"\u002Fpython\u002Finternals\u002Fcpython-model",[123,127,131,135,139,143],{"id":124,"difficulty":35,"q":125,"a":126},"source-to-bytecode","How does CPython go from source to running code?","CPython first **compiles** your `.py` source into **bytecode** — a compact,\nplatform-independent instruction set for the Python **virtual machine**. That\nbytecode is then executed by the **interpreter loop** (a big evaluation loop,\nhistorically a giant `switch`), which runs one bytecode instruction at a time.\nCompiled bytecode is cached as **`.pyc`** files in `__pycache__`.\n\n```python\n# mymod.py  ->  compiled to  __pycache__\u002Fmymod.cpython-3xx.pyc\n# The .pyc is reused if the source is unchanged (matched by hash\u002Ftimestamp),\n# so imports skip recompiling. It is NOT machine code — still bytecode.\ndef add(a, b):\n    return a + b\n```\n\nThe key points: bytecode is an **intermediate** representation (not native\nmachine code), `.pyc` is just a **cache** to skip recompilation on import, and the\nVM interprets it at runtime. Rule of thumb: source -> bytecode -> interpreter\nloop, with `.pyc` caching the middle step.\n",{"id":128,"difficulty":35,"q":129,"a":130},"cpython-vs-others","What is CPython, and how does it differ from PyPy and Jython?","**CPython** is the **reference implementation** written in C — it's what you get\nfrom python.org and what most people mean by \"Python.\" The **language** is a\nspec; an **implementation** runs it. Alternatives include **PyPy** (with a\n**JIT** that often runs much faster), **Jython** (runs on the **JVM**), and\n**IronPython** (on **.NET**).\n\n```python\nimport platform\nprint(platform.python_implementation())   # 'CPython', 'PyPy', 'Jython', ...\n```\n\nThey differ in performance and integration: PyPy speeds up long-running pure\nPython via JIT, Jython\u002FIronPython interoperate with Java\u002F.NET libraries, and\nCPython has the widest C-extension ecosystem (NumPy, etc.) plus the **GIL**. Rule\nof thumb: \"Python\" is the language; CPython is the dominant implementation, and\nothers trade ecosystem reach for speed or platform integration.\n",{"id":132,"difficulty":43,"q":133,"a":134},"dis-module","How do you inspect bytecode with the dis module?","The **`dis`** (\"disassemble\") module shows the **bytecode instructions** a\nfunction compiles to — useful for understanding what Python actually does under\nthe hood and for comparing the cost of two approaches.\n\n```python\nimport dis\n\ndef add(a, b):\n    return a + b\n\ndis.dis(add)\n# Example output (abbreviated):\n#   LOAD_FAST   a\n#   LOAD_FAST   b\n#   BINARY_OP   +        # add the two\n#   RETURN_VALUE\n```\n\nEach line is one VM instruction the interpreter loop executes. `dis` is great for\nanswering \"is this comprehension really faster?\" or seeing how the compiler\ndesugars a construct. Rule of thumb: when you want to know what the interpreter\n*literally* runs, disassemble it with `dis`.\n",{"id":136,"difficulty":35,"q":137,"a":138},"frames-call-stack","What are frames and the call stack?","Every time a function is called, CPython creates a **frame object** — a record\nholding that call's **local variables**, the current **instruction pointer**, and\na reference to the caller. Frames are pushed onto the **call stack** as functions\ncall each other and popped as they return. This is the structure a **traceback**\nwalks when printing an error.\n\n```python\nimport inspect\n\ndef inner():\n    frame = inspect.currentframe()\n    print(frame.f_code.co_name)        # 'inner'\n    print(frame.f_back.f_code.co_name) # 'outer' — the caller's frame\n\ndef outer():\n    inner()\n\nouter()\n```\n\nFrames are why each call has **isolated locals** and why exceptions can report the\nfull chain of calls. Deep\u002Finfinite recursion piles up frames until\n`RecursionError` (the stack limit). Rule of thumb: one frame per active call, all\nchained together as the call stack.\n",{"id":140,"difficulty":43,"q":141,"a":142},"compiled-or-interpreted","Is Python compiled or interpreted?","**Both.** Python source is first **compiled** to **bytecode** (a real compilation\nstep), and that bytecode is then **interpreted** by the CPython virtual machine at\nruntime. So the common \"Python is interpreted\" is only half the story — there's a\ncompile phase, just to bytecode rather than to native machine code.\n\n```python\nimport py_compile\npy_compile.compile(\"mymod.py\")   # explicitly produces the .pyc bytecode\n\n# At runtime, the VM's interpreter loop executes that bytecode.\n```\n\nThe distinction that matters: Python compiles to **portable bytecode**, not to\nCPU-specific machine code (the way C does ahead-of-time). PyPy adds a **JIT** that\n*does* compile hot bytecode to machine code at runtime. Rule of thumb: Python is\ncompiled to bytecode and then interpreted.\n",{"id":144,"difficulty":35,"q":145,"a":146},"gil-role","What role does the GIL play in CPython's execution model?","The **GIL** (Global Interpreter Lock) is a single mutex that lets **only one\nthread execute Python bytecode at a time** in a CPython process. The interpreter\nloop holds it while running instructions and periodically releases it (and during\nblocking I\u002FO), so threads take turns rather than running Python code in parallel.\n\n```python\n# Two threads doing CPU-bound Python work do NOT run in parallel:\n# the GIL serializes their bytecode execution on one core.\n# threads -> good for I\u002FO-bound (GIL released during I\u002FO)\n# processes -> needed for CPU-bound parallelism (each has its own GIL)\n```\n\nIt exists largely to make CPython's memory management (reference counting)\nsimpler and C extensions safer. The consequence is that **threads don't give CPU\nparallelism** — that's what multiprocessing is for (see the Concurrency &\nParallelism topic). Rule of thumb: the GIL means one-bytecode-at-a-time per\nprocess, so use processes for CPU-bound parallelism.\n",{"description":32},"Python interview questions on the CPython execution model: source to bytecode to .pyc, the interpreter loop, the dis module, frames and the call stack, and whether Python is compiled or interpreted.","python\u002Finternals\u002Fcpython-model","The CPython Execution Model","BTfRbdwE60ns7SueJvUjjVuR4jxN9D_oqBK90FfzmXg",1781808675510]