Inheritance in Object-Oriented Programming: Types, Examples in C++, Java and Python, and When Not to Use It

One base class and two subclasses written in C++, Java and Python, the five types of inheritance, and why a subclass counted six tags when three were added.

A blue uncut key blank above two green keys cut from the same blank with different teeth, representing subclasses that inherit a base class and change one part of it.

Write a class that counts how many tags are added to a set by extending an existing tag set, add three tags, and it reports six. The same subclass gives the same wrong answer in C++, Java and Python, because the base class’s add_all happens to call add, and the subclass counted both. Inheritance is the most direct way to reuse code in object-oriented programming, and that example is the clearest picture of what it costs: a subclass depends on how its parent is written, not only on what it promises.

This guide explains inheritance as an object-oriented concept, with the same example written in C++, Java and Python: what it is and why it exists, the five types of inheritance and which languages support them, overriding and calling the parent’s version, the fragile base class problem, and when composition is the better choice. Every program was run for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 (-std=c++17 -Wall -Wextra -pedantic -Werror), OpenJDK 21 (javac -Xlint:all -Werror) and Python 3.13 (-W error), and the three versions of each example print identical output, captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit. For the C++ details (access control, constructors, virtual destructors) see inheritance in C++.

The Short Answer

What is inheritance in OOP? A way to define a new class from an existing one. The new class (the subclass) gets the existing class’s data and methods, can add its own, and can replace methods it inherited. A subclass object can be used wherever the parent type is expected.

What are the types of inheritance? Single, multilevel, hierarchical, multiple and hybrid. C++ and Python support all five with classes. Java allows one parent class, so multiple and hybrid inheritance are possible only through interfaces.

Is inheritance bad? No, but it couples a subclass to its parent’s implementation. Use it when the subclass truly is a kind of the parent and will be used through the parent’s type; for reusing code alone, composition is usually safer.

What Is Inheritance?

Inheritance in object-oriented programming is a relationship between classes in which a subclass (derived or child class) acquires the attributes and methods of a superclass (base or parent class). The subclass can add members and override inherited methods, and an object of the subclass can be used wherever an object of the superclass is expected. It expresses an “is-a” relationship: an email notification is a notification.

Each language uses its own vocabulary for the same idea:

ConceptC++JavaPython
The existing classBase classSuperclassBase class, parent class
The new classDerived classSubclassSubclass, derived class
Declaring itclass Email : public Notificationclass Email extends Notificationclass Email(Notification):
Calling the parent’s methodNotification::format(m)super.format(m)super().format(m)
Marking an overrideoverride (checked by the compiler)@Override (checked by the compiler)No keyword; @typing.override is checked by type checkers only
Root of every classNonejava.lang.Objectobject

Inheritance is one of the four ideas usually listed as the pillars of object-oriented programming, with encapsulation, abstraction and polymorphism. It is the mechanism that makes one form of polymorphism possible: code written against the parent type works with every subclass, as shown in polymorphism in C++, Java and Python.

One Example in Three Languages

A notification system has one way to send a message and several ways to format it. The base class owns the shared behavior (send), and each subclass changes one step (format).

C++:

// notifications.cpp - inheritance in C++: a base class with shared behavior,
// two derived classes that override one step of it.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

class Notification {
public:
    explicit Notification(std::string recipient) : recipient_(std::move(recipient)) {}
    virtual ~Notification() = default;

    // Shared by every kind of notification.
    void send(const std::string& message) const {
        std::cout << "to " << recipient_ << ": " << format(message) << '\n';
    }

protected:
    // The step each kind can change.
    virtual std::string format(const std::string& message) const { return message; }

private:
    std::string recipient_;
};

class Email : public Notification {
public:
    using Notification::Notification;
protected:
    std::string format(const std::string& message) const override {
        return "[email] " + Notification::format(message);    // extend the parent's version
    }
};

class Sms : public Notification {
public:
    using Notification::Notification;
protected:
    std::string format(const std::string& message) const override {
        return "[sms] " + message.substr(0, 19);               // replace it
    }
};

int main() {
    std::vector<std::unique_ptr<Notification>> outbox;
    outbox.push_back(std::make_unique<Notification>("ops-team"));
    outbox.push_back(std::make_unique<Email>("<!--email_off-->[email protected]<!--/email_off-->"));
    outbox.push_back(std::make_unique<Sms>("+1-555-0100"));

    for (const auto& n : outbox)
        n->send("Disk usage on db-01 is above 90 percent");
}

Java:

// Notifications.java - inheritance in Java: the same three classes.
import java.util.List;

public class Notifications {
    static class Notification {
        private final String recipient;

        Notification(String recipient) { this.recipient = recipient; }

        final void send(String message) {
            System.out.println("to " + recipient + ": " + format(message));
        }

        protected String format(String message) { return message; }
    }

    static class Email extends Notification {
        Email(String recipient) { super(recipient); }

        @Override
        protected String format(String message) {
            return "[email] " + super.format(message);
        }
    }

    static class Sms extends Notification {
        Sms(String recipient) { super(recipient); }

        @Override
        protected String format(String message) {
            return "[sms] " + message.substring(0, Math.min(19, message.length()));
        }
    }

    public static void main(String[] args) {
        List<Notification> outbox = List.of(
            new Notification("ops-team"),
            new Email("<!--email_off-->[email protected]<!--/email_off-->"),
            new Sms("+1-555-0100"));

        for (Notification n : outbox)
            n.send("Disk usage on db-01 is above 90 percent");
    }
}

Python:

"""notifications.py - inheritance in Python: the same three classes."""


class Notification:
    def __init__(self, recipient):
        self._recipient = recipient

    def send(self, message):
        print(f"to {self._recipient}: {self.format(message)}")

    def format(self, message):
        return message


class Email(Notification):
    def format(self, message):
        return "[email] " + super().format(message)


class Sms(Notification):
    def format(self, message):
        return "[sms] " + message[:19]


if __name__ == "__main__":
    outbox = [Notification("ops-team"), Email("<!--email_off-->[email protected]<!--/email_off-->"), Sms("+1-555-0100")]
    for n in outbox:
        n.send("Disk usage on db-01 is above 90 percent")

Output (identical for all three):

to ops-team: Disk usage on db-01 is above 90 percent
to <!--email_off-->[email protected]<!--/email_off-->: [email] Disk usage on db-01 is above 90 percent
to +1-555-0100: [sms] Disk usage on db-01

The three programs make the same design decisions in different syntax:

  • send is inherited unchanged. None of the subclasses defines it. The loop calls send on each object, and send calls format, which runs the subclass’s version: [email] and [sms] appear without the loop knowing which kind it holds. In Java, send is declared final so no subclass can change the shared part.
  • Email extends the parent’s behavior. It calls the parent’s format (Notification::format, super.format, super().format) and adds a prefix. That is the usual way to add to an inherited method rather than replace it.
  • Sms replaces it. It never calls the parent and truncates the message to 19 characters instead.
  • The recipient stays private. Each subclass sets it through the parent’s constructor and never touches it directly. In C++ the constructor is inherited with using Notification::Notification;; Java needs an explicit super(recipient); Python inherits __init__ like any other method.

One difference matters more than the syntax. Java (except final, static and private methods) and Python methods can be overridden by default, while in C++ only functions declared virtual can be; without virtual (and with override removed, since it would no longer compile), the C++ loop would print three plain messages.

Types of Inheritance

The five types of inheritance Arrows point from the derived class to its base. Shaded boxes are root classes. Single C++ yes Java yes Python yes A B Multilevel C++ yes Java yes Python yes A B C Hierarchical C++ yes Java yes Python yes A B C D Multiple C++ yes Java interfaces only Python yes A B C Hybrid (here, a diamond) C++ yes Java interfaces only Python yes A B C D Java allows one base class; a class can implement any number of interfaces.
Five shapes a class hierarchy can take. C++ and Python allow all of them with ordinary classes. Java allows one parent class, so its multiple and hybrid hierarchies are built from interfaces.

The five types are names for the shapes a class hierarchy can take:

  1. Single inheritance: one subclass, one parent. Email derives from Notification.
  2. Multilevel inheritance: a chain. C derives from B, which derives from A; C inherits from both.
  3. Hierarchical inheritance: several subclasses of one parent. Email and Sms are both notifications.
  4. Multiple inheritance: one class with several parents. Supported for classes in C++ and Python; Java allows it only for interfaces. Covered in depth in multiple inheritance in C++.
  5. Hybrid inheritance: any combination of the above. The common case is the diamond, where two parents share a grandparent.

Python can show how it resolves each shape. Every class has a method resolution order (MRO): the sequence of classes searched when a method is looked up.

"""types_of_inheritance.py - single, multilevel, hierarchical and multiple
inheritance, and the method resolution order Python uses for each."""


class Device: pass                      # base
class Scanner(Device): pass             # single
class Printer(Device): pass             # hierarchical: two classes from one base
class Copier(Scanner, Printer): pass    # multiple (and a diamond through Device)
class ColorCopier(Copier): pass         # multilevel: Device -> Scanner -> Copier -> ColorCopier

for cls in (Scanner, Copier, ColorCopier):
    print(f"{cls.__name__:12} MRO: " + " -> ".join(c.__name__ for c in cls.__mro__))
print("Copier is a Device:", issubclass(Copier, Device))

Output:

Scanner      MRO: Scanner -> Device -> object
Copier       MRO: Copier -> Scanner -> Printer -> Device -> object
ColorCopier  MRO: ColorCopier -> Copier -> Scanner -> Printer -> Device -> object
Copier is a Device: True

Copier inherits from both Scanner and Printer, and Device appears once in its MRO, after both of them. Python always merges a shared ancestor into one, using an algorithm called C3 linearization. C++ makes the opposite default choice: a shared base appears twice unless it is declared virtual, which is why the diamond problem is a C++ topic more than a Python one.

The Fragile Base Class Problem

Inheritance lets a subclass call the parent’s methods, but it also lets the parent call the subclass’s overrides. That second direction is where most inheritance bugs come from. This tag set counts every attempt to add a tag:

"""fragile_base.py - the same dependency on a base class's implementation."""


class Tags:
    def __init__(self):
        self._tags = set()

    def add(self, tag):
        self._tags.add(tag)

    def add_all(self, tags):
        for t in tags:
            self.add(t)

    def size(self):
        return len(self._tags)


class CountingTags(Tags):
    def __init__(self):
        super().__init__()
        self.attempts = 0

    def add(self, tag):
        self.attempts += 1
        super().add(tag)

    def add_all(self, tags):
        self.attempts += len(tags)
        super().add_all(tags)


if __name__ == "__main__":
    t = CountingTags()
    t.add_all(["urgent", "billing", "eu"])
    print(f"tags stored: {t.size()}, attempts counted: {t.attempts}")

Output:

tags stored: 3, attempts counted: 6

Three tags were added, three were stored, and six were counted. CountingTags.add_all added 3 and then called Tags.add_all, which calls self.add for each tag, and self.add is the subclass’s override, which added 1 three more times. The Java and C++ versions in the repository (FragileBase.java, fragile_base.cpp) are the same program and printed the same line. C++-specific traps that compile cleanly are collected in inheritance in C++.

Nothing in Tags‘s public interface says that add_all calls add. It is an implementation detail, and the subclass’s correctness depends on it. If a later version of Tags rewrote add_all to insert tags directly, this subclass would start counting correctly without any change of its own. And if the subclass were fixed by dropping its add_all override and relying on add being called, that same change to Tags would break it the other way: three tags, zero counted. This is the fragile base class problem: changes inside a parent class that are invisible to its callers can still break its subclasses.

Inheritance vs Composition

The same counter built by composition holds a Tags object instead of inheriting from one:

"""composition.py - the counting set again, built by composition: it holds a
Tags object instead of inheriting from it, so add_all cannot call back into it."""
from fragile_base import Tags


class CountingTags:
    def __init__(self):
        self._tags = Tags()
        self.attempts = 0

    def add(self, tag):
        self.attempts += 1
        self._tags.add(tag)

    def add_all(self, tags):
        self.attempts += len(tags)
        self._tags.add_all(tags)

    def size(self):
        return self._tags.size()


if __name__ == "__main__":
    t = CountingTags()
    t.add_all(["urgent", "billing", "eu"])
    print(f"tags stored: {t.size()}, attempts counted: {t.attempts}")

Output:

tags stored: 3, attempts counted: 3

Tags.add_all still calls self.add, but self is now the inner Tags object, not the counter, so the counter’s add is not called back. The count is correct, and it would stay correct whatever Tags does internally. The cost is that the counter must forward every method it wants to expose (size here), and it is no longer usable as a Tags.

QuestionInheritanceComposition
Relationshipis-a: an Email is a Notificationhas-a: a counter has a tag set
Usable wherever the parent type is expectedYesNo, unless it implements the same interface
Depends on the other class’s internalsYes, through overridden methods the parent callsNo, only on its public methods
EffortInherits everything automaticallyForwards each method explicitly
FitsA family of types used through one interfaceReusing behavior inside a class

The guideline usually quoted from Design Patterns (Gamma, Helm, Johnson and Vlissides, 1994) is “favor object composition over class inheritance”. It does not mean “never inherit”: the notification example is a good use, because callers work with Notification and every subclass honors what format means.

When to Use Inheritance

Inheritance fits when all of these hold:

  1. The subclass is a kind of the parent, and code will use it through the parent’s type.
  2. It keeps the parent’s promises. Anywhere a Notification works, an Email must work too. This is the Liskov substitution principle.
  3. The parent was designed to be extended: it documents which methods subclasses may override and what the parent calls internally, or it is an abstract class or interface with no implementation to depend on.

If any of these fails, compose instead. A practical sign is a subclass that overrides methods only to disable them, or that exists only to reuse two or three of the parent’s methods.

Key Takeaways

  • Inheritance defines a new class from an existing one: the subclass gets the parent’s members, can add its own, and can override methods.
  • The same design reads the same in C++, Java and Python; the differences are the keyword, how the parent is called, and that C++ methods must be virtual to be overridden.
  • There are five types: single, multilevel, hierarchical, multiple and hybrid. Java supports the last two only through interfaces.
  • Python merges a shared ancestor once, by its method resolution order; C++ duplicates it unless inherited virtual.
  • A subclass depends on its parent’s implementation. A counter that extended a tag set reported 6 for 3 tags in all three languages.
  • Composition avoids that dependency: the same counter holding a tag set reported 3.

Frequently Asked Questions

Conclusion

Inheritance gives a class two things at once: the parent’s code and the parent’s type. The notification example needs both, which is why it reads cleanly in all three languages. The tag counter wanted only the code, and paid for the type it did not need by depending on details it could not see. Asking which of the two a design really needs is most of the decision between inheriting and composing.

For the C++ specifics, see the guides to inheritance in C++ and multiple inheritance. More object-oriented programming topics are collected in the OOP section.

Source Code and Tests

inheritance-concepts

All programs on this page are in the oop/inheritance-concepts directory of the MYCPLUS C++ examples repository.

Build and test:

bash tests/run_tests.sh g++

What the build checks. On Linux, with GCC and with Clang, it compiles the C++ programs with -Wall -Wextra -pedantic -Werror, compiles the Java programs with javac -Xlint:all -Werror on Java 21, runs the Python programs with -W error on Python 3.12, and checks that every version of each example prints exactly the output shown on this page.

Scroll to Top