AlgoMaster Logo

Inheritance Basics

High Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Inheritance lets one class build on another without copying its code. You define a general class once, then carve out specialized versions of it that reuse the general behavior and add or change pieces as needed. This lesson covers single inheritance: one subclass, one parent, the super() call, method overriding, and the two builtins (isinstance and issubclass) you use to ask about the relationship.

The "is-a" Relationship

Inheritance models an "is-a" relationship. A DigitalProduct is a Product. A PhysicalProduct is also a Product. They share the things every product has (an ID, a name, a price), but each has its own twist: a digital product has a download link, a physical product has a weight for shipping.

When you find yourself writing two classes that share most of their attributes and methods, that's the signal. Instead of repeating the shared parts, you put them in a parent class and let the specialized classes inherit them. Each subclass keeps everything from the parent and adds whatever makes it different.

The opposite of "is-a" is "has-a". A Cart has a list of Product objects, but a Cart is not a Product, so a Cart doesn't inherit from Product. Mixing these up is one of the most common design mistakes when learners first meet inheritance. If the relationship is "has-a", you store the other object as an attribute; only "is-a" earns inheritance.

The arrows point from parent to child, which is the standard reading direction in a class hierarchy. DigitalProduct and PhysicalProduct both inherit from Product. Everything Product defines is automatically available on both children, and each child is free to add its own attributes and methods on top.

Defining a Subclass

The syntax is a single change to the class line. Where a normal class is class Name:, a subclass puts the parent's name in parentheses after its own:

DigitalProduct inherits from Product and defines nothing of its own. The pass is just a placeholder body. Even so, DigitalProduct instances have an __init__, a describe method, and a name attribute, all of them coming from the parent. That's the whole point of inheritance: you get the parent's behavior for free.

When Python looks up an attribute on a DigitalProduct instance, it first checks the instance itself, then the DigitalProduct class, then Product, then the implicit object at the top. The first match wins. For book.describe(), the search misses on the instance and on DigitalProduct, then hits on Product, so that's the method that runs.

A useful mental model: the subclass starts out as a perfect copy of the parent, and you're free to add to it or change parts of it. You don't have to redefine anything that's already correct.

Adding New Attributes and Methods

A subclass that does nothing is just a renamed parent. The real value shows up when the subclass adds something the parent doesn't have. The simplest way to add behavior is to define new methods directly on the subclass:

DigitalProduct keeps everything from Product and adds a download_link method. The new method uses self.product_id, which was set by the parent's __init__. The subclass can freely read any attribute the parent created; from the method's point of view, there's just one self object with all the attributes on it.

Methods are easy. Adding new attributes is where most learners trip up, because attributes are typically created in __init__, and a subclass that needs its own attributes has to handle the initialization carefully. That's the next section.

Overriding __init__ and Using super()

If a subclass needs an extra attribute that the parent doesn't know about, you can't just sneak it in. You have to give the subclass its own __init__ that accepts the new piece of data and stores it. The catch is that the parent's __init__ still needs to run, otherwise the parent's attributes never get set.

The wrong way is to retype the parent's setup:

This works, but it duplicates the parent's logic. If Product.__init__ ever changes (a validation check, a default value, an extra computed attribute), the subclass silently drifts out of sync. The right approach is to ask the parent to do its own job, then add the new piece on top. That's what super() is for:

super() returns a proxy that lets you call methods on the parent class as if you were the parent. super().__init__(product_id, name, price) tells Python to run Product.__init__ with the given arguments, which sets self.product_id, self.name, and self.price on the new instance. Then self.download_url = download_url adds the subclass's own piece.

The order matters. Call super().__init__ first so the parent's attributes exist before the subclass touches anything. Adding subclass-specific attributes goes after the super() call.

A second subclass with different extras works the same way:

PhysicalProduct calls the same parent __init__ to set the shared three attributes, then adds weight_kg. The new shipping_cost method uses weight_kg to compute the cost. Both subclasses share describe because neither overrides it; both reach back to Product for the common setup; each adds the bit that's specific to its kind of product.

Method Overriding

Inheriting a method is convenient when the parent's version is already right. When it isn't, the subclass can replace the method by defining one with the same name. This is overriding, and it's how subclasses customize behavior they inherited.

Say Product.describe produces a fine generic description, but for digital products you'd rather show the download URL too. Define describe in DigitalProduct:

Two calls, two different methods. generic.describe() finds describe on Product because there's nothing more specific. ebook.describe() finds the override on DigitalProduct first, so it never reaches the parent's version. The attribute-lookup rule from earlier is doing the work: Python checks the most specific class first and walks up the chain only if nothing matches.

Sometimes you don't want to replace the parent's method entirely; you want to extend it. The pattern is to call super().method() from inside the override and add to its result:

super().describe() runs Product.describe(self) and returns its string. The subclass appends the shipping note and returns the combined result. This pattern, "do the parent's thing, then add my thing", is the natural use of super() outside __init__. It avoids duplicating the parent's logic and stays correct even if the parent's describe changes later.

A Customer Hierarchy

Inheritance also shows up in places that aren't about products. Say the store has regular customers and premium customers. Both have a name and an email; premium customers also have a tier (silver, gold, platinum) and get a discount on every order.

A few things are worth pointing out. The parent's discount_percent returns 0, which is the right default for a regular customer. The subclass overrides it to look up the tier's discount instead. The parent's summary returns the basic line; the subclass extends it with the tier badge and percentage. TIER_DISCOUNTS is a class-level attribute on PremiumCustomer only, which means it lives on the subclass and the parent doesn't know about it. That's fine. Subclasses can hold class-level data the parent doesn't share.

PremiumCustomer keeps everything Customer has and adds or overrides a few pieces. The diagram is a quick visual summary of what's shared (in the parent) versus what's new or changed (in the child).

Checking Types: isinstance and issubclass

When you have an inheritance chain, you sometimes need to ask "is this object one of these types?" or "is this class a subclass of that one?". Python has two builtins for that.

isinstance(obj, ClassName) returns True if obj is an instance of ClassName or any of its subclasses. That second half is the important part. A PremiumCustomer instance is an instance of Customer, because every PremiumCustomer is a Customer.

regular is a plain Customer, so the test against Customer is True and the test against PremiumCustomer is False. premium is both a PremiumCustomer and a Customer, because of inheritance. That's how Python answers "is this object of this kind, or any more specific kind?".

issubclass(ChildClass, ParentClass) answers the same question at the class level. No instance involved, just the class objects:

PremiumCustomer is a subclass of Customer, but not the other way around. The direction matters: issubclass(A, B) is true when A inherits from B (or A is B), not when B inherits from A.

Both functions also accept a tuple of classes for the second argument, which is handy when you want to check membership in any of several types:

A note before moving on: it's easy to overuse isinstance to branch on the type of an object, like if isinstance(p, DigitalProduct): ... elif isinstance(p, PhysicalProduct): .... That's a code smell. The cleaner approach is to put the differing behavior in methods on each subclass and just call the method, which works the same way regardless of the specific type. That technique is called polymorphism and it's the topic of a later chapter. For now, isinstance is mainly useful for sanity checks and for guards at the edges of your code where data of unknown type arrives.

Every Class Inherits From object

Even when you write class Product: with no parent in parentheses, Python silently sets the parent to a builtin class called object. It sits at the top of every class hierarchy in Python.

__bases__ is the tuple of immediate parent classes. For a class with no explicit parent, it's (object,). Every class is a subclass of object, and every instance is an instance of object. There are no exceptions, this is true for builtins like list and int as well.

object is what gives every Python class its baseline behavior: the methods that make objects work with print, with ==, with hash, with repr. Useful methods you've already met (__init__, __str__, __repr__, __eq__) all have default versions on object that your classes inherit. When you write __init__ in your own class, you're really overriding object.__init__, and super().__init__() in the most general case is reaching all the way up to object to do nothing in particular.

This matters for two reasons. First, you never have to explicitly inherit from object; Python 3 does it automatically. (In old Python 2 code, you might see class Product(object):, which was required for some features back then. In Python 3 it's redundant and you can drop it.) Second, when your class inherits from another class, the chain doesn't stop at the immediate parent: it continues up to object. So PremiumCustomer inherits from Customer, and Customer inherits from object, which means PremiumCustomer indirectly inherits from object too.

The takeaway is simple: there's always a parent. If your class doesn't name one, the parent is object. The full chain from your subclass up through object is what Python walks when looking up an attribute or a method.

Quiz

Inheritance Basics Quiz

10 quizzes