Deep Engineering
Expert·Published·3.12 · 3.13 · 3.14·30 MIN

Calling a function: what it costs, what does not happen, and where cells live

Three common claims about the cost of a call — a method costs more than a function, binding a method in advance is cheaper, staticmethod is faster — the measurement confirms none of them and contradicts two. The reason is visible in the bytecode: on the fast path no bound-method object is created at all. That same fact opens the second half of the article — closures: the variable a nested function sees is stored through a separate object — a cell the frame only points at — and everything else follows from that.

Full technical treatment

Three claims about the cost of a call travel together, and one measurement checks all of them: "calling a method costs more than calling a function", "binding a method in advance is cheaper than going through the dot every time", "staticmethod is faster than an ordinary method — it has no self".

The measurement barely supports the first. It contradicts the second and the third, and the second in the opposite direction.

All three have the same explanation, and it is not about nanoseconds: on the fast path of a method call, the bound-method object IS NOT CREATED. There is nothing to save on its creation, and whoever "optimises" the call by binding the method in advance brings the object back.

The second half of the article is about closures, and it is not here by coincidence. A cell is where the variable a nested function sees actually lives, and how it works explains the rest: the variable two functions share, the three identical answers from a loop, and why nonlocal exists.

Part I. The cost of a call

What is being measured

Every number was taken on a call to an EMPTY function: the body does nothing, so only what happens around the body is measured. Beside it stands a line with no call at all — reading an attribute on an object with __slots__, the cheapest read there is — to give the nanoseconds a scale. Measured on 3.13.7 with bench/calls/cost.py:

Formns
p.x — reading a slot, no call8.16
f0() — a function with no arguments15.35
f1(1) — one positional16.65
f3(1, 2, 3) — three positional20.22
f1(a=1) — the same, by name25.39
fdef(1) — two defaulted arguments23.61
fdef(1, 2, 3) — the same ones passed20.24
fkwonly(a=1) — keyword-only25.64
lam() — a lambda15.36
clo() — a closure with one cell20.88
o.m() — a method through the dot17.74
bound() — a method bound in advance18.34
C.s() — a staticmethod27.88
C.c() — a classmethod46.89
o() — an object with __call__36.36
len_(data) — a C function9.48

Read it this way: an empty call on this machine is about fifteen nanoseconds — roughly twice the cheapest attribute read available here. That is a comparison of scale between two different operations, not a decomposition of the cost of a call: what those fifteen nanoseconds are made of is not something this measurement says at all. Everything else in the table is a surcharge for the form.

measured observation

Every number comes from one run of 3.13.7 and is compared only with the others in it. The absolute values are a property of the measuring machine (Intel Xeon 2.80 GHz, 2 vCPU); what carries to other hardware is the ratio, not the nanosecond. Time is never compared across versions in this project.

The three claims and what became of them

From the same run:

ClaimThe pairRatio
a method call costs more than a function callf0() 15.35 ns, o.m() 17.74 ns×1.16
binding in advance is cheaper than the doto.m() 17.74 ns, bound() 18.34 ns×1.03
staticmethod is faster: it has no selfo.m() 17.74 ns, C.s() 27.88 ns×1.57

The first barely holds: sixteen per cent is not a reason to rewrite anything, and it is less than the gap between f1(1) and f1(a=1) in the same table: 16.65 against 25.39.

The second does not hold either: a method bound in advance is at any rate no cheaper. That it is MORE EXPENSIVE is not something these two numbers prove, and why is taken up below, in a section of its own about spread.

The third is contradicted in the other direction: staticmethod is slower than an ordinary method and classmethod noticeably slower than both. But that comparison changes two things at once, so below it is separated by axis.

First — what is being compared with what

The third row puts o.m() next to C.s(), and TWO things differ between those two spellings: the kind of method and what it is read through — the instance or the class. While both axes are mixed, the difference cannot be attributed to the kind of method: it could have come from reading through the class.

So both axes are separated by a run of their own, in which all six forms were taken together — bench/calls/forms.py, block 1:

FormnsAgainst f0()
f0() — a plain function15.45×1.00
o.m() — a method through the instance17.33×1.12
o.s() — staticmethod through the instance26.60×1.72
C.s() — staticmethod through the class28.45×1.84
o.c() — classmethod through the instance47.15×3.05
C.c() — classmethod through the class51.26×3.32

Now each comparison changes exactly one thing. With the reading held fixed — through the instance — staticmethod costs ×1.54 of an ordinary method and classmethod ×2.72. The way of reading, with the kind of method held fixed, adds ×1.07 and ×1.09 — a few per cent.

Separating the axes did not overturn the conclusion: staticmethod really is more expensive than an ordinary method, and it is the kind of method that does it, not the reading through the class. But that can only be said now.

measured observation

The numbers in this block come from a different run than the table above: they were taken by bench/calls/forms.py, not bench/calls/cost.py. They must not be compared with each other, so 17.33 here and 17.74 there are not a discrepancy but two separate measurements of the same thing on the same machine. The ratios inside each run held.

Why: what exactly does not happen

The answer to all three at once is in the bytecode. bench/calls/shapes.py disassembles these forms after two hundred warm-up calls each:

What is calledInstructions after specialisation
plain(t)LOAD_GLOBAL_MODULE + CALL_PY_EXACT_ARGS
o.m(t)LOAD_ATTR_METHOD_WITH_VALUES + CALL_PY_EXACT_ARGS
bound(t)CALL_BOUND_METHOD_EXACT_ARGS
C.s(t)LOAD_GLOBAL_MODULE + LOAD_ATTR + CALL_PY_EXACT_ARGS
C.c(t)LOAD_GLOBAL_MODULE + LOAD_ATTR + CALL_BOUND_METHOD_EXACT_ARGS
len(data)LOAD_GLOBAL_BUILTIN + CALL_LEN
o.m with no callLOAD_ATTR

Look at the o.m(t) row and the last one. The attribute-reading instruction is DIFFERENT: LOAD_ATTR_METHOD_WITH_VALUES when a call follows immediately, and a plain LOAD_ATTR when it does not.

The difference is in what goes on the stack. On the path with a call, the function and the instance go there separately: there is nothing to bind them into, and no bound-method object is created. On the path without a call it is created, because it was asked for.

That it really is created when asked for needs no bytecode to check:

PYTHON
o.m is o.m    # False
o.m == o.m    # True

Two consecutive reads give two DIFFERENT objects. That work is exactly what the fast path does not do.

The third claim is visible in the same table. C.s(t) takes three instructions rather than two: the class name is read first, then the attribute on it, and there is no specialisation on the instance at all. That is the "way of reading" axis, to which the matrix above assigns ×1.07; the rest of the ×1.57 is the kind of method, ×1.54 with the reading held fixed through the instance. This run does not disassemble o.s(t), so the split comes from time rather than from instructions. C.c(t) has the same extra attribute read and calls through a bound method on top of it: CALL_BOUND_METHOD_EXACT_ARGS. What each of the two additions costs on its own, this measurement does not separate — only the total is visible, 46.89 against 27.88.

Hence the second claim as well. Binding a method in advance means creating a PyMethodObject once and calling through it: CALL_BOUND_METHOD_EXACT_ARGS. There is nothing to save — the fast path never created that object — while a level of indirection remains.

The numbers, though, say less than one would like. 18.34 against 17.74 is three per cent, and before leaning on a difference that size one has to know the spread. It is printed by bench/calls/forms.py, block 2. The numbers there are its own — a different run, not comparable with the ones above; what has to be compared is the gap inside that run against its own spread:

FormBestWorstSpread
f0()15.3615.720.37
o.m()17.2719.512.24
bound()18.5519.380.82
o.s()27.1227.540.42
C.c()49.3351.321.99

The gap between bound() and o.m() is 1.28 ns against a spread of up to 2.24 ns within the rounds — that is LESS than the spread. So "a method bound in advance is slower" does NOT follow from these measurements: the two numbers lie inside their own noise. What does follow is that it is no cheaper, and that is exactly the conclusion practice needs: binding a method in advance for speed is not worth it, because there is no gain in any run. Binding in advance is for when the method has to be PASSED somewhere.

language contract

The data model, meanwhile, describes o.m precisely as creating an object: When a non-data attribute of an instance is referenced, the instance's class is searched. If the name denotes a valid class attribute that is a function object, references to both the instance object and the function object are packed into a method object. That remains true: the observable behaviour has not changed, and o.m without a call does create the object. What changed is how the interpreter reaches the same result when it sees a call.

Star-args: one form out of five is expensive

"Stars are expensive" is one more durable habit, and it is right in exactly one case out of five (bench/calls/cost.py):

FormnsAgainst direct passing
f3(1, 2, 3) — direct20.17×1.00
f3(*args) — unpacking a tuple23.49×1.16
f3(**kwargs) — unpacking a dict104.35×5.17
fstar(1, 2, 3) — received into *args42.27×2.10
fstar(a=1) — received into **kwargs46.12×2.29
fstar(*args, **one) — both125.77×6.24

Unpacking a tuple is nearly free — sixteen per cent. Receiving into stars costs double, which is a fair price for an arbitrary signature. But unpacking a DICT when passing costs five times as much, and that is the one place worth thinking about.

The last row, at ×6.24, is not an exception but the same cause: it contains a dict unpack too, and that alone accounts for nearly the whole gap. The tuple beside it adds little.

The reason is in how the fast calling protocol is built. Its description says in what shape the callee receives the arguments:

args is a C array consisting of the positional arguments followed by the values of the keyword arguments

And the names of those arguments travel separately:

kwnames is a tuple containing the names of the keyword arguments; in other words, the keys of the kwargs dict

Positional arguments already lie in an array: they can be handed over as they are. A dict lies neither in an array of values nor in a tuple of names, and it has to be taken apart into both. Hence the fivefold gap — the cost is not in how many values are passed but in having to turn one structure into two others. The run does not disassemble this form: the explanation comes from the description of the protocol, and how those hundred nanoseconds break down step by step the measurement does not show.

And straight away about the bounds of that ratio. All the numbers were taken on an EMPTY function, which makes it a ratio of overhead to overhead. The same thing on a function with a body was taken by a separate run — bench/calls/forms.py, block 3:

FormnsAgainst direct passing
fwork(1, 2, 3) — direct passing1663.47×1.00
fwork(*args) — tuple unpack1659.83×1.00
fwork(**kwargs) — dict unpack1763.83×1.06
fworkstar(*args, **one) — both1755.32×1.06

Same mechanism, different ratio: ×1.06 against ×5.17 on the empty function. Those two numbers come from different runs, and what is compared here is not them but the ratios inside each. What carries over from this measurement is not the coefficient but WHICH form of passing is the expensive one — and the fact that avoiding it makes sense where the body of the function is comparable to the overhead of the call.

Part II. Cells

A closure holds a box, not a value

"A function remembers its environment" is an explanation from which none of the observable behaviour follows. All of it follows from one fact: the variable a nested function sees is stored THROUGH A SEPARATE OBJECT — a cell. From here on it is called a box, because that is the part of it that matters: the value sits in the box, not in the function itself.

Saying "it does not live in the frame" would be inaccurate, and the inaccuracy matters. The reference to the cell is in the frame: a frame's local variables hold references to cells, and the LOAD_DEREF and STORE_DEREF instructions work through them. The separate object is not instead of the frame but between the frame and the value — which is exactly why several closures can hold the same cell, and why it outlives the frame it appeared in.

A cell is not a CPython implementation detail but part of the data model, and it is described directly:

Cell objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contain a reference to the cells from outer scopes which also use that variable

"For each such VARIABLE" is the whole thing. A cell is made per variable, not per function and not per call.

Two functions, one box

From bench/calls/cells.py — identical on 3.12.3, 3.13.7 and 3.14.7:

CheckResult
increment.__closure__[0] is read.__closure__[0]True
read() before calling increment0
read() after two increment() calls2
the value inside the cell2

Neither function passed the other anything. They simply refer to the same object, and is confirms it. That is what a "shared variable" actually is — not scoping magic but a shared object.

Why a loop yields three identical functions

The classic surprise comes from the same place:

PYTHON
[lambda: i for i in range(3)]        # [2, 2, 2]
[lambda i=i: i for i in range(3)]    # [0, 1, 2]

The run counts the cells, and that answers the question better than any explanation: the first three functions share ONE cell, the second three have none at all.

The first line is not a Python bug, nor a separate rule called "late binding". A cell is made per variable, the loop has one variable, and by the time the lambdas are called it holds the last value. The second line does not "fix the binding" — it removes the need for a shared box: the value goes into a default argument, and that is computed when each function is CREATED.

Who decides that a variable becomes a cell

The compiler decides, once, and the decision is visible in the code (bench/calls/cells.py):

What is inspectedValue
outer.__code__.co_varnames('param', 'local', 'inner')
outer.__code__.co_cellvars('box',)
inner.co_freevars('box',)

box is assigned in outer, yet it is not in co_varnames: the compiler saw that a nested function takes it and moved it into a cell. The rest of the chain is visible in the instructions the same run prints:

WhereInstruction
in outerMAKE_CELL box
in outerSTORE_DEREF box
in innerCOPY_FREE_VARS
in innerLOAD_DEREF box

MAKE_CELL creates the box on entry to outer, STORE_DEREF puts the value in, COPY_FREE_VARS hands the same box to inner, LOAD_DEREF takes the value out. At no step is the value copied.

language contract

The output of bench/calls/cells.py on 3.12.3, 3.13.7 and 3.14.7 agrees in everything but the addresses in the cell's repr. That is what should be expected: the observable BEHAVIOUR of closures is part of the data model. One cell for everyone who sees the variable; a value that outlives the frame; nonlocal, which turns an assignment into a write to the cell — that is the contract.

implementation detail · 3.13

HOW that is arranged, on the other hand, is CPython's implementation, and code must not depend on it. PyCellObject, the MAKE_CELL, STORE_DEREF, COPY_FREE_VARS and LOAD_DEREF instructions, the split of names across co_varnames, co_cellvars and co_freevars, the names of call specialisations — all of that is how one implementation is built. It is observable from Python and therefore convenient for explaining; but it has changed between releases and will change again, and what is promised out of it is only what the previous note lists.

Why a box is needed at all, and where nonlocal comes in

A function's frame lives until it returns; the box lives longer. That is what bench/calls/cells.py, block 5, shows:

What the run reportsValue
short_lived has returned, its frame is gone
keeper() returnsa value from a function that has already returned
keeper.__closure__[0].cell_contents is aliveTrue

That is where nonlocal comes from. An ordinary assignment inside a function is a write into the frame, and the compiler decides where to write by one rule: a name assigned anywhere in the body is local. For a nested function that is not enough — it has to write not into its own frame but into the box shared with the enclosing function. nonlocal does not "permit changing an outer variable"; it tells the compiler the name is not local, so the assignment must become STORE_DEREF.

What reading from a cell costs

One question remains: are closures slow? Each name below is read INSIDE a function, so the call is in the number; the second column has the empty call subtracted (bench/calls/cost.py):

What is readCall + read, nsThe read, ns
a local name — LOAD_FAST19.313.96
a name from a cell — LOAD_DEREF19.253.91
a global name — LOAD_GLOBAL19.133.78
for comparison: f0() — an empty call15.350.00

The three reading instructions came out at 3.96, 3.91 and 3.78 nanoseconds — less than five per cent apart, and each about a quarter of an empty call. What is paid for is not reading from a cell but making a call at all.

Part III. Practice

Practice · predict the output

Three lambdas from a loop, three with a default argument, and two reads of one method. What does this code print?
def make():
  out = []
  for i in range(3):
      out.append(lambda: i)
  return out


def make_bound():
  out = []
  for i in range(3):
      out.append(lambda i=i: i)
  return out


class C:
  def m(self):
      return 1


o = C()

print([f() for f in make()])
print([f() for f in make_bound()])
print(o.m is o.m, o.m == o.m)

Practice · estimate

A function takes three positional arguments. How many times more expensive is f3(**kwargs) with a dict of the same three values than the direct f3(1, 2, 3)?
times

Part IV. What follows

Four conclusions the measurements support

Do not rewrite o.m() as bound = o.m. No bound-method object is created on the fast path, so there is nothing to save, and such an edit brings it back — and it is no cheaper either: the 18.34 against 17.74 nanoseconds lies inside the round-to-round spread. The one case where binding in advance is right is when the method has to be PASSED somewhere, and then it is not about saving anything.

Do not pick staticmethod for speed. With the way of reading held fixed it is one and a half times slower than an ordinary method, and classmethod nearly three times slower. Pick them by meaning: does the method need the instance or the class.

Only one star form is worth avoiding. f(**kwargs) on a hot path with an empty body costs five times what direct passing costs; f(*args) costs sixteen per cent and needs no thought. The ratio is tied to how much the function itself does: on a body of fifty rounds of arithmetic, six per cent is all that is left of the fivefold gap. What to look at is not the coefficient but whether the body of the function is comparable to the cost of the call.

Do not fear closures over their cost. Reading from a cell is indistinguishable in time from reading a local or a global name, and all three are a fraction of a call. What is worth fearing in closures is something else: that the cell is ONE for everyone who sees the variable.

Version history

VersionChangeWhat this means for your code
3.12The call specialisations are the same as later, but some carry different names: the len specialisation is called CALL_NO_KW_LEN. Cells, __closure__ and co_cellvars behave exactly as in 3.13 and 3.14 — the output of bench/calls/cells.py agrees in everything but the addresses in the cell's repr.
3.13The len specialisation is renamed to CALL_LEN. That is the rule illustrating itself: none of these names is in dis.opmap or in the dis module's documentation, and code must not rely on them.
3.14The instruction set on the disassembled call paths is the same as in 3.13. Cell behaviour has not changed once across the three versions.

How this was measured

The numbers in this article come from these scripts. Each opens from here, together with the record of its run.

Time — two records, both on one build:

Bytecode and objects — comparable across versions, recorded on all three:

The basis for the exercises in "Practice":

Python 3.12.3 (GCC 13.3.0), 3.13.7 (Clang 20.1.4), 3.14.7 (Clang 22.1.3); Intel Xeon 2.80 GHz, 2 vCPU.

The three kinds of claim in this article have different bounds, and they are worth separating:

  • Behaviour — the same on all three versions: o.m without a call creates a method object, o.m is o.m gives False, one cell goes per variable. Checked on 3.12.3, 3.13.7 and 3.14.7.
  • Specialisation names — a property of the version: on 3.12.3 the len specialisation is called CALL_NO_KW_LEN, on 3.13.7 and 3.14.7 CALL_LEN. Those are properties of code and may be compared across versions.
  • Time — 3.13.7 with the GIL enabled only, and only within each of the two runs separately. It is never compared across versions: the builds have different compilers and different flags. Free-threaded builds were not measured here.

Common misconceptions

Claim

A method call is noticeably more expensive than a function call: a bound-method object is created first

Actually

The object is not created. The interpreter sees that a call follows the attribute read immediately, and puts the function and the instance on the stack separately — the instruction LOAD_ATTR_METHOD_WITH_VALUES instead of a plain LOAD_ATTR. Hence the number: o.m() costs 17.74 ns against 15.35 for a function call, a factor of 1.16. The object really is created when asked for without a call, and one line checks it: o.m is o.m gives False.

Claim

Binding a method in advance (bound = o.m) is cheaper than going through the dot

Actually

It is no cheaper — and that it is more expensive is not something the measurement proves. In one run it is 18.34 against 17.74 ns; in another, which also prints the spread, the gap is 1.28 ns against a spread of up to 2.24 ns for the same o.m(). There is no gain in either. There is nothing to save — the fast path never created the bound-method object — while such an edit brings it back, and the call now goes through it: CALL_BOUND_METHOD_EXACT_ARGS instead of a read-and-call pair. Binding in advance is right when the method has to be PASSED somewhere; for speed, no.

Claim

staticmethod is faster than an ordinary method: it has no self

Actually

It is slower — and that only shows once the way of reading is held fixed. In a separate run where both axes were taken, o.s() costs ×1.54 of o.m() read through the instance, while the way of reading adds only ×1.07. The reason is in the bytecode: the staticmethod path carries an extra LOAD_ATTR, while o.m() gets by with a specialisation on the instance. classmethod is more expensive still, ×2.72 of an ordinary method in that same run. Choose between them by meaning, not by speed.

Claim

Stars in a call are expensive

Actually

One form out of five is. Unpacking a tuple, f3(*args), costs ×1.16 against direct passing; receiving into *args and **kwargs costs ×2.10 and ×2.29. But f3(**kwargs) costs ×5.17 on an empty function — on a body of fifty rounds of arithmetic the same form costs only ×1.06, so the fivefold gap belongs to the empty function — and the reason is the shape in which the callee receives its arguments: args is a C array consisting of the positional arguments followed by the values of the keyword arguments, with the names travelling in a separate tuple. Positional arguments already lie in an array; a dict does not — it has to be taken apart into an array of values and a tuple of names.

Claim

A closure remembers the value of a variable

Actually

It holds a CELL — a separate object with the value inside. Everything else follows: two functions created side by side refer to the same cell (increment.__closure__[0] is read.__closure__[0] gives True), and a change through one is visible through the other. A cell is part of the data model rather than a CPython detail: Cell objects are used to implement variables referenced by multiple scopes.

Claim

A loop makes three functions with the same answer because of “late binding” — a separate rule of Python

Actually

There is no separate rule. A cell is made per VARIABLE, the loop has one variable, and all three functions refer to it: the run counts distinct cells and finds one. By the time they are called it holds the last value — hence [2, 2, 2]. Writing lambda i=i: i does not "fix the binding" but removes the need for a shared cell: those functions have an empty __closure__, and the value was computed when each was created.

Claim

Closures are slower than ordinary functions: reading from a cell costs more

Actually

The three reading instructions — LOAD_FAST, LOAD_DEREF and LOAD_GLOBAL — cost 3.96, 3.91 and 3.78 nanoseconds, indistinguishable within this machine's noise, and all three are a fraction of an empty call (15.35 ns). What is paid for is not reading from a cell but making a call at all.

Claim

Specialisation names like CALL_PY_EXACT_ARGS are part of the language and can be found in the documentation

Actually

None of them is in dis.opmap — the run checks that by enumeration; nor are they described in the dis module's documentation. They are visible only through dis.dis(..., adaptive=True) and change between releases: the len specialisation is CALL_NO_KW_LEN on 3.12.3 and CALL_LEN on 3.13.7 and 3.14.7. Code must not rely on these names; looking at them to understand what happens is both allowed and worthwhile.

Check yourself

Question 1 of 6

Why does o.m() barely cost more than calling an ordinary function?

Sources & further reading

6 SOURCES

  1. PEP 590 — Vectorcall: a fast calling protocol for CPythonPEP. The document that introduced the protocol underlying every number in this article. The goal is in the abstract: “This PEP introduces a new C API to optimize calls of objects. It introduces a new "vectorcall" protocol and calling convention”. Where the old cost came from is in the Motivation: “The poor performance is largely a result of having to create intermediate tuples, and possibly intermediate dicts, during the call”. Final, implemented in 3.8. How the arguments actually travel under this protocol is described not here but in the C API reference — see the next source.https://peps.python.org/pep-0590/
  2. Call protocol: the shape in which the callee receives its argumentsOfficial documentation. Where the explanation for the dict unpack being more expensive than the tuple unpack comes from: “args is a C array consisting of the positional arguments followed by the values of the keyword arguments” and “kwnames is a tuple containing the names of the keyword arguments; in other words, the keys of the kwargs dict”. Positional arguments already lie in the required shape; a dict does not — it has to be taken apart into an array of values and a tuple of names.https://docs.python.org/3/c-api/call.html
  3. Objects/classobject.c — what a bound method isCPython source code. The `PyMethodObject` struct of three fields — `im_func`, `im_self` and weak references — and `method_vectorcall`, the function through which an already-created bound method is called. This is precisely the object that is NOT created on the `o.m()` path, and precisely the one created when a method is bound in advance. Read at tag v3.13.7.https://github.com/python/cpython/blob/v3.13.7/Objects/classobject.c
  4. dis — the instruction listOfficial documentation. Cited as a source of ABSENCE: neither `CALL_PY_EXACT_ARGS` nor `LOAD_ATTR_METHOD_WITH_VALUES` nor `CALL_BOUND_METHOD_EXACT_ARGS` is described in this document, and none of them is in `dis.opmap` — which the run prints by enumeration. They are visible only through `dis.dis(..., adaptive=True)` and change between releases: on 3.12.3 the `len` specialisation is called `CALL_NO_KW_LEN`, on 3.13.7 and 3.14.7 `CALL_LEN`.https://docs.python.org/3/library/dis.html
  5. Data model: function objects and method bindingOfficial documentation. The normative description of what happens on `o.m`: “When a non-data attribute of an instance is referenced, the instance's class is searched. If the name denotes a valid class attribute that is a function object, references to both the instance object and the function object are packed into a method object”. That is exactly what is NOT carried out literally on the fast call path — while the observable behaviour is unchanged, and `o.m` without a call still creates the object.https://docs.python.org/3/tutorial/classes.html#method-objects
  6. Data model: cell objectsOfficial documentation. A cell is described as an ordinary object of the language: “Cell objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contain a reference to the cells from outer scopes which also use that variable”. Hence what the figure shows: one cell for all the functions that see the variable, outliving the frame.https://docs.python.org/3/c-api/cell.html