Inheritance lets a new class borrow the data and behavior of an existing one. Write class Dog(Animal) and Dog is a subclass that automatically gets everything Animal defines — you only add or change what is different.
This matters because copying code is dangerous. When you fix a bug in one copy, you usually forget the others. Inheritance keeps shared behavior in one place — the parent — and lets each child specialise. The result is less code, fewer bugs, and a clear structure that mirrors real-world categories: a Dog is an Animal. That IS-A relationship is the heart of inheritance.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
class Dog(Animal):
def speak(self):
return self.name + ' woofs'
Dog redefines — overrides — speak, but inherits __init__ from Animal unchanged. That is the point: reuse what is the same, replace what is different.
flowchart TD A["class Animal\nspeak()"] --> D["class Dog\nspeak() override"] A --> C["class Cat\nspeak() override"] style A fill:#3776ab,color:#fff
Extending with super()
Use super() inside a subclass method to call the parent's version — handy when you want to extend rather than replace it. super().speak() runs Animal.speak, then you add extra behaviour on top:
class Dog(Animal):
def speak(self):
return super().speak() + ' ... then WOOF'
This is cleaner than rewriting the parent's logic inside the child. It also survives changes: if Animal.speak is fixed later, Dog automatically gets the fix because it delegates upward instead of copying. The super() call looks up the method on the parent class and invokes it with the same instance, so self.name is still available. Without super(), you would have to duplicate the parent's code, and duplicated code always drifts out of sync.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
class Dog(Animal):
def speak(self):
return super().speak() + ' ... then WOOF'
class Cat(Animal):
def speak(self):
return self.name + ' purrs'
pets = [Dog('Rex'), Cat('Bo'), Animal('Mia')]
for p in pets:
print(p.speak())
Polymorphism: same call, different behaviour
Because every subclass has its own speak, you can loop over mixed objects and each one responds its own way. That is polymorphism — one piece of code works with many types because they share an interface.
The loop above does not know whether p is a Dog, a Cat, or a plain Animal. It only knows that p has a speak method, and that is enough. This is the real power of inheritance: not just saving typing, but letting you write general code that specialises automatically. You can add a Bird tomorrow and the same loop will work without a single change.
Inheritance means IS-A
isinstance respects the family tree too. A Dog IS-A Animal, so isinstance(Dog('Rex'), Animal) is True. Inheritance means a subclass instance is acceptable anywhere code expects the parent type — that is what makes polymorphism safe. You can pass a Dog to a function that asks for an Animal, and it will work perfectly.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
class Dog(Animal):
def speak(self):
return self.name + ' woofs'
class Cat(Animal):
def speak(self):
return self.name + ' purrs'
def describe(pet):
print('A pet says:', pet.speak())
describe(Dog('Rex'))
describe(Cat('Bo'))
print('isinstance check:', isinstance(Dog('Rex'), Animal))
flowchart TD L["for p in pets:"] -->|p = Dog| D["Dog.speak()\nRex woofs"] L -->|p = Cat| C["Cat.speak()\nBo purrs"] L -->|p = Animal| A["Animal.speak()\nMia makes a sound"] style L fill:#3776ab,color:#fff
Making objects print nicely with __str__
Print a bare object and Python shows something like <__main__.Dog object at 0x...> — useful to a debugger, meaningless to anyone else. The fix is a specially named method called __str__ (double underscore, str, double underscore) that returns the string you want people to see. Whenever print() or an f-string needs text from your object, Python calls __str__ behind the scenes and uses what it returns.
Because __str__ is just a method, a subclass can override it the same way it overrides speak. Give Animal one version and Dog another, and printing each object now produces a clean, human-readable line instead of an address. This is polymorphism showing up in a second place: the same print(p) call does the right thing for every kind of object in the list.
flowchart TD
C["look up speak() on a Dog"] --> Q1{"defined in Dog?"}
Q1 -- yes --> R1["use Dog.speak"]
Q1 -- no --> P["look in Animal next"]
P --> Q2{"defined in Animal?"}
Q2 -- yes --> R2["use Animal.speak"]
Q2 -- no --> ERR["AttributeError"]
style C fill:#3776ab,color:#fff
style ERR fill:#b91c1c,color:#fff
class Animal:
def __init__(self, name):
self.name = name
def __str__(self):
return f"<Animal {self.name}>"
class Dog(Animal):
def __str__(self):
return f"<Dog {self.name}>"
pets = [Animal("Mia"), Dog("Rex")]
for p in pets:
print(p)
is-a versus has-a: when inheritance is the wrong tool
Inheritance answers an is-a question. A Dog is a more specific sort of Animal, so the relationship fits: whatever is true of animals is true of dogs. That test is stricter than it looks. A car has an engine, but a car is not an engine, so making Car inherit from Engine would be a design blunder — the car would suddenly sprout pistons and its own fuel mix.
That has-a relationship is called composition, and it is the safer default for most designs. A class holds another object as an attribute and delegates work to it, so a Car can keep an engine attribute and call self.engine.start() without pretending to be an engine. Reach for inheritance only when a subclass truly is a specialized kind of its parent; reach for composition whenever one object merely uses another.
Duck typing: polymorphism without a parent
Python's polymorphism is looser than the family tree suggests. The loop earlier worked because every pet could speak, and in Python that is all the language asks for. It never inspects an object's family tree before calling a method; it simply tries the call and trusts the object to answer. If it walks like a duck and quacks like a duck, the code treats it as a duck.
This is called duck typing, and it is why a function that calls .speak() will happily accept any class you hand it, related or not. Inheritance gives you a tidy way to share code; duck typing means you rarely have to prove a relationship before your objects work together.
Animal is given. Define a Dog subclass that inherits __init__ (do not rewrite it) but overrides speak to return self.name + ' woofs'.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
# Define Dog below: inherit __init__, override speak.
class Dog(Animal):
pass
class Dog(Animal): inherits init for free — don't write one.
Define speak(self) inside Dog returning self.name + ' woofs'.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
class Dog(Animal):
def speak(self):
return self.name + ' woofs'
What does this print? Square overrides area.
class Shape:
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.area())
Square overrides area, so Shape's version isn't used.
4 * 4 is 16.
Write a Bird subclass of Animal that calls super().speak() and appends ' ... then chirps' to the result. Do NOT rewrite __init__.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
class Bird(Animal):
pass
Use
super().speak()to get the parent's string.Concatenate the extra text and return it.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + ' makes a sound'
class Bird(Animal):
def speak(self):
return super().speak() + ' ... then chirps'
This Dog subclass adds a breed attribute, but it forgets to call the parent's __init__. Fix it with super().
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
self.breed = breed
Call
super().__init__(name)before setting self.breed.This runs Animal's init so self.name is created.
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
Write loudest(pets) that takes a list of Animal objects (or subclasses) and returns the one whose speak() string has the most characters. Use a loop and len(). Return None for an empty list.
def loudest(pets):
# return the pet with the longest speak() string
pass
Track the best length seen so far and the matching pet.
Return None for an empty list.
def loudest(pets):
winner = None
best = -1
for pet in pets:
length = len(pet.speak())
if length > best:
best = length
winner = pet
return winner
Recap
class Dog(Animal):makesDoga subclass that inherits everything fromAnimal.- Override a method by defining it again in the child; use
super()to extend the parent's version instead of replacing it. - Polymorphism means one loop or function can work with many types because they share an interface.
isinstancerespects the family tree: aDogcounts as anAnimal.- When a subclass adds new attributes, call
super().__init__(...)first so the parent state is set up correctly. - Inheritance saves duplication; polymorphism lets you write general code that adapts automatically.
Next you'll explore the difference between data that belongs to a whole class and data that belongs to one object — the distinction between class and instance attributes.