Class scopes are isolated from enclosing class scopes because of how python code is executed. When the interpreter encounters class Outer:, it creates a new namespace to execute the class body in. The class object itself does not get created until the class block is exited. That means that by the time class Inner: is exited and Inner is created, Outer does not exist yet.
In short, Python nested classes behave nothing like nested classes in say Java, which can belong to an instance of the outer class. You can see this in your code, where you have to access Outer.Inner from the global scope to get your class. Your definition could have been written equivalently as follows:
class Inner:
inner_counter = 0
def __init__(self):
self.id = Inner.inner_counter
Inner.inner_counter += 1
class Outer:
pass
If you want a counter for each instance of Outer, you need to maintain the counter in that instance. Here is an example:
class Outer:
def __init__(self):
self._inner_counter = 0
def make_inner(self):
return self._Inner(self)
class _Inner:
def __init__(self, parent):
self.parent = parent # this line is optional
self.id = parent._inner_counter
parent._inner_counter += 1
Here, the factory method make_inner is used to construct _Inner objects in preference to calling Outer._Inner. I suggest marking _inner_counter and _Inner with leading underscores to let users know that they are implementation-dependent attributes that they shouldn't touch.
This is the straightforward implementation. If you want to simulate the behavior of the code in your question, you can create a property (or other descriptor, property is just so convenient) for Inner to contain the factory method:
from functools import partial
class Outer:
def __init__(self):
self._inner_counter = 0
@property
def Inner(self):
return partial(self._Inner, self)
class _Inner:
def __init__(self, parent):
self.parent = parent
self.id = parent._inner_counter
parent._inner_counter += 1
@property
def inner_counter(self):
return self.parent._inner_counter
With this version, you can run the original code exactly to get the desired result. I would recommend dropping the inner_counter property entirely and accessing the counter directly as _inner_counter on the Outer object.
Totally irrelevant, but your printouts would look cleaner using f-strings:
c0 = Outer()
print(f"c0 : {c0._inner_counter}")
c0a = c0.Inner()
print(f"c0 : {c0._inner_counter}")
c1 = Outer()
print(f"c1 : {c1._inner_counter}")