Difficulty: Intermediate
How does Python handle abstract classes and interfaces? What are Protocols?
Python provides abstract classes through the abc module. Abstract classes cannot be instantiated and can define abstract methods that subclasses must implement.
Python also embraces duck typing: 'If it walks like a duck and quacks like a duck, it's a duck.' You don't need explicit interfaces.
Protocols (Python 3.8+, typing module) provide structural subtyping - a class satisfies a Protocol if it has the required methods, without explicit inheritance. This bridges the gap between duck typing and static type checking.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Calculate the area of the shape."""
pass
@abstractmethod
def perimeter(self):
"""Calculate the perimeter of the shape."""
pass
def describe(self):
"""Non-abstract method: shared by all shapes."""
return f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius 2
def perimeter(self):
return 2 * 3.14159 * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# shape = Shape() # TypeError: Can't instantiate abstract class
c = Circle(5)
r = Rectangle(3, 4)
print(c.describe()) # Circle: area=78.54, perimeter=31.42
print(r.describe()) # Rectangle: area=12.00, perimeter=14.00
Abstract classes enforce a contract: subclasses MUST implement all abstract methods. Non-abstract methods provide shared default behavior.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
class Text:
def render(self) -> str: # Different method name
return "Rendering text"
# No explicit inheritance needed!
def render_all(items: list[Drawable]) -> None:
for item in items:
print(item.draw())
shapes = [Circle(), Square()]
render_all(shapes) # Works!
# Runtime checking with @runtime_checkable
print(isinstance(Circle(), Drawable)) # True
print(isinstance(Text(), Drawable)) # False - no draw() method
Protocols define interfaces structurally: any class with matching methods satisfies the protocol, without inheriting from it. This is Python's answer to Go's interfaces.
# Duck typing: focus on behavior, not type
def get_length(obj):
"""Works with anything that has __len__."""
return len(obj)
print(get_length([1, 2, 3])) # 3 - list
print(get_length("hello")) # 5 - string
print(get_length({1, 2, 3})) # 3 - set
print(get_length({'a': 1})) # 1 - dict
# EAFP: Easier to Ask Forgiveness than Permission
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
except TypeError:
return None
# vs LBYL (Look Before You Leap) - less Pythonic
def safe_divide_lbyl(a, b):
if isinstance(b, (int, float)) and b != 0:
return a / b
return None
# ABC registration: make existing classes satisfy an ABC
from abc import ABC, abstractmethod
class Printable(ABC):
@abstractmethod
def __str__(self):
pass
# All objects have __str__, so register built-in types
Printable.register(int)
Printable.register(str)
print(isinstance(42, Printable)) # True
print(isinstance("hello", Printable)) # True
Python's EAFP style: try the operation and handle failure, rather than checking types first. ABC.register() retroactively makes classes satisfy an ABC.
ABC, Abstract Methods, Interfaces, Protocols, Duck Typing