skip to content

What does a Python function's __code__ attribute give you?

level: juniorimportance: should knowfreq 28%

answer

  1. A function is a wrapper around something
  2. The compiler's output, one per def
  3. Immutable, shared, reached through a dunder
  4. Its attributes share a two-letter prefix
  5. Holds bytecode plus static metadata only

basics

~20 s

A function's code is its code object: the compiled bytecode for the body plus the static facts the compiler worked out — name, filename, argument count, local names, constants and flags. It is immutable, and every function built from the same def shares it.

solid answer

~40 s

`__code__` exposes the **code object** the compiler produced for that function's body. It carries the bytecode and purely static metadata: `co_name` and `co_filename`, `co_firstlineno`, `co_argcount`, `co_varnames` (the local-variable slots), `co_names` (names looked up at runtime, such as globals and attributes), `co_consts` (folded literals, the docstring, and nested `def`/`class` code objects) and `co_flags` (a bitfield saying whether the body is a generator, a coroutine, an async generator, and so on). It carries nothing that depends on *when the function was created* — default values, globals, the closure and `__dict__` all live on the function object. Code objects are immutable and hashable; `co.replace(...)` returns a modified copy rather than editing in place.

code

python · 7 lines
python
def area(w, h=2):
    return w * h

co = area.__code__
print(co.co_name, co.co_argcount, co.co_varnames)
print(co.co_consts)
print(area.__defaults__)

go deeper

for a junior

Be able to say that f.__code__ is the compiled code object for the body, and name two or three of its attributes, such as co_name, co_argcount and co_varnames.

for a middle

Explain the split precisely: static compiler output on the code object, per-creation state such as __defaults__, __globals__ and __closure__ on the function object, and why the code object is immutable and shared.

for a senior

Show where this matters in practice — tracebacks rendered from co_filename/co_firstlineno, introspection and instrumentation tools, and the limits of rebinding __code__ in a running process.

for a principal

Own the policy on runtime code manipulation: when generating or swapping code objects is a legitimate tool for tracing and coverage, and when it becomes machinery nobody on the team can debug.

## Two objects behind every `def` A Python `def` statement produces **two** distinct objects at two distinct moments, and `__code__` is the link between them. When CPython compiles a module, each `def` in the source is compiled into a **code object** — an immutable object holding the bytecode for that function's body plus every fact the compiler could work out statically. That happens once, at compile time. Later, when the module actually *runs* and control reaches the `def` statement, CPython creates a **function object** and points its `__code__` attribute at the code object that was already compiled. Executing the same `def` a second time (a factory called twice, a decorator applied twice, a method defined inside a loop) creates a second function object over the *same* code object. ## What the code object carries The attributes are conventionally prefixed `co_`: * `co_name` and `co_qualname` — the name written in the source (`co_qualname`, the dotted qualified name, was added in 3.11). Rebinding `f.__name__` does **not** change `co_name`, which is why tracebacks keep showing the original name. * `co_filename` and `co_firstlineno` — where the source lives. Tracebacks are rendered from these. * `co_argcount`, `co_posonlyargcount`, `co_kwonlyargcount` — how the parameter names in `co_varnames` split into positional-only, positional-or-keyword and keyword-only. * `co_varnames` — the tuple of local-variable names, parameters first. These are compiled to fixed integer slots. * `co_names` — names that must be resolved by name at run time: globals, builtins, attribute names, imported module names. * `co_consts` — the constants the compiler baked in: literals, the results of constant folding, the docstring if there is one, and a nested code object for every `def` or `class` in the body. * `co_flags` — a bitfield. `inspect.CO_GENERATOR`, `inspect.CO_COROUTINE`, `inspect.CO_ASYNC_GENERATOR`, `inspect.CO_VARARGS` and `inspect.CO_VARKEYWORDS` are the bits worth knowing; `inspect.isgeneratorfunction` and `inspect.iscoroutinefunction` are the readable front ends for them. * `co_stacksize`, `co_freevars`, `co_cellvars` and the raw `co_code` round it out. ```python def area(w, h=2): return w * h print(area.__code__.co_varnames) # ('w', 'h') print(area.__code__.co_argcount) # 2 print(area.__code__.co_consts) # (None,) print(area.__defaults__) # (2,) ``` Notice the last two lines: the default `2` is **not** in `co_consts`. It is on the function. ## What the code object does not carry Everything whose value depends on when the function object was created stays on the function: `__defaults__` and `__kwdefaults__`, `__globals__` (the module namespace the function will resolve globals in), `__closure__`, `__dict__` (attributes you attach to the function), `__module__`, and `__doc__` — the docstring starts out as `co_consts[0]`, but assigning `f.__doc__` changes only the function, leaving the code object untouched. Since 3.14, annotations are computed lazily through the function's `__annotate__` and `__annotations__`, again on the function, not the code. ## Immutable, shared, and swappable Code objects cannot be mutated. `types.CodeType.replace` returns a *new* code object with some fields changed, which is how instrumentation tools adjust a filename or a line number. What you *can* do is rebind the attribute: `f.__code__ = g.__code__` swaps the body of `f` wholesale, subject to one check — the new code object must want the same number of free variables, otherwise CPython raises `ValueError`. That trick is the basis of some hot-patching and coverage tools; it is not something to reach for in application code. ## Code objects without a `def` The built-in `compile()` hands you one directly, for a whole module or a single expression, which is the quickest way to inspect what the compiler produced: ```python mod = compile("def outer():\n def inner():\n pass\n", "<s>", "exec") print(mod.co_consts[0].co_consts[0]) # <code object inner ...> ``` Nested definitions nest the same way in the constants tuple: the module's code object holds `outer`'s code object, which holds `inner`'s. `dis.code_info` prints a readable summary of any of them, and `types.FunctionType(code, globals)` turns one back into something callable. That round trip is the clearest proof of what the two objects are for — the code object is the compiled artefact, the function object is the thing that can be called. ## Why interviewers ask The question separates people who think a function *is* its body from people who know a function is a small runtime record pointing at a compiled body. That distinction explains decorators, `functools.wraps`, why mutable default arguments persist, why a traceback names a function you thought you had renamed, and how introspection tools work — so it is worth being able to name three or four `co_` attributes without hesitating.

  • Where does a function's docstring actually live?
    The compiler stores it as the first entry of `co_consts`, and the function object's `__doc__` is initialised from there. Assigning `f.__doc__ = 'new'` rebinds only the function attribute; the code object still holds the original string, and another function built from the same code object still starts with the original docstring.
  • Can you modify a code object in place?
    No. Code objects are immutable and hashable. `types.CodeType.replace` gives you a new code object with selected fields changed, and you then rebind `f.__code__` to it. CPython validates the swap only loosely: it checks that the new code object needs the same number of free variables, and raises `ValueError` otherwise.
  • Why does a traceback still show the original name after you rebind __name__?
    Traceback frames are rendered from the executing code object, so they use `co_name`, `co_filename` and the line table. `__name__` is a separate, writable attribute of the function object. That is also why `functools.wraps` fixes `repr` and documentation tooling but does not change what a traceback prints for the wrapper's own frame.

The code object is the printed sheet music; the function object is one performer holding that sheet, with their own tuning, key and cue notes. Two performers can read the same sheet and still sound different.

saying these in an interview costs you the question

  • Says the code object stores default argument values
  • Thinks each call compiles the function body again
  • Believes co_names lists the function's local variables
  • Claims code objects can be mutated in place
  • Cannot name a single co_ attribute
  • Confuses the function object with its code object

context