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 # ... diff --git a/peps/pep-0837.rst b/peps/pep-0837.rst new file mode 100644 index 00000000000..81ee1feaba8 --- /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.