Closures capture the variable, not the value — and the scope chain walks past the class body
A closure holds a cell, not a copy: the value is read when the function is called, and one cell is shared by every inner function at once. The familiar “local → enclosing → global → builtins” chain leaves out the two things people actually trip on: a class body is not part of it, and an assignment anywhere in a body makes the name local for the whole body.
Full technical treatment
TL;DR
A function created inside another one goes on using that function's variables after the outer function has finished. That is what a closure is. What matters is precisely what it uses: not a copy of the value taken at creation time, but the variable itself — so the value is read when the inner function is called, not when it was created.
Hence the main consequence: a variable is captured, not a value, and that
diverges from expectation in three places. A change made to the variable after
the function was created reaches it: v becomes "second" after def show, and
show() returns "second". The access is also shared: two inner functions
reading the same name point at the same object, which an is comparison
confirms. And the familiar "local → enclosing → global → builtins" chain leaves
out two things: a class body is not part of it (a method reading x takes
x from the enclosing function even when the class right next to it defines its
own), and an assignment anywhere in a body makes the name local for the whole
body — so reading it earlier in the code raises UnboundLocalError without
ever reaching a global of the same name.
Beyond that is what separates knowing from having read. The object that shared access goes through is called a cell, and a name a function uses but does not define is a free variable; the reference states the rule for them outright: “Name resolution of free variables occurs at runtime, not at compile time”. The cost of all this is small and resolvable at the same time: reading a cell takes 9.1 ns against 6.9 ns for a local — a third more, two nanoseconds (3.13.7). A global costs the same as a cell down to the hundredths of the ratio, which is not what “globals are slow” would lead you to expect.
- that a function can be defined inside another function and returned from it like any other value;
- that a function has variables of its own — parameters and locals — that live while the function is running;
- that the function you get back can be called any time later, not only right away.
- what a cell, a free variable and late binding are — those words turn up along the way and are explained there;
nonlocal,global,UnboundLocalError,co_freevars,LOAD_DEREF,MAKE_CELL.
Base: an inner function remembers the outer one's variables
The place to start is the most ordinary picture there is, with no special vocabulary in it yet: one function creates another and hands it back.
def make_adder(step): # step is the outer function's parameter
def add(value):
return value + step # ...and the inner function uses it
return add
add1 = make_adder(1) # make_adder has run and finished
add1(10) # 11 — and step is still availableLook at the last two lines. make_adder is over: its call has completed, there
is nothing left to return. Normally a function's locals and parameters disappear
along with the call — they exist only for the duration of it. And yet step
works.
Here is the question this lesson is about: why does the outer function's parameter still exist after the outer function has finished?
The answer that is enough at the basic level: because the inner function keeps
access to the outer function's variable. It was created inside make_adder,
it used that step — and on its way out it took the access with it. As long as
add1 is alive, so is its step. A function that carried away access to the
variables of the place it was created in is what we call a closure.
One thing in that answer should be read literally: what is kept is access to the variable, not a copy of its value. The difference is invisible while the variable never changes, and becomes the whole story the moment it does.
That is already enough to answer the basic interview question. Everything below is about how that access is actually built, what happens when there is more than one inner function, and the two places where the name-lookup rule does not work the way it is usually retold.
Mechanism 1: a cell, not a copy
An inner function that reads a name from the outer one does not receive its value. It receives a cell — a separate object both sides reach the variable through.
def outer():
x = 1
def inner():
return x
return innerThe compiler decides this, and the decision shows up in two fields of the code object:
outer.__code__.co_cellvars # ('x',) — the cell is created here
outer().__code__.co_freevars # ('x',) — and read hereBoth fields are filled in before the first call: the code for inner sits inside
outer's code object from compile time. The second line calls outer() only for
brevity — to reach that code through a ready-made function.
co_freevars holds exactly what the reference calls free variables: “If a
variable is used in a code block but not defined there, it is a free variable”.
A function with no such names has no closure at all, and
__closure__ is None — not an empty tuple, but None.
The value itself lives inside the cell and is taken from there:
inner = outer()
inner.__closure__[0].cell_contents # 1Mechanism 2: the value is read on the call, not on creation
Since a cell is held rather than a copy, changing the variable after the function is created reaches it:
def make():
v = "first"
def show():
return v
v = "second" # changed after show already exists
return show
make()() # 'second'This is not a quirk of nested functions. The reference states the rule for all free names — “Name resolution of free variables occurs at runtime, not at compile time” — and gives an example that behaves the same at module level:
i = 10
def f():
print(i)
i = 42
f() # 42The famous loop bug, where three functions all return the last value, grows from
the same root. It is worked through in the lambda
lesson — along with the fact that it has nothing to
do with lambda, since a plain def in a loop breaks in exactly the same way.
Mechanism 3: one cell for all of them
If one function returns two inner functions reading the same name, their cell is shared — not one copy each:
def two():
x = 1
def a(): return x
def b(): return x
return a, b
a, b = two()
a.__closure__[0] is b.__closure__[0] # TrueThat is the same object, not two equal ones: the check is is, that is,
identity. This is where the whole family of “why do they all return the same
thing” bugs comes from: there is nothing to differ — one cell, one value.
Mechanism 4: a cell keeps its object alive
A cell is a reference, and everything known about references applies to it. While
the function lives, the cell lives; while the cell lives, the object inside it
lives. Hence a class of leak that looks mysterious in a profiler and harmless in
the code (bench/closure-lifetime/retention.py, identical output on 3.12, 3.13 and 3.14).
First, the boundary worth knowing so as not to be frightened for nothing. A
closure holds not the whole frame but only the names in co_freevars (here
and below the script's output is abridged to the relevant lines):
2) after make_callback returned:
data, mentioned in the body : alive
unused, not mentioned : dead
the closure's __closure__ : 1 cell(s)
co_freevars : ('data',)
(Labels translated from the script's output.) The compiler puts into cells exactly the names actually read in the body. A local the nested function never mentions dies with the call, closure or no closure. The documentation describes the contents directly:
A tuple containing the names of free (closure) variables that a nested
scope references in an outer scope. […] Note: references to global and builtin
names are not included.
The practical case is a handler registered somewhere:
5) the connection was released by the caller, the handler is in the registry: alive
what holds it: ('self',)
after clearing the registry: dead
del conn does nothing here: the object is held by the self in the handler's
co_freevars, and the handler is held by the registry. This is the commonest
shape — a method taken as a callback drags the whole object behind it.
The second case is quieter: a cycle through a cell. The node holds the handler, and the handler holds the node through a cell.
6) cycle "node -> handler -> cell -> node", collector off: alive
after gc.collect(): dead | objects collected: True
Reference counting does not break such a cycle — the cycle collector is required. Until it runs, the memory is occupied, and with collection disabled or delayed that shows up as growth.
Releasing the object without deleting the function is possible: a cell may be
rewritten from inside through nonlocal.
7) before release: alive
after release(): dead | cb2() = released
The cell is still there — it now holds None. That is the supported way to build
a "closeable" callback, and it also explains why weakref solves this class of
problem outright: a weak reference is not enough to keep an object alive.
Mechanism 5: where things actually live — the cell, the snapshot and partial
The lambda: i trap above was explained by a cell. There are in fact three ways,
and telling them apart is useful not for choosing between them — two of them give
the same result in the loop — but for knowing what you are looking at in someone
else's function (bench/introspection/closure_vars.py, identical output on
3.12, 3.13 and 3.14):
8) three ways in a loop:
closure over i : [2, 2, 2]
default argument : [0, 1, 2]
functools.partial : [0, 1, 2]
6) storage:
closure: __closure__ = (<cell ...>,) -> cell_contents = ['second']
default: __defaults__ = ('first',) | __closure__ = None
partial: .args = ('first',) | .keywords = {}
(Labels translated from the script's output.) Three different places. And the difference is not cosmetic: only the first is live.
5) before rebinding: first | first | first
after rebind('second'): second | first | first
the closure sees the new value, the snapshots the old
The gap between a default argument and partial is smaller than people assume:
both take a snapshot at creation, both miss the rebinding. They differ in where
the value is kept, and in that __defaults__ can be rewritten from outside while
partial is immutable — you need a new object.
The tool that shows the first storage and not the other two is
inspect.getclosurevars:
Get the mapping of external name references in a Python function or method
func to their current values. A named tuple ClosureVars(nonlocals, globals, builtins, unbound) is returned. nonlocals maps referenced names to lexical
closure variables, globals to the function's module globals and builtins to
the builtins visible from the function body. unbound is the set of names
referenced in the function that could not be resolved at all given the current
module globals and builtins.
It sorts a function's external names into four buckets at once:
1) getclosurevars(by_closure):
nonlocals: {'n': 2}
globals : {'GLOBAL_RATE': 10}
builtins : {'len': <built-in function len>}
unbound : set()
The unbound bucket is separately useful: it collects names that do not resolve
in the current module at all. A typo in a global's name shows up there before the
call rather than in production.
But the tool's boundary has to be known. The snapshot in __defaults__ does not
exist for it, and partial it refuses outright:
2) getclosurevars(by_default) — n is NOT in nonlocals, it is a parameter:
nonlocals: {}
and the snapshot's value is here: (2,)
3) getclosurevars(partial) -> TypeError: functools.partial(...) is not a Python function
the snapshot's value on partial: (2,) | func: body
So "the closure is empty" in its output means "there are no cells", not "the
function remembered nothing". Where to look for what was remembered:
__defaults__ and .args.
Mechanism 6: the scope chain, and the two places it is not what you think
The four buttons above the diagram are four situations for one and the same name
x: in three of them it is found in a different scope, and in the fourth the
search stops at the very first step. The class body sits in the list as its own
struck-through row.
The search order is usually taught as “local → enclosing → global → builtins”. That is correct as far as it goes — and it leaves out two things.
The class body is skipped
The language has three kinds of block: “a module, a function body, and a class definition”. A class body is a block, but it is not part of the name-search chain for methods:
def make():
x = "from the function"
class C:
x = "from the class"
def get(self):
return x # ← which x?
return C
make()().get() # 'from the function'The method takes x from the enclosing function, even though the class
defines its own x and it sits closer. The reference is explicit: “The scope of
names defined in a class block is limited to the class block; it does not extend
to the code blocks of methods”. PEP 227, which introduced nested scopes, puts it
even more bluntly: “If a class definition occurs in a chain of nested scopes, the
resolution process skips class definitions”.
With no enclosing function, the name is simply not found:
class C:
y = "class"
def get(self):
return y # NameError: name 'y' is not definedA class attribute has to be reached through self or the class name — as an
attribute, not as a free name.
An assignment makes the name local for the whole body
The second place is not about the order of scopes but about which scope the name belongs to in the first place:
x = "global"
def trap():
print(x) # UnboundLocalError
x = "local"The error happens on the line above the assignment. The decision was made by
the compiler for the entire body at once, and it is visible in the code object:
the name lands in co_varnames, the list of locals, rather than in co_names,
where names looked up from outside go.
trap.__code__.co_varnames # ('x',) — local, for the whole bodyThe message is precise about it: cannot access local variable 'x' where it is not associated with a value. Not “name not found”, but “the local variable
exists and has no value yet”.
Mechanism 7: nonlocal and global — two different operations
Reading a free name works on its own. To assign to an outer variable, you have to say which one:
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
c = counter()
c(), c(), c() # (1, 2, 3)nonlocal points the name, in the reference's words, “to previously bound
variables in the nearest enclosing function scope”, and when several qualify,
“the nearest binding is used”. The word “function” matters for the same
reason it did in the section on classes: a class body does not qualify.
One more difference matters more than it first appears: nonlocal requires an existing
variable, and its absence is not a runtime error but a compile-time one:
def outer():
def inner():
nonlocal zz # SyntaxError: no binding for nonlocal 'zz' found
zz = 1The reference states it directly: “If a name is not bound in any nonlocal scope,
or if there is no nonlocal scope, a SyntaxError is raised”. global works the
other way round: it does not require the name to exist, and it points it at the
module. Lookup there goes through the global namespace first and builtins
second.
nonlocal | global | |
|---|---|---|
| points at | the nearest enclosing function | the module namespace |
| requires an existing variable | yes, otherwise SyntaxError | no |
| works at the top level | no | yes |
Deeper: what the bytecode shows
The cell is not a documentation abstraction; it has instructions of its own. The
outer function creates the cell and writes into it, the inner one picks it up and
reads it (here and below, python3.13):
outer: inner:
MAKE_CELL x COPY_FREE_VARS
LOAD_CONST 1 RESUME
STORE_DEREF x LOAD_DEREF x
LOAD_FAST x RETURN_VALUE
BUILD_TUPLE
MAKE_FUNCTION
SET_FUNCTION_ATTRIBUTE closure
These are two independent listings: each reads top to bottom, and the columns do not correspond line by line.
Note STORE_DEREF where STORE_FAST would normally be: even inside its own
function, a variable that has a cell is written through the cell. Otherwise the
inner function would not read what the outer one wrote.
The opcodes around building a closure have moved:
| 3.12.3 | 3.13.7 | 3.14.7 | |
|---|---|---|---|
| cell goes into the closure | LOAD_CLOSURE | LOAD_FAST | LOAD_FAST_BORROW |
| closure is attached to the function | MAKE_FUNCTION closure | SET_FUNCTION_ATTRIBUTE closure | SET_FUNCTION_ATTRIBUTE closure |
The read itself — LOAD_DEREF — has not changed once. This is exactly the thing
worth remembering when reading any bytecode listing: instruction names are tied
to a version, the mechanism behind them is not.
Deeper: what it costs
Reading a cell costs more than reading a local, but so little that restructuring code for it makes no sense. Two hundred reads of one name in a row, minimum of nine repeats, 3.13.7:
| access | opcode | per read | vs local |
|---|---|---|---|
| local variable | LOAD_FAST | 6.9 ns | ×1.00 |
| closure cell | LOAD_DEREF | 9.1 ns | ×1.32 |
| global | LOAD_GLOBAL | 9.1 ns | ×1.32 |
Two nanoseconds per read — a third of what a local costs. Whether that is much or little follows from the fact that a single read is under ten nanoseconds to begin with: for a third of that to show up as a win, the name has to be read inside a loop that is itself the bottleneck.
The third row deserves its own moment: it landed on top of the second, and that is not a rounding in favour of a convenient conclusion. The run repeats the whole measurement five times and compares the distance between rows against its own wobble (labels translated from the script's output):
SPREAD OVER 5 REPEATS OF THE WHOLE MEASUREMENT, as ratios to the local
LOAD_FAST (local) x1.00 .. x1.01 spread 0.01
LOAD_DEREF (cell) x1.32 .. x1.33 spread 0.01
LOAD_GLOBAL (global) x1.32 .. x1.33 spread 0.01
distance between cell and global 0.00
widest spread of those same two rows 0.01
can the instrument resolve the gap no
“Global variables are slow” is advice that has outlived its reasons: the instrument sees no difference between a global and a cell at all. The interpreter specialises a global lookup while the module dictionary is unchanged; that machinery is taken apart in the article on the interpreter loop.
That clause is also the boundary of the claim, and the rule is worth writing from the boundary rather than from the number. "A global costs the same" holds where the specialisation holds: CPython 3.13.7, with the module dictionary not being rewritten as the code runs. The rule that survives being carried elsewhere reads differently: at these magnitudes you pick the kind of access by meaning, not by price — and if a decision really does come down to nanoseconds, you measure them on your own code and your own build.
Timings are never compared across versions here, and the reason is in the builds.
3.12 is built with GCC 13.3.0; 3.13 and 3.14 use Clang 20.1.4,
and 3.14 additionally has --with-tail-call-interp, which 3.13 does not. More
than the compiler differs, and the gap between builds swamps the gap between
versions. Only rows of one table are comparable: they come from a single run on
3.13.7.
Version history
Python 2.1 — PEP 227 introduces nested scopes: a name from an enclosing function becomes visible. The same PEP fixes the exception for class bodies that still holds today.
Python 3 — nonlocal arrives, making it possible not only to read an outer
variable but to assign to it; the reference names PEP 3104 as its specification.
Python 3.11 — an explicit MAKE_CELL appears in the bytecode: creating the
cell becomes its own instruction at the start of the function's code. It carries
“Added in version 3.11” in the dis documentation, as does COPY_FREE_VARS.
Python 3.13 — LOAD_CLOSURE leaves the listing: the closure is attached to
the function by a separate SET_FUNCTION_ATTRIBUTE, documented as “Added in
version 3.13”.
Python 3.14 — loading the cell into the closure tuple goes through
LOAD_FAST_BORROW, like every other local load. Observable behaviour is
unchanged.
How to answer in an interview
Short answer: a closure is a function that kept access to the variables of the place it was created in, and goes on using them after that place has finished running. What it kept is access to the variable, not a copy of its value: the value is read at call time. That is why a change made to the variable after the function was created reaches it.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
The object that access goes through is called a cell, and the reference states
the rule outright: name resolution of free variables occurs at runtime, not at
compile time. The cell is shared, too: two inner functions reading one name
point at the same object, which an is comparison settles.
What separates a good answer: naming what the familiar chain "local → enclosing
→ global → builtins" leaves out. A class body is not part of the chain: a
method reading x takes it from the enclosing function even when the class
right next to it defines its own. And an assignment anywhere in a body makes
the name local for the whole body — reading it earlier in the code raises
UnboundLocalError without ever reaching a global of the same name. The cost
of all this is small: 9.1 ns against 6.9 for a local — two nanoseconds — and a
global costs exactly the same, which is not what "globals are slow" would lead
you to expect.
Next they ask
Then how do you make each function from the loop remember its own value?
Give it as an argument with a default, or through functools.partial — that is,
store a snapshot rather than a cell. A closure holds the variable, and the
value is read at call time: a change made to the variable after the function was
created reaches it.
A closure holds a reference. What does that cost in memory?
A class of leak that looks mysterious in a profiler and harmless in the code:
while the function lives the cell lives, and while the cell lives so does the
object in it. The boundary is worth knowing so as not to fear the wrong thing:
what is held is not the whole frame, only the names in co_freevars.
Common misconceptions
“A closure saves the variable's value as of the moment the function was created.”
It saves the cell. A variable changed after the function was created reaches it: v becomes “second” after def show, and show() returns “second”. The reference states this as a rule: “Name resolution of free variables occurs at runtime, not at compile time”.
“Each inner function gets its own copy of the outer variable.”
The cell is shared. Two functions reading one name give a.__closure__[0] is b.__closure__[0] → True: one object, not two equal ones. This is also where the “why do they all return the same thing” family of bugs comes from.
“A method sees its own class's attributes as ordinary names.”
A class body is not part of the search chain. A method reading x takes it from the enclosing function even when the class defines its own, and with no enclosing function it gets a NameError. The reference: “The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods”. An attribute is reached through self or the class name.
“UnboundLocalError means you forgot to create the variable.”
It means the name is local — because of an assignment somewhere below in the body — and holds no value yet. A global of the same name may well exist, but it is never reached. The decision was made by the compiler for the whole body at once, and it shows up in co_varnames.
“Reading global variables is noticeably slower.”
On 3.13.7 a global costs 9.1 ns per read — exactly the same as a closure cell: over five repeats of the measurement the distance between them is 0.00 against a spread of 0.01 in each, so the instrument does not separate them at all. Both cost more than a local, by two nanoseconds — a third, but a third of nine nanoseconds.
Practice
Two exercises. Answer first, then check against the real output: in both, the right answer comes from a recorded run rather than from an editor.
Practice · predict the output
x = "module"
def outer():
x = "enclosing"
class C:
x = "class"
def read(self):
return x
return C().read()
def shadow():
try:
got = x
except UnboundLocalError:
return "UnboundLocalError"
x = "local"
return got
print(outer())
print(shadow())Practice · estimate
Knowledge check
make binds x = 'from the function'; inside it, class C binds x = 'from the class', and the method get returns a free x. What does make()().get() return?
What measured this
The numbers in this article come from these scripts. Each one opens from here, together with the record of the run: what it was measured on, what came out, and with what spread.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A function created inside another one goes on using that function's variables after the outer function has finished. That is what a closure is. What matters is precisely what it uses: not a copy of the value taken at creation time, but the variable itself — so the value is read when the inner function is called, not when it was created.
- Hence the main consequence: a variable is captured, not a value, and that diverges from expectation in three places. A change made to the variable after the function was created reaches it:
vbecomes "second" afterdef show, andshow()returns "second". The access is also shared: two inner functions reading the same name point at the same object, which aniscomparison confirms. And the familiar "local → enclosing → global → builtins" chain leaves out two things: a class body is not part of it (a method readingxtakesxfrom the enclosing function even when the class right next to it defines its own), and an assignment anywhere in a body makes the name local for the whole body — so reading it earlier in the code raisesUnboundLocalErrorwithout ever reaching a global of the same name. - Beyond that is what separates knowing from having read. The object that shared access goes through is called a cell, and a name a function uses but does not define is a free variable; the reference states the rule for them outright: “Name resolution of free variables occurs at runtime, not at compile time”. The cost of all this is small and resolvable at the same time: reading a cell takes 9.1 ns against 6.9 ns for a local — a third more, two nanoseconds (3.13.7). A global costs the same as a cell down to the hundredths of the ratio, which is not what “globals are slow” would lead you to expect.
In fact
- It saves the cell. A variable changed after the function was created reaches it:
vbecomes “second” afterdef show, andshow()returns “second”. The reference states this as a rule: “Name resolution of free variables occurs at runtime, not at compile time”. - The cell is shared. Two functions reading one name give
a.__closure__[0] is b.__closure__[0]→True: one object, not two equal ones. This is also where the “why do they all return the same thing” family of bugs comes from. - A class body is not part of the search chain. A method reading
xtakes it from the enclosing function even when the class defines its own, and with no enclosing function it gets aNameError. The reference: “The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods”. An attribute is reached throughselfor the class name. - It means the name is local — because of an assignment somewhere below in the body — and holds no value yet. A global of the same name may well exist, but it is never reached. The decision was made by the compiler for the whole body at once, and it shows up in
co_varnames. - On 3.13.7 a global costs 9.1 ns per read — exactly the same as a closure cell: over five repeats of the measurement the distance between them is 0.00 against a spread of 0.01 in each, so the instrument does not separate them at all. Both cost more than a local, by two nanoseconds — a third, but a third of nine nanoseconds.
What is covered
- Base: an inner function remembers the outer one's variables
- Mechanism 1: a cell, not a copy
- Mechanism 2: the value is read on the call, not on creation
- Mechanism 3: one cell for all of them
- Mechanism 4: a cell keeps its object alive
- Mechanism 5: where things actually live — the cell, the snapshot and `partial`
- Mechanism 6: the scope chain, and the two places it is not what you think
- Mechanism 7: `nonlocal` and `global` — two different operations
- Deeper: what the bytecode shows
- Deeper: what it costs
- Version history
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
- What measured this
Sources & further reading
5 SOURCES
- Language reference — execution model, scopes and name bindingOfficial documentation. The primary source for every rule in this lesson. On blocks: “A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition”. On free variables: “If a variable is used in a code block but not defined there, it is a free variable”. And the one that matters most here: “Name resolution of free variables occurs at runtime, not at compile time” — the reference gives the i = 10 / i = 42 example reproduced in the text.https://docs.python.org/3.14/reference/executionmodel.html
- Language reference — a class body does not continue the chainOfficial documentation. Verbatim: “The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods”. Hence the consequence for methods: a class attribute is reachable through self or the class name, but not as a free name. This is one of the two places where the familiar scope chain misleads (the other is not about the order of scopes but about which scope a name belongs to). Checked by running it on 3.12, 3.13 and 3.14.7.https://docs.python.org/3.14/reference/executionmodel.html
- Language reference — the nonlocal statementOfficial documentation. The full specification of nonlocal, the source of both claims made here. On the nearest scope: “If a name is bound in more than one nonlocal scope, the nearest binding is used”. On failing before the program ever runs, verbatim: “If a name is not bound in any nonlocal scope, or if there is no nonlocal scope, a SyntaxError is raised”. The reference names PEP 3104 as the specification for the statement. Checked by compiling on 3.12, 3.13 and 3.14.7.https://docs.python.org/3.14/reference/simple_stmts.html#the-nonlocal-statement
- PEP 227 — Statically Nested ScopesPEP. Jeremy Hylton; Final, Python 2.1. The PEP that introduced nested scopes at all: “The proposal changes the rules so that names bound in B are visible in A”. It also states the exception this lesson devotes a section to: “Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions”.https://peps.python.org/pep-0227/
- dis — MAKE_CELL, STORE_DEREF, LOAD_DEREF, COPY_FREE_VARSOfficial documentation. Where the opcodes in the bytecode section come from, and how they moved between versions: on 3.12 the cell goes into the closure via LOAD_CLOSURE and MAKE_FUNCTION with a closure flag, on 3.13 and 3.14 via an ordinary load plus a separate SET_FUNCTION_ATTRIBUTE. It also dates the instructions: MAKE_CELL and COPY_FREE_VARS carry “Added in version 3.11”, SET_FUNCTION_ATTRIBUTE “Added in version 3.13”. Disassembled on all three versions.https://docs.python.org/3.14/library/dis.html