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 3stored at addresses0x1000,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 0x1000my_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 = 10print(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 (
gcmodule) 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:
type(a)returns the class ofa. Example: ifa = "hello",type(a)returns<class 'str'>type(type(a))asks: "What is the class of the class?"- 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 Address | Object State (data) | |
|---|---|---|
| Operator | is (identity operator) | == (equality operator) |
| Example | var_1 is var_2 | var_1 == var_2 |
Negation:
| Memory Address | Object State (data) | |
|---|---|---|
| Operator | is not | != |
| Example | var_1 is not var_2not(var_1 is var_2) | var_1 != var_2not(var_1 == var_2) |
9. The None Object
Nonecan be assigned to variables to indicate they are "not set" — an "empty" value (or null pointer)- But
Noneis 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, andcall point to the sameNoneobject in memory (e.g.0x1000)
Testing for "not set":
- Compare a variable's memory address to
None's using theisoperator a is None→ ✅ correct wayx = 10→x is None→ ❌ (would beFalse, but this isn't the intended check —xisn't set toNone)
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 + btranslates toa.__add__(b)(orb.__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
isafter interning is much faster thana == 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