Python Single Inheritance: Syntax and Behavior
python single inheritance: Learn how Python single inheritance works: class syntax, attribute lookup, super() delegation, method overriding, and when composition is a...
Python single inheritance means a class can extend exactly one parent class. The child class inherits the parent's attributes and methods, and it can override or extend them without changing the parent's behavior. This is the default inheritance model in Python, and it keeps the method resolution order simple: a linear chain from the child up to the base object class.
Declaring a Child Class in Python
The syntax is minimal. Put the parent class name in parentheses after the child class name:
class Base: def __init__(self, name): self.name = name def describe(self): return f"Base named {self.name}" class Child(Base): pass
Child now has __init__ and describe available, even though it defines no methods of its own. Instances of Child are also instances of Base:
c = Child("demo") print(isinstance(c, Child)) # True print(isinstance(c, Base)) # True print(issubclass(Child, Base)) # True
The pass body is valid but unusual in real code. A child class normally adds attributes or overrides behavior.
How Attribute and Method Lookup Works
When you access an attribute on an instance, Python searches in this order:
- The instance's own
__dict__ - The class's
__dict__ - Each parent class's
__dict__, walking up the inheritance chain - The built-in
objectclass
This means a child class automatically sees everything the parent defines, unless the child shadows it with a same-named attribute. The lookup is dynamic: if the parent gains a method at runtime, existing child instances see it immediately.
Using super() to Delegate to the Parent
super() returns a proxy that forwards method calls to the next class in the method resolution order. In single inheritance, that is always the parent class.
class Base: def __init__(self, name): self.name = name class Child(Base): def __init__(self, name, level): super().__init__(name) self.level = level
Calling super().__init__(name) runs the parent's constructor so the parent-owned state is initialized. Skipping it leaves self.name undefined, which often surfaces later as an AttributeError when the parent's methods run.
super() works in any method, not just __init__. A common pattern is extending a parent method while keeping its original behavior:
class Base: def describe(self): return f"Base named {self.name}" class Child(Base): def describe(self): return super().describe() + f", level {self.level}"
The child reuses the parent's logic and appends its own detail. This keeps the parent's behavior in one place instead of duplicating it.
Overriding Methods Without Breaking Parent Behavior
Overriding means defining a method with the same name as one in the parent. The child's version wins during lookup. The decision is whether to call super() inside the override.
If the override fully replaces the parent's behavior, no super() call is needed:
class Child(Base): def describe(self): return f"Child named {self.name}, level {self.level}"
If the override extends the parent's behavior, call super() and combine the results. The important constraint is that the parent method still runs with the same instance, so it sees any state the child has already set up. Call super() after the child has initialized the attributes the parent method reads.
Chained Single Inheritance and the MRO
Single inheritance does not limit you to one level. A class can inherit from a child of another class, producing a chain:
class A: pass class B(A): pass class C(B): pass
The method resolution order for C is C → B → A → object. Python exposes it through C.__mro__. Because the chain is linear, there is no ambiguity about which class provides a given method, and the diamond problem that plagues multiple inheritance cannot occur.
super() in a chained chain still works correctly. In C, super() resolves to B; in B, super() resolves to A. Each class only needs to know its immediate parent.
Common Mistakes With Single Inheritance
The most frequent error is forgetting to call super().__init__() in the child constructor. The child then inherits methods that depend on parent state, but that state was never created. The failure appears later, often as an AttributeError inside a method that assumes the attribute exists.
Another mistake is calling the parent method directly by class name:
class Child(Base): def __init__(self, name, level): Base.__init__(self, name) # works, but fragile self.level = level
This works in single inheritance, but it hardcodes the parent class name. If the hierarchy changes, the call breaks. super() resolves the parent dynamically and survives refactoring.
A third mistake is overriding a method and forgetting that the parent's version also runs. If the parent method has side effects, replacing it entirely can skip important setup. Decide explicitly whether the override is a replacement or an extension, and document that choice.
When Single Inheritance Becomes a Maintainability Problem
Deep chains are the main maintainability risk. A chain of five or six levels makes it hard to trace where a method is actually defined and which super() call initializes which attribute. The class that owns a given piece of state becomes unclear.
A practical limit is two or three levels. Beyond that, composition usually serves better: instead of inheriting behavior from a distant ancestor, hold an instance of the collaborator and delegate to it.
class Logger: def log(self, message): print(message) class Service: def __init__(self, logger): self._logger = logger def run(self): self._logger.log("running")
This keeps the dependency explicit and testable. The rule of thumb: use single inheritance when the child genuinely is a specialized version of the parent, and prefer composition when the relationship is "has a" rather than "is a".