From a385f66081da22939eb68727c597e647e0ae3819 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:44:56 +0200 Subject: [PATCH 1/5] Create pep-0837.rst First draft of PEP: Name Resolution in Class Namespaces --- peps/pep-0837.rst | 182 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 peps/pep-0837.rst diff --git a/peps/pep-0837.rst b/peps/pep-0837.rst new file mode 100644 index 00000000000..0d82b3cb8fc --- /dev/null +++ b/peps/pep-0837.rst @@ -0,0 +1,182 @@ +PEP: 837 +Title: Name Resolution in Class Namespaces +Author: Jeremy Hylton +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. From 4b8cf90941233cd077609498929d0bb20267bd7a Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:46:08 +0200 Subject: [PATCH 2/5] Update CODEOWNERS For PEP 837 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 509e23f3e1e..d3decb4aaf2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 # ... From 679a456f68e11ade0114b99d1cb1a9977f50eb1d Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:56:21 +0200 Subject: [PATCH 3/5] Update pep-0837.rst Fix references to other PEPs. --- peps/pep-0837.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/peps/pep-0837.rst b/peps/pep-0837.rst index 0d82b3cb8fc..07d5c166c2f 100644 --- a/peps/pep-0837.rst +++ b/peps/pep-0837.rst @@ -48,7 +48,7 @@ Here is an example of resolution of a free variable in a class: 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`. +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 @@ -112,7 +112,7 @@ 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. +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." From b2c66d865d5e5091a912d60116f06ef633282ee2 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:00:23 +0200 Subject: [PATCH 4/5] Update pep-0837.rst Once again try to fix PEP references --- peps/pep-0837.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/peps/pep-0837.rst b/peps/pep-0837.rst index 07d5c166c2f..dded82ec21d 100644 --- a/peps/pep-0837.rst +++ b/peps/pep-0837.rst @@ -48,7 +48,7 @@ Here is an example of resolution of a free variable in a class: 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`. +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 @@ -112,7 +112,7 @@ 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. +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." From 23aced3bba6acf96018514b443683dae1df8ce1c Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:05:01 +0200 Subject: [PATCH 5/5] Update pep-0837.rst Trailing whitespace --- peps/pep-0837.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/peps/pep-0837.rst b/peps/pep-0837.rst index dded82ec21d..81ee1feaba8 100644 --- a/peps/pep-0837.rst +++ b/peps/pep-0837.rst @@ -41,7 +41,7 @@ Here is an example of resolution of a free variable in a class: ... class A: ... a = x ... return A -... +... >>> A = f(1) >>> A.a # the value of the parameter x, not the global x 1 @@ -61,7 +61,7 @@ differently. ... a = x ... x = 42 ... return A -... +... >>> A = f(1) >>> A.a # the value of the global x, not the parameter x 0 @@ -81,7 +81,7 @@ resolved using the global namespace. ... del locals()[var] ... a = x ... return A -... +... >>> A1 = f("x") >>> A2 = f("y") >>> A1.a, A2.a @@ -105,7 +105,7 @@ 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 + 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 @@ -155,7 +155,7 @@ enclosing scope, use that namespace rather than the global namespace. ... del x ... a = x # Should this use f's local or global? ... return A -... +... >>> A = f() >>> A.a ?