Inheritance lets one class reuse and extend another. You write class Dog(Animal), and Dog automatically gets everything Animal has, plus whatever you add or change. Dunder methods (double-underscore methods such as __str__, __len__ and __add__) let your own objects work with Python’s built-in syntax and functions, so len(obj), a + b and print(obj) do something sensible.
This guide builds on Python Classes and Objects for Beginners. Every example is run, and the output is real.
In this guide
- Inheritance: reusing a class
- Overriding and super()
- isinstance, issubclass and the method lookup order
- Polymorphism: same call, different behaviour
- Dunder methods: teaching your objects Python’s syntax
- __str__ vs __repr__
- Operators and sorting
- Making an object act like a collection
- Common mistakes
- Common dunder methods
- Try it yourself
- FAQ
The short version
class Child(Parent):– the child inherits every attribute and method of the parent.- Define a method with the same name in the child to override it.
super()reaches the parent’s version. - Dunder methods are called by Python itself:
__str__forprint,__len__forlen(),__add__for+. - Use inheritance for an is-a relationship: a Dog is an Animal.
Inheritance: reusing a class
Here Dog and Cat are children of Animal. They did not write __init__ or describe themselves, yet they work, because they inherited them. Each child overrides only speak:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
def describe(self):
return f"{self.name} says {self.speak()}"
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
print(Dog("Rex").describe())
print(Cat("Kit").describe())
print(Animal("Thing").describe())
Output
Rex says Woof
Kit says Meow
Thing says ...
Notice that describe(), written once in Animal, calls self.speak(). Python picks the right speak depending on the actual object, which is why each animal describes itself differently.
Overriding and super()
A child can replace a parent’s method by defining one with the same name. Often you want to add to the parent’s behaviour instead of replacing it entirely. super() gives you the parent’s version to call:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def annual(self):
return self.salary * 12
class Manager(Employee):
def __init__(self, name, salary, team):
super().__init__(name, salary)
self.team = team
def annual(self):
return super().annual() + 10000
m = Manager("Asha", 5000, ["Ravi", "Meera"])
print(m.name, m.team)
print(m.annual())
Output
Asha ['Ravi', 'Meera']
70000
Manager.__init__ called super().__init__(name, salary) so the parent could set up name and salary, then added its own team. And annual() reused the parent’s calculation and added a bonus.
If you forget the super().__init__(...) call, the parent’s setup never runs:
class Base:
def __init__(self):
self.ready = True
class Child(Base):
def __init__(self):
pass
c = Child()
print(hasattr(c, "ready"))
Output
False
isinstance, issubclass and the method lookup order
isinstance(obj, Class) is true for the object’s own class and for every parent class. issubclass compares classes directly. When you call a method, Python looks for it in the object’s class first, then in each parent in turn. That order is stored in __mro__ (method resolution order):
class Animal:
pass
class Dog(Animal):
pass
d = Dog()
print(isinstance(d, Dog), isinstance(d, Animal), isinstance(d, str))
print(issubclass(Dog, Animal), issubclass(Animal, Dog))
print([c.__name__ for c in Dog.__mro__])
Output
True True False
True False
['Dog', 'Animal', 'object']
Every class in Python ultimately inherits from object, which is where the default __init__ and __str__ come from. A class can even inherit from more than one parent, which is powerful but easy to overuse.
Polymorphism: same call, different behaviour
Because each child provides its own version of a method, you can treat different objects uniformly and let each one do the right thing. The loop below does not care whether it has a square or a circle. It just calls area():
import math
class Shape:
def area(self):
raise NotImplementedError
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return round(math.pi * self.r ** 2, 2)
for s in [Square(3), Circle(2)]:
print(type(s).__name__, s.area())
try:
Shape().area()
except NotImplementedError as e:
print(type(e).__name__)
Output
Square 9
Circle 12.57
NotImplementedError
The parent Shape defines area() only to raise NotImplementedError, a common way of saying “every child must supply this”. Calling it on a bare Shape fails, as the last line shows.
Dunder methods: teaching your objects Python’s syntax
You have already used one, __init__. Python looks for other special methods when you use built-in syntax on your object. If they exist, Python calls them; if not, you get a default behaviour or an error. That is how you make print(p), p + q, len(p) or p in group work for your own classes.
__str__ vs __repr__
Both turn an object into text, for different audiences. __str__ is the friendly version for users, used by print() and str(). __repr__ is the precise version for developers, used by repr(), in the interactive shell, and when the object appears inside a list:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(2, 3)
print(p)
print(repr(p))
print([p, Point(0, 0)])
print(f"{p!r}")
Output
(2, 3)
Point(2, 3)
[Point(2, 3), Point(0, 0)]
Point(2, 3)
A good rule: always write __repr__ (ideally something that looks like the code to create the object). Add __str__ only if you want a different, friendlier text. If you define only __repr__, print() falls back to it:
class Item:
def __init__(self, n):
self.n = n
def __repr__(self):
return f"Item({self.n!r})"
print(Item("pen"))
Output
Item('pen')
Operators and sorting
Operators are dunder methods in disguise. a + b calls a.__add__(b), a == b calls __eq__, and a < b calls __lt__. Define __lt__ and Python can sort your objects and find their maximum:
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __eq__(self, other):
return self.amount == other.amount
def __lt__(self, other):
return self.amount < other.amount
def __repr__(self):
return f"Money({self.amount})"
a, b = Money(50), Money(70)
print(a + b)
print(a == Money(50), a == b)
print(a < b)
print(sorted([b, a]))
print(max(a, b))
Output
Money(120)
True False
True
[Money(50), Money(70)]
Money(70)
Making an object act like a collection
__len__ powers len(), __getitem__ powers indexing and even lets you loop over the object, and __contains__ powers the in operator:
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
def __contains__(self, song):
return song in self.songs
p = Playlist(["a", "b", "c"])
print(len(p), p[0], p[-1])
print("b" in p)
for s in p:
print(s)
Output
3 a c
True
a
b
c
Common mistakes
Mistake 1: forgetting super().__init__()
Shown above. When a child defines its own __init__, it must call the parent’s if the parent sets up attributes.
Mistake 2: defining __eq__ and expecting objects to work in sets or as keys
When you define __eq__ without __hash__, Python makes the class unhashable, so it cannot go in a set or be a dictionary key:
class P:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x
try:
{P(1)}
except TypeError as e:
print(type(e).__name__)
Output
TypeError
Define __hash__ too (using the same fields as __eq__), or avoid hashing these objects.
Mistake 3: inheriting when you should hold
Inheritance means “is a”. A Car is not an Engine. It has an engine. For “has a” relationships, store the other object as an attribute. That is called composition, and it is often simpler and more flexible than a deep family tree of classes.
Mistake 4: confusing __str__ and __repr__
If print(obj) shows something different from the value inside a list of objects, that is the difference at work. When in doubt, define __repr__.
Common dunder methods
| Method | Triggered by | Example |
|---|---|---|
| __init__ | Creating an object | Point(2, 3) |
| __str__ | print(obj), str(obj) | print(p) |
| __repr__ | repr(obj), the shell, inside lists | [p, q] |
| __len__ | len(obj) | len(playlist) |
| __getitem__ | obj[index], looping | playlist[0] |
| __contains__ | x in obj | 'b' in playlist |
| __eq__ / __lt__ | ==, < | a == b, sorted(items) |
| __add__ | + | a + b |
| __call__ | obj() | Making an object callable |
Try it yourself
Work out each answer first, then open the solution.
1. Add a Bird class that inherits from Animal and says “Tweet”.
Show solution
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
def describe(self):
return f"{self.name} says {self.speak()}"
class Bird(Animal):
def speak(self):
return "Tweet"
print(Bird("Tweety").describe())
Output
Tweety says TweetOnly speak needs to change. Everything else is inherited.
2. What does issubclass(bool, int) return?
Show answer
Output
TrueYes, bool is a subclass of int, which is why True + True is 2.
3. Give a Team class a __len__ so that len(team) returns the number of members.
Show solution
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
print(len(Team(["a", "b"])))
Output
2Run these in our free Python compiler.
Frequently asked questions
What is inheritance in Python?
Inheritance lets a class (the child) take on the attributes and methods of another class (the parent), and then add or change behaviour. You write it as class Child(Parent):.
What does super() do?
It gives you access to the parent class, so a child can call the parent’s version of a method, most often super().__init__(...) in the constructor.
What are dunder methods in Python?
They are special methods with double underscores on both sides, like __init__, __str__ and __add__. Python calls them automatically when you use built-in syntax or functions on your objects.
What is the difference between __str__ and __repr__?
__str__ is the readable text for users, used by print(). __repr__ is the unambiguous text for developers, used by repr(), the shell and inside containers. If only __repr__ exists, print() uses it.
Can a class inherit from more than one class?
Yes. Python supports multiple inheritance and decides which method to use with the method resolution order (__mro__). It is powerful, but keep hierarchies simple.
When should I use composition instead of inheritance?
When the relationship is “has a” instead of “is a”. Store the other object as an attribute rather than inheriting from its class. Composition is usually more flexible.
Related reading
- Python Classes and Objects for Beginners – start here if classes are new.
- Python Exception Handling: try, except, finally – including custom exception classes.
- Python Decorators Explained Step by Step – functions that wrap functions and methods.
- Python Iterators and Generators Explained – the __iter__ and __next__ methods in action.
Run this code in your browser
The free Upskly compiler runs Python with nothing to install. Paste the example, change it and see what happens.
Stuck on a traceback?
AI Assist works inside the Python notebook, so you can ask about an error or a concept without leaving the cell.
Test yourself
Timed questions on Classes and Object-Oriented Python, with an explanation for every answer.