Skip to main content

Variables & Memory Management

1. Python Memory Manager​

  • Python objects are stored in memory, each with a unique memory address (e.g., 0x1000)
  • The heap is the region of memory where these objects live
  • Example: object 1, object 2, object 3 stored at addresses 0x1000, 0x1003, 0x1005

2. Variable References​

  • A variable does not store the value itself — it stores a reference (pointer) to the object's memory address
  • Example:
    my_var_1 = 10 # my_var_1 references object at 0x1000
    my_var_2 = 'hello' # my_var_2 references object at 0x1002

3. Finding Memory Addresses​

  • Use id() to get an object's memory address (returned as a base-10 number)
  • Use hex() to convert that number to hexadecimal
  • Example:
    a = 10
    print(hex(id(a)))

4. Reference Counting​

Primary memory management mechanism in CPython, supplemented by a cyclic garbage collector for cases it can't handle.

How it works

  • Every object has a hidden counter (ob_refcnt) tracking how many references point to it
  • Object is deallocated immediately when count hits zero

Count increases when:

  • Object assigned to a new variable (b = a)
  • Passed as a function argument
  • Stored in a container (list, dict, tuple, etc.)
  • Returned from a function

Count decreases when:

  • A reference goes out of scope
  • Variable is reassigned to something else
  • Name is deleted with del
  • Container holding it is deleted/cleared

Inspecting refcount:

import sys

a = []
print(sys.getrefcount(a)) # includes a temporary reference from getrefcount's own argument
b = a
print(sys.getrefcount(a)) # +1, since b now also refers to it

Why it matters

  • Deterministic cleanup: object usually dies the instant its last reference disappears — why __del__ cleanup (e.g. closing files) tends to be predictable in CPython
  • Cost: every assignment, function call, and container operation increments/decrements a counter → overhead. One reason CPython has historically needed the GIL (thread-safe refcount updates without expensive atomic ops on every one) — changing with free-threaded CPython builds

The gap: reference cycles

  • Reference counting alone can't collect cycles — objects referencing each other but unreachable from anywhere else:
class Node:
def __init__(self):
self.other = None

a = Node()
b = Node()
a.other = b
b.other = a
del a, b # refcounts don't hit zero — they reference each other!
  • CPython's separate generational garbage collector (gc module) periodically scans for and collects unreachable cycles
  • Runs independently of refcounting; mainly relevant for objects that can hold references to others (custom classes, containers) — not simple immutables like ints/strings
  • Garbage collector can deal with circular references, which the reference counting mechanism cannot
  • Can be disabled: gc.disable()
  • Can be run manually: gc.collect()

5. Dynamic Typing & type()​

  • Python is a dynamically typed language
  • type() returns the type of the value currently referenced by a variable

How it works:

  1. type(a) returns the class of a. Example: if a = "hello", type(a) returns <class 'str'>
  2. type(type(a)) asks: "What is the class of the class?"
  3. In Python, everything is an object — even classes. The class that defines and creates classes is called a metaclass. By default, Python's built-in metaclass is type

6. Immutable Types​

  • All number types, including Boolean
  • String
  • Tuple
  • Frozen sets
  • User-defined classes with no mutable functions

7. Pass by Reference & Mutability​

  • Everything in Python is passed by reference
  • It is the value's mutability or immutability that decides whether the receiver can modify it in place or not

8. Variable Equality: is vs ==​

Two fundamental ways to think about variable equality:

Memory AddressObject State (data)
Operatoris (identity operator)== (equality operator)
Examplevar_1 is var_2var_1 == var_2

Negation:

Memory AddressObject State (data)
Operatoris not!=
Examplevar_1 is not var_2
not(var_1 is var_2)
var_1 != var_2
not(var_1 == var_2)

9. The None Object​

  • None can be assigned to variables to indicate they are "not set" — an "empty" value (or null pointer)
  • But None is a real object managed by the Python memory manager
  • The memory manager always uses a shared reference when assigning a variable to None

Example:

a = None
b = None
c = None
  • a, b, and c all point to the same None object in memory (e.g. 0x1000)

Testing for "not set":

  • Compare a variable's memory address to None's using the is operator
  • a is None → ✅ correct way
  • x = 10 → x is None → ❌ (would be False, but this isn't the intended check — x isn't set to None)

10. Everything Is an Object​

Data types encountered throughout the course:

  • Integers (int)
  • Booleans (bool)
  • Floats (float)
  • Strings (str)
  • Lists (list)
  • Tuples (tuple)
  • Sets (set)
  • Dictionaries (dict)
  • None (NoneType)

Other constructs:

  • Operators (+, -, ==, is, …)
  • Functions
  • Classes
  • Types

Key idea:

  • Everything in common with all these things: they are all objects (instances of classes)
  • This includes: Functions (function), Classes (class) — not just instances, but the class itself, Types (type)
  • This means they all have a memory address

As a consequence:

  • Any object can be assigned to a variable — including functions
  • Any object can be passed to a function — including functions
  • Any object can be returned from a function — including functions

Example:

def my_func():
...

my_func # references function object at 0x1000
id(my_func) # → 0x1000

Operators — syntax, not objects:

  • The operators themselves (+, -, *, etc.) are syntax, not objects
  • But everything they act on is an object — the operator is shorthand for calling a method on that object
  • a + b translates to a.__add__(b) (or b.__radd__(a) if needed)
  • These __add__, __sub__, __mul__, etc. methods are called "dunder" (double underscore) methods
  • Dunder methods let you customize operator behavior for your own classes

Example:

class Point:
def __init__(self, x, y):
self.x, self.y = x, y

def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # calls p1.__add__(p2)
  • Summary: operators aren't objects, but they're implemented as methods on objects — which is why Python lets you overload them

11. Integer Interning​

Interning: reusing objects on-demand

  • At startup, Python (CPython) pre-loads (caches) a global list of integers in the range [-5, 256]
  • Any time an integer is referenced in that range, Python uses the cached version of that object
  • Singletons — optimization strategy, since small integers show up often

Example — within the cached range:

a = 10
  • Python just points to the existing reference for 10 (no new object created)

Example — outside the cached range:

a = 257
  • Python does not use the global list — a new object is created every time

12. String Interning​

  • Some strings are also automatically interned — but not all!

Identifiers are interned as Python code is compiled:

  • variable names
  • function names
  • class names
  • etc.

Identifiers (naming rules):

  • must start with _ or a letter
  • can only contain _, letters and numbers

Some string literals may also be automatically interned:

  • string literals that look like identifiers (e.g. 'hello_world')
  • although if it starts with a digit, even though that is not a valid identifier, it may still get interned

Important: don't count on it — string interning behavior isn't guaranteed, so don't rely on it in your code

Forcing interning with sys.intern():

  • Not all strings are automatically interned by Python
  • You can force strings to be interned using sys.intern()
import sys

a = sys.intern('the quick brown fox')
b = sys.intern('the quick brown fox')

a is b # → True
  • Comparing with is after interning is much faster than a == b

When should you do this?

  • Dealing with a large number of strings that could have high repetition — e.g. tokenizing a large corpus of text (NLP)
  • Lots of string comparisons

In general: you do not need to intern strings yourself — only do this if you really need to