Missing Parts // Doubts
Great question — this is a classic Python interview trap 🔥
Let’s break it down slowly and precisely.
The code
a = 257
b = 257
print(a == b) # True
print(a is b) # False
1️⃣ == → Value equality
== checks whether the values are equal
a == b
ahas value257bhas value257- Values are the same → ✅ True
✅ This will always be true here.
2️⃣ is → Object identity
is checks whether both variables point to the SAME object in memory
a is b
This depends on how Python manages memory.
3️⃣ Python Integer Interning (IMPORTANT 🔥)
Python interns (reuses) small integers for performance.
📌 Integer cache range:
-5 to 256 (CPython implementation)
Example:
x = 100
y = 100
x is y # True
Why?
- Python reuses the same object for small integers.
4️⃣ Why 257 breaks this rule
a = 257
b = 257
257is outside the cached range- Python creates two separate integer objects
- Values same ❌ memory different
So:
a == b → True (values same)
a is b → False (objects different)
5️⃣ Proof (Memory Address)
print(id(a))
print(id(b))
You’ll see different IDs.
6️⃣ Compare with Cached Integers
a = 256
b = 256
print(a == b) # True
print(a is b) # True
But:
a = 257
b = 257
print(a == b) # True
print(a is b) # False
7️⃣ Why Python does this?
- Performance optimization
- Saves memory
- Faster comparisons for common integers
⚠️ This behavior is CPython-specific, not guaranteed by the Python language spec.
8️⃣ Interview Rule (VERY IMPORTANT ❗)
✅ Use == for value comparison
❌ Use is ONLY for:
None
True / False
Singleton objects
Example:
if x is None:
...
9️⃣ One-line Summary (Interview Ready)
==checks value equality,ischecks memory identity. Python caches integers from -5 to 256, so257creates separate objects, makinga is bfalse.
If you want, I can now show:
- 🔥 how this behaves differently in REPL vs script
- 🔥 how strings behave similarly
- 🔥 why
iscan randomly return True sometimes - 🔥 how this appears in FAANG interviews
Just tell me 👌
Short answer: yes — but with an important correction ❗
Your intuition is close, but the terms stack, heap, and hash need to be fixed for Python.
Let’s clean this up precisely and interview-correct.
❌ First, let’s correct the misconceptions
❌ “Saved differently in stack”
Python does not store integers in a traditional stack like C/C++.
❌ “Saved differently in hash”
Integers are not stored in a hash table just because they are integers.
✅ The REAL Python Memory Model (CPython)
Python uses:
- Heap → for all objects (including integers)
- Names (variables) → references to heap objects
- No user-visible stack memory model
What REALLY happens for:
a = 390
b = 390
Step-by-step (CPython):
- Python sees literal
390 390is outside the integer cache (-5 to 256)- Python creates two separate integer objects in the heap
apoints to object #1bpoints to object #2
So:
a ──▶ int(390) [heap]
b ──▶ int(390) [heap]
Result:
a == b # True (values same)
a is b # False (different objects)
🔥 What about HASH?
Important clarification:
- Integers ARE hashable
- But hash ≠ storage location
hash(390) == hash(390) # True
That just means:
- If used as a dict key / set element, both hash the same
- It does NOT mean they share memory
Example:
d = {390: "x"}
Python:
- Calls
hash(390) - Uses it to place the key in a hash table
- Stores a reference to the integer object
➡️ Hashing is for lookup, not memory identity.
Why small integers behave differently
a = 100
b = 100
Python uses integer interning:
a ──▶ int(100) ◀── b
So:
a is b # True
⚠️ Key Rule (Memorize This)
Variables do not store values.
They store references to objects on the heap.
Interview-Grade Summary (Perfect Answer)
When writing
a = 390andb = 390, Python creates two separate integer objects on the heap because integers outside the cached range are not interned. Both variables reference different objects with the same value, soa == bis true, buta is bis false. Hashing is unrelated to memory identity and is only used for dictionary/set lookups.
🔥 Extra trap (VERY IMPORTANT)
In interactive REPL, sometimes:
a = 390
b = 390
a is b # MAY appear True
Why?
- Compiler optimizations
- Constant folding
- Implementation detail
👉 Never rely on is for value comparison
If you want next, I can explain:
- Why strings behave similarly
- Why
isworks withNone - Why this differs in PyPy
- How garbage collection interacts here
Just say the word 👌
Great follow-up — this is exactly the right question 👍
Let’s answer it cleanly, correctly, and interview-safe.
Short answer (pin this in your head)
The references for
aandbare stored in a namespace (symbol table), and the integer objects live in the heap.
No stack. No hash table.
Now the exact picture for:
a = 390
b = 390
1️⃣ Where are the objects stored?
✅ Heap
Python stores ALL objects in the heap:
- integers
- strings
- lists
- functions
- classes
So you get:
Heap:
[ int object: 390 ] ← object #1
[ int object: 390 ] ← object #2
These are two separate heap objects.
2️⃣ Where are the references (a and b) stored?
✅ Namespace (symbol table)
Depending on scope:
| Scope | Where reference lives |
|---|---|
| Global | globals() dict |
| Function | local namespace (fast locals array + dict view) |
| Class | class __dict__ |
Example:
a = 390
b = 390
At module level:
globals() = {
'a': <ref to int(390)>,
'b': <ref to int(390)>
}
Visual diagram (mental model)
globals() (dictionary)
├── 'a' ─────────▶ int(390) [heap object #1]
└── 'b' ─────────▶ int(390) [heap object #2]
3️⃣ What is NOT happening ❌
| Myth | Reality |
|---|---|
| Stored on stack | ❌ Python hides stack |
| Stored in hash table automatically | ❌ Only dict/set use hashing |
| Variables store values | ❌ Variables store references |
| Same hash ⇒ same memory | ❌ Hash ≠ identity |
4️⃣ Why is fails here
a is b
Checks:
id(a) == id(b)
Since:
heap_object_1 ≠ heap_object_2
➡️ False
5️⃣ Why small ints are different
a = 100
b = 100
Python reuses a single cached heap object:
globals()
├── 'a' ───▶ int(100) [heap]
└── 'b' ───▶ int(100) [heap]
➡️ a is b → True
6️⃣ Ultra-important interview line (gold 🥇)
In Python, names are stored in namespaces as references, and all objects live in the heap. Whether two names point to the same object depends on interning and implementation details.
7️⃣ One-line memory rule (remember forever)
Names live in namespaces.
Objects live in the heap.iscompares object identity, not values.
If you want, next I can show:
- how function locals are stored differently (fast locals)
- how
id()maps to actual memory - how garbage collection cleans unused integers
- why
del adoesn’t delete the object immediately
You’re asking exactly the right questions — this is deep Python 👍
Excellent — this is core CPython internals territory.
I’ll explain all 4 points clearly, step-by-step, with correct mental models (no myths).
1️⃣ How function locals are stored differently (Fast Locals)
Global / class variables
Stored in a dictionary:
globals()['x']
Dict lookup → slower.
Function locals (special optimization 🚀)
Inside functions, CPython does NOT use a dict for every local access.
Instead it uses:
- A fixed-size array called fast locals
- Index-based access (much faster)
Example
def f():
a = 10
b = 20
return a + b
Internally:
Local variable table:
index 0 → a
index 1 → b
Bytecode:
LOAD_FAST 0 # a
LOAD_FAST 1 # b
🔥 This is why:
a + b # very fast
globals()['a'] + globals()['b'] # slower
Why locals() still shows a dict?
locals()
➡️ Python creates a temporary dict view of the fast locals array
(Not the real storage)
Key rule
Fast locals = array
Globals = dict
2️⃣ How id() maps to actual memory
In CPython only:
id(obj) == memory_address_of_obj
Example:
a = 390
print(id(a)) # e.g. 140291884530096
That number is the actual address of the object in RAM.
Proof (CPython)
import ctypes
ctypes.cast(id(a), ctypes.py_object).value is a
# True
⚠️ Other Python implementations:
- PyPy →
id()is NOT a memory address - Jython → abstract identifier
Interview-safe line
In CPython,
id()returns the memory address of the object; in other implementations, it’s just a unique identifier.
3️⃣ How garbage collection cleans unused integers
Python uses two systems:
1️⃣ Reference Counting (primary)
Each object tracks:
refcount = number of references
Example:
a = 390 # refcount = 1
b = a # refcount = 2
del a # refcount = 1
del b # refcount = 0 → destroyed immediately
2️⃣ Cyclic Garbage Collector (secondary)
Handles:
a = []
a.append(a)
Refcount ≠ 0 but object unreachable → GC detects cycles.
Integers specifically
- Small ints (
-5to256) are never freed - Larger ints are freed when:
- refcount → 0
- not referenced by internals
When freed?
- Immediately (refcount hits zero)
- OR delayed until GC run (cycles)
4️⃣ Why del a doesn’t delete the object immediately
Important rule:
del a
❌ does NOT delete the object
✅ deletes the reference name
Example
a = 390
b = a
del a
Memory state:
b ──▶ int(390)
Object still alive (refcount = 1).
Only when:
del b
Now:
refcount = 0 → object destroyed
Another example
x = [1, 2, 3]
y = x
del x
List is still alive via y.
⚠️ Extra subtlety (very important)
Even if refcount = 0:
- CPython may reuse memory
- OS may not reclaim immediately
- Object destructor (
__del__) may delay cleanup
🧠 Unified Mental Model (Save this)
Names → References → Heap Objects
delremoves names- GC removes objects
iscompares object identity==compares value- Locals use fast arrays
- Globals use dicts
🥇 Final Interview-Perfect Summary
Function locals in CPython are stored in a fast array rather than a dictionary for performance. The
id()function returns the actual memory address of an object in CPython. Python primarily uses reference counting for garbage collection, supplemented by a cyclic garbage collector. Thedelstatement removes a name binding, not the object itself, so an object is only destroyed when its reference count drops to zero.
If you want next, I can explain:
- bytecode (
LOAD_FAST,STORE_FAST) - frame objects and call stack
- why closures behave differently
- why mutable defaults are dangerous
You’re now thinking like a Python runtime engineer 🧠🔥
Perfect — this is deep CPython execution model now.
I’ll explain all 4 topics with mental models + bytecode-level clarity (interview-grade).
1️⃣ Python Bytecode: LOAD_FAST, STORE_FAST, etc.
Python code is first compiled into bytecode, which the Python Virtual Machine (PVM) executes.
Example
def f():
a = 10
b = 20
return a + b
Disassemble:
import dis
dis.dis(f)
Output (simplified)
0 LOAD_CONST 1 (10)
2 STORE_FAST 0 (a)
4 LOAD_CONST 2 (20)
6 STORE_FAST 1 (b)
8 LOAD_FAST 0 (a)
10 LOAD_FAST 1 (b)
12 BINARY_ADD
14 RETURN_VALUE
Meaning of key opcodes
🔹 STORE_FAST
STORE_FAST index
- Store value into fast locals array
- Index-based (very fast)
🔹 LOAD_FAST
LOAD_FAST index
- Read from fast locals array
- No dictionary lookup
🔹 LOAD_GLOBAL
LOAD_GLOBAL name
- Used for globals
- Requires dict lookup → slower
Why this matters
a + b # LOAD_FAST → fast
global_a + global_b # LOAD_GLOBAL → slower
💡 This is why Python recommends local variables for performance.
2️⃣ Frame Objects & Call Stack
Every function call creates a frame object.
What is a frame?
A frame stores:
- Local variables (fast locals array)
- Global namespace reference
- Bytecode instruction pointer
- Operand stack
- Reference to previous frame
Example
def outer():
x = 10
inner()
def inner():
y = 20
outer()
Call stack (top → bottom)
Frame(inner)
Frame(outer)
Frame(<module>)
Frame lifecycle
- Function called → frame created
- Bytecode executed
- Function returns
- Frame destroyed (unless referenced)
Inspect current frame
import sys
sys._getframe()
⚠️ Used in debuggers, profilers, trace tools.
Key insight
Python does NOT use a C-style stack you can access — frames are heap objects, linked like a stack.
3️⃣ Why Closures Behave Differently
Closures capture variables, not values.
Example
def outer():
x = 10
def inner():
return x
return inner
f = outer()
print(f()) # 10
What really happens
xis not copied- It is stored in a cell object
- Inner function holds reference to the cell
cell → x → 10
Proof
print(f.__closure__)
Late Binding Problem
funcs = []
for i in range(3):
def f():
return i
funcs.append(f)
[f() for f in funcs] # [2, 2, 2]
Why?
- All functions reference same
i ievaluated at call time, not creation time
Fix
for i in range(3):
def f(i=i):
return i
Closure rule (memorize)
Closures capture variables, not values.
4️⃣ Why Mutable Default Arguments Are Dangerous
Default arguments are evaluated once, at function definition time.
Dangerous example
def add_item(item, lst=[]):
lst.append(item)
return lst
Calls:
add_item(1) # [1]
add_item(2) # [1, 2]
add_item(3) # [1, 2, 3]
Why this happens
lstis created once- Stored in
function.__defaults__ - Shared across all calls
Proof
print(add_item.__defaults__)
Correct pattern
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Immutable defaults are safe
def f(x, n=10): # safe
return x + n
🔥 Unified Mental Model (SAVE THIS)
Source Code
↓
Bytecode
↓
Frame Object
↓
Fast Locals Array
↓
Heap Objects
🥇 Interview-Perfect Summary
Python compiles code into bytecode executed by the Python Virtual Machine. Local variables are accessed using fast array-based instructions like
LOAD_FAST. Each function call creates a frame object that holds execution state. Closures capture variables via cell objects, leading to late binding behavior. Mutable default arguments are evaluated once at function definition time, causing shared state across calls if not handled carefully.
If you want next (advanced level 🔥):
- Python GIL internals
- Stack vs heap myths in Python
- How exceptions unwind frames
- How async/await changes frames
- Memory leaks via closures
Just tell me 🚀
Excellent — this is advanced Python runtime & concurrency internals.
I’ll explain each topic with correct mental models, what’s real vs myth, and why it matters in production & interviews.
1️⃣ Python GIL Internals (Global Interpreter Lock)
What the GIL really is
The GIL is a mutex that allows only ONE thread to execute Python bytecode at a time in CPython.
It does NOT:
- block I/O
- affect multiprocessing
- exist in PyPy/Jython in the same way
Why the GIL exists
CPython uses:
- reference counting for memory management
- refcount updates are not atomic
Without GIL:
Two threads increment/decrement refcount simultaneously → memory corruption
GIL = simpler + faster single-threaded performance
How the GIL works internally
Simplified loop:
while thread holds GIL:
execute N bytecode instructions
OR release on blocking I/O
OR release after time slice
Key points:
- Default switch interval ≈ 5ms
- Checked between bytecode instructions
- I/O operations release GIL (
read,sleep,socket)
CPU-bound vs I/O-bound
| Task | Threads help? |
|---|---|
| CPU-bound Python | ❌ No |
| I/O-bound Python | ✅ Yes |
| NumPy / Pandas | ✅ Yes (GIL released in C) |
| Multiprocessing | ✅ Yes |
Interview one-liner 🥇
The GIL ensures thread safety of CPython’s memory model but prevents true parallel execution of Python bytecode.
2️⃣ Stack vs Heap Myths in Python
❌ Common myth
“Local variables live on the stack”
❌ WRONG in Python
Reality
| Concept | Python reality |
|---|---|
| Stack | Call frames linked together |
| Heap | ALL objects live here |
| Variables | Names bound to objects |
Frames themselves are heap objects.
What Python does have
Heap:
- int objects
- list objects
- frame objects
- function objects
Frames link together → behaves LIKE a stack
But:
- no raw stack memory
- no user control over stack allocation
Correct statement
Python has a call stack conceptually, but frames and objects are heap-allocated.
3️⃣ How Exceptions Unwind Frames
Example
def a():
b()
def b():
c()
def c():
1 / 0
a()
What happens internally
- Exception raised in
c - Python looks for
exceptin same frame - None found → frame
cdestroyed - Goes to frame
b - Same process
- Continues until:
- handler found
- OR reaches top → crash
Stack unwinding visualization
Frame(c) ❌ destroyed
Frame(b) ❌ destroyed
Frame(a) ❌ destroyed
Frame(module) → exception printed
finally blocks
try:
risky()
finally:
cleanup()
finally always runs, even during unwinding.
Why this matters
- resource cleanup
- file/socket safety
- DB connections
4️⃣ How async / await Changes Frames
Traditional functions
Call → Frame created → Execute → Destroy
Async functions
async def f():
await g()
What changes?
- Frame is SUSPENDED
- Instruction pointer saved
- Frame stored inside coroutine object
- Event loop resumes it later
Coroutine lifecycle
Coroutine created
↓
Execution until await
↓
Suspended (frame kept alive)
↓
Resumed
↓
Finished
Key difference
| Normal function | Async function |
|---|---|
| Frame destroyed on return | Frame survives suspension |
| Linear execution | Cooperative multitasking |
| Stack-like | Graph of suspended frames |
Why async scales
- No OS threads
- No context switching
- Thousands of suspended frames in memory
Interview gold 🥇
Async functions suspend frames instead of destroying them, allowing cooperative multitasking without threads.
5️⃣ Memory Leaks via Closures (REAL PROBLEM)
Problem example
def make_handler():
large_data = [1_000_000 items]
def handler():
return len(large_data)
return handler
h = make_handler()
What happens
large_datastored in closure cell- Cell referenced by
handler large_dataNEVER freed
Even if:
del make_handler
Leak pattern
Function → Closure → Cell → Large Object
Worse with globals + closures
handlers.append(handler)
Leak persists for entire program life.
How to fix
Option 1: Avoid closure
def handler(data):
return len(data)
Option 2: Weak references
import weakref
Option 3: Explicit cleanup
handler.__closure__ = None
(advanced, rare)
Debugging leaks
gc.get_objects()objgraph- tracemalloc
🧠 Unified Expert Mental Model (Save This)
Python = Heap-based runtime
GIL = Bytecode execution lock
Frames = Heap objects
Async = Suspended frames
Closures = Hidden references
GC = Refcount + cycle detector
🥇 Ultimate Interview Summary
CPython uses a Global Interpreter Lock to protect reference counting, preventing parallel bytecode execution. Python frames and objects are heap-allocated rather than stack-based. Exceptions unwind frames until a handler is found. Async functions suspend and resume frames instead of destroying them, enabling cooperative multitasking. Closures can unintentionally retain large objects, leading to memory leaks if not designed carefully.
If you want the final boss level 🔥 next:
- GIL removal attempts (PEP 703)
- Why Python threads still matter
- Async vs threading vs multiprocessing
- Debugging real memory leaks
- CPython vs PyPy internals
Just say go 🚀
Great question — this is where the GIL myths end and real system design begins.
Let’s go practical + conceptual, the way it’s expected in senior interviews and real projects.
1️⃣ Why Python Threads STILL Matter (Despite the GIL)
Many people say:
“Python threads are useless because of the GIL”
❌ This is wrong
What the GIL actually blocks
- Only parallel execution of Python bytecode
- It does NOT block:
- I/O operations
- C extensions that release the GIL
- OS-level scheduling
Threads are PERFECT for I/O-bound work
Examples:
- API calls
- Database queries
- File reads/writes
- Network sockets
- Waiting on external systems
import threading
import requests
def fetch(url):
return requests.get(url)
threads = []
for url in urls:
t = threading.Thread(target=fetch, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
Why this works:
- While one thread waits for I/O → GIL released
- Another thread runs → CPU not wasted
Threads + C Extensions = REAL parallelism
Libraries that release the GIL:
- NumPy
- Pandas
- OpenCV
- TensorFlow
- PyTorch
# NumPy matrix multiplication
# runs in parallel on multiple cores
➡️ Threads matter a lot in data/ML workloads.
Threads are lightweight
- Cheaper than processes
- Shared memory (no serialization)
- Faster startup
When threads are BAD
- CPU-bound pure Python loops
- Heavy numeric computation in Python code
Interview one-liner 🥇
Python threads are ideal for I/O-bound workloads and C-extension-heavy computation, even though they don’t provide parallelism for pure Python CPU-bound code.
2️⃣ Async vs Threading vs Multiprocessing (THE REAL COMPARISON)
This is one of the most important system design tables in Python.
Mental Model First
| Model | How it works |
|---|---|
| Threading | OS threads, shared memory |
| Async | Single thread, event loop |
| Multiprocessing | Multiple processes, separate memory |
2.1 Async (asyncio)
Best for:
- Massive I/O concurrency
- 10k–100k simultaneous tasks
- APIs, web servers, sockets
How it works:
- One thread
- Cooperative multitasking
- Tasks yield control using
await
async def fetch():
await aiohttp.get(url)
Pros
✅ Extremely scalable
✅ No thread overhead
✅ Predictable performance
Cons
❌ CPU-bound work blocks everything
❌ Requires async-compatible libraries
❌ Harder mental model
2.2 Threading
Best for:
- Moderate I/O concurrency
- Blocking libraries
- Legacy code
- Mixed workloads
Thread(target=blocking_io)
Pros
✅ Simple mental model
✅ Works with blocking libraries
✅ Shared memory
Cons
❌ GIL limits CPU-bound work
❌ Context switching overhead
❌ Race conditions
2.3 Multiprocessing
Best for:
- CPU-bound workloads
- Parallel computation
- Bypassing the GIL
from multiprocessing import Process
Pros
✅ True parallelism
✅ Full CPU utilization
✅ No GIL issues
Cons
❌ High memory usage
❌ Serialization overhead
❌ Slow process startup
🔥 The Ultimate Comparison Table
| Feature | Async | Threading | Multiprocessing |
|---|---|---|---|
| Uses multiple cores | ❌ | ❌* | ✅ |
| GIL affected | ❌ | ✅ | ❌ |
| Best for | Massive I/O | Blocking I/O | CPU-bound |
| Memory | Low | Medium | High |
| Context switching | Minimal | Medium | High |
| Shared memory | ✅ | ✅ | ❌ |
| Debugging | Hard | Medium | Easier |
| Scalability | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
* Threads can use multiple cores only if GIL is released (C extensions).
3️⃣ Real-World Decision Rules (VERY IMPORTANT)
Rule 1
If it’s CPU-bound → Multiprocessing
Rule 2
If it’s massive I/O (APIs, sockets) → Async
Rule 3
If it’s blocking I/O or legacy libs → Threading
Rule 4
If it uses NumPy/Pandas → Threads CAN scale
4️⃣ Real Production Architectures
Web servers (FastAPI)
- Async for request handling
- Thread pool for blocking I/O
- Process workers (Gunicorn/Uvicorn)
Data engineering
- Multiprocessing for CPU transforms
- Threads for DB/API reads
- Async for ingestion pipelines
ML pipelines
- Multiprocessing for training
- Threads inside NumPy / PyTorch
- Async for serving
🥇 Interview-Perfect Final Answer
Python threads still matter because they efficiently handle I/O-bound tasks and leverage C extensions that release the GIL. Async programming excels at massive I/O concurrency with minimal overhead, while multiprocessing enables true parallelism for CPU-bound workloads. Choosing between them depends on whether the bottleneck is I/O, CPU, or library behavior.
If you want next (this is the ultimate level 🔥):
- How FastAPI mixes async + threads internally
- Why Gunicorn uses multiple workers
- When async is actually slower
- Hybrid designs (async + process pools)
- PEP 703 (GIL removal future)
Just say next 🚀
\