Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ peps/pep-0832.rst @brettcannon
peps/pep-0833.rst @dstufft
peps/pep-0835.rst @ilevkivskyi
peps/pep-0836.rst @savannahostrowski @Fidget-Spinner @brandtbucher
peps/pep-0837.rst @jeremyhylton
# ...
peps/pep-2026.rst @hugovk
# ...
Expand Down
182 changes: 182 additions & 0 deletions peps/pep-0837.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
PEP: 837
Title: Name Resolution in Class Namespaces
Author: Jeremy Hylton <jeremy@python.org>
Status: Draft
Type: Standards Track
Created: 15-Jul-2026
Python-Version: 3.16


Abstract
========

There is a long-standing inconsistency in the way variable names are
resolved in classes. Several alternatives to resolve this
inconsistency are discussed.

Movtivation
===========

Name resolution in a class namespace uses a partial dynamic lookup. A
name can be either local or global depending on whether an assignment
to the name has occured during the exection of the class body. If a
name is used before assignment, it will be looked up in globals (and
builtins).

If a class is defined within a function scope, a name may be resolved
to the nearest enclosing scope. If name local to an enclosing scope,
but the name is not bound, a NameError will occur.

The apparent consistency problem is that names may be both local and
global in the same class block, but they cannot be both local and
free. Or, name resolution behaves differently in a class depending on
whether it is defined at the top-level or inside another block. If a
global variable exists, it can be used to resolve a free variable
unless that variable is bound in an enclosing scope of the class.

Here is an example of resolution of a free variable in a class:

>>> x = 0
>>> def f(x):
... class A:
... a = x
... return A
...
>>> A = f(1)
>>> A.a # the value of the parameter x, not the global x
1

The variable x is resolved to the parameter of the function x. The
global is ignored. This behavior is the standard name resolution
defined in :pep:`227` and :pep:`3104`.

If x is used as a local variable, the behavior is different at the
top-level and inside another block. If the same code also uses x as a
local variable, then the initial reference to x is resolved
differently.

>>> x = 0
>>> def f(x):
... class A:
... a = x
... x = 42
... return A
...
>>> A = f(1)
>>> A.a # the value of the global x, not the parameter x
0

The first reference to x is no longer bound to the local variable x
defined in f, but instead uses the global variable.

The set of namespaces consulted are determined lexically, but it
cannot be determined statically whether a particular reference will be
resolved using the global namespace.

>>> x = 0
>>> def f(var):
... class A:
... x = 10
... if var in locals():
... del locals()[var]
... a = x
... return A
...
>>> A1 = f("x")
>>> A2 = f("y")
>>> A1.a, A2.a
(0, 10)


Background
==========

The Python 2.0 definition specifies exactly three namespaces to
check for each name -- the local namespace, the global namespace,
and the builtin namespace. Starting with Python 2.1, lexical scoping
was used and free variables could be resolved to a binding in an
enclosing scope. Class namespaces were a special case, and free variables
were not resolved in enclosing scopes. They used special LOAD_NAME /
STORE_NAME opcodes that explicit checked local and global namespaces.

In Python 3.4, free variables in classes were resolved in enclosing scopes,
but a local variable used before assignment was still resolved in the global
scope. The language specification was not updated at this time, so it
still specifies the pre-3.4 behavior:

A class definition is an executable statement that may use and define names.
These references follow the normal rules for name resolution with an exception
that unbound local variables are looked up in the global namespace.

Class namespaces and top-level module namespaces both use LOAD_NAME / STORE_NAME
opcodes that reflect the early local / global / builtin scoping rules. Class
namespaces are also special, because the class namespace become the attributes
of the class object. Given these differences, the different scoping rules for
classes weren't considered when :pep:`227` was written.

A bug was filed in 2002. The PEP author closed the bug with the
comment, "Just don't write code that abuses the wart."
https://github.com/python/cpython/issues/36300
Another bug was filed in 2010, leading the same author to suggest a change.
https://github.com/python/cpython/issues/53472
Apparently, inconsistent behavior in namespaces can lead to
inconsistent responses to identical bugs.

Proposal
========

There are several alternatives we could choose among. A few simple
alternatives seem wrong. We could revert to pre-3.4 behavior, but it
was surprising the free variables did not work in class scopes. We
could keep the current behavior, but it is seems inconsistent.

One approach is to make class namespaces work more like function
namespaces. If a local variable is used at a time when it is not
bound, it raises a NameError. This rule is simple and consistent. The
primary drawback of this approach is that it will break code. Code
that depends on this feature is inscrutable, depending on the reader
to understand whether the local or global reference was
intended. Inscrutable, though, is not the same as undefined or broken

Another approach is to change the behavior of free variable
resolution to use the global namespaces when a name is unbound. If
current code would raise a NameError, because a variable was unbound
in the enclosing scope, it would be resolved in the global namespace.

There is a third approach that is a subtle variant of the second. If
a local variable is unbound, and that variable has a binding in an
enclosing scope, use that namespace rather than the global namespace.

>>> x = 0
>>> def f():
... x = 1
... class A:
... x = 2
... del x
... a = x # Should this use f's local or global?
... return A
...
>>> A = f()
>>> A.a
?

If a local variable is unbound and it can be resolved in another
namespace anyway, why only the global namespace? Why not use the
full set of standard name resolution rules?

This option seems most consistent, but makes the implementation more
complicated. It can't be statically determinted whether a variable is
bound at a particular point, so we would need to allocate a cell for
any local variable of the class that shadows a local variable in an
enclosing scope. Since dynamic manipulation of local variables is
unusual, we would expect the extra closures required to almost always
be unused. It would nonetheless be a somewhat unusual case-- a nested
class definition that has a local variable that shadows a local
variable in an enclosing scope.


Copyright
=========

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.
Loading