The “def” keyword does two things: It creates a function object, and then assigns a variable (our function name) to that function object. So when I say:
def foo(): return "I'm foo!"
Python creates a new function object. Inside of that object, we can see the bytecodes, the arity (i.e., number of parameters), and a bunch of other stuff having to do with our function.
Most of those things are located inside of the function object’s __code__ attribute. Indeed, looking through __code__ is a great way to learn how Python functions work. The arity of our function “foo”, for example, is available via foo.__code__.co_argcount. And the byte codes are in foo.__code__.co_code.
The individual attributes of the __code__ object are read-only. But the __code__ attribute itself isn’t! We can thus be a bit mischievous, and perform a brain transplant on our function:
def foo(): return "I'm foo!" def bar(): return "I'm bar!" foo.__code__ = bar.__code__
Now, when we run foo(), we’re actually running the code that was defined for “bar”, and we get:
"I'm in bar!"
This is not likely to be something you want to put in your actual programs, but it does demonstrate some of the power of the __code__ object, and how much it can tell us about our functions. Indeed, I’ve found over the last few years that exploring the __code__ object can be quite interesting!