skip to content

On a Python code object, how do co_varnames and co_names differ?

level: middleimportance: should knowfreq 30%

answer

  1. The compiler sorts names into two buckets
  2. One bucket becomes numbered frame slots
  3. The other is resolved while running
  4. Attribute names land in the run-time bucket
  5. Parameters come first in the local tuple

basics

~20 s

co_varnames lists the function's local variable names, parameters first, which the compiler turned into fixed numbered slots. co_names lists names that must be resolved at run time by name — globals, builtins, attributes and imported modules. Constants live in a third tuple, co_consts.

solid answer

~40 s

The compiler classifies every name in a function body once, and the two tuples record the outcome. `co_varnames` holds the **locals**: parameters first (split by `co_argcount`, `co_posonlyargcount` and `co_kwonlyargcount`), then `*args`, `**kwargs`, then other assigned names. Each becomes a fixed integer slot in the frame, so reading one is an array index. `co_names` holds every name that could not be resolved statically — globals, builtins, attribute names, imported module names — and each of those is looked up **by name at run time**, through the module globals and then builtins for a global, or through the object for an attribute. That gap is why binding a hot global to a local before a loop measurably helps, and why the compile-time classification, not run-time order, decides whether a name is local at all.

code

python · 9 lines
python
def total(n):
    acc = 0
    for i in range(n):
        acc += i
    return acc

print(total.__code__.co_varnames)
print(total.__code__.co_names)
print(total.__code__.co_argcount)

go deeper

for a junior

Recall the split in one line: co_varnames is the function's own local names, co_names is names it has to look up while running, such as globals and attributes.

for a middle

Explain the ordering inside co_varnames, how co_argcount and co_kwonlyargcount partition it, and why a slot read is cheaper than a name lookup through globals and builtins.

for a senior

Turn it into judgement: know when hoisting a global or method into a local before a hot loop is worth the readability cost, and know the limits of reading co_names for static analysis.

for a principal

Decide how far the team goes down this road — micro-optimisations grounded in the code object are legitimate in a measured hot path and noise everywhere else, and that boundary needs to be stated, not improvised.

## One classification pass, two tuples When CPython compiles a function body it makes a decision about every name it sees: can this name be resolved to a slot in the frame, or must it be looked up by name while the code runs? The answer for each name is recorded in one of the code object's tuples. ```python def total(n): acc = 0 for i in range(n): acc += i return acc print(total.__code__.co_varnames) # ('n', 'acc', 'i') print(total.__code__.co_names) # ('range',) print(total.__code__.co_argcount) # 1 ``` `n`, `acc` and `i` are assigned inside the function, so they are locals. `range` is never assigned there, so it stays a run-time lookup. ## co_varnames — the local slots `co_varnames` is ordered, and the order is part of the calling convention: 1. positional parameters — the first `co_argcount` entries, of which the first `co_posonlyargcount` are positional-only; 2. keyword-only parameters — the next `co_kwonlyargcount` entries; 3. `*args` and then `**kwargs`, if present (`co_flags` carries `inspect.CO_VARARGS` and `inspect.CO_VARKEYWORDS` to say so); 4. every other name assigned in the body. `co_nlocals` is simply the length of that tuple. Because each name has a fixed index, the frame can keep locals in a plain array and access them by number — no dictionary, no hashing, no lookup chain. The classification is *static and body-wide*, which produces one of Python's most-reported surprises: if a name is assigned anywhere in the body, it is local for the *whole* body, so reading it before that assignment raises `UnboundLocalError` rather than falling back to the global of the same name. `global` and `nonlocal` are the declarations that move a name out of `co_varnames`. ## co_names — the run-time lookups `co_names` collects the names the compiler could not turn into slots: * globals and builtins read from the body (`range`, `len`, a module-level constant); * **attribute** names — in `obj.method`, the string `method` is in `co_names`, because attribute access is always resolved on the object at run time; * names bound or read by `import`; * at module or class level, the assigned names themselves, since module and class bodies do not use fast local slots the way a function does. ```python co = compile("import math\nr = math.pi * 2", "<string>", "exec") print(co.co_names) # ('math', 'pi', 'r') ``` Resolving one of these is a dictionary lookup in the module globals and then, on a miss, in builtins — cheap, but not free, and repeated on every execution. This is the mechanical reason behind the familiar micro-optimisation of binding a frequently used global or method to a local name just before a hot loop: it converts repeated name lookups into slot reads. It is also why a global can be monkey-patched between calls and the function picks up the new value, whereas a local cannot be reached from outside at all. ## co_consts — the third tuple For completeness, `co_consts` is neither: it holds the *values* the compiler could bake in — literals, results of constant folding, the docstring if there is one, and a nested code object for each `def` or `class` in the body. Two more tuples, `co_freevars` and `co_cellvars`, cover names shared with an enclosing function, which is the closure mechanism's own subject. ## Not every code object uses fast locals Only function bodies get the slot treatment. Compile a module and `co_varnames` comes back empty — every assigned name goes into `co_names` instead, because a module's namespace *is* a dictionary that other code imports from and writes to. Class bodies behave the same way. The `inspect.CO_OPTIMIZED` and `inspect.CO_NEWLOCALS` bits of `co_flags` record the difference, and they are the reason an identical assignment statement is a slot write inside a function and a dictionary write at module level. ## Where this shows up in an interview Usually as a follow-up to "why is a local variable faster than a global?" or "why does this raise `UnboundLocalError`?". Being able to point at the code object and say *the compiler already decided, and here is the tuple that records it* converts a memorised rule into an explanation. It also grounds introspection work: tooling that wants a function's parameter names reads `co_varnames[:co_argcount]`, and tooling that wants to know what module-level names a snippet touches reads `co_names`. One caution: neither tuple is a complete dependency list. `co_names` does not distinguish a global from a builtin from an attribute, and dynamic access — `getattr(obj, name)`, a lookup through a dict — leaves no entry at all. It is a compile-time record of static name usage, and it should be read as exactly that.

  • How would you recover a function's parameter names from its code object?
    Slice `co_varnames` with the counts: the first `co_argcount` entries are positional (the leading `co_posonlyargcount` of those positional-only), the next `co_kwonlyargcount` are keyword-only, and `co_flags` tells you whether `*args` and `**kwargs` follow. In real code you would use the higher-level signature introspection instead, but knowing the layout explains what it reads.
  • Why does reading a name before assigning it in the same function raise UnboundLocalError?
    Because the classification is static and covers the whole body: an assignment anywhere makes the name local, so it occupies a slot in `co_varnames` and never falls back to the global of the same name. The slot is simply empty until the assignment runs. Declaring `global` or `nonlocal` moves the name out of `co_varnames`.
  • Is co_names a reliable list of everything a function depends on?
    No. It records only statically written names, without distinguishing globals from builtins from attributes, and anything reached dynamically — through `getattr` with a computed string, or a dictionary lookup — leaves no entry. It is useful for rough static analysis, but it cannot be treated as a complete dependency set.

saying these in an interview costs you the question

  • Says co_names lists the local variables
  • Thinks locals are stored in a per-call dictionary
  • Does not know attribute names appear in co_names
  • Confuses co_consts with co_names
  • Believes name classification happens at call time
  • Treats co_names as a complete dependency list

context