DEV Community

Cover image for Python Internals with Classes
Priyanshu Soni
Priyanshu Soni

Posted on

Python Internals with Classes

I wrote a class in Python with num_instances = 0 and expected it to reset every time a new object was created.

It didn't. Here's why β€” and it completely changed how I think about memory. 🧠


The code that confused me

class ObjectCounter:
    num_instances = 0  # I thought this resets every time

    def __init__(self):
        ObjectCounter.num_instances += 1

obj1 = ObjectCounter()  # num_instances = 1
obj2 = ObjectCounter()  # num_instances = 2
# Never reset to 0. Why??
Enter fullscreen mode Exit fullscreen mode

My assumption was wrong. num_instances = 0 does NOT run on every object creation. It runs once β€” when Python first reads the class definition.

Think of the class body like a blueprint being drawn once. The __init__ is the construction happening each time. The blueprint is never redrawn.


Diagram 1 β€” What actually happens step by step

[ Class defined ]  ──►  [ obj1 = ObjectCounter() ]  ──►  [ obj2 = ObjectCounter() ]
num_instances = 0            __init__ runs                   __init__ runs
  runs ONCE here              count = 1                        count = 2
  lives in class memory       mutates class attribute          counter keeps growing
Enter fullscreen mode Exit fullscreen mode

vs a function:

num = 0  β†’  created fresh on every call  β†’  dies when function returns  β†’  always resets to 0
Enter fullscreen mode Exit fullscreen mode

Diagram 2 β€” Stack vs Heap: where things actually live

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        STACK MEMORY         β”‚     β”‚            HEAP MEMORY               β”‚
β”‚  Function calls & locals    β”‚     β”‚   Classes, objects, lists, dicts     β”‚
β”‚                             β”‚     β”‚                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚     β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ main()              β”‚    β”‚     β”‚  β”‚  ObjectCounter (class object) β”‚    β”‚
β”‚  β”‚ x, y, z β†’ born &   β”‚    β”‚     β”‚  β”‚  __dict__: {num_instances: 2} β”‚    β”‚
β”‚  β”‚ die here            β”‚    β”‚     β”‚  β”‚  Stays alive entire program   β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                             β”‚     β”‚             β–²          β–²             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚     β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”  β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ my_function()       β”‚    β”‚     β”‚   β”‚    obj1     β”‚  β”‚    obj2    β”‚    β”‚
β”‚  β”‚ num = 0             β”‚    β”‚     β”‚   β”‚  instance   β”‚  β”‚  instance  β”‚    β”‚
β”‚  β”‚ reset every call    β”‚    β”‚     β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚     β”‚                                      β”‚
β”‚                             β”‚     β”‚                                      β”‚
β”‚  βœ• temporary                β”‚     β”‚  βœ“ permanent (until GC)             β”‚
β”‚  βœ• dies when fn returns     β”‚     β”‚  βœ“ shared across all instances      β”‚
β”‚  βœ• no shared state          β”‚     β”‚  βœ“ mutated in place, never reset    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Diagram 3 β€” The mind-bending part: variables are just labels

In Python, a variable doesn't hold a value. It's just a label pointing to an object on the heap. Even x = 5 β€” that integer is a full heap object with its own memory address.

Stack (names)            Heap (objects)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  a = ─────┼──────────► β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”Œβ”€β”€β”€β”€β–Ίβ”‚  list: [1, 2, 3, 4] β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚     β”‚  ref count = 2       β”‚
β”‚  b = β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

b.append(4) also changes a!
Both names point to the same object.
Enter fullscreen mode Exit fullscreen mode

Diagram 4 β€” Python's secret: integer interning

Python pre-creates integers from -5 to 256 and reuses them forever. Beyond that, new objects are made each time.

a = 5;  b = 5
print(a is b)    # True  β†’ SAME object in memory!

a = 1000;  b = 1000
print(a is b)    # False β†’ different objects
Enter fullscreen mode Exit fullscreen mode
Small int (-5 to 256): interned        Large int: new object each time

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  a = 5   │──┐                        β”‚ a = 1000 │────►│ int: 1000 (A)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”œβ”€β”€β–Ί β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚    β”‚   int: 5     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚    β”‚ shared foreverβ”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  b = 5   β”‚β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚ b = 1000 │────►│ int: 1000 (B)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  a is b β†’ True                          a is b β†’ False
  id(a) == id(b) βœ“                       id(a) != id(b) βœ—
Enter fullscreen mode Exit fullscreen mode

Diagram 5 β€” How memory gets freed: reference counting

x = [1,2,3]   ──►   y = x      ──►    del y      ──►    del x
ref count = 1       ref count = 2    ref count = 1     ref count = 0
                                                          β†’ freed! πŸ—‘οΈ
Enter fullscreen mode Exit fullscreen mode

When ref count hits 0, Python automatically frees that memory from the heap.
No manual malloc/free needed β€” unlike C/C++.


The full picture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   HEAP MEMORY                    β”‚
β”‚                                                  β”‚
β”‚   type (metaclass)                               β”‚
β”‚     β–²        β–²                                   β”‚
β”‚     β”‚        β”‚                                   β”‚
β”‚    int      str        ← built-in classes        β”‚
β”‚     β–²                                            β”‚
β”‚     β”‚                                            β”‚
β”‚  ObjectCounter         ← your class              β”‚
β”‚  { num_instances: 2 }                            β”‚
β”‚     β–²        β–²                                   β”‚
β”‚     β”‚        β”‚                                   β”‚
β”‚  instance_1  instance_2  ← your objects          β”‚
β”‚                                                  β”‚
β”‚  5, 256, "hello"  ← interned primitives          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  STACK MEMORY                    β”‚
β”‚  x, y ────────────────────────► (point to        β”‚
β”‚                                   heap objects)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

One question about a class counter led me to understand stack vs heap, reference semantics, interning, and garbage collection.

Python hides a lot of complexity behind clean syntax. But when you peek under the hood, it's elegant all the way down.

If this was useful, I write about things I'm actively learning as an SDE. Follow along. πŸš€

#Python #SoftwareEngineering #WebDevelopment #Programming #LearningInPublic #ComputerScience #100DaysOfCode

Top comments (0)