In Python (I'm mostly talking about CPython here, but other implementations do similar things), when you write the following:
    def spam(x):
        return x+1
    spam(3)
What happens?

Really, it's not that complicated, but there's no documentation anywhere that puts it all together. Anyone who's tried hacking on the eval loop, understands it, but explaining it someone else is very difficult. In fact, the original version of this was some notes to myself, which I tried to show to someone who I'm pretty sure is at least as smart as me, and he ended up completely lost. So I reorganized it so that, at least hopefully, you can start each section and then skip the rest of the section when you get over your head (and maybe skip the last few sections entirely) and still learn something useful. If I've failed, please let me know.

Compilation

To make things simpler to explore, let's put all of that inside an outer function for the moment. This does change a few things (notably, spam becomes a local within that outer function rather than a global), but it means the code that creates and calls the function sticks around for us to look at. (Top-level module code is executed and discarded at module import time; anything you type at the REPL is compiled, executed, and discarded as soon as the statement is finished; scripts work similar to the REPL.)

So, there are two top-level statements here.

The def statement creates a function object out of a name and a body and then stores the result in a local variable corresponding to that name. In pseudocode:
    store_local("spam", make_function("spam", spam_body))
The call expression statement takes a function object and a list of arguments (and an empty dict of keyword arguments) and calls the function (and then throws away the result). In pseudocode:
    call_function(load_local("spam"), [3], {})
So, the only tricky bit here is, what is that spam_body? Well, the compiler has to work recursively: it sees a def statement, and it knows that's going to compile into a make_function that takes some kind of body-code object, so it compiles the body suite of the def into that object, then stashes the result as a constant. Just like the number 3 and the string "spam" are constants in the outer function, so is the code object spam_body.

So, what does the body do? It's pretty simple:
    return(add(load_local("x"), 1))
Notice that parameters like x are just local variables, the same as any you define inside the function; the only difference is that they get an initial value from the caller's arguments, as we'll see later.

Obviously real Python bytecode is a bit different from this pseudocode, but we'll get back to that at the very end. There are a few easier questions to get through first, starting with: what kind of thing is a code object?

Code objects

You need more than just compiled bytecode to store and execute a function body, because the bytecode references things like constants, which have to go somewhere, and because that call_function is going to need some information about its parameters to know how to map the first argument 3 to the local variable x.

The object with all this information is a code object. The inspect module docs give some detail of what's in a code object. But some of the key members are:
  • co_consts, a tuple of constant values. In spam this just has the number 1. In the top-level function, it has the number 3, the string "spam", and the code object for the spam body.
  • co_argcount and related values that specify the calling convention. For spam, there's an argcount of 1, meaning it's expecting 1 positional parameter, and also meaning its first 1 local variables in co_varnames are parameters. (This is why you can call it with spam(x=3) instead of spam(3).) The full details of how arguments get mapped to parameters is pretty complicated (and I think I've written a whole blog post about it).
  • co_code, which holds the actual compiled bytecode, which we'll get to later.
There's also a bunch of stuff there that's only needed for tracebacks, reflection, and debugging, like the filename and line number the source code came from.

So, the compiler, after recursively compiling the inner function body into bytecode, then builds the code object around it. You can even do this yourself in Python, although it's a bit of a pain—type help(types.CodeType) and you'll see the 15-parameter constructor. (Some of those parameters are related to closures, which I'll get to later.)

Function objects

The make_function pseudocode above just takes a code object and a name and builds a function object out of it.

Why do we need a separate function object? Why can't we just execute code objects? Well, we can (via exec). But function objects add a few things.

First, function objects store an environment along with the code, which is how you get closures. If you have 16 checkboxes, and an on_check function that captures the checkbox as a nonlocal variable, you have 16 function objects, but there's no need for 16 separate code objects.

In the next few sections, we'll ignore closures entirely, and come back to them later, because they make things more complicated (but more interesting).

Function objects also store default values, which get used to fill in parameters with missing arguments at call time. The fact that these are created at def time is useful in multiple ways (although it also leads to the common unexpected-mutable-default bug).

If you look at the inspect module, you'll see that the key attributes of a function are just the code object in __code__, the default values in __defaults__ and __kwdefaults__, and the closure environment in __closure__ (for nonlocals, which I'll explain later) and __globals__ (for globals—these aren't captured individually; instead, an entire globals environment is). And, of course, a bunch of stuff to aid tracebacks, debugging, reflection, and static type-checking.

You can do the same thing as that make_function pseudocode instruction from Python—try help(types.FunctionType) to see the parameters to the constructor. And now you know enough to do some simple hackery, like turning spam from a function that adds 1 to a function that adds 2:
    c = spam.__code__
    consts = tuple(2 if const==1 else const 
                   for const in c.co_consts)
    nc = types.CodeType(
        c.co_argcount, c.co_nlocals, c.co_stacksize, 
        c.co_flags, c.co_code, consts, c.co_names, 
        c.co_varnames, c.co_filename, c.co_name, 
        c.co_firstlineno, c.co_lnotab, c.co_freevars, 
        c.co_cellvars)
    spam = types.FunctionType(
        nc, spam.__name__, spam.__defaults__, spam.__closure__)
There are a few limits, but most things you can imagine are doable, and work the way you'd expect. Of course if you want to get fancy, you should consider using a library like byteplay instead of doing it manually.

Fast Locals

In reality, Python doesn't do load_local("x"), looking up x in a dict and returning the value, except in special cases. (It does do that for globals, however.)

At compile time, the compiler makes a list of all the locals (just x here) and stores their names in the code object's co_varnames, and then turns every load_local("x") into local_fast(0). Conceptually, this means to do a load_local(co_varnames[0]), but of course that would be slower, not faster. So what actually happens is that the local variables are stored in an array that gets created at the start of the function call, and load_fast(0) just reads from slot #0 of that array.

Looking things up by name (a short string whose hash value is almost always cached) in a hash table is pretty fast, but looking things up by index in an array is even faster. This why the def foo(*, min=min) microoptimization works—turning load_global("min") into load_local("min") might help a little (because the local environment is usually smaller, not to mention that builtins require an extra step over normal globals), but turning it into load_fast(0) helps a lot more.

But it does mean that if you call locals(), Python has to build a dict on the fly—and that dict won't be updated with any further changes, nor will any changes you make to the dict have any effect on the actual locals. (Usually. If, say, you're executing top-level code, the locals and globals are the same environment, so changing locals does work.)

That's also why you usually can't usefully exec('x = 2')—that again just creates a dict on the fly with a copy of the locals, then executes the assignment against that dict. (In Python 2, exec was a statement, and it would create a dict on the fly, execute the code, and then try to copy changed values back into the array. Sometimes that worked, but there were tons of edge cases. For example, if you never had a non-exec assignment to x, the compiler couldn't know to put x in co_varnames in the first place.)

Finally, that means closures can't be implemented as just as stack of dicts (as in many Lisp implementations), but Python has a different optimization for them anyway, as we'll see later.

Calling

OK, so now we know everything about how the def statement works (except for the actual bytecode, which I'll get to later, and closure-related stuff, which I'll also get to later). What about the call expression?

To call a function, all you need to supply is the function object and a list of positional arguments (and a dict of keyword arguments, and, depending on the Python version, *args and/or **kw may be part of the call rather than splatting them into the list/dict in-place… but let's keep it simple for now). What happens inside the interpreter when you do this?

Well, first, calling a function actually just looks up and calls the function object's __call__ method. That's how you can call class objects, or write classes whose instances are callable. Near the end, we'll get into that. But here, we're talking about calling an actual function object, and the code for that works as follows.

The first thing it needs to do is create a new stack frame to run the function's code in. As with codes and functions, you can see the members of a frame object in the inspect docs, and you can explore them in the interactive interpreter, and you can find the type as types.FrameType (although, unlike code and function objects, you can't construct one of these from Python). But the basic idea is pretty simple:

A frame is a code object, an environment, an instruction pointer, and a back-pointer to the calling frame. The environment is a globals dict and a fast locals array, as described earlier. That's it.

You may wonder how the compiler builds that list of co_varnames in the first place. While it's parsing your code, it can see all the assignments you make. The list of local variables is just the list of parameters, plus the list of names you assign to somewhere in the function body. Anything you access that isn't assigned to anywhere is (ignoring the case of closures, which we'll get to later) is a global; it goes into co_names, and gets looked up as a global (or builtin) at runtime.

To construct a frame, in pseudocode:
    code = func.__code__
    locals = [_unbound for _ in range(code.co_nlocals)]
    do_fancy_arg_to_param_mapping(locals, args, kwargs)
    frame = Frame()
    frame.f_back = current_frame
    frame.f_lasti = 0
    frame.f_code = code
    frame.f_locals = locals
And then all the interpreter has to do is recursively call interpreter(frame), which runs along until the interpreter hits a return, at which point it just returns to the recursive caller.

The interpreter doesn't actually need to be recursive; a function call could just be the same as any other instruction in the loop except that it sets current_frame = frame, and then returning would also be the same as any other instruction except that it sets current_frame = current_frame.f_back. That lets you do deep recursion in Python without piling up on the C stack. It makes it easier to just pass frame objects around and use them to represent coroutines, which is basically what Stackless Python is all about. But mainline Python can, and does, already handle the latter just by wrapping frames up in a very simple generator object. Again, see inspect to see what's in a generator, but it's basically just a frame and a couple extra pieces of information needed to handle yield from and exhausted generators.

Notice the special _unbound value I used in the pseudocode. In fact, at the C level, this is just a null pointer, although it could just as easily be a real sentinel object. (It could even be exposed to the language, like JavaScript's undefined, although in JS, and most other such languages, that seems to cause a lot more problems than it solves.) If you try to access a local variable before you've assigned to it, the interpreter sees that it's unbound and raises an UnboundLocalError. (And if you del a local, it gets reset to _unbound, with the same effect.)

Defaults

I glossed over the issues with default values above. Let's look at them now.

Let's say we defined def spam(x=0):. What would that change?

First, the body of spam doesn't change at all, so neither does its code object. It still just has an x parameter, which it expects to be filled in by the function-calling machinery in the interpreter, and it doesn't care how. You can dis it and explore its members and nothing has changed.
Its function object does change, however—__defaults__ now has a value in it.

If you look at the outer function, its code changes. It has to store the 0 as a constant, and then load that constant to pass to make_function. So the first line of pseudocode goes from this:
    store_local("spam", make_function("spam", spam_body))
… to this:
    store_local("spam", 
        make_function("spam", spam_body, defaults=(0,)))
Inside the interpreter's make_function implementation, it just stores that tuple in the created function's __defaults__ attribute.

At call time, the function-calling machinery is a bit more complicated. In our case, we passed a value for x, so nothing much changes, but what if we didn't? Inside call_function, if the function expects 1 positional parameter, and we passed 0 positional arguments, and we didn't provide a keyword argument matching the name of the function's code's first positional parameter, then it uses the first value from the function's __defaults__ instead, and puts that in the frame's locals array the same way it would for a value we passed in explicitly.

(If we hadn't set a default value, and then called without any arguments, it would try to use the first value from __defaults__, find there isn't one, and raise a TypeError to complain.)

This explains why mutable default values work the way they do. At def time, the value gets constructed and stored in the function's __defaults__. Every time the function is called without that argument, nobody constructs a new value, it just copies in the same one from the same tuple every time.

Closures

As soon as you have nonlocal variables, things get more fun. So let's start with the most trivial example:
    def eggs():
        y = 1
        def spam(x):
            return x + y
        spam(3)
Our inner function has a free variable, y, which it captures from the outer scope. Its code doesn't change much:
return(add(load_local("x"), 
        load_local_cell("y").cell_contents))
But the outer function changes a bit more. In pseudocode:
    load_local_cell("y").set_cell_contents(1)
    store_local("spam", make_function("spam", spam_body, 
        closure=[load_local_cell("y")]))
    call_function(load_local("spam"), [], {})
The first thing to notice is that y is no longer a normal local variable. In both functions, we're doing a different call, load_local_cell to look it up. And what we get is a cell object, which we have to look inside to get or set the actual value.

Also notice that when we're calling make_function, we pass the cell itself, not its contents. This is how the inner function ends up with the same cell as the outer one. Which means if either function changes the cell's contents, the other one sees it.

The only hard part is how the compiler knows that y is a cellvar (a local variable you share with a nested inner function) in eggs and a freevar in spam (a local variable that an outer function is sharing with you, or with a deeper nested function).

Remember that the compiler scans the code for assignments to figure out what's local. If it finds something that isn't local, then it walks the scope outward to see if it's local to any containing function. If so, that variable becomes a cellvar in the outer function, and a freevar in the inner function (and any functions in between). If not, it becomes a global. (Unless there's an explicit nonlocal or global declaration, of course.) Then the compiler knows which code to emit (local, cell, or global operations) for each variable. Meanwhile, it stores the list of cellvars and freevars for each code object in co_cellvars and co_freevars. When compiling a def statement, the compiler also needs to look at the co_freevars and inserts that closure = [load_load_cell("y")] and passes it along to the make_function.

Inside call_function, if the code object has any cellvars, the interpreter creates an empty (_unbound) cell for each one. If it has any freevars, the interpreter copies the cells out of the function object's __closure__.

And that's basically all there is to closures, except for the fast local optimization.

For historical reasons, the way things get numbered is a little odd. The f_locals array holds the normal locals first, then the cellvars, then the freevars. But cellvars and freevars are numbered starting from the first cellvar, not from the start of the array. So if you have 3 normal locals, 2 cellvars, and 2 freevars, freevar #2 matches slot 0 in co_freevars, and slot 5 in f_locals. Confused? It's probably easier to understand in pseudocode than English. But first…

For an additional optimization, instead of just one load_fast_cell function, Python has a load_closure that just loads the cell, load_deref that loads the cell's cell_contents in a single step (without having to load the cell object itself onto the stack), and store_deref that stores into the cell's cell_contents in a single step.

So, the pseudocode to construct a frame looks like this:
    code = func.__code__
    locals = [_unbound for _ in range(code.co_nlocals)]
    cells = [cell() for _ in range(len(code.co_cellvars))]
    do_fancy_arg_to_param_mapping(locals, args, kwargs)
    frame = Frame()
    frame.f_back = current_frame
    frame.f_lasti = 0
    frame.f_code = code
    frame.f_locals = locals + cells + list(func.__closure__)
And the pseudocode for load_closure, load_deref, and store_deref, respectively:
    frame.f_locals[frame.f_code.co_nlocals + i]
    frame.f_locals[frame.f_code.co_nlocals + i].cell_contents
    frame.f_locals[frame.f_code.co_nlocals + i].set_cell_contents(value)
These cells are real Python objects. You can look at a function's __closure__ list, pick out one of the cells, and see its cell_contents. (You can't modify it, however.)

Example

It may be worth working through an example that actually relies on closure cells changing:
def counter():
    i = 0
    def count():
        nonlocal i
        i += 1
        return i
    return count
count = counter()
count()
count()
If you want to go through all the details, you can easily dis both functions (see the section on bytecode below), look at their attributes and their code objects' attributes, poke at the cell object, etc. But the short version is pretty simple.

The inner count function has a freevar named i. This time, it's explicitly marked nonlocal (which is necessary if you want to rebind it). So, it's going to have i in its co_freevars, and some parent up the chain has to have a cellvar i or the compiler will reject the code; in our case, of course, counter has a local variable i that it can convert into a cellvar.

So, count is just going to load_deref the freevar, increment it, store_deref it, load_deref it again, and return it.

At the top level, when we call counter, the interpreter sets up a frame with no locals and one cellvar, so f_locals has one empty cell in it. The i = 0 does a store_deref to set the cell's value. The def does a load_closure to load the cell object, then passes it to make_function to make sure it ends up in the defined function's __closure__, and then it just returns that function.

When we call the returned function, the interpreter sets up a frame with no locals and one freevar, and copies the first cell out of __closure__ into the freevar slot. So, when it runs, it updates the 0 in the cell to 1, and returns 1. When we call it again, same thing, so now it updates the 1 in the cell to 2, and returns 2.

Other callable types

As mentioned earlier, calling a function is really just looking up and calling the function's __call__ method. Of course if calling anything means looking up its __call__ method and calling that, things have to bottom out somewhere, or that would just be an infinite recursion. So how does anything work?

First, if you call a special method on a builtin object, that also gets short-circuited into a C call. There's a list of slots that a builtin type can provide, corresponding to the special dunder methods in Python. If a type has a function pointer in its nb_add slot, and you call __add__ on an instance of that type, the interpreter doesn't have to look up __add__ in the dict, find a wrapper around a builtin function, and call it through that wrapper; it can just find the function pointer in the slot for the object's type and call it.

One of those slots is tp_call, which is used for the __call__ method.

Of course the function type defines tp_call with a C function that does the whole "match the args to params, set up the frame, and call the interpreter recursively on the frame" thing described earlier. (There's a bit of extra indirection so this can share code with eval and friends, and some optimized special cases, and so on, but this is the basic idea.)

What if you write a class with a __call__ method and then call an instance of it? Well, spam.__call__ will be a bound method object, which is a simple builtin type that wraps up a self and a function. So, when you try to call that bound method, the interpreter looks for its __call__ by calling its tp_call slot, which just calls the underlying function with the self argument crammed in. Since that underlying function will be a normal Python function object (the one you defined with def __call__), its tp_call does the whole match-frame-eval thing, and your __call__ method's code gets run and does whatever it does.

Finally, most builtin functions (like min) don't create a whole type with a tp_call slot, they're just instances of a shared builtin-function type that just holds a C function pointer along with a name, docstring, etc. so its tp_call just calls that C function. And similarly for methods of builtin types (the ones that aren't already taken care of by slots, and have to get looked up by dict). These builtin function and method implementations get a self (or module), list of args, and dict of kwargs, and have to use C API functions like PyArg_ParseTupleAndKeywords to do the equivalent of argument-to-parameter matching. (Some functions use argclinic to automate most of the annoying bits, and the goal is for most of the core and stdlib to do so.) Beyond that, the C code just does whatever it wants, returning any Python object at the end, and the interpreter then puts that return value on the stack, just as if it came from a normal Python function.

You can see much of this from Python. For example, if f is a function, f.__call__ is a <method-wrapper '__call__' of function object>, f.__call__.__call__ is a <method-wrapper '__call__' of method-wrapper object>, and if you pile on more .__call__ you just get method-wrappers around method-wrappers with the same method. Similarly, min is a builtin function, min.__call__ is a <method-wrapper '__call__' of builtin_function_or_method object>, and beyond that it's method-wrappers around method-wrappers. But if you just call f or min, it doesn't generate all these wrappers; it just calls the C function that the first method-wrappers is wrapping.

Actual bytecode

Real Python bytecode is the machine language for a stack machine with no registers (except for an instruction pointer and frame pointer). Does that sound scary? Well, that's the reason I've avoided it so far—I think there are plenty of people who could understand all the details of how function calls work, including closures, but would be scared off by bytecode.

But the reality is, it's a very high-level stack machine, and if you've made it this far, you can probably get through the rest. In fact, I'm going to go out of my way to scare you more at the start, and you'll get through that just fine.

Let's go back to our original trivial example:
    def spam(x):
        return x+1
    spam(3)
Don't put this inside a function, just paste it at the top level of your REPL. That'll get us a global function named spam, and we can look at what's in spam.__code__.co_code:
    b'|\x00d\x01\x17\x00S\x00'
Well, that's a bit ugly. We can make it a little nicer by mapping from bytes to hex:
    7c 00 00 64 01 00 17 00 00 53 00 00
But what does that mess mean?

Bytecode is just a sequence of instructions. Each instruction has a 1-byte number; if it's up to 0x90, it's followed by a 2-byte (little-endian) operand value. So, we can look up 0x7c in a table and see that it's LOAD_FAST, and the 2-byte operand is just 0, just like our pseudocode load_fast(0). So, this is taking the frame's f_locals[0] (which we know is x, because co.varnames[0] is 'x'), and pushing it on the stack.

Fortunately, we don't have tc do all this work; the dis module does it for us. Just call dis.dis(outer.__code__.co_consts[0]) and you get this:
     0 LOAD_FAST 0 (x)
     3 LOAD_CONST 1 (1)
     6 BINARY_ADD
     7 RETURN_VALUE
The dis docs also explain what each op actually does, so we can figure out how this function works: It pushes local #0 (the value of x) onto the stack, then pushes constant #1 (the number 1) onto the stack. Then it pops the top two values, adds them (which in general is pretty complicated—it needs to do the whole deal with looking up __add__ and __radd__ methods on both objects and deciding which one to try, as explained in the data model docs), and puts the result on the stack. Then it returns the top stack value.

Brief aside: If operands are only 16 bits, what if you needed to look up the 65536th constant? Or, slightly more plausibly, you needed to jump to instruction #65536? That won't fit in a 16-bit operand. So there's a special opcode EXTENDED_ARG (with number 0x90) that sets its 16-bit operand as an extra (high) 16 bits for the next opcode. So to load the 65536th constant, you'd do EXTENDED_ARG 1 followed by LOAD_FAST 0, and this means LOAD_FAST 65536.

Anyway, now let's add the outer function back in, and compare the pseudocode to the bytecode:
    def outer():
        def spam(x):
            return x+1
        spam(3)
In pseudocode:
    store_fast(0, make_function("spam", spam_body))
    call_function(load_fast(0), [3], {})
In real bytecode:
     0 LOAD_CONST 1 (<code object spam at 0x12345678>)
     3 LOAD_CONST 2 ('spam')
     6 MAKE_FUNCTION 0
     9 STORE_FAST 0 (spam)
    12 LOAD_FAST 0 (spam)
    15 LOAD_CONST 3 (3)
    18 CALL_FUNCTION 1
    21 POP_TOP
    22 LOAD_CONST 0 (None)
    25 RETURN_VALUE
So, 0-9 map to the first line of pseudocode: push constant #1, the code object (what we called spam_body), and constant #2, the name, onto the stack. Make a function out of them, and store the result in local #0, the spam variable.

MAKE_FUNCTION can get pretty complicated, and it tends to change pretty often from one Python version to another, read the dis docs for your version. Fortunately, when you have no default values, annotations, closures, etc., so the pseudocode is just make_function(name, code), you just push the name and code and do MAKE_FUNCTION 0.

Line 12-18 map to the second line of pseudocode: push local #0 (spam) and constant #3 (the number 3) onto the stack, and call a function.

Again, CALL_FUNCTION can get pretty complicated, and change from version to version, but in our case it's dead simple: we're passing nothing but one positional argument, so we just put the function and the argument on the stack and do CALL_FUNCTION 1.

The interpreter's function-call machinery then has to create the frame out of the function object and its code object, pop the appropriate values off the stack, figure out which parameter to match our argument to and copy the value into the appropriate locals array slot, and recursively interpret the frame. We saw above how that function runs. When it returns, that leaves a return value onto the stack.

Line 21 just throws away the top value on the stack, since we don't want to do anything with the return value from spam.

Lines 22-25 don't map to anything in our pseudocode, or in our source code. Remember that in Python, if a function that falls off the end without returning anything, it actually returns None. Maybe this could be handled magically by the function-call machinery, but it's not; instead, the compiler stores None in the code object's constants, then adds explicit bytecode to push that constant on the stack and return it.

By the way, you may have noticed that the bytecode does some silly things like storing a value into slot 0 just to load the same value from slot 0 and then never use it again. (You may have noticed that some of my pseudocode does the same thing.) Of course it would be simpler and faster to just not do that, but Python's limited peephole optimizer can't be sure that we're never going to load from slot 0 anywhere else. It could still dup the value before storing so it doesn't need to reload, but nobody's bothered to implement that. There have been more detailed bytecode optimizer projects, but none of them have gotten very far—probably because if you're serious about optimizing Python, you probably want to do something much more drastic—see PyPy, Unladen Swallow, ShedSkin, etc., which all make it so we rarely or never have to interpret this high-level bytecode instruction by instruction in the first place.

MAKE_FUNCTION and CALL_FUNCTION

As mentioned above, these two ops can get pretty complicated. As also mentioned, they're two of the most unstable ops, changing from version to version because of new features or optimizations. (Optimizing function calls is pretty important to overall Python performance.) So, if you want to know how they work, you definitely need to read the dis docs for your particular Python version. But if you want an example (for pre-3.5), here goes.

Take this pseudocode:
    make_function("spam", spam_body,
        defaults=(1,), kw_defaults={'x': 2},
        closure=(load_closure(0),))
The bytecode is:
     0 LOAD_CONST 6 ((1,))
     3 LOAD_CONST 2 (2)
     6 LOAD_CONST 3 ('x')
     9 BUILD_MAP 1
    12 LOAD_CLOSURE 0 (y)
    15 BUILD_TUPLE 1
    18 LOAD_CONST 4 (<code object spam at 0x12345678>)
    21 LOAD_CONST 5 ("spam")
    15 MAKE_CLOSURE 9
Yuck. Notice that we're doing MAKE_CLOSURE rather than MAKE_FUNCTION, because, in addition to passing a name and code, we're also passing a closure (a tuple of cells). And then we're passing 9 as the operand instead of 0. This breaks down into 1 | (1<<8) | (0<<16), meaning 1 positional default, 1 keyword default, and 0 annotations, respectively. And of course we have to push all that stuff on the stack in appropriate format and order.

If we'd had an annotation on that x parameter, that would change the operand to 1 | (1<<8) | (1<<16), meaning we'd need EXTENDED_ARG, and pushing the annotations is a few more lines, but that's really about as complicated as it ever gets.

More info

If you're still reading, you probably want to know where to look for more detail. Besides the inspect and dis docs, there's the C API documentation for Object Protocol ( PyObject_Call and friends) and Function and Code concrete objects, PyCFunction, and maybe Type objects. Then there's the implementations of all those types in the Objects directory in the source. And of course the main interpreter loop in Python/ceval.c.
2

View comments

Blog Archive
About Me
About Me
Loading
Dynamic Views theme. Powered by Blogger. Report Abuse.