Why can't a method see a name bound in its own class body without self or the class name?
answer
- Only one kind of scope nests
- A class namespace is not that kind
- Methods jump straight to module level
- The value is there, but as an attribute
- Body statements themselves behave differently
basics
~20 sA class body executes in its own namespace that becomes the class attributes, but it is skipped when functions inside it resolve bare names. A method's search goes from its locals straight to module globals, so use self.NAME.
solid answer
~50 sOnly function scopes act as enclosing scopes. A class body runs once, in its own namespace, and that namespace is turned into the class's attribute dictionary — but it is **not** part of the lexical chain for functions defined inside it. So inside a method, a bare `RETRIES` skips the class body entirely and looks in the module globals and then the built-ins, raising `NameError` if neither has it. The fix is to name the owner: `self.RETRIES` (an attribute lookup that walks the instance, then the class, then the MRO) or `ClassName.RETRIES` (a global lookup for the class, then an attribute lookup). Code running *directly* in the class body — not inside a `def` — does see the names already bound there, which is why `DOUBLED = RETRIES * 2` works one line below `RETRIES = 3`.
code
python · 18 linesclass Config:
RETRIES = 3
DOUBLED = RETRIES * 2 # body statements see earlier body names
def limit(self):
return self.RETRIES # attribute lookup: instance, class, MRO
class Broken:
RETRIES = 3
def limit(self):
return RETRIES # bare name: class body is skipped
print(Config.DOUBLED, Config().limit()) # 6 3
try:
Broken().limit()
except NameError as exc:
print("NameError:", exc) # name 'RETRIES' is not definedgo deeper
Recall that inside a method you reach class-level values through self, and that a bare name there does not find them. Being able to predict the NameError on a small snippet is enough at this level.
Explain the mechanic: only function scopes join the enclosing chain, so a method's search goes locals, enclosing functions, module globals, built-ins — and be ready to say why statements in the body itself behave differently.
Show the design consequence: self.NAME respects subclass overrides while ClassName.NAME pins the value, and choosing wrongly produces configuration that silently ignores a subclass. Explain the choice on real class hierarchies.
Frame it as an API-surface decision — class-level constants that subclasses are meant to override are a contract, and whether that contract goes through attributes, a classmethod hook or explicit configuration is a call you should be able to defend.
## Two different namespaces that look alike Executing a `class` statement creates a fresh namespace, runs the body's statements in it, and then hands that namespace to the metaclass to build the class object; the entries become the class attributes. Because assignment inside the body works normally, it is easy to assume the body behaves like a function scope. It does not, and the difference shows up the moment a `def` appears inside it. ## The rule: only function scopes nest Python's enclosing-scope chain is built from function scopes alone. When the compiler resolves a bare name inside a method, it walks outward through enclosing *functions* — and a class body is not one, so it is skipped. The search therefore runs: the method's own locals, then any enclosing functions (only if the class itself is nested inside a function), then the module globals, then the built-ins. The class namespace never appears in that list. ```python class Broken: RETRIES = 3 def limit(self): return RETRIES # NameError: name 'RETRIES' is not defined ``` The binding exists — as `Broken.RETRIES` — but it is reachable only through an attribute lookup, never as a bare name. ## Why the language does it this way If class namespaces did nest, every method would silently see every class attribute as a bare name, and adding an attribute to a class could quietly capture a name that a method was reading from the module. Attribute access would also become ambiguous with plain names, and inheritance would have to be folded into the lexical rules — a name inherited from a base class would have to become visible as a bare name inside a subclass's methods, at compile time, which is impossible because bases are only known at run time. Skipping the class body keeps name resolution decidable while compiling, and keeps `self.x` the single, explicit way to reach per-class state. ## What the class body itself can see Statements executed directly in the body are a different matter. They run top to bottom in the class namespace, so they see the names already bound above them, plus enclosing function scopes, module globals and built-ins. `DOUBLED = RETRIES * 2` immediately after `RETRIES = 3` is fine. A default-argument expression in a method signature also runs at class-definition time, in the class body, so `def limit(self, n=RETRIES)` works even though `return RETRIES` in the same method's body does not — one of the sharpest demonstrations of the boundary. ## Reaching the value properly Use `self.NAME` when subclasses should be able to override it: attribute lookup checks the instance dictionary, then the class, then each class along the MRO, so a subclass that rebinds the attribute changes what the inherited method sees. Use `ClassName.NAME` when you deliberately want the value defined on that specific class regardless of subclass overrides — note that this is an ordinary global lookup of `ClassName` followed by an attribute access, which is why it works even though the bare name does not. Inside a `classmethod`, `cls.NAME` gives you the subclass-aware version. ## The one compiler-provided exception A method that mentions zero-argument `super()` or `__class__` gets an implicit closure over the class being defined, inserted by the compiler specifically to make `super()` work. That is a targeted compiler mechanism, not the class body acting as an enclosing scope; every other name in the method still skips the class namespace. ## The interview shape Expect to be shown a small class where a method reads a class attribute as a bare name and asked what happens. Say `NameError`, name the rule (only function scopes nest; class bodies are skipped), point out that the value is genuinely there as a class attribute, and give both fixes with the reason to prefer `self.` — subclass overrides. Mentioning that code directly in the body *can* see earlier names shows you understand the boundary rather than having memorised a gotcha.
- If the class body is skipped, how should a method reach that class attribute?Through `self.NAME`, which walks the instance dictionary, then the class, then the MRO — so a subclass override is honoured. Use `ClassName.NAME` when you want that specific class's value regardless of overrides; that works because the class name itself is an ordinary module global, followed by an attribute access. In a `classmethod`, `cls.NAME` is the subclass-aware form.
- Can code in a class body read module globals and enclosing function locals?Yes. The body is executed like any block of code and its own bare names follow the normal search, so it can read names bound earlier in the body, enclosing function scopes, module globals and built-ins. What is special is only that the resulting class namespace is not offered as an enclosing scope to the functions defined inside it.
- If class bodies are skipped, how does zero-argument super() know which class it is in?The compiler notices that the method's body mentions `super` or `__class__` and gives the method an implicit closure over the class object being defined. It is a special-cased compiler mechanism added to make the zero-argument form possible — not the class namespace becoming an enclosing scope, which is why no other class-body name is reachable that way.
saying these in an interview costs you the question
- Says the class body acts as an enclosing scope
- Expects a bare class attribute name to work in a method
- Claims the attribute does not exist at all
- Thinks self.NAME and ClassName.NAME are interchangeable
- Cannot explain why body statements see earlier body names