Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

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

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 used
  • co_varnames: local variable names
  • co_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 dictionary
  • f_globals: the module's global namespace
  • f_builtins: built-in functions
  • f_lineno: what line you're currently on
  • f_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.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.