Python Code Object vs Function Object Explained

In Python, code execution depends on two related but fundamentally different internal structures: code objects and function objects. This article breaks down the internal architecture of both, explaining how a code object serves as an immutable, static container for bytecode and compilation metadata, while a function object acts as a dynamic runtime wrapper that binds that bytecode to execution contexts, variable scopes, closures, and default arguments.

The Code Object (PyCodeObject)

A code object represents compiled, executable bytecode. It is produced by Python’s compiler when a module, class, function, or string of code is parsed and compiled (via the compile() built-in or during standard source execution).

Internally, code objects are entirely static and immutable. They contain no reference to runtime state or execution environments. A code object contains only the instructions and the literals needed to carry out an operation:

Because code objects lack an execution environment, they cannot be called directly like regular functions. To execute a code object on its own, it must be evaluated using exec() or eval(), which manually supply the global and local namespaces.

The Function Object (PyFunctionObject)

A function object is created at runtime when Python executes a def statement or a lambda expression. It serves as a dynamic, callable interface that wraps around a code object and injects the context necessary for execution.

Unlike code objects, function objects are mutable and contain runtime state:

Function objects implement Python's call protocol (__call__). When a function is called, the interpreter constructs a frame object (PyFrameObject) combining the function’s __code__ with its __globals__, __closure__, and runtime arguments.

Core Internal Differences

Aspect Code Object (types.CodeType) Function Object (types.FunctionType)
Creation Time Compile time (static) Runtime (when the definition is evaluated)
Mutability Strictly immutable Mutable (attributes, defaults, and docstrings can change)
Direct Invocation Cannot be invoked directly with () Callable via the () operator
Environment Scope-agnostic; no knowledge of runtime variables Bound to a specific __globals__ dict and __closure__
Default Arguments Unaware of defaults Stores default argument values

One Code Object, Multiple Function Objects

Because code objects are immutable and decoupled from runtime data, Python optimizes performance by reusing them. When a factory function or closure creates multiple inner functions, the interpreter compiles the inner block once into a single code object. Each call to the factory then generates a distinct function object with unique closures or defaults, all pointing to the exact same underlying __code__.