Single inheritance is the most common form of inheritance: one subclass that extends one parent class. This lesson goes deeper on the patterns: how inherited methods flow from parent to child, how to add new behavior without breaking the parent, how to verify type relationships with isinstance and issubclass, and when to step back and use composition instead.
In single inheritance, a class names exactly one parent in its declaration. The subclass automatically gets every attribute and method the parent defines, and it can add or replace pieces as needed. The shape of the hierarchy is a straight line: each class has one direct parent, all the way up to object.
AdminUser defines nothing, yet an AdminUser instance has a working __init__, a summary method, and three attributes. They all come from User. The pass body is a placeholder; the inheritance does the real work.
The chain runs from object at the top, through User, down to AdminUser. A method call on an AdminUser instance makes Python walk this chain from the bottom up. AdminUser is checked first, then User, then object. The first class that defines the method wins.
This is the difference single inheritance makes: the walk is a straight line. There's never any ambiguity about which parent to consult, because there's only one. With multiple inheritance, that walk can fork through several parents, which is where the rules get more involved.
selfA subtle but important point: methods inherited from the parent operate on the same self as methods defined on the subclass. There's only one instance, and self always refers to it, no matter which class actually wrote the method.
summary lives on User, but inside it self refers to the AdminUser instance. self.user_id reads the attribute that was set on this specific admin object. From the method's point of view, self is "the object I was called on", and that object happens to have all the attributes both classes contributed.
The same logic flows the other direction: a method on the subclass can read attributes the parent set, because they live on the same self. permission_list could reference self.name, even though name was created by User.__init__. There's no wall between parent and child attributes once the instance is built.
This is why super().__init__(...) matters. It's not about the parent owning some separate piece of state. The parent's __init__ runs on the same instance and adds attributes to the same self. Skip the super() call, and those attributes never exist on the object, so any method (parent or child) that reads them raises AttributeError.
The cleanest use of single inheritance is when the subclass adds something new without touching what the parent already does. AdminUser from the example above doesn't replace summary or change how user identity works; it adds a new method (permission_list) and a new attribute (permissions) on top of the parent's behavior.
This is the additive pattern. The parent's contract stays intact: every operation that works on a User still works on an AdminUser, since AdminUser hasn't removed or weakened anything. The subclass is strictly more capable than the parent.
A second example with animals:
Dog keeps describe exactly as the parent defined it. It adds a breed attribute (a piece of data dogs have that animals in general don't) and a fetch method (a behavior specific to dogs). The constructor still calls super().__init__, but it fixes sound="Woof" since every dog makes the same sound, while the caller picks the name and the breed.
When a problem can be solved by adding rather than replacing, prefer adding. Adding is safe. Replacing is where bugs creep in, because the subclass is now responsible for not breaking any expectation that callers had about the parent's method. For now, prefer adding.
A useful check before overriding: can the same result be achieved by adding a new method with a different name? If yes, the new method is usually the better choice. Overriding is for cases where existing callers should automatically pick up the new behavior, not when extra functionality is the goal.
isinstance and issubclassOnce an inheritance chain exists, the next question is sometimes whether an object is of a certain type, or whether one class is a subclass of another. Python has two builtins for that.
isinstance(obj, ClassName) returns True when obj is an instance of ClassName or any of its subclasses. The "or any of its subclasses" part is what makes inheritance useful here:
alice is a User (and only a User), so the first check is True and the last is False. admin is both an AdminUser and a User, since every AdminUser is a User by inheritance. The rule that gets used most: an instance of a subclass is also an instance of the parent.
issubclass(Child, Parent) answers the same question at the class level. No instance is involved, just the class objects:
The direction is important. issubclass(A, B) is True when A inherits from B (or A is B), not the other way around. AdminUser inherits from User, so issubclass(AdminUser, User) is True. The reverse is False. Every class inherits from object, so the third check is True even without writing class AdminUser(User, object):.
Both functions also accept a tuple of classes for the second argument, which is handy for testing against several types:
Strictly speaking, the tuple here is redundant since isinstance(obj, User) would already match admin thanks to inheritance. The tuple form is useful when the types aren't related by inheritance, for example checking isinstance(value, (int, float)).
The "is-a" test decides whether inheritance fits: a PremiumCustomer is a Customer, so inheritance fits; a Cart has products but isn't itself a Product, so it doesn't. The test is simple, but it hides a subtler requirement with a name: the Liskov substitution principle.
The principle says: anywhere code expects an instance of the parent, it should accept an instance of the subclass without any surprises. The subclass has to honor the parent's contract. If User.summary() returns a string, then AdminUser.summary() must also return a string. If User.user_id is always set after construction, the same has to be true on an AdminUser. The subclass is allowed to do more, but it can't do less or do something incompatible.
A violation that passes the "is-a" test linguistically but breaks Liskov in practice:
SilentAnimal is an Animal by inheritance, but it can't be substituted anywhere an Animal is expected, since announce will crash. The parent's contract said "I have a make_sound method that returns something", and the subclass broke it by raising instead. The fix is usually one of two things: don't inherit (a silent animal is something different, not a special kind of Animal), or design the parent's contract so that "no sound" is an allowed return value.
A cleaner subclass keeps the contract intact:
Cat overrides make_sound to return a different string, but the contract (returns a string describing the sound) is the same. announce works on both, and a future function that expects an Animal will work on a Cat too.
Liskov as a formal principle isn't required for good Python. The intuitive version is enough: if substituting a subclass for the parent would surprise someone reading the calling code, the design has drifted from "is-a" into something else, and inheritance might be the wrong tool.
Single inheritance shows up most often in domain modeling: capturing the structure of the business or problem at hand. An e-commerce system has products of different kinds; an HR system has employees of different kinds; a content platform has posts of different kinds. The parent class captures what they share; each subclass captures what makes its variant different.
Consider a shop that sells both physical and digital goods. Every item has an ID, a name, and a price. Beyond that, the two kinds diverge:
display_label works on every item in the catalog because every item is a Product and Product defines the method. The loop doesn't care which subclass each item is; it calls the shared method. That's the payoff: code that operates on the parent type works automatically on any subclass.
When the code does need the difference, it can drill in:
The isinstance branches handle the differences. This pattern is fine for narrow cases but becomes awkward when the branches grow long. The cleaner alternative is polymorphism: a method like delivery_info on each subclass, called uniformly. Single inheritance gives the structure: shared code at the top, specializations below.
A useful design check: is the parent class ever instantiated directly? If callers always use one of the subclasses and never the parent on its own, the parent might be better expressed as an abstract base class so that mistakenly instantiating it raises an error.
Inheritance is one of the most overused tools in object-oriented programming. The pattern "I need behavior X, and there's a class that has it, so I'll inherit from it" is tempting but often wrong. The relationship has to be "is-a", not "I want some of your methods".
A classic mistake: a Cart that wants list-like behavior, so the author writes class Cart(list):. The cart now has append, pop, __len__, and all the other list methods. The trouble starts when the cart needs invariants that a plain list doesn't, like "every item has a positive price" or "the total can never go negative". Every list method that mutates the cart is a hole in those invariants, and the author has to override each one to enforce the rules, or the rules break.
The cleaner approach is composition: the Cart holds a list internally and exposes only the operations that make sense:
Cart is not a list. It uses a list internally to store items, but its public API only exposes the operations that respect the cart's invariants (add validates the quantity, total knows how to compute the sum). Callers can't accidentally cart.pop() or cart.clear() because those methods don't exist on the cart's public interface.
The rule: if the question is "is X a special kind of Y?", inheritance is appropriate. If the question is "does X need some of the things Y can do?", composition is usually better. Composition provides the same code reuse without locking the class into the parent's full surface area.
The left column is inheritance: each pair is a genuine "is-a". The right column is composition: each pair is a "has-a" or "uses-a". When the choice is unclear, the safer default is composition. Refactoring from composition to inheritance later is easier than the reverse.
10 quizzes