Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

25. What is the difference between an `abstract method` and a `virtual method` in Python?

The concepts of "abstract method" and "virtual method" are related to object-oriented programming and polymorphism. While Python doesn't use the exact terms "virtual method" as explicitly as languages like C++ or Java, it does have a clear concept of abstract methods and its default method behavior aligns with what's typically meant by "virtual."

1. Abstract Method:

  • Concept: An **abstract method** is a method declared in an abstract base class (ABC) that has a declaration but no implementation. It serves as a placeholder for a method that *must* be implemented by concrete (non-abstract) subclasses.
  • Purpose:
    • To enforce a contract: Abstract methods define an interface or a set of behaviors that any concrete subclass must provide.
    • To prevent instantiation of incomplete classes: An abstract class cannot be instantiated directly if it contains unimplemented abstract methods. Subclasses must implement all abstract methods to become concrete and instantiable.
  • Implementation in Python:
    • Python achieves abstract methods using the `abc` (Abstract Base Classes) module.
    • You define an abstract class by inheriting from `ABC` and decorate abstract methods with `@abstractmethod`.
  • Example Use: Think of a `Shape` abstract class with an `area()` abstract method. All concrete shapes (Circle, Square) *must* implement their own `area()` calculation.

2. Virtual Method (Python's Default Behavior):

  • Concept: In languages like C++ or Java, a **virtual method** is a method in a base class that can be overridden by methods in derived classes. The key aspect is **dynamic dispatch** (or late binding): when a virtual method is called on an object through a pointer or reference to a base class, the runtime system determines which specific implementation (base class or derived class) to call based on the actual type of the object, not the type of the pointer/reference.
  • Python's Approach:
    • **All methods in Python are inherently "virtual" by default.** Python's method dispatch mechanism is always dynamic. When you call `obj.method()`, Python looks up `method` in `obj`'s class, then its parent classes in the MRO, and executes the first one it finds. This means that if a subclass defines a method with the same name as a superclass method, the subclass's method will always override and be called for instances of that subclass.
    • You don't need a special keyword like `virtual` (as in C++) or `override` (as in Java) to make a method overrideable. It's the default behavior.
  • Purpose: To enable **polymorphism**, where objects of different classes can be treated as objects of a common base type, yet exhibit their specific behaviors.

Summary Table:

Feature Abstract Method "Virtual Method" (Python's Default)
Definition Declared in ABC, no implementation (placeholder). Must be overridden. Any method in a base class that can be overridden by a subclass.
Implementation Must be implemented by concrete subclasses. Has an implementation in the base class, can be overridden by subclass.
Enforcement Strictly enforced by `abc` module: subclass *must* implement or be abstract. No explicit enforcement; overriding is optional.
Instantiability of Base Class Cannot be instantiated if it has unimplemented abstract methods. Can be instantiated, and its methods will be called unless overridden.
Keywords `from abc import ABC, abstractmethod`, `@abstractmethod` None (it's Python's default method behavior)
Core Principle Defines an interface/contract. Enables dynamic dispatch and polymorphism.

In essence, an **abstract method** dictates *what* a subclass *must* do, providing a blueprint. Python's default method behavior, akin to **virtual methods**, means *how* a subclass *chooses* to implement or override a method will be dynamically selected at runtime based on the object's actual type, enabling flexible polymorphic behavior.


import abc

# --- Example 1: Abstract Method ---
print("--- Abstract Method Example ---")

# Define an Abstract Base Class (ABC)
class Shape(abc.ABC): # Inherit from abc.ABC
    def __init__(self, name):
        self.name = name

    # This is an abstract method. It has no implementation here.
    # Any concrete subclass of Shape MUST implement 'area()'.
    @abc.abstractmethod
    def area(self):
        pass # No implementation

    # This is a regular (virtual) method that can be inherited or overridden
    def describe(self):
        return f"This is a {self.name}."

# try:
#     # Cannot instantiate an abstract class with unimplemented abstract methods
#     abstract_shape = Shape("Generic")
# except TypeError as e:
#     print(f"Cannot instantiate Shape directly: {e}")

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius

    # Must implement the abstract 'area' method
    def area(self):
        return 3.14159 * self.radius * self.radius

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("Rectangle")
        self.width = width
        self.height = height

    # Must implement the abstract 'area' method
    def area(self):
        return self.width * self.height

# Class AttemptToBreakContract does not implement 'area()'
# class AttemptToBreakContract(Shape):
#     def __init__(self, name):
#         super().__init__(name)
#
# try:
#     invalid_instance = AttemptToBreakContract("Broken")
# except TypeError as e:
#     print(f"\nCaught expected TypeError for un-implemented abstract method: {e}")


# Create instances of concrete subclasses
circle = Circle(5)
rectangle = Rectangle(4, 6)

print(f"{circle.describe()} Area: {circle.area():.2f}")
print(f"{rectangle.describe()} Area: {rectangle.area():.2f}")


# --- Example 2: "Virtual Method" (Python's default behavior) ---
print("\n--- 'Virtual Method' (Python's Default) Example ---")

class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    # This method is "virtual" because it can be overridden
    def start_engine(self):
        print(f"{self.brand} vehicle engine starts with a generic sound.")

    def drive(self):
        print(f"{self.brand} vehicle is driving.")

class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)
        self.model = model

    # Overriding the start_engine method
    def start_engine(self):
        print(f"{self.brand} {self.model}'s engine starts smoothly (Car specific).")

    # Adding a new method specific to Car
    def honk(self):
        print(f"{self.brand} {self.model} goes Honk!")

class Bicycle(Vehicle):
    def __init__(self, brand, type):
        super().__init__(brand)
        self.type = type

    # Overriding the start_engine method to do nothing for a bike
    def start_engine(self):
        print(f"A {self.brand} {self.type} bicycle has no engine to start.")

# Create instances
generic_vehicle = Vehicle("GenericMotors")
my_car = Car("Toyota", "Camry")
my_bike = Bicycle("Giant", "Mountain Bike")

print("\n--- Calling Methods ---")
generic_vehicle.start_engine()
generic_vehicle.drive()

print("-" * 20)
my_car.start_engine() # Calls Car's overridden method
my_car.drive()       # Inherits Vehicle's drive method
my_car.honk()        # Car-specific method

print("-" * 20)
my_bike.start_engine() # Calls Bicycle's overridden method
my_bike.drive()        # Inherits Vehicle's drive method

# Demonstrating Polymorphism
print("\n--- Polymorphism Example ---")
vehicles = [generic_vehicle, my_car, my_bike]

for v in vehicles:
    v.start_engine() # Dynamic dispatch: Calls the correct overridden method
        

Explanation of the Example Code:

  • **Abstract Method Example (`Shape` ABC):**
    • `Shape` is defined as an `abc.ABC` (Abstract Base Class).
    • `area()` is decorated with `@abc.abstractmethod`. This signifies that any concrete subclass of `Shape` *must* provide an implementation for `area()`. If it doesn't (like the commented-out `AttemptToBreakContract`), Python will raise a `TypeError` when you try to instantiate that subclass, enforcing the contract.
    • The commented-out line `abstract_shape = Shape("Generic")` also shows that you cannot directly instantiate `Shape` because it has an unimplemented abstract method.
    • `Circle` and `Rectangle` successfully implement `area()`, allowing them to be instantiated and their specific area calculations to be performed.
  • **"Virtual Method" (Python's Default Behavior) Example (`Vehicle` and Subclasses):**
    • The `Vehicle` class has a `start_engine()` method. This method is "virtual" by default in Python because it can be overridden by subclasses.
    • `Car` and `Bicycle` subclasses both provide their own implementations of `start_engine()`. They don't need any special keyword (`virtual`, `override`) because Python's method lookup is dynamic.
    • When `my_car.start_engine()` is called, Python finds and executes `Car`'s `start_engine()`. When `my_bike.start_engine()` is called, it executes `Bicycle`'s version.
    • The `drive()` method in `Vehicle` is not overridden, so both `Car` and `Bicycle` instances inherit and use `Vehicle`'s `drive()` method.
    • **Polymorphism Demonstration:** The `for v in vehicles:` loop iterates through a list containing instances of `Vehicle`, `Car`, and `Bicycle`. When `v.start_engine()` is called inside the loop, Python's dynamic dispatch ensures that the *correct* `start_engine()` method (from `Vehicle`, `Car`, or `Bicycle`) is executed based on the actual type of the object `v` at that moment. This is the essence of polymorphism.

These examples highlight that abstract methods define a mandatory interface that subclasses must fulfill, whereas Python's default method behavior (which is akin to "virtual" in other languages) enables flexible overriding and polymorphic behavior without special keywords.