Cuando diseñas una jerarquía de clases, a veces necesitas decirle al mundo: “esta clase no existe para ser instanciada directamente — existe para definir una forma”. Eso es exactamente lo que hace ABC (Abstract Base Class). No es azúcar sintáctico ni una convención de nombres; es un mecanismo real del intérprete que impide instanciar una clase si no se han implementado todos sus métodos abstractos.
La mecánica interna es sencilla pero elegante. Cuando una clase hereda de ABC (que usa ABCMeta como metaclase), Python registra en __abstractmethods__ un frozenset con los nombres de todos los métodos decorados con @abstractmethod. Antes de crear cualquier instancia, ABCMeta.__call__ comprueba ese conjunto: si no está vacío, lanza TypeError. Una subclase concreta elimina entradas de ese conjunto al implementar cada método. Si una subclase no implementa todos, el frozenset sigue teniendo elementos y esa subclase también es abstracta, aunque no lo declares explícitamente.
Esto importa porque el error aparece en el momento de instanciar, no cuando llamas al método faltante en tiempo de ejecución. Es la diferencia entre enterarte del problema al arrancar y enterarte tres días después en producción cuando se ejecuta ese código path.
¿Cuándo lo usas? Cuando tienes comportamiento compartido real que quieres poner en la clase base, y al mismo tiempo quieres forzar que las subclases implementen ciertos puntos de extensión. Si solo necesitas decir “este objeto responde a este protocolo” sin herencia, Protocol (PEP 544) encaja mejor — no requiere que las clases concretas sepan nada de tu jerarquía. ABC implica herencia; Protocol implica forma.
from abc import ABC, abstractmethod
from typing import Iterator
class DataSource(ABC):
"""Abstracción de cualquier origen de datos iterable."""
# Método abstracto ordinario: toda subclase debe implementarlo.
@abstractmethod
def connect(self) -> None: ...
@abstractmethod
def disconnect(self) -> None: ...
# abstractmethod + property: la subclase debe definir un @property,
# no un método ordinario con ese nombre.
@property
@abstractmethod
def name(self) -> str: ...
# abstractmethod + classmethod: factory que cada subclase personaliza.
@classmethod
@abstractmethod
def from_config(cls, config: dict) -> "DataSource": ...
# Método concreto en la clase base: comportamiento compartido real.
def read_all(self) -> list[dict]:
self.connect()
try:
return list(self._iter_records())
finally:
self.disconnect()
@abstractmethod
def _iter_records(self) -> Iterator[dict]: ...
class CsvSource(DataSource):
def __init__(self, path: str) -> None:
self._path = path
self._file = None
# Implementa la @property abstracta: debe ser también @property.
@property
def name(self) -> str:
return f"csv:{self._path}"
@classmethod
def from_config(cls, config: dict) -> "CsvSource":
return cls(config["path"])
def connect(self) -> None:
self._file = open(self._path, encoding="utf-8")
def disconnect(self) -> None:
if self._file:
self._file.close()
self._file = None
def _iter_records(self) -> Iterator[dict]:
import csv
reader = csv.DictReader(self._file)
yield from reader
# Subclase incompleta — no implementa _iter_records ni from_config.
class IncompleteSource(DataSource):
def connect(self) -> None: ...
def disconnect(self) -> None: ...
@property
def name(self) -> str:
return "incomplete"
# Comprobación en tiempo de definición (no de instanciación):
print(IncompleteSource.__abstractmethods__)
# frozenset({'_iter_records', 'from_config'})
# Esto explota con TypeError aunque IncompleteSource no diga ABC:
# obj = IncompleteSource()
# CsvSource sí es concreta:
print(CsvSource.__abstractmethods__)
# frozenset()
src = CsvSource.from_config({"path": "data.csv"})
print(src.name) # csv:data.csv
Desglosando las decisiones
DataSource hereda de ABC en lugar de object. Eso activa ABCMeta como metaclase y habilita toda la maquinaria. No necesitas declarar metaclass=ABCMeta explícitamente; heredar de ABC lo hace por ti.
Fíjate en el orden de los decoradores en name: primero @property, luego @abstractmethod. El orden importa — si lo inviertes obtienes un resultado inesperado porque abstractmethod necesita envolver el descriptor final, no al revés. En from_config ocurre lo mismo: @classmethod antes de @abstractmethod.
read_all es concreto. Aquí está el valor real de ABC frente a Protocol: puedes poner lógica real en la base. El patrón Template Method vive aquí — la secuencia connect/iterate/disconnect está definida una vez, y las subclases solo rellenan los huecos. Con Protocol eso no es posible porque el chequeo es estructural puro.
IncompleteSource ilustra algo que sorprende a muchos: una clase puede heredar de tu abstracta sin decorarse como abstracta y aún así ser abstracta si le faltan implementaciones. __abstractmethods__ lo refleja: frozenset({'_iter_records', 'from_config'}). El intérprete no te permite hacer IncompleteSource() aunque no hayas escrito ABC en su firma.
Errores que debes conocer
Error: implementar un método abstracto marcado como @property con un método ordinario hace que Python acepte la clase como concreta, pero el acceso al atributo devuelve el método bound en lugar del valor esperado.
# ❌ Wrong
class BrokenSource(DataSource):
def connect(self): ...
def disconnect(self): ...
def from_config(cls, config): ...
def _iter_records(self): yield {}
def name(self) -> str: # método ordinario, no @property
return "broken"
print(BrokenSource.__abstractmethods__) # frozenset() — Python lo acepta
src = BrokenSource()
print(src.name) # <bound method BrokenSource.name ...> — no es un string
# ✅ Right
class FixedSource(DataSource):
# ... otros métodos ...
@property
def name(self) -> str:
return "fixed"
Al implementar un miembro abstracto decorado con @property, la subclase también debe usar @property; de lo contrario satisface el nombre pero no el descriptor, y obtienes el método en lugar del valor.
Error: asumir que @abstractmethod protege contra llamar al método en la base desde código externo. No lo hace — puedes llamarlo con super() desde una subclase, y es perfectamente válido hacerlo para reutilizar lógica parcial.
# ❌ Wrong — pensar que esto falla:
class VerboseSource(CsvSource):
def connect(self) -> None:
print(f"Connecting to {self.name}")
super().connect() # esto funciona y es idiomático
# ✅ Right — aprovecharlo explícitamente cuando la base tiene lógica:
class RetrySource(CsvSource):
def connect(self) -> None:
for attempt in range(3):
try:
super().connect()
return
except OSError:
if attempt == 2:
raise
@abstractmethod solo bloquea la instanciación directa de la clase que lo declara; no convierte el método en “sin cuerpo” — puedes darle implementación y delegarle con super().
N° 102