I guess the tutorial is unfortunately (because ambiguously) phrased and by
[d]ata attributes override method attributes with the same name
it actually means "Data attributes override previously assigned/defined method attributes of the same name and vice versa: method attributes override previously assigned/defined data attributes of the same name."
"Duh", you might think "data attributes also override previously assigned data attributes of the same name, so what's the big deal? Why is this even mentioned?" Assigning and re-assigning (called "overriding" in the cited tutorial) to variables (whether called "attributes" of something or not) is after all one of the prototypical features of an imperative programming language.
Well, let me introduce you to
Namespaces
Python classes are namespaces. So what the tutorial might try to tell us here is that data attributes and method attributes within a class share a namespace.
This isn't the case for attributes of different classes, though. If a class inherits from another class, it has access to its parent's names. If a name is reused for method definition or data assignment within the inheriting class, the parent class keeps the original values. In the child class they are merely temporarily shadowed. If you remove the name from the child class, it, too, will again provide access to the parent's attribute of the same name:
class A:
x = 111
class B1(A):
x = 123 # Shadows A.x
assert B1.x == 123
del B1.x # But after removing B1's own x attribute ...
assert B1.x == 111 # ... B1.x is just an alias to A.x !
# Shadowing can happen at any time:
class B2(A):
pass
assert B2.x == A.x == 111
B2.x = 5 # shadowing attributes can also be added after the class definition
assert B2.x == 5
assert A.x == 111
del B2.x
assert B2.x == A.x == 111
Contrast this with re-definition a.k.a. re-assignment (or "overriding" as the tutorial calls it):
class C:
x = 555
def x(self):
print('I am x')
C().x() # outputs "I am x"
del C.x
print(C.x) # AttributeError: 'C' object has no attribute 'x'
Method C.x()
didn't temporarily shadow data attribute C.x
. It replaced it, so when we delete the method, x
is missing completely within C
, rather than the data attribute re-appearing under that name.
More Namespaces
Instantiation adds another namespace and thus another chance for shadowing:
a = A()
assert a.x == 111 # instance namespace includes class namespace
a.x = 1000
assert a.x == 1000
assert A.x == 111 # class attribute unchanged
del a.x
assert a.x == 111 # sees A.x again
In fact, all (nested) namespaces in Python work that way: packages, modules, classes, functions and methods, instance objects, inner classes, nested functions ...
When reading a variable, the namespace hierarchy is walked bottom-up until the name is found. (Reading here means finding the value (object, function/method or built-in) to which the variable's name is bound. If the value is mutable, this can also be used to change the value.)
On the other hand, when setting (defining or redefining) a variable, a name of the current namespace is used: Rebound to the new value if the name already exists in that very namespace (rather than only being included there from another namespace) or a newly created name if it didn't exist before.