A strong answer
In CPython, there are no unboxed primitive variables. Every value—even an integer—is a heap-allocated C struct (PyObject or PyVarObject) containing at least a 64-bit reference count (ob_refcnt, 8 bytes), a 64-bit type pointer (ob_type, 8 bytes), and the payload (ob_digit, 12+ bytes for int = 28 bytes per integer). A Python variable name is merely a dictionary or fast-local slot holding an 8-byte pointer (PyObject*) to that heap struct. Assignment b = a never copies the object—it copies the 8-byte pointer and increments ob_refcnt += 1 (Py_INCREF). While == calls __eq__ to compare payload values, is compares the raw memory addresses (id(a) == id(b)). CPython pre-allocates a singleton array of small integers [-5, 256], which is why 5 is 5 is always True while freshly allocated 1000 objects have distinct addresses.
At every bytecode instruction boundary, an object's ob_refcnt equals the exact number of live PyObject* references pointing to it from stack frames, containers, and runtime singletons; the moment ob_refcnt drops to 0, CPython immediately invokes its type's deallocator (tp_dealloc). Because every assignment (STORE_FAST), container insertion, and argument pass executes Py_INCREF and every scope exit, del, or reassignment executes Py_DECREF, deterministic reference counting reclaims non-cyclic objects at the exact instruction their last reference disappears.
A single CPython int costs 28 bytes on 64-bit systems (vs 4 or 8 bytes in C/Rust), and every assignment or function pass touches the object header in memory to increment and decrement ob_refcnt. In production systems (CPython 3.12+ (including Immortal Objects PEP 683 for None, True, False, and small ints so multi-core workers don't bounce cache lines on ob_refcnt), PyTorch tensor wrappers, and NumPy array headers all build on PyObject reference semantics.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how CPython name binding and PyObject reference counting operates in memory step by step rather than giving a vague textbook definition.
- States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (Name Assignment (
b = a), 64-bit CPythonint(PyLongObject)) with concrete numbers. - Identifies real-world failure modes (Writing
if status is 'OK':instead ofif status == 'OK':) and explains how production systems mitigate them.
Common mistakes
- Treating CPython name binding and PyObject reference counting as a black box without explaining the underlying memory or state transitions.
- Ignoring worst-case tail latency or memory overhead (Creating a
listof 50 millionfloatobjects in pure Python). - Forgetting boundary conditions and error recovery (Holding a reference to a giant DataFrame in an outer scope variable).