According to the docs on inheritance:
Derived classes may override methods of their base classes. Because methods have no special privileges when calling
When an attribute look-up is performed on an instance of the class, the class dictionary and the dictionaries of its base classes are searched in a certain order (see: Method Resolution Order) for the appropriate method. What is found first is going to get called.
Using the following Spam example:
class Spam:
def produce_spam(self):
print("spam")
def get_spam(self):
self.produce_spam()
class SuperSpam(Spam):
def produce_spam(self):
print("super spam")
Spam defines the functions produce_spam and get_spam. These live in its Spam.__dict__ (class namespace). The sub-class SuperSpam, by means of inheritance, has access to both these methods. SuperSpam.produce_spam doesn't replace Spam.produce_spam, it is simply found first when the look-up for the name 'produce_spam' is made on one of its instances.
Essentially, the result of inheritance is that the dictionaries of any base classes are also going to get searched if, after an attribute look-up on the sub-class is made, the attribute isn't found in the sub-class's dictionary.
When the function get_spam is first invoked with:
s = SuperSpam()
s.get_spam()
the sequence of events roughly goes like this:
SuperSpams __dict__ for get_spam.SuperSpams __dict__ look into the dictionaries of it's base classes (mro chain). Spam is next in the mro chain, so get_spam is found in Spam's dictionary.Now, when produce_spam is looked up in the body of get_spam with self.produce_spam, the sequence is much shorter:
SuperSpam's (self) __dict__ for produce_spam.produce_spam is found in the __dict__ first so that gets fetched.