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 thatcall_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. Inspam
this just has the number1
. In the top-level function, it has the number3
, the string"spam"
, and the code object for thespam
body.co_argcount
and related values that specify the calling convention. Forspam
, there's an argcount of 1, meaning it's expecting 1 positional parameter, and also meaning its first 1 local variables inco_varnames
are parameters. (This is why you can call it withspam(x=3)
instead ofspam(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.
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
Themake_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 doload_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 thedef
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 = localsAnd 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 00But 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_VALUEThe
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_VALUESo, 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 9Yuck. 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
.
View comments