Get AI summaries of any video or article — Sign up free
Python OOP Tutorial 4: Inheritance - Creating Subclasses thumbnail

Python OOP Tutorial 4: Inheritance - Creating Subclasses

Corey Schafer·
4 min read

Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Inheritance lets subclasses reuse parent attributes and methods while adding or overriding behavior without duplicating code.

Briefing

Python inheritance lets developers build specialized subclasses that reuse a parent class’s attributes and methods, then selectively override or extend behavior—without duplicating code or breaking the parent. Using an `Employee` class as the baseline, the tutorial creates `Developer` and `Manager` subclasses that automatically gain shared functionality like initialization, email handling, and salary-related methods, while each subclass adds its own customization.

The walkthrough starts by defining `Developer` as a subclass of `Employee` and leaving `Developer` initially empty. Instantiating `Developer` objects still works because Python searches for missing methods using the method resolution order (MRO). When `Developer` lacks an `__init__`, Python climbs the inheritance chain until it finds `Employee.__init__`. The `help()` function is used to make this search path visible, showing that `Developer` inherits methods such as `__init__`, `apply_raise`, and `full_name`, along with class attributes like `raise_amount` from `Employee`. The practical payoff is immediate: `Developer` instances can be created with the same inputs as `Employee` and can access the same inherited behavior.

Next comes customization. By changing `raise_amount` inside `Developer`, the tutorial demonstrates that `apply_raise` uses the subclass’s value for developer instances, while `Employee` instances remain unaffected. Switching an object back to `Employee` restores the original 4% raise behavior, reinforcing the separation between parent and subclass configuration.

The tutorial then addresses a common inheritance need: subclasses often require extra initialization data. `Developer` is modified so it accepts an additional argument—`programming_language`—even though `Employee.__init__` only accepts first name, last name, and pay. Rather than copying the entire parent `__init__` (which would harm maintainability), the subclass defines its own `__init__` and delegates shared initialization to the parent using `super().__init__(first, last, pay)`. After the parent sets the common fields, `Developer.__init__` stores `self.programming_language`. Instantiations confirm that both the inherited email setup and the new programming-language attribute are correctly populated.

A parallel example builds `Manager` as another subclass of `Employee`. `Manager.__init__` accepts a list of supervised employees (defaulting to `None` and then converting to an empty list inside the method to avoid mutable default arguments). The subclass adds methods to manage that list: `add_employee`, `remove_employee`, and `print_employees`, which prints each supervised employee’s full name. Creating a manager shows inherited attributes like email and salary behavior alongside the manager-specific methods.

Finally, the tutorial ties inheritance to Python’s type-checking tools: `isinstance` confirms whether an object belongs to a class, while `issubclass` checks class relationships. Both `Developer` and `Manager` are subclasses of `Employee`, but neither is a subclass of the other. The closing real-world example points to Python’s `exceptions` hierarchy—such as `HTTPException` inheriting from `Exception` and specialized HTTP errors inheriting from `HTTPException`—illustrating how inheritance scales code reuse as projects grow.

Cornell Notes

Inheritance in Python enables subclasses to reuse a parent class’s attributes and methods, then override or extend behavior. The tutorial uses an `Employee` base class and creates `Developer` and `Manager` subclasses that automatically inherit shared functionality via Python’s method resolution order (MRO). Customization works by overriding class attributes (e.g., `raise_amount`) so only subclass instances change behavior, while parent instances remain unchanged. When subclasses need extra initialization parameters, they define their own `__init__` and delegate common setup to the parent using `super().__init__`, keeping code DRY. The examples also show practical subclass methods for managing state (like a manager’s employee list) and how `isinstance`/`issubclass` reflect inheritance relationships.

How does Python successfully create a `Developer` object when `Developer` doesn’t define its own `__init__`?

When `Developer` lacks `__init__`, Python uses the method resolution order (MRO) to search for the needed method. It checks `Developer` first, doesn’t find `__init__`, then moves up to `Employee` where `__init__` exists. That parent initializer runs, setting up shared attributes like name, email, and salary-related fields.

What does changing `raise_amount` inside `Developer` accomplish, and why doesn’t it affect `Employee` instances?

Overriding `raise_amount` in the subclass changes how inherited methods like `apply_raise` behave for `Developer` instances, because the method reads the subclass’s class attribute. `Employee` instances still use `Employee.raise_amount`, so their raise behavior stays at the original value (the tutorial contrasts 10% for developers with 4% for employees).

Why use `super().__init__(first, last, pay)` when adding `programming_language` to `Developer`?

The parent `Employee.__init__` already handles first name, last name, and pay. Using `super()` delegates that shared logic to the parent, avoiding copy-pasting and keeping the code maintainable. After `super()` sets the common fields, `Developer.__init__` stores the extra argument by assigning `self.programming_language`.

How does `Manager` handle a list of supervised employees without using a mutable default argument?

`Manager.__init__` sets the parameter default to `None`. Inside the method, it checks `if employees is None` and then assigns `self.employees` to a new empty list; otherwise it assigns the provided list. This prevents the same list object from being reused across calls, which can cause unexpected shared state.

What’s the difference between `isinstance` and `issubclass` in the inheritance examples?

`isinstance(obj, Class)` checks whether an object is an instance of a class (or its parents). `issubclass(SubClass, Class)` checks whether a class inherits from another class. The tutorial shows `manager_one` is an instance of both `Manager` and `Employee`, but not an instance of `Developer`, and it shows `Developer` and `Manager` are subclasses of `Employee` while `Manager` is not a subclass of `Developer`.

How does the exceptions hierarchy demonstrate inheritance in real code?

In Python’s exceptions module, `HTTPException` acts as a base class for HTTP-related errors. More specific exceptions like `BadRequest` inherit from `HTTPException`, gaining shared behavior and then overriding or extending details such as status code and description—without rewriting the entire base logic.

Review Questions

  1. When Python can’t find a method in a subclass, what mechanism determines where it searches next, and what role does MRO play?
  2. Why does overriding a class attribute in a subclass change behavior for that subclass’s instances but not for parent-class instances?
  3. What problem can arise from using a mutable list as a default argument, and how does the `Manager` example avoid it?

Key Points

  1. 1

    Inheritance lets subclasses reuse parent attributes and methods while adding or overriding behavior without duplicating code.

  2. 2

    Python’s method resolution order (MRO) determines how it finds missing methods like `__init__` by walking up the inheritance chain.

  3. 3

    Overriding subclass class attributes (such as `raise_amount`) changes behavior for subclass instances while leaving parent instances unchanged.

  4. 4

    When subclasses need extra initialization parameters, defining a subclass `__init__` and delegating shared setup to `super().__init__` keeps code DRY and maintainable.

  5. 5

    Avoid mutable default arguments by using `None` and creating a new list inside `__init__` when needed.

  6. 6

    `isinstance` checks object-to-class relationships, while `issubclass` checks class-to-class inheritance relationships.

  7. 7

    Python’s built-in exception hierarchy provides a practical example of scalable inheritance for specialized error types.

Highlights

Subclassing `Developer` from `Employee` works immediately even with an empty `Developer` class because Python falls back to `Employee.__init__` via MRO.
Changing `Developer.raise_amount` affects only developer raises; switching back to `Employee` restores the original raise percentage.
Using `super().__init__` lets `Developer` add `programming_language` without copying the parent initializer logic.
`Manager.__init__` uses `employees=None` and then creates a new empty list to avoid shared mutable defaults.
The exceptions module mirrors this pattern: specialized HTTP errors inherit shared behavior from `HTTPException` and only adjust what’s different.

Topics

  • Class Inheritance
  • Method Resolution Order
  • Overriding Class Attributes
  • super().__init__
  • Mutable Default Arguments

Mentioned

  • MRO