Python: Functions — Code Objects

Python Code Objects

Learning outcomes:

  • What are code objects
  • The __code__ attribute
  • The co_nlocals code object attribute
  • The co_varnames code object attribute
  • The co_names code object attribute
  • The co_freevars and co_cellvars code object attributes

Introduction

Python is a really flexible and powerful language capable of doing nearly every thing a developer can think of. Functions in Python are given special importance, as we've already seen in the last two chapters of this unit.

In this chapter, we shall see what are code objects for given functions and how they can be used to make certain decisions within functions.

Let's begin!

What are code objects?

Code objects are simply looks into the different aspects of a compiled function.

A code object describes different aspects of a function, based on its byte code.

They expose various details about a function such as the number of positional arguments it must have; the number of its local variables; its free variables; and so on and so forth.

All these details regarding a function are collected when the bytecode for the Python program is generated. That's why these objects are called 'code' objects — they basically represent information given in the bytecode.

Every function in Python has a __code__ attribute that holds its code object.

This attribute contains a reference to the code object for the function. In the sections below, we'll see how to work with the __code__ attribute.

Inspecting the __code__ attribute

The __code__ attribute is one of the many attributes available on a Python function. As stated above, it represents the code object for the function.

Consider the code below:

Python
def f(x):
    return x

It simply defines a function f() that returns the provided argument.

Let's inspect the __code__ attribute of this function f:

f.__code__
<code object f at 0x7f45b5d1e9d0, file "<stdin>", line 1>

We get back some representation of the code object, which is almost useless to us. What's a lot more useful is to inspect further attributes on this code object.

Based on the docs of Python, a code object has the following properties on it:

  1. co_nlocals — is the number of local variables used by the function (including arguments).
  2. co_argcount — is the total number of positional arguments (including positional-only arguments and arguments with default values).
  3. co_varnames — is a tuple containing the names of the local variables (starting with the argument names).
  4. co_names — is a tuple containing the names used by the bytecode.
  5. co_cellvars — is a tuple containing the names of local variables that are referenced by nested functions.
  6. co_freevars — is a tuple containing the names of free variables; co_code is a string representing the sequence of bytecode instructions.
  7. co_posonlyargcount — is the number of positional-only arguments (including arguments with default values).
  8. co_kwonlyargcount — is the number of keyword-only arguments (including arguments with default values).
  9. co_firstlineno — is the first line number of the function.
  10. co_lnotab — is a string encoding the mapping from bytecode offsets to line numbers (for details see the source code of the interpreter).
  11. co_stacksize — is the required stack size.
  12. co_code — is a string representing the sequence of bytecode instructions.
  13. co_consts — is a tuple containing the literals used by the bytecode.
  14. co_flags — is an integer encoding a number of flags for the interpreter.

Let's take a closer look at the most common and useful of these properties.

co_nlocals

co_nlocals holds the total number of local variables of a function. This obviously includes arguments as well, since arguments are also local variables.

Consider the function below:

Python
def f(a, b, c):
    d = 40
    print(a, b, c, d)

print(f.__code__.co_nlocals)
4

As you can see, f.__code__.co_nlocals returns 4 since f() has a total of 4 local variables. One is a pure local variable (i.e d) while the rest three are parameters (i.e a, b and c).

In the code above, the function f() is not called, yet its information is accessible to us.

co_argcount

As mentioned in Python's docs:

co_argcount is the total number of positional arguments (including positional-only arguments and arguments with default values).

As we know from the previous chapter, we can make 5 classifications of parameters of a function:

  1. Positional-only
  2. Positional or keyword
  3. Keyword-only
  4. *args
  5. **kwargs

The arg_count attribute considers the first two of these.

Let's check it out:

Python
def f(a, /, b, *, c, d):
    pass

Here we have a function with four parameters: one is positional-only, one is normal while the rest two are keyword-only. There isn't really a need to execute anything in the function, likewise we leave it empty using the keyword pass.

If you don't know what's the purpose of the characters / and * over here, you can consider reading the previous Python Function Arguments chapter.

Below we inspect its co_argcount:

f.__code__.co_argcount
2

The co_argcount here is 2 owing to the first two parameters of the function i.e a and b.

Why does co_argcount use the word 'arg' ?

You might be confused as to why does the attribute co_argcount have the word 'arg', which we know stands for 'argument'.

Technically speaking, a function can have a variable number of arguments so co_argcount can only get a value at the time of calling the function. But we know that the attribute gets a value when the function is compiled, — not yet run.

Isn't this confusing?

The thing is that 'arg' here technically refers to the term 'formal arguments' of the function, which are also known as parameters. That's it!

co_varnames

The co_varnames attribute contains a tuple holding all the names of local variables of the function starting from the names of parameters.

Consider the code below:

Python
def f(a, b, c = 10):
    x = 10
    y = 20

Here the function has three parameters a, b and c, and two pure local variables x and y.

As expected, the co_varnames tuple begins at the parameters and then goes to the actual local variables:

f.__code__.co_varnames
('a', 'b', 'c', 'x', 'y')
For a function f(), len(f.__code__.co_varnames) is equal to f.__code__.co_nlocals.

The order of parameters and local variables in the co_varnames tuple, is the same as that of the parameters and local variables in the function's definition.

This is illustrated below:

Python
def f(b, a, c = 10):
    y = 10
    x = 20

Compared to the previous code, here we've merely changed the order of parameters and the variables of the function.

Let's now see what we get in co_varnames:

f.__code__.co_varnames
('b', 'a', 'c', 'y', 'x')

The order of elements in the tuple has changed according to the function's definition, just as was stated.

co_names

The co_names attribute holds a tuple containing all global and built-in names used by a function.

As the simplest example, consider the code below:

Python
a = 10

def foo(b, c, d):
   def bar():
      print(a, b, c, d)
      pass

   print(bar.__code__.co_names)

foo(1, 2, 3)

What interests us here is the output of line 7, that tells us the co_names for the function bar(). Following is the output:

('print', 'a')

The tuple holds two names, 'print' and 'a', referring to a built-in function and a global variable, respectively.

See how the function bar() refers to some variables of the enclosing function — b, c and d — yet they don't get mentioned in co_names. This is because they aren't global or built-in names.

co_freevars

The co_freevars attribute returns a tuple containing the names of all free variables of a function.

So what exactly are free variables?

Any non-local, non-global, variable used by a function is referred to as a free variable of the function.

For instance, consider the code below:

Python
def foo():
    x = 10

    def bar():
        print(x)
    return bar

The function bar() refers to a variable x that is neither a global variable, nor a local variable of the function. Rather, it belongs to the local scope of the enclosing function foo().

Hence, for bar(), x is a free variable.

bar = foo()
bar.__code__.co_freevars
('x',)

The concept of free variables is crucial to understand before being able to fully comprehend the idea of closures in Python. Both of these concepts will be discussed in detail in the next chapter on Python Function Closures.

So if free variables trouble you right now, don't worry — we'll dig into every single bit of information regarding them in the next chapter.

co_cellvars

The co_cellvars attribute, for a given function, returns a tuple containing the names of all cell variables of the function.

Once again, what are cell variables?

A cell variable is an idea related to that of a free variable. If some variable x is a free variable for a function A() then that same variable would be a cell variable for a function B().

Formally:

For a given function, a cell variable is a local variable that is used by a nested function.

Consider the same previous code below:

Python
def foo():
    x = 10

    def bar():
        print(x)
    return bar

As we know from the previous section, x here is a free variable for bar() since it's not local to bar(), neither a global variable.

But for foo(), this same variable x is a cell variable. This is because, it's local to foo() and used by an inner function — bar() in this case.

foo.__code__.co_cellvars
('a',)

As before, if you are having a hard time understanding what exactly is a cell variable, or that why is it even called a cell variable, don't worry — the next chapter will address all your questions.