Master abstraction in Python — abstract base classes (ABC), @abstractmethod, interface design, abstract properties, concrete vs abstract classes, and real-world design patterns.
🧠 Real-World Analogy — Smartphone
You use your smartphone — making calls, taking photos, running apps. Do you know how the processor's exact circuit works inside? The battery's chemistry? The signal processing? No — and you don't need to. This is abstraction. This is Abstraction — hiding internal complexity and exposing only the necessary interface. In code: an Abstract Class defines a contract — "what to do" — without specifying "how to do it."
📦 The abc Module
from abc import ABC, abstractmethod, abstractproperty
| Tool | Purpose |
|---|
ABC | Base class to inherit from for abstract classes |
@abstractmethod | Marks a method as abstract (must be overridden) |
ABCMeta | Metaclass alternative to ABC |
✅ Example 1 — Shape Interface
from abc import ABC, abstractmethod
import math
class Shape(ABC): # abstract class — cannot instantiate directly
"""Abstract interface for all geometric shapes."""
def __init__(self, color="white"):
self.color = color
@abstractmethod
def area(self) -> float: # MUST be overridden in subclasses
"""Return the area of the shape."""
...
@abstractmethod
def perimeter(self) -> float:
"""Return the perimeter of the shape."""
...
@abstractmethod
def draw(self) -> str:
"""Return ASCII representation."""
...
# Concrete method — shared by ALL shapes
def describe(self):
print(f"Shape: {self.__class__.__name__:12} | Color: {self.color:8} "
f"| Area: {self.area():8.2f} | Perimeter: {self.perimeter():.2f}")
# ── Concrete implementations ──────────────────────────────
class Circle(Shape):
def __init__(self, radius, color="white"):
super().__init__(color)
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def draw(self) -> str:
return " ○ "
class Rectangle(Shape):
def __init__(self, length, width, color="white"):
super().__init__(color)
self.length, self.width = length, width
def area(self) -> float:
return self.length * self.width
def perimeter(self) -> float:
return 2 * (self.length + self.width)
def draw(self) -> str:
return " ■ "
class Triangle(Shape):
def __init__(self, a, b, c, color="white"):
super().__init__(color)
if a + b <= c or a + c <= b or b + c <= a:
raise ValueError("Invalid triangle sides.")
self.a, self.b, self.c = a, b, c
def area(self) -> float:
s = (self.a + self.b + self.c) / 2
return math.sqrt(s*(s-self.a)*(s-self.b)*(s-self.c))
def perimeter(self) -> float:
return self.a + self.b + self.c
def draw(self) -> str:
return " △ "
# ── Cannot instantiate abstract class ────────────────────
try:
s = Shape()
except TypeError as e:
print(f"Error: {e}")
# ── Using concrete classes polymorphically ────────────────
shapes = [
Circle(7, "red"),
Rectangle(10, 5, "blue"),
Triangle(3, 4, 5, "green"),
Circle(3, "yellow"),
]
for shape in shapes:
shape.describe()
print(f" {shape.draw()}")
total_area = sum(s.area() for s in shapes)
print(f"\nTotal area: {total_area:.2f} sq units")
Output:
| Error | Can't instantiate abstract class Shape with abstract methods area, draw, perimeter |
| Shape | Circle | Color: red | Area: 153.94 | Perimeter: 43.98 |
| Shape | Rectangle | Color: blue | Area: 50.00 | Perimeter: 30.00 |
| Shape | Triangle | Color: green | Area: 6.00 | Perimeter: 12.00 |
| Shape | Circle | Color: yellow | Area: 28.27 | Perimeter: 18.85 |
| Total area | 238.21 sq units |
🗄️ Example 2 — Database Driver Abstraction
from abc import ABC, abstractmethod
from typing import List, Dict, Any
class DatabaseDriver(ABC):
"""Abstract interface for database drivers."""
def __init__(self, host: str, port: int):
self.host = host
self.port = port
self._connected = False
@abstractmethod
def connect(self) -> bool: ...
@abstractmethod
def execute(self, query: str, params: tuple = ()) -> Any: ...
@abstractmethod
def fetch_all(self) -> List[Dict]: ...
@abstractmethod
def close(self) -> None: ...
# Concrete method in abstract class
def ping(self) -> bool:
print(f"[{self.__class__.__name__}] Pinging {self.host}:{self.port}...")
return self._connected
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
class PostgreSQLDriver(DatabaseDriver):
def __init__(self, host, port=5432, database=""):
super().__init__(host, port)
self.database = database
self._cursor = None
def connect(self) -> bool:
print(f"🐘 PostgreSQL: Connecting to {self.host}:{self.port}/{self.database}...")
self._connected = True
print(" ✅ PostgreSQL connected.")
return True
def execute(self, query, params=()):
print(f" 📝 [PG] Executing: {query} | params={params}")
self._cursor = f"PG_CURSOR_{hash(query) % 1000}"
return self._cursor
def fetch_all(self) -> List[Dict]:
print(f" 📦 [PG] Fetching results from {self._cursor}...")
return [{"id": 1, "name": "Arjun"}, {"id": 2, "name": "Priya"}]
def close(self):
self._connected = False
print(" 🔒 PostgreSQL connection closed.")
class MySQLDriver(DatabaseDriver):
def __init__(self, host, port=3306, database=""):
super().__init__(host, port)
self.database = database
def connect(self) -> bool:
print(f"🐬 MySQL: Connecting to {self.host}:{self.port}/{self.database}...")
self._connected = True
print(" ✅ MySQL connected.")
return True
def execute(self, query, params=()):
print(f" 📝 [MySQL] Executing: {query} | params={params}")
return "MYSQL_RESULT_SET"
def fetch_all(self) -> List[Dict]:
print(" 📦 [MySQL] Fetching results...")
return [{"id": 10, "user": "Rahul"}, {"id": 20, "user": "Sneha"}]
def close(self):
self._connected = False
print(" 🔒 MySQL connection closed.")
def run_user_query(driver: DatabaseDriver):
"""Works with ANY database driver — abstraction in action!"""
with driver:
driver.ping()
driver.execute("SELECT * FROM users WHERE active = %s", (True,))
results = driver.fetch_all()
print(f" Got {len(results)} records: {results}\n")
print("=" * 55)
run_user_query(PostgreSQLDriver("db.wohotech.com", database="app_db"))
run_user_query(MySQLDriver("mysql.wohotech.com", database="users_db"))
Output:
| 🐘 PostgreSQL | Connecting to db.wohotech.com:5432/app_db... |
| [PostgreSQLDriver] Pinging db.wohotech.com | 5432... |
| 📝 [PG] Executing | SELECT * FROM users WHERE active = %s | params=(True,) |
| Got 2 records | [{'id': 1, 'name': 'Arjun'}, {'id': 2, 'name': 'Priya'}] |
| 🐬 MySQL | Connecting to mysql.wohotech.com:3306/users_db... |
| [MySQLDriver] Pinging mysql.wohotech.com | 3306... |
| 📝 [MySQL] Executing | SELECT * FROM users WHERE active = %s | params=(True,) |
| Got 2 records | [{'id': 10, 'user': 'Rahul'}, {'id': 20, 'user': 'Sneha'}] |
🔔 Example 3 — Notification System
from abc import ABC, abstractmethod
class NotificationService(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> bool: ...
@abstractmethod
def validate_recipient(self, recipient: str) -> bool: ...
# Template method — defines the algorithm skeleton
def notify(self, recipient: str, message: str) -> bool:
print(f"\n[{self.__class__.__name__}] Sending to: {recipient}")
if not self.validate_recipient(recipient):
print(f" ❌ Invalid recipient format.")
return False
result = self.send(recipient, message)
if result:
print(f" ✅ Sent: '{message[:40]}...'")
return result
class EmailService(NotificationService):
def validate_recipient(self, email: str) -> bool:
return "@" in email and "." in email.split("@")[-1]
def send(self, email: str, message: str) -> bool:
print(f" 📧 Email → {email}")
return True
class SMSService(NotificationService):
def validate_recipient(self, phone: str) -> bool:
return phone.startswith("+") and len(phone) >= 10
def send(self, phone: str, message: str) -> bool:
print(f" 📱 SMS → {phone} | {len(message)} chars")
return True
class SlackService(NotificationService):
def validate_recipient(self, channel: str) -> bool:
return channel.startswith("#")
def send(self, channel: str, message: str) -> bool:
print(f" 💬 Slack → {channel}")
return True
# Polymorphic usage
services = [
EmailService(),
SMSService(),
SlackService(),
]
alerts = [
("arjun@wohotech.com", "Your order #1234 has been shipped!"),
("+919876543210", "OTP: 482910. Valid for 5 minutes."),
("#alerts", "🚨 Server CPU > 90% on prod-server-01"),
("bad-email", "This should fail validation"),
("bad-phone", "This should also fail"),
]
for (service, (recipient, msg)) in zip(services * 2, alerts):
service.notify(recipient, msg)
🔌 Abstract Properties
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def fuel_type(self) -> str: ... # abstract property
@property
@abstractmethod
def max_speed(self) -> int: ...
@abstractmethod
def start_engine(self) -> str: ...
def info(self):
return (f"{self.__class__.__name__}: "
f"Fuel={self.fuel_type}, MaxSpeed={self.max_speed} km/h")
class PetrolCar(Vehicle):
@property
def fuel_type(self) -> str: return "Petrol"
@property
def max_speed(self) -> int: return 200
def start_engine(self) -> str: return "Vroom! 🔥"
class ElectricCar(Vehicle):
@property
def fuel_type(self) -> str: return "Electric"
@property
def max_speed(self) -> int: return 250
def start_engine(self) -> str: return "Whirr... ⚡"
vehicles = [PetrolCar(), ElectricCar()]
for v in vehicles:
print(v.info())
print(f" Engine: {v.start_engine()}")
Output:
| PetrolCar | Fuel=Petrol, MaxSpeed=200 km/h |
| Engine | Vroom! 🔥 |
| ElectricCar | Fuel=Electric, MaxSpeed=250 km/h |
| Engine | Whirr... ⚡ |
🇮🇳 Hindi Explanation (हिंदी में समझें)
Abstraction kya hai? Abstraction means hiding complexity and showing only the essentials. Just as you drive a car without knowing engine mechanics — an abstract class does the same: it tells you "what" methods must exist, child classes decide "how".
How to use ABC and @abstractmethod? Import from abc import ABC, abstractmethod. Make your base class inherit from ABC. Decorate the methods that MUST be implemented in every subclass with @abstractmethod.
Why can't we directly instantiate an abstract class? Because an abstract class is incomplete — some methods only have a "name" but no implementation. If you instantiate it directly, those methods won't work. Python therefore throws TypeError to prevent incomplete objects.
What is the difference between an abstract class and an interface? Python doesn't have a formal "interface" keyword (unlike Java). ABC serves as Python's interface. Abstract classes can also have concrete methods (partial implementation), whereas a pure interface only has method signatures.
❓ Interview Questions
Q1: What is abstraction? How is it different from encapsulation?
Answer:
- Abstraction: Hiding *complexity* — showing only what's necessary (what an object does, not how). Implemented via abstract classes.
- Encapsulation: Hiding *data* — bundling data and methods, restricting direct access. Implemented via private/protected attributes.
"Abstraction is about design; encapsulation is about implementation."
Q2: What is an abstract class? Can you instantiate it?
Answer: An abstract class (inheriting ABC) contains at least one @abstractmethod. It cannot be instantiated directly — doing so raises TypeError. It serves as a contract that all concrete subclasses must fulfill.
Q3: What is the difference between @abstractmethod and raise NotImplementedError?
Answer:
@abstractmethod: Enforced at class creation time — Python raises TypeError if a subclass doesn't implement it.raise NotImplementedError: Only raises if the method is actually called at runtime. No compile-time check.
@abstractmethod is strictly safer and preferred.
Q4: Can an abstract class have concrete methods?
Answer: Yes! An abstract class can have both abstract methods (must be overridden) and concrete methods (provide default implementations). This is the Template Method pattern — the abstract class defines the algorithm skeleton, subclasses fill in the blanks.
Q5: How do you create an abstract property?
Answer:
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float: ...
Both @property and @abstractmethod decorators are needed, in that order.
Q6: What is the difference between an abstract class and a protocol (typing.Protocol)?
Answer:
ABC requires explicit inheritance: class Dog(Animal):typing.Protocol supports structural subtyping (duck typing): any class with the required methods qualifies, no explicit inheritance needed — useful for type hints and mypy.
Q7: Can a subclass of an abstract class still be abstract?
Answer: Yes. If a subclass doesn't implement all abstract methods of its parent, it is itself abstract and also cannot be instantiated. You can build multi-level abstract hierarchies this way.
🧪 Practice Exercises
- Plugin System: Create an abstract
Plugin class with name, version (abstract properties), and execute(data) (abstract method). Implement LogPlugin, FilterPlugin, TransformPlugin. - Report Generator: Abstract
ReportGenerator with fetch_data(), format_data(), render(). Implement PDFReport, HTMLReport, CSVReport. - Authentication: Abstract
Authenticator with authenticate(username, password). Implement PasswordAuth, OTPAuth, BiometricAuth. - Serializer: Abstract
Serializer with serialize(obj) and deserialize(data). Implement JSONSerializer, XMLSerializer, BinarySerializer. - Game Character: Abstract
GameCharacter with abstract properties health, attack_power, speed and abstract methods attack(), defend(), move().
💡 Next Topic → Method Overriding: Deep dive into overriding in depth — super(), cooperative inheritance, and best practices.