In object-oriented programming (OOP), when defining a class, we can inherit from an existing class. The new class is called a Subclass, and the inherited class is referred to as the Base class, Parent class, or Super class.
For example, we have already written a class named Animal with a run() method that prints directly:
class Animal(object):
def run(self):
print('Animal is running...')
When we need to write the Dog and Cat classes, we can inherit directly from the Animal class:
class Dog(Animal):
pass
class Cat(Animal):
pass
For Dog, Animal is its parent class; for Animal, Dog is its subclass. The same applies to Cat and Dog.
What are the benefits of inheritance? The biggest advantage is that the subclass inherits all the functionality of the parent class. Since Animal implements the run() method, Dog and Cat—as its subclasses—automatically gain the run() method without any additional code:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
The output is as follows:
Animal is running...
Animal is running...
Of course, we can also add new methods to subclasses (e.g., the Dog class):
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
The second benefit of inheritance requires a slight improvement to the code. As you can see, both Dog and Cat print “Animal is running…” when their run() method is called. Logically, they should print “Dog is running…” and “Cat is running…” respectively. Therefore, we modify the Dog and Cat classes as follows:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
Running the code again produces the following output:
Dog is running...
Cat is running...
When both a subclass and its parent class have a run() method with the same name, we say the subclass’s run()overrides the parent class’s run(). During code execution, the subclass’s run() is always called. This gives us another benefit of inheritance: Polymorphism.
To understand polymorphism, we first need to clarify data types. When we define a class, we essentially define a new data type. The data types we define are no different from Python’s built-in types (e.g., str, list, dict):
a = list() # a is of type list
b = Animal() # b is of type Animal
c = Dog() # c is of type Dog
We can check if a variable belongs to a specific type using isinstance():
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
It is clear that a, b, and c correspond to the list, Animal, and Dog types, respectively.
But wait—try this:
>>> isinstance(c, Animal)
True
It turns out c is not only a Dog but also an Animal!
On reflection, this makes sense: since Dog inherits from Animal, when we create an instance c of Dog, it is correct to say c is of type Dog—and it is equally correct to say c is of type Animal, as Dog is inherently a type of Animal!
Thus, in an inheritance relationship, if an instance is of a subclass type, it can also be considered an instance of the parent class. However, the reverse is not true:
>>> b = Animal()
>>> isinstance(b, Dog)
False
A Dog can be treated as an Animal, but an Animal cannot be treated as a Dog.
To understand the benefits of polymorphism, let’s write a function that accepts an Animal type variable:
def run_twice(animal):
animal.run()
animal.run()
When we pass an instance of Animal to run_twice(), it prints:
>>> run_twice(Animal())
Animal is running...
Animal is running...
When we pass an instance of Dog, run_twice() prints:
>>> run_twice(Dog())
Dog is running...
Dog is running...
When we pass an instance of Cat, run_twice() prints:
>>> run_twice(Cat())
Cat is running...
Cat is running...
This may seem trivial, but consider this: if we now define a Tortoise class that also inherits from Animal:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
When we call run_twice() with a Tortoise instance:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
You will notice that adding a new subclass of Animal requires no modifications to run_twice(). In fact, any function or method that relies on Animal as a parameter will work without changes—thanks to polymorphism.
The advantage of polymorphism is that when we need to pass Dog, Cat, Tortoise, etc., we only need to accept an Animal type parameter. Since Dog, Cat, Tortoise, etc., are all Animal types, we can operate on them as Animal instances. Because the Animal type has a run() method, any passed type (as long as it is Animal or its subclass) will automatically call the run() method of its actual type. This is the essence of polymorphism:
For a variable, we only need to know it is of type Animal (without knowing its exact subclass) to safely call the run() method. The specific run() method executed (on an Animal, Dog, Cat, or Tortoise object) is determined by the object’s actual type at runtime. This is the true power of polymorphism: the caller only needs to focus on the call, not the implementation details. When adding a new subclass of Animal, we only need to ensure the run() method is implemented correctly—no changes to existing calling code are required. This is the famous Open/Closed Principle:
Animal subclasses can be added freely.run_twice() that depend on the Animal type do not need to be modified.Inheritance can be hierarchical (e.g., from grandfather to father to son). Every class ultimately traces back to the root class object, forming an inverted tree structure of inheritance relationships. For example:
┌───────────────┐
│ object │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Animal │ │ Plant │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Cat │ │ Tree │ │ Flower │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
For static languages (e.g., Java), if a function expects an Animal type parameter, the passed object must be an Animal or its subclass—otherwise, the run() method cannot be called.
For dynamic languages like Python, passing an Animal type is not mandatory. We only need to ensure the passed object has a run() method:
class Timer(object):
def run(self):
print('Start...')
This is the Duck Typing characteristic of dynamic languages: it does not require a strict inheritance hierarchy. An object is considered a “duck” if it “looks like a duck and walks like a duck.”
Python’s “file-like object” is an example of duck typing. A real file object has a read() method that returns its content. However, many objects with a read() method are treated as “file-like objects.” Many functions accept “file-like objects” as parameters—you do not need to pass a real file object, only any object that implements the read() method.
animals.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Animal(object):
def run(self):
print("Animal is running...")
class Dog(Animal):
def run(self):
print("Dog is running...")
class Cat(Animal):
def run(self):
print("Cat is running...")
def run_twice(animal):
animal.run()
animal.run()
a = Animal()
d = Dog()
c = Cat()
print("a is Animal?", isinstance(a, Animal))
print("a is Dog?", isinstance(a, Dog))
print("a is Cat?", isinstance(a, Cat))
print("d is Animal?", isinstance(d, Animal))
print("d is Dog?", isinstance(d, Dog))
print("d is Cat?", isinstance(d, Cat))
run_twice(c)
get_instance.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Animal(object):
pass
class Dog(Animal):
pass
class Husky(Dog):
pass
a = Animal()
d = Dog()
h = Husky()
print("check a = Animal()...")
print("isinstance(a, Animal) =", isinstance(a, Animal))
print("isinstance(a, Dog) =", isinstance(a, Dog))
print("isinstance(a, Husky) =", isinstance(a, Husky))
print("check d = Dog()...")
print("isinstance(d, Animal) =", isinstance(d, Animal))
print("isinstance(d, Dog) =", isinstance(d, Dog))
print("isinstance(d, Husky) =", isinstance(d, Husky))
print("check h = Husky()...")
print("isinstance(h, Animal) =", isinstance(h, Animal))
print("isinstance(h, Dog) =", isinstance(h, Dog))
print("isinstance(h, Husky) =", isinstance(h, Husky))