In Python, the preferred terminology is attribute, not “variable”.
In Python, classes are themselves objects which we can treat as values. So if an int variable is a variable that stores an int, and string variable is a variable intended to store strings, and a list variable is a variable that stores lists, a class variable would be a variable that stores classes.
Attribute lookup from a class object can only see attributes defined on the class itself, or one of its superclasses. To keep things simple, I will ignore superclasses, and just have a single class:
attr = 'from the class'
instance = MyClass()
So looking up
MyClass.attr will return ‘from the class’.
Attribute lookup from an instance can see attributes defined on the class or on the instance. That is necessary so that methods can be found. In Python, methods are just plain old attributes that happen to also be callable like a function:
value = "abcd".upper # Look up, but don't call, the "upper" attribute on a string.
Attribute lookup from an instance starts the search at the instance, then if not found, it goes on to the class, and the superclasses. So looking up
instance.attr will also return ‘from the class’.
But when we assign to the instance attribute:
instance.attr = 'from the instance'
now the class attribute is untouched, but access to it is blocked by the newly created instance attribute:
print(instance.attr) # will print 'from the instance'
print(MyClass.attr) # will print 'from the class'
Remember that Python is dynamic, and the interpreter doesn’t know what attributes exist at compile time, so attribute lookup has to occur at run time. Attributes are (usually) stored as objects in a special dictionary, called
__dict__. Each class, and most (but not all) instances, have that dictionary.
The simplified attribute lookup process goes something like this, halting on the first successful lookup:
if the object is an instance:
check the instance `__dict__`
check the class `__dict__`
for each superclass in the class' inheritance tree:
check the superclass `__dict__`
if still not found:
The real search process is much more complex, with memory-efficient slots, dynamic attributes created on the fly with the
__getattr__ method, the descriptor protocol, metaclasses and more. But you don’t need to care about any of that right now: it is all carefully designed so that the simple cases remain simple.