Back to Blog
Python

Python Method Overriding: How It Works and When to Use It

python method overriding: Understand Python method overriding, including super(), MRO, and abstract base classes, to write clean, maintainable inheritance hierarchies.

PythonOOPInheritancesuper()Abstract Base ClassesMethod Resolution Order
Illustration of Python method overriding showing a child class replacing a parent method

Python method overriding is a core feature of object-oriented programming: a subclass can define a method with the same name as a method in its parent class, and calls to that method on instances of the subclass will use the subclass version. This behavior is resolved at runtime, not compile time, and it depends on the method resolution order (MRO) of the class hierarchy. Understanding how overriding works, how to call the parent implementation, and when to use it correctly is essential for writing maintainable inheritance-based code.

The Basic Mechanics of Method Overriding

When a subclass defines a method with the exact same name and signature as a method in its base class, the subclass method overrides the base method. Consider a simple example:

class Animal: def speak(self): return "Some sound" class Dog(Animal): def speak(self): return "Woof" animal = Animal() dog = Dog() print(animal.speak()) # Some sound print(dog.speak()) # Woof

The Dog class overrides speak with its own implementation. When you call dog.speak(), Python looks up the method on the instance's class first, then walks up the inheritance chain until it finds a definition. This lookup is dynamic, so the override takes effect even if the call is made through a variable typed as the base class:

def make_sound(entity): return entity.speak() print(make_sound(dog)) # Woof

This runtime dispatch is the foundation of polymorphism in Python. The method that runs is determined by the actual type of the object, not by the declared type of the variable.

How Python Resolves Overridden Methods

Python uses a deterministic algorithm called the C3 linearization to compute the method resolution order (MRO) for a class. The MRO is a list of classes that Python checks, in order, when looking up a method. You can inspect it with the mro() class method:

class A: def method(self): return "A" class B(A): def method(self): return "B" class C(A): def method(self): return "C" class D(B, C): pass print(D.mro()) # [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]

For a single inheritance chain, the MRO is straightforward: the subclass comes first, then its parent, then the grandparent, and so on. Multiple inheritance makes the order more complex. The C3 algorithm preserves the order of base classes as listed in the class definition and ensures that a class always appears before its parents. This matters when two parent classes provide the same method. In the example above, D inherits from B and C, and both override method. Because B appears before C in the class definition, D will use B.method unless D itself overrides it.

When you call a method on an instance, Python searches the MRO from left to right and uses the first class that defines the method. This is why the order of base classes in a multiple inheritance declaration can change behavior.

Calling the Parent Implementation with super()

Often an override needs to extend the parent method rather than replace it entirely. The super() built-in returns a proxy object that delegates method calls to the next class in the MRO. This is the standard way to call the parent implementation:

class Base: def __init__(self, name): self.name = name class Child(Base): def __init__(self, name, age): super().__init__(name) self.age = age c = Child("Alice", 30) print(c.name, c.age) # Alice 30

Using super() in a method override is not limited to __init__. Any method can call the parent version:

class Shape: def area(self): return 0 class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side ** 2 class ColoredSquare(Square): def __init__(self, side, color): super().__init__(side) self.color = color def area(self): base_area = super().area() print(f"Computing area for {self.color} square") return base_area

In a multiple inheritance scenario, super() does not simply call the immediate parent. It follows the MRO of the instance, which allows cooperative multiple inheritance. This is powerful but requires that all classes in the hierarchy use super() consistently, otherwise method calls may skip classes unexpectedly.

Overriding init and Other Special Methods

Special methods, also called dunder methods, follow the same overriding rules. The most commonly overridden is __init__, but you can also override __str__, __repr__, __eq__, __lt__, and others. Overriding __init__ is often necessary to initialize subclass-specific attributes while reusing the parent's initialization logic:

class Person: def __init__(self, name): self.name = name class Employee(Person): def __init__(self, name, employee_id): super().__init__(name) self.employee_id = employee_id

When overriding special methods, the signature must match what Python expects. For example, __eq__ must accept another object as its argument. If you override __eq__, you should also override __hash__ to maintain the invariant that equal objects have equal hashes, unless you explicitly want to disable hashing.

Overriding __str__ is a common way to provide a readable string representation:

class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" p = Point(3, 4) print(str(p)) # (3, 4)

Special method lookup differs from normal method lookup in one important way: Python looks up special methods on the type of the object, not on the instance. This means that overriding a special method in a class works as expected, but assigning a special method to an instance does not.

Using Abstract Base Classes to Enforce Overrides

Sometimes you want to force subclasses to override a method. The abc module provides ABC and abstractmethod for this purpose. A class that inherits from ABC and contains at least one abstract method cannot be instantiated. Subclasses must override all abstract methods, or they too become abstract.

from abc import ABC, abstractmethod class Vehicle(ABC): @abstractmethod def start(self): pass @abstractmethod def stop(self): pass class Car(Vehicle): def start(self): return "Car engine started" def stop(self): return "Car engine stopped" # vehicle = Vehicle() # TypeError: Can't instantiate abstract class Vehicle car = Car() print(car.start()) # Car engine started

Abstract base classes are valuable when you are designing a framework or a library where the base class defines a contract that all subclasses must fulfill. They also help catch missing overrides at instantiation time rather than at runtime when the method is called. However, abstract methods should not be used excessively; if a method has a sensible default implementation, it is usually better to provide that default and let subclasses override it only when necessary.

Overriding vs. Overloading: What Python Does Not Do

In languages like Java or C++, method overloading allows multiple methods with the same name but different parameter types or counts. Python does not support overloading in that sense. If you define multiple methods with the same name in a class, the last definition wins. This is a direct consequence of Python's dynamic nature: methods are attributes of the class dictionary, and assigning a new value to the same key replaces the old one.

class Example: def method(self, a): return a def method(self, a, b): return a + b e = Example() # e.method(1) # TypeError: method() missing 1 required positional argument: 'b' print(e.method(1, 2)) # 3 ```n The second `method` definition completely replaces the first. To simulate overloading, you typically use default arguments, `*args`, `**kwargs`, or explicit type checks. This is a different design from Java-style overloading, and it affects how you think about method design in inheritance hierarchies. When overriding a method, you must match the signature that callers expect. If the parent method accepts certain arguments, the override should accept at least those arguments, or the callers may break. ## Design Considerations and Maintainability Method overriding is a powerful tool, but it can lead to fragile code if used carelessly. The Liskov substitution principle states that a subclass should be substitutable for its base class without altering the correctness of the program. If an override changes the method's contract—for example, by raising a different exception type, returning a different type, or requiring different arguments—it can break code that relies on the base class behavior. Before overriding a method, ask whether the subclass genuinely needs a different behavior or whether the base class should be refactored to be more flexible. Overriding is appropriate when the subclass represents a more specific variant of the base concept. It is not appropriate when the base class method is not designed to be extended. One common pitfall is forgetting to call `super()` in an override. If the parent method performs essential initialization or cleanup, skipping `super()` can leave the object in an inconsistent state. This is especially critical in `__init__`, where the parent attributes may not be set. Another maintainability concern is the depth of the inheritance hierarchy. Deep chains of overrides make it harder to trace which implementation actually runs. The MRO can become difficult to reason about, especially with multiple inheritance. Prefer composition over inheritance when the relationship is not a clear "is-a" relationship. Overriding is most maintainable when the hierarchy is shallow and each override adds a small, well-defined change. ## Common Pitfalls and Runtime Behavior Several runtime behaviors can surprise developers who are new to Python method overriding. One is that overriding a method changes the behavior of all calls to that method, including those made from the parent class itself. If the parent class calls a method that the subclass overrides, the subclass version will be used, even if the call happens inside a parent method. This is often desirable, but it can lead to subtle bugs if the parent method was not designed for that override. ```python class Base: def process(self): return self.step() def step(self): return "base step" class Child(Base): def step(self): return "child step" c = Child() print(c.process()) # child step

Here, Base.process calls self.step(), and because self is a Child instance, Child.step runs. This is a form of the Template Method pattern, and it is a deliberate design choice. But if you are not aware of it, it can cause unexpected behavior.

Another pitfall is overriding a method with an incompatible signature. Python does not enforce parameter types, but it does enforce the number of positional arguments at call time. If a caller expects to call the method with two arguments and the override only accepts one, the call will raise a TypeError. The override must be compatible with the parent's signature, or the code that uses the base class will break.

Finally, remember that super() is not just a way to call the parent class. It is a dynamic proxy that respects the full MRO. In a multiple inheritance hierarchy, super() can call a method from a sibling class, not just the direct parent. This is the basis of cooperative multiple inheritance, but it requires that all methods in the chain use super() consistently. If one class in the hierarchy does not call super(), the chain is broken and subsequent classes in the MRO will not be reached.

Understanding these runtime behaviors allows you to use method overriding intentionally rather than accidentally. The key is to keep the contract clear, use super() where appropriate, and rely on abstract base classes when you need to enforce that a method must be overridden.

python method overriding: Practical Usage and Code Examples | RYUSLOG DEV