Python
Python Functions Are Objects: Understanding Code Objects, Frames, and Bytecode
Every Python function is actually an object with a code object, frame, and callable API. Learn how bytecode, local variable slots, closures, and frames work under the hood to demystify Python's execution model.
June 2026 · 8 min read · 1 views · 0 hearts
Advertisement
Your Python Function Is a Lie (And That's Brilliant)
You write def hello(): and Python creates something far stranger than you think. Not just a chunk of code — but an honest-to-goodness object that can be passed around, inspected, and even swapped at runtime. Let's pop open the hood.
Functions Are Just Objects with Parentheses
When Python parses def, it doesn't just store instructions. It creates an object of type function, assigns it to the name you gave, and puts it in your namespace. You can prove this in 30 seconds:
def greet():
return "Hello"
print(type(greet)) # <class 'function'>
print(greet.__name__) # "greet"
That greet variable? It holds a reference to a function object. The parentheses () are just the trigger that calls __call__ on that object. No magic — just consistent object-oriented design.
The Real Magic: The Code Object
Deep inside every Python function lives a code object (accessible via greet.__code__). This is a compiled, immutable blob containing:
co_code: raw bytecode instructions (the actual machine-independent VM code)co_consts: all the constants in the function (strings, numbers, tuples)co_names: variable names usedco_varnames: local variable namesco_stacksize: how much stack space Python's VM needs
When you run def foo(x): return x + 1, Python compiles that body to bytecode once and stores it in foo.__code__. Every call reuses the same code object — no recompilation.
The Frame: Where the Action Happens
When you call a function, Python creates a frame object. This is runtime memory, not code. It contains:
f_locals: your local variables dictionaryf_globals: the module's global namespacef_builtins: built-in functionsf_lineno: what line you're currently onf_back: the calling frame (this is your stack trace chain)
You can inspect your own call stack at any time:
import sys
def inner():
frame = sys._getframe()
print(f"Running on line {frame.f_lineno}")
print(f"Local vars: {list(frame.f_locals.keys())}")
inner() # prints live frame info
Python functions aren't "running" as a single blob — they're lazily executing bytecode inside a frame that's pushed onto the call stack, one instruction at a time.
The Local Variables Mystery
Ever wonder how locals() works? It returns f_locals from the current frame. But here's a gotcha: inside a function, Python knows all local variable names at compile time (they're in co_varnames). The frame pre-allocates slots for them — no dictionary needed until you actually call locals().
That's why Python is faster than you'd think for local variables. No hash lookups. Just slot index into an array. x = 5 doesn't hash "x" — it stores 5 at slot 0 of f_locals.
Closures Are Captured Frames
When you write a closure:
def outer(x):
def inner():
return x + 1
return inner
Python doesn't just return the inner function object. It bundles inner with a cell object holding x. That cell survives even after outer() finishes. The cell lives in inner.__closure__ — a tuple of cell objects, one for each captured variable.
The Performance Trick: Caching in Bytecode
CPython's VM can skip some lookups. When your function calls len(something), Python checks at compile-time if len is a built-in from __builtins__. If so, it uses an optimized LOAD_BUILD_CLASS style opcode — no global dict lookup every call. Same for None, True, False — they're stored in co_consts and loaded directly.
What Actually Breaks When You're Debugging
Next time you pdb.set_trace() inside a function, you're literally stopping the frame mid-execution. The bytecode pointer (offset in co_code) freezes. You can inspect f_lineno, change local variables by modifying f_locals (though CPython won't re-read them for live code — classic gotcha), and even dump the raw bytecode:
import dis
dis.dis(your_function)
That prints the human-readable version of those bytecodes. You'll see LOAD_FAST, STORE_FAST, BINARY_ADD — the machine language of the Python VM.
The Takeaway
Python functions aren't "running code" — they're executable objects that push frames onto a virtual machine stack, executing precompiled bytecode against pre-allocated local variable slots, all wrapped in a callable API.
Understanding this makes closures, generators, decorators, and even recursion stack traces click into place. You're not writing code. You're orchestrating objects that orchestrate frames. And that's the kind of lie you can depend on.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.