Cheat SheetsPython A–ZInternals & Performance

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 -lv
3

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 index
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/python