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:
co_code: The raw sequence of bytecode instructions.co_consts: A tuple of literal constants loaded during execution (numbers, strings,None, and nested code objects).co_names: Names of global variables and attributes referenced by the bytecode.co_varnames: A tuple containing local variable names.co_argcount: The number of positional arguments accepted.co_cellvarsandco_freevars: Variable names needed to support lexical scoping and closures.
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:
__code__: The internal reference to the underlying code object containing the bytecode to be executed.__globals__: A reference to the global dictionary of the module where the function was defined.__closure__: A tuple of cell objects providing values for free variables captured from enclosing scopes.__defaults__and__kwdefaults__: Stored default values for positional and keyword-only arguments.__doc__and__annotations__: Metadata such as docstrings and type hints.__dict__: An attribute dictionary allowing arbitrary attributes to be attached to the function instance.
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__.