New: 6 free SQL practice datasets with 300+ questions — try the SQL Compiler →
Python

Python Inheritance and Dunder Methods Explained

Python inheritance and dunder methods explained: super(), overriding, __str__ vs __repr__ and operators, with real code and output.

Upskly AI Team September 26, 2026 10 min read
Python Inheritance and Dunder Methods Explained

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

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__ for print, __len__ for len(), __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

Special methods and what triggers them
MethodTriggered byExample
__init__Creating an objectPoint(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], loopingplaylist[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 Tweet

Only speak needs to change. Everything else is inherited.

2. What does issubclass(bool, int) return?

Show answer

Output

True

Yes, 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

2

Run 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.

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.

Open Python Compiler

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.

Try AI Assist

Test yourself

Timed questions on Classes and Object-Oriented Python, with an explanation for every answer.

Take the Quiz
Upskly AI Team
Learning made simple
Scroll to Top