AiTechWorlds
AiTechWorlds
Imagine you run a company and you hire employees. Every employee — whether they're an accountant, a software engineer, or a janitor — must do certain things: show up to work, complete assigned tasks, and submit timesheets. You write these requirements into a formal employment contract.
The contract doesn't say how the accountant does accounting or how the engineer writes code. It just says: "If you want to work here, you MUST fulfill these obligations." The specific how is left entirely to the employee.
Abstract classes in Python work exactly the same way. They define a contract — a set of methods that every subclass must implement — without providing the actual implementation. They are blueprints, not buildings.
Before abstract classes, Python developers would write a base class, forget to override a method in a subclass, and only discover the bug at runtime — sometimes in production. Abstract classes prevent that mistake at class definition time, not at runtime.
"An abstract class is a promise: every concrete subclass will have these methods."
Without enforcement, there's nothing stopping you from forgetting to implement a critical method. With an abstract class, Python raises a TypeError the moment you try to instantiate an incomplete subclass.
Python's abc module (Abstract Base Classes) provides the tools.
# Step 1: Import the tools
from abc import ABC, abstractmethod
# Step 2: Inherit from ABC to mark the class as abstract
class Payment(ABC):
# Step 3: Use @abstractmethod to mark required methods
@abstractmethod
def charge(self, amount: float) -> bool:
"""Charge the customer. Must return True on success."""
pass # No implementation here — subclasses must provide it
@abstractmethod
def refund(self, amount: float) -> bool:
"""Refund the customer."""
pass
# Regular methods ARE allowed — shared logic lives here
def validate_amount(self, amount: float) -> bool:
return amount > 0
What happens if you try to instantiate Payment directly?
p = Payment()
# TypeError: Can't instantiate abstract class Payment
# with abstract methods charge, refund
Python refuses. The contract is not fulfilled.
Now let's fulfill the contract with three different payment providers.
class CreditCard(Payment):
def __init__(self, card_number: str):
self.card_number = card_number # Store card number
def charge(self, amount: float) -> bool:
# Validate amount using inherited method
if not self.validate_amount(amount):
return False
print(f"Charged ${amount:.2f} to card ending {self.card_number[-4:]}")
return True
def refund(self, amount: float) -> bool:
print(f"Refunded ${amount:.2f} to card ending {self.card_number[-4:]}")
return True
class PayPal(Payment):
def __init__(self, email: str):
self.email = email # PayPal account email
def charge(self, amount: float) -> bool:
if not self.validate_amount(amount):
return False
print(f"Charged ${amount:.2f} via PayPal to {self.email}")
return True
def refund(self, amount: float) -> bool:
print(f"Refunded ${amount:.2f} to PayPal account {self.email}")
return True
class Crypto(Payment):
def __init__(self, wallet_address: str, currency: str = "BTC"):
self.wallet = wallet_address # Blockchain wallet address
self.currency = currency # Cryptocurrency type
def charge(self, amount: float) -> bool:
if not self.validate_amount(amount):
return False
print(f"Charged ${amount:.2f} worth of {self.currency} to {self.wallet[:8]}...")
return True
def refund(self, amount: float) -> bool:
print(f"Refunded ${amount:.2f} worth of {self.currency} to {self.wallet[:8]}...")
return True
Using the payment system:
payments = [
CreditCard("4111111111111234"),
PayPal("user@example.com"),
Crypto("1A2B3C4D5E6F7G8H9I"),
]
for processor in payments:
processor.charge(49.99)
Output:
Charged $49.99 to card ending 1234
Charged $49.99 via PayPal to user@example.com
Charged $49.99 worth of BTC to 1A2B3C...
Because all three classes fulfill the Payment contract, they can be used interchangeably. The calling code doesn't care which one it has — it just calls charge().
You can also require subclasses to define properties, not just methods.
from abc import ABC, abstractmethod
class LibraryItem(ABC):
@property
@abstractmethod
def item_type(self) -> str:
"""Every item must declare what type it is."""
pass
class Book(LibraryItem):
@property
def item_type(self) -> str:
return "Book" # Concrete implementation of abstract property
Python offers multiple ways to define contracts. Choosing the right one matters.
| Approach | How It Works | Enforcement | Best For |
|---|---|---|---|
| Abstract Class (ABC) | Inherit from ABC, use @abstractmethod | Checked at instantiation | When you control the class hierarchy |
| Protocol | Define a class with Protocol, no inheritance needed | Checked by type checkers (mypy) | When you don't control subclasses |
| Duck Typing | Just call the method — if it works, it works | No enforcement | Quick scripts, small projects |
# Protocol approach — no inheritance required
from typing import Protocol
class Chargeable(Protocol):
def charge(self, amount: float) -> bool: ...
def refund(self, amount: float) -> bool: ...
# ANY class with charge() and refund() methods satisfies this
# even if it never imports or mentions Chargeable
@abstractmethod decorator forces subclasses to implement marked methods.TypeError at instantiation if any abstract method is unimplemented — this catches errors early.Protocol is the modern, composition-friendly alternative when you don't own the class hierarchy.The employment contract doesn't tell employees how to do their jobs. It just guarantees they will do them.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises