Internals & Performance — Cheat Sheet
Python A–Z · 3 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Internals & Performance
Python A–Z3 topicsQuick revision reference
1
Memory Model — Names, References, refcount & GC
Every value is a heap object; variables are just names bound to objects. Reference counting frees most objects instantly, the cycle collector handles the rest, and __slots__ shrinks per-instance memory.
- ✓Variables are names bound to heap objects; assignment never copies
- ✓Refcount hits 0 → freed instantly; the generational GC only breaks cycles
- ✓is checks identity — use it for None, never for numbers or strings
- ✓__slots__ drops the per-instance __dict__: big memory wins for hot classes
Assignment binds names; is = identity, == = value
import sys a = [1, 2, 3] b = a # binds b to the SAME list object b.append(4) print(a) # [1, 2, 3, 4] — one object, two names print(a is b, id(a) == id(b)) # True True # Reference count: object is freed the instant this reaches 0 print(sys.getrefcount(a)) # 3 → a, b, plus the temporary argument # Small-int caching / string interning — why 'is' lies about values x, y = 256, 256 print(x is y) # True — cached singleton x, y = 257, 257 print(x is y) # often False — separate objects, equal values print(x == y) # True — ALWAYS compare values with == # The only correct uses of 'is': None, True, False, sentinels flag = None print(flag is None) # ✓ idiomatic
2
Profiling & Optimization — Measure Before You Tune
timeit for micro-benchmarks, cProfile to find where a real program spends time — then apply the standard wins: builtins and comprehensions, set/dict lookups, functools.cache, and NumPy for numeric loops.
- ✓Measure first: timeit for snippets, cProfile -s cumtime for programs
- ✓Optimize only the top of the profile — everything else is noise
- ✓Sets/dicts for membership, builtins/comprehensions for loops, cache for pure repeats
- ✓NumPy vectorization for numeric arrays; native rewrites are the last resort
timeit for snippets, cProfile -s cumtime for programs
import timeit
# Which is faster? Never guess — timeit it.
concat = timeit.timeit(
's = ""\nfor w in words: s += w',
setup='words = ["x"] * 1000',
number=2000,
)
join = timeit.timeit(
'"".join(words)',
setup='words = ["x"] * 1000',
number=2000,
)
print(f"+= loop : {concat:.3f}s")
print(f"join : {join:.3f}s") # typically ~10x faster
# Whole-program profiling:
# python -m cProfile -s cumtime app.py | head -20
# ncalls tottime cumtime function
# 1000 0.02 8.41 fetch_report ← optimize THIS
# 50000 3.90 3.90 parse_row
# cumtime = time including callees; start at the top, ignore the rest.
# Line-level detail: pip install line_profiler → @profile + kernprof -lv3
Bytecode & How CPython Runs Your Code
CPython compiles source to bytecode (inspect it with dis), caches it in __pycache__, and executes it in the interpreter loop — since 3.11, an adaptive specializing interpreter rewrites hot bytecode for big speedups.
- ✓Source compiles to bytecode for a stack VM; dis.dis shows the instructions
- ✓LOAD_FAST (locals, array index) beats LOAD_GLOBAL (dict lookup) — locals are faster
- ✓__pycache__/.pyc caches bytecode per interpreter version; auto-invalidates on edit
- ✓PEP 659 adaptive specialization makes 3.11+ dramatically faster — upgrades are free perf
Bytecode answers atomicity and locals-vs-globals questions
import dis
def total_marks(marks):
total = 0
for m in marks:
total += m
return total
dis.dis(total_marks)
# LOAD_CONST 1 (0) ← push 0
# STORE_FAST 1 (total) ← total is slot 1: array access, fast
# LOAD_FAST 0 (marks)
# GET_ITER
# FOR_ITER ...
# LOAD_FAST 1 (total) ← load, add, store: three separate steps
# LOAD_FAST 2 (m)
# BINARY_OP 13 (+=)
# STORE_FAST 1 (total) ← a thread could be switched mid-sequence:
# ... this is WHY += is not atomic
import math
def use_global(): return math.pi # LOAD_GLOBAL — dict lookups
def use_local():
pi = math.pi # bind once...
return pi # LOAD_FAST — array indexLearn this free with Aria, your AI tutor → AiCanCode.org/learn/python