You have multiple shapes — Rectangle, Circle, Triangle. Each can compute its area, but each computes it differently. How do you say "every shape must have an area() method" without copy-pasting it?
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
print(Rectangle(3, 4).area())
print(Circle(1).area())What does inheriting from ABC and adding @abstractmethod actually do?
Two things. (1) Can't instantiate. Shape() raises TypeError: Can't instantiate abstract class Shape with abstract method area. (2) Subclasses must override. If Rectangle forgets to define area, instantiating Rectangle(3, 4) raises the same error. The framework forces you to implement every abstract method.
Is that worth the boilerplate?
Sometimes. ABCs are great when you need a guaranteed contract — every plugin must implement these methods, every database adapter must support these operations. They're overkill when you just want a few related classes that happen to share an interface. Tomorrow's Protocol is the lighter alternative.
Why not just use plain inheritance with a normal method that raises NotImplementedError?
That works — and it's what people did before abc. The problem: the error only fires when someone calls the method. With @abstractmethod, the error fires at instantiation — earlier, clearer, and it's documented in the class definition rather than buried in the body.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def perimeter(self) -> float:
...Shape() raises TypeError — abstract classes can't be instantiated directly@abstractmethod — otherwise instantiating the subclass also failsclass Square(Shape):
def __init__(self, side):
self.side = side
def area(self) -> float:
return self.side ** 2
# forgot perimeter!
Square(5) # TypeError: Can't instantiate abstract class Square with abstract method perimeter... is a valid placeholder bodyThe ... (literal Ellipsis value) is conventional for "this method has no implementation." pass works the same. The body of an abstract method doesn't matter — it's never called.
Not every method has to be abstract. Common pattern: abstract methods for the variable parts, concrete methods that use them:
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
def describe(self) -> str:
return f"area is {self.area()}"
# Subclass only has to implement area; gets describe for free
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(5).describe()) # area is 25This is the template method pattern — the base defines the skeleton, subclasses fill in the variable bits.
Yes:
No:
Protocol (tomorrow)Protocol is lighter| ABC | Protocol | |
|---|---|---|
| Inheritance required? | Yes — class X(ABC) | No — duck typing |
| Runtime check at instantiation? | Yes | Only with @runtime_checkable |
| Conceptual model | "is-a" relationship | "behaves-like" relationship |
| When to reach for | Strict plugin contracts | Loose interface descriptions |
Use ABCs when you want to enforce the contract. Use Protocols when you want to describe the contract for type checkers without forcing inheritance.
You have multiple shapes — Rectangle, Circle, Triangle. Each can compute its area, but each computes it differently. How do you say "every shape must have an area() method" without copy-pasting it?
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
print(Rectangle(3, 4).area())
print(Circle(1).area())What does inheriting from ABC and adding @abstractmethod actually do?
Two things. (1) Can't instantiate. Shape() raises TypeError: Can't instantiate abstract class Shape with abstract method area. (2) Subclasses must override. If Rectangle forgets to define area, instantiating Rectangle(3, 4) raises the same error. The framework forces you to implement every abstract method.
Is that worth the boilerplate?
Sometimes. ABCs are great when you need a guaranteed contract — every plugin must implement these methods, every database adapter must support these operations. They're overkill when you just want a few related classes that happen to share an interface. Tomorrow's Protocol is the lighter alternative.
Why not just use plain inheritance with a normal method that raises NotImplementedError?
That works — and it's what people did before abc. The problem: the error only fires when someone calls the method. With @abstractmethod, the error fires at instantiation — earlier, clearer, and it's documented in the class definition rather than buried in the body.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def perimeter(self) -> float:
...Shape() raises TypeError — abstract classes can't be instantiated directly@abstractmethod — otherwise instantiating the subclass also failsclass Square(Shape):
def __init__(self, side):
self.side = side
def area(self) -> float:
return self.side ** 2
# forgot perimeter!
Square(5) # TypeError: Can't instantiate abstract class Square with abstract method perimeter... is a valid placeholder bodyThe ... (literal Ellipsis value) is conventional for "this method has no implementation." pass works the same. The body of an abstract method doesn't matter — it's never called.
Not every method has to be abstract. Common pattern: abstract methods for the variable parts, concrete methods that use them:
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
def describe(self) -> str:
return f"area is {self.area()}"
# Subclass only has to implement area; gets describe for free
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(5).describe()) # area is 25This is the template method pattern — the base defines the skeleton, subclasses fill in the variable bits.
Yes:
No:
Protocol (tomorrow)Protocol is lighter| ABC | Protocol | |
|---|---|---|
| Inheritance required? | Yes — class X(ABC) | No — duck typing |
| Runtime check at instantiation? | Yes | Only with @runtime_checkable |
| Conceptual model | "is-a" relationship | "behaves-like" relationship |
| When to reach for | Strict plugin contracts | Loose interface descriptions |
Use ABCs when you want to enforce the contract. Use Protocols when you want to describe the contract for type checkers without forcing inheritance.
Create a free account to get started. Paid plans unlock all tracks.