Herencia en Python: super(), override y clases abstractas

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 23 min de lectura

La herencia es el mecanismo que permite que una clase nueva aproveche todo el trabajo ya hecho en otra clase, añadiendo o cambiando solo lo que necesita. Sin herencia, una empresa con empleados, directores y técnicos tendría tres clases con los mismos campos nombre, email y calcular_sueldo() copiados y pegados. Con herencia, defines eso una vez y el resto hereda.

Python implementa la herencia de forma pragmática: sin modificadores de visibilidad obligatorios, con herencia múltiple real y con el mecanismo super() que hace que todo funcione correctamente aunque la jerarquía sea compleja.

🏗️ Sintaxis básica: la clase hija en Python

Crear una clase que herede de otra es tan simple como poner el nombre del padre entre paréntesis. La clase hija recibe automáticamente todos los atributos y métodos del padre.

class Animal:
    """Clase base para todos los animales."""

    def __init__(self, nombre, especie):
        self.nombre  = nombre
        self.especie = especie
        self.vivo    = True

    def respirar(self):
        return f"{self.nombre} respira."

    def info(self):
        return f"{self.nombre} ({self.especie})"

    def __str__(self):
        return self.info()


# Herencia: Perro es-un Animal
class Perro(Animal):
    pass   # hereda TODO — no añade nada aún


rex = Perro("Rex", "Canis lupus")

# Métodos del padre funcionan en la hija:
print(rex.info())      # "Rex (Canis lupus)"
print(rex.respirar())  # "Rex respira."
print(rex.vivo)        # True
print(type(rex))       # <class '__main__.Perro'>

Con pass, Perro es simplemente un Animal sin personalizaciones. El valor de la herencia aparece cuando empezamos a añadir comportamiento específico.

🔑 super(): delegar al padre sin hardcodear su nombre

super() devuelve un proxy al padre (o al siguiente en el MRO). Su uso más común es en __init__: la clase hija inicializa sus propios atributos y delega el resto al padre.

class Perro(Animal):
    def __init__(self, nombre, raza):
        # Llama al __init__ del padre con los argumentos que necesita
        super().__init__(nombre, especie="Canis lupus familiaris")
        # Después define los atributos propios de Perro
        self.raza = raza

    def ladrar(self):
        return f"{self.nombre}: ¡Guau!"


class Gato(Animal):
    def __init__(self, nombre, indoor=True):
        super().__init__(nombre, especie="Felis catus")
        self.indoor = indoor

    def maullar(self):
        return f"{self.nombre}: ¡Miau!"


rex  = Perro("Rex", "Labrador")
luna = Gato("Luna", indoor=True)

print(rex.info())       # "Rex (Canis lupus familiaris)"  ← método del padre
print(rex.raza)         # "Labrador"                     ← atributo propio
print(rex.ladrar())     # "Rex: ¡Guau!"
print(luna.maullar())   # "Luna: ¡Miau!"
print(luna.vivo)        # True                           ← heredado de Animal

¿Por qué super() y no Animal.__init__(self, ...)? Porque la segunda opción hardcodea el nombre del padre. Si cambias la jerarquía o usas herencia múltiple, se rompe. super() siempre hace lo correcto siguiendo el MRO.

Medallón de Augusto, Emperador de Roma
Festina lente
«Apresúrate despacio»
Augusto, Emperador de Roma · 63 a.C. – 14 d.C.
Cuando tengas prisa por hacer funcionar tu jerarquía de clases, recuerda que un diseño apresurado de herencia es de los más difíciles de refactorizar después. Dedica cinco minutos a dibujar el árbol antes de escribir la primera línea.
Diagrama del árbol de herencia en Python: object → Animal → Perro/Gato/Ave; y a la derecha herencia múltiple con MRO C(A,B) y super() siguiendo el orden
Árbol de herencia: la flecha amarilla indica «hereda de». A la izquierda, herencia simple desde object hasta las clases hija. A la derecha, herencia múltiple con el MRO que Python calcula automáticamente — super() sigue ese orden de izquierda a derecha. Infografía: Ciberaula.

🔄 Sobrescribir métodos: especialización

Una clase hija puede sobrescribir cualquier método del padre simplemente definiéndolo con el mismo nombre. Python usará la versión de la hija. También puede extenderlo: llamar al padre con super() y añadir comportamiento extra.

class Animal:
    def __init__(self, nombre):
        self.nombre = nombre

    def hablar(self):
        return f"{self.nombre} hace un sonido."

    def presentar(self):
        return f"Soy {self.nombre}, un {type(self).__name__}."


class Perro(Animal):
    # SOBRESCRIBE: reemplaza completamente el método del padre
    def hablar(self):
        return f"{self.nombre}: ¡Guau!"


class Gato(Animal):
    def hablar(self):
        return f"{self.nombre}: ¡Miau!"


class PerroEntrenado(Perro):
    # EXTIENDE: llama al padre y añade más
    def hablar(self):
        ladrido_base = super().hablar()         # llama a Perro.hablar()
        return ladrido_base + " (y se sienta)"


# Polimorfismo: la misma interfaz, comportamientos distintos
animales = [Perro("Rex"), Gato("Luna"), PerroEntrenado("Max")]
for a in animales:
    print(a.hablar())

# "Rex: ¡Guau!"
# "Luna: ¡Miau!"
# "Max: ¡Guau! (y se sienta)"
Retrato familiar sepia de principios del siglo XX, padre y madre con hijos en estudio fotográfico
En una familia, los hijos heredan rasgos físicos de los padres pero desarrollan su propia personalidad. Las clases hija en Python hacen exactamente lo mismo: heredan atributos y métodos, y sobrescriben lo que necesitan cambiar. Fuente: Pexels (licencia libre).

🔀 Herencia múltiple y el MRO

Python permite heredar de más de una clase a la vez. Esta es una característica que falta en Java y C#, y que puede ser muy útil pero también confusa si se abusa.

class Volador:
    def volar(self):
        return f"{self.nombre} vuela."

    def aterrizar(self):
        return f"{self.nombre} aterriza."


class Nadador:
    def nadar(self):
        return f"{self.nombre} nada."

    def bucear(self):
        return f"{self.nombre} bucea."


# Pato hereda de Animal, Volador y Nadador
class Pato(Animal, Volador, Nadador):
    def __init__(self, nombre):
        super().__init__(nombre)    # super() sigue el MRO

    def hablar(self):
        return f"{self.nombre}: ¡Cuac!"


donald = Pato("Donald")
print(donald.hablar())    # "Donald: ¡Cuac!"
print(donald.volar())     # "Donald vuela."    ← de Volador
print(donald.nadar())     # "Donald nada."     ← de Nadador

# Ver el MRO:
print(Pato.__mro__)
# (<class 'Pato'>, <class 'Animal'>, <class 'Volador'>,
#  <class 'Nadador'>, <class 'object'>)

El MRO define el orden en que Python busca métodos. Si Animal y Volador tuvieran un método con el mismo nombre, Python usaría el de Animal porque aparece antes en el MRO. Puedes ver el MRO con Pato.__mro__ o Pato.mro().

🧩 Mixins: herencia para comportamientos transversales

Un mixin es una clase diseñada específicamente para añadir un comportamiento reutilizable mediante herencia múltiple. No se instancia sola — existe para mezclarse con otras clases.

import json
from datetime import datetime


class JSONMixin:
    """Añade serialización JSON a cualquier clase."""
    def to_json(self):
        return json.dumps(vars(self), default=str, indent=2)

    @classmethod
    def from_json(cls, texto):
        datos = json.loads(texto)
        obj = cls.__new__(cls)
        obj.__dict__.update(datos)
        return obj


class LogMixin:
    """Añade registro de acciones con timestamp."""
    def __init__(self):
        self._log = []

    def registrar(self, accion):
        entrada = f"[{datetime.now().strftime('%H:%M:%S')}] {accion}"
        self._log.append(entrada)

    def ver_log(self):
        return "\n".join(self._log)


# Combinar mixins con la clase real
class Empleado(LogMixin, JSONMixin, Animal):
    def __init__(self, nombre, puesto):
        Animal.__init__(self, nombre, especie="Homo sapiens")
        LogMixin.__init__(self)
        self.puesto = puesto

    def trabajar(self, tarea):
        self.registrar(f"Trabajando en: {tarea}")
        return f"{self.nombre} hace: {tarea}"


emp = Empleado("Luis", "Desarrollador")
emp.trabajar("refactorizar módulo")
emp.trabajar("revisar PRs")
print(emp.ver_log())        # log con timestamps
print(emp.to_json())        # serializa el objeto como JSON

🔒 Clases abstractas con ABC: contratos obligatorios

Una clase abstracta define una interfaz que las subclases deben implementar obligatoriamente. Si intentas instanciar una clase abstracta o una subclase que no implementó todos los métodos abstractos, Python lanza un TypeError.

from abc import ABC, abstractmethod
import math


class Forma(ABC):
    """Contrato: toda forma debe poder calcular su área y perímetro."""

    @abstractmethod
    def area(self):
        """Las subclases DEBEN implementar esto."""
        ...

    @abstractmethod
    def perimetro(self):
        ...

    # Método concreto: disponible en todas las subclases
    def describir(self):
        return (f"{type(self).__name__}: "
                f"área={self.area():.2f}, "
                f"perímetro={self.perimetro():.2f}")

    def __lt__(self, otra):
        return self.area() < otra.area()


class Circulo(Forma):
    def __init__(self, radio):
        self.radio = radio

    def area(self):
        return math.pi * self.radio ** 2

    def perimetro(self):
        return 2 * math.pi * self.radio


class Rectangulo(Forma):
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto  = alto

    def area(self):
        return self.ancho * self.alto

    def perimetro(self):
        return 2 * (self.ancho + self.alto)


# Forma()    → TypeError: no se puede instanciar clase abstracta
formas = [Circulo(5), Rectangulo(4, 6)]
for f in formas:
    print(f.describir())

menor = min(formas)
print(f"La menor: {menor.describir()}")
Referencia rápida de herencia en Python: sintaxis, super(), isinstance, issubclass, override y ABCMeta
Referencia rápida — Sintaxis y override; isinstance/issubclass con tuplas, type() estricto, vars/hasattr/getattr/setattr; clases abstractas con ABC y @abstractmethod; mixin con JSONMixin. Ficha: Ciberaula.

🔍 isinstance(), issubclass() e introspección

rex   = Perro("Rex", "Labrador")
luna  = Gato("Luna")

# isinstance: ¿es este objeto una instancia de esa clase (o de sus padres)?
isinstance(rex, Perro)     # True
isinstance(rex, Animal)    # True ← herencia funciona
isinstance(rex, Gato)      # False
isinstance(rex, (Perro, Gato))   # True ← acepta tupla de clases

# issubclass: relación entre clases (no instancias)
issubclass(Perro, Animal)  # True
issubclass(Perro, Perro)   # True (una clase es subclase de sí misma)
issubclass(Animal, Perro)  # False
# ⚠ issubclass recibe clases, no instancias:
# issubclass(rex, Animal)  → TypeError

# type() — comprobación estricta (no herencia):
type(rex) == Perro         # True
type(rex) == Animal        # False — rex es Perro, no Animal directamente

# Introspección de la jerarquía:
Perro.__bases__            # (Animal,)
Perro.__mro__              # (Perro, Animal, object)
Perro.__name__             # "Perro"
rex.__class__              # <class 'Perro'>

# Atributos del objeto:
vars(rex)                  # {"nombre":"Rex", "especie":"...", "vivo":True, "raza":"Labrador"}
hasattr(rex, "ladrar")     # True
hasattr(rex, "maullar")    # False
getattr(rex, "raza", "desconocida")   # "Labrador" (con default)

⚖️ Herencia vs composición: cuándo usar cada una

La herencia tiene un problema: crea un acoplamiento fuerte. Si cambias la clase padre, todos los hijos se ven afectados. La composición — donde una clase contiene instancias de otras en lugar de heredar de ellas — es más flexible.

# ──────────────────────────────────────────────
# CON HERENCIA (cuando tiene sentido: "es-un")
# ──────────────────────────────────────────────
class Vehiculo:
    def __init__(self, marca, modelo):
        self.marca  = marca
        self.modelo = modelo

    def info(self):
        return f"{self.marca} {self.modelo}"


class Coche(Vehiculo):           # Coche ES-UN Vehiculo ✅
    def __init__(self, marca, modelo, puertas):
        super().__init__(marca, modelo)
        self.puertas = puertas


# ──────────────────────────────────────────────
# CON COMPOSICIÓN (cuando tiene sentido: "tiene-un")
# ──────────────────────────────────────────────
class Motor:
    def __init__(self, cilindrada, combustible):
        self.cilindrada  = cilindrada
        self.combustible = combustible

    def arrancar(self):
        return f"Motor {self.cilindrada}cc arranca."


class CocheCompleto(Vehiculo):
    def __init__(self, marca, modelo, cilindrada):
        super().__init__(marca, modelo)
        self.motor  = Motor(cilindrada, "gasolina")   # TIENE-UN motor

    def arrancar(self):
        return self.motor.arrancar()   # delega al motor


c = CocheCompleto("Seat", "León", 1500)
print(c.info())       # "Seat León"
print(c.arrancar())   # "Motor 1500cc arranca."

# Ventaja: puedo cambiar el motor sin tocar CocheCompleto:
c.motor = Motor(2000, "diesel")
print(c.arrancar())   # "Motor 2000cc arranca."
Estructura esquelética de hormigón de un edificio en construcción con vigas de acero
El esqueleto de un edificio en construcción muestra los puntos de apoyo que todo lo demás debe respetar. Las clases abstractas funcionan igual: definen los métodos obligatorios que cada subclase concreta debe implementar para que el sistema se sostenga. Fuente: Pexels (licencia libre).

🛠️ Programa completo: sistema de empleados

from abc import ABC, abstractmethod
from datetime import date


class Persona(ABC):
    """Clase base abstracta para personas en la empresa."""

    def __init__(self, nombre, email, fecha_alta=None):
        self.nombre      = nombre
        self.email       = email
        self.fecha_alta  = fecha_alta or date.today()
        self.activo      = True

    @abstractmethod
    def calcular_sueldo(self):
        ...

    @abstractmethod
    def rol(self):
        ...

    def antiguedad_anos(self):
        delta = date.today() - self.fecha_alta
        return delta.days // 365

    def resumen(self):
        return (f"[{self.rol()}] {self.nombre} "
                f"— {self.antiguedad_anos()} año(s) "
                f"— Sueldo: {self.calcular_sueldo():.2f} €")

    def __str__(self):
        return self.resumen()

    def __lt__(self, otro):
        return self.calcular_sueldo() < otro.calcular_sueldo()


class EmpleadoFijo(Persona):
    def __init__(self, nombre, email, sueldo_base, fecha_alta=None):
        super().__init__(nombre, email, fecha_alta)
        self.sueldo_base = sueldo_base

    def rol(self):
        return "Fijo"

    def calcular_sueldo(self):
        bonus = self.sueldo_base * 0.02 * self.antiguedad_anos()
        return self.sueldo_base + bonus


class EmpleadoPorHoras(Persona):
    def __init__(self, nombre, email, tarifa_hora, horas_mes=160):
        super().__init__(nombre, email)
        self.tarifa_hora = tarifa_hora
        self.horas_mes   = horas_mes

    def rol(self):
        return "Por horas"

    def calcular_sueldo(self):
        return self.tarifa_hora * self.horas_mes


class Director(EmpleadoFijo):
    def __init__(self, nombre, email, sueldo_base, fecha_alta=None):
        super().__init__(nombre, email, sueldo_base, fecha_alta)
        self._equipo = []

    def rol(self):
        return "Director"

    def calcular_sueldo(self):
        sueldo_base = super().calcular_sueldo()
        return sueldo_base * 1.20

    def incorporar(self, empleado):
        self._equipo.append(empleado)

    def ver_equipo(self):
        if not self._equipo:
            return f"{self.nombre} no tiene equipo asignado."
        return f"Equipo de {self.nombre}:\n" + "\n".join(
            f"  · {e}" for e in sorted(self._equipo, reverse=True)
        )


# ── Uso del sistema ─────────────────────────────
ana  = EmpleadoFijo("Ana García", "ana@empresa.com", 2500, date(2020, 1, 15))
luis = EmpleadoPorHoras("Luis Martín", "luis@empresa.com", 18, 120)
dir1 = Director("María López", "maria@empresa.com", 4000, date(2018, 3, 1))

dir1.incorporar(ana)
dir1.incorporar(luis)

print(ana)
print(luis)
print(dir1)
print()
print(dir1.ver_equipo())

# Ordenar por sueldo:
for e in sorted([ana, luis, dir1]):
    print(f"  {e.calcular_sueldo():7.2f} € — {e.nombre}")

🐛 Errores clásicos con herencia

1. No llamar a super().__init__() y perder atributos del padre

# ❌
class Perro(Animal):
    def __init__(self, nombre, raza):
        self.raza = raza   # olvidamos llamar a super()
        # self.nombre → AttributeError más adelante

# ✅
class Perro(Animal):
    def __init__(self, nombre, raza):
        super().__init__(nombre, especie="Canis lupus")
        self.raza = raza

2. Hardcodear el nombre del padre en lugar de usar super()

# ❌ Frágil: si cambias Animal a Mamifero, se rompe
class Perro(Animal):
    def __init__(self, nombre, raza):
        Animal.__init__(self, nombre, "Canis lupus")   # hardcoded

# ✅ Flexible
class Perro(Animal):
    def __init__(self, nombre, raza):
        super().__init__(nombre, "Canis lupus")

3. Confundir type() con isinstance() en comprobaciones polimórficas

# ❌ Demasiado estricto: rompe el polimorfismo
def procesar(animal):
    if type(animal) == Animal:   # Director no pasará aunque herede
        print("es animal")

# ✅ Respeta la herencia
def procesar(animal):
    if isinstance(animal, Animal):
        print("es animal (o subclase)")

4. Heredar solo para reutilizar código sin relación "es-un"

# ❌ Coche no "es-un" Motor
class Coche(Motor):
    pass

# ✅ Coche "tiene-un" motor
class Coche:
    def __init__(self):
        self.motor = Motor(1600, "gasolina")

✅ Resumen y próximos pasos

La herencia permite que las clases hija reutilicen atributos y métodos del padre sin duplicar código. Usa siempre super() — nunca hardcodees el nombre del padre. El MRO define el orden de búsqueda de métodos en herencia múltiple, y super() lo respeta automáticamente. Las clases abstractas con ABC crean contratos que las subclases están obligadas a cumplir. Y si la relación no es conceptualmente «es-un», la composición suele ser una solución más limpia y flexible.

La siguiente lección: polimorfismo en profundidad — duck typing vs herencia para polimorfismo, protocolos y el módulo typing para Type Hints con jerarquías de clases.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Herencia en Python: super(), override y clases abstractas

Las dudas más comunes respondidas de forma clara y directa.

La regla práctica es el test "es-un" vs "tiene-un". Si Perro "es un" Animal, la herencia tiene sentido. Si Coche "tiene un" Motor, la composición es mejor. La herencia crea un acoplamiento fuerte entre clases: cualquier cambio en la clase padre se propaga a todos los hijos. La composición es más flexible porque puedes cambiar los componentes independientemente. En Python moderno se tiende a preferir composición sobre herencia para todo excepto jerarquías conceptualmente claras. El problema de la herencia aparece cuando la jerarquía crece y empiezas a tener clases que heredan cosas que no necesitan solo para reutilizar una parte del código.
MRO (Method Resolution Order) es el orden en que Python busca un método cuando tienes herencia múltiple. Python usa el algoritmo C3 para calcularlo, que garantiza que cada clase aparece antes que sus padres y se respeta el orden de izquierda a derecha. Puedes ver el MRO con MiClase.__mro__ o MiClase.mro(). Por qué importa: si tienes class C(A, B) y tanto A como B definen el mismo método, Python lo resolverá siguiendo el MRO — primero busca en C, luego en A, luego en B, luego en object. Entender esto evita sorpresas cuando dos padres tienen métodos con el mismo nombre.
No es obligatorio, pero sí muy recomendable cuando la clase padre tiene un __init__ con lógica importante. Si omites super().__init__(), los atributos definidos en el padre no se inicializarán. Si la clase padre solo tiene pass o no define __init__, Python lo llama automáticamente. La norma es: siempre llama a super().__init__() con los argumentos que necesite, excepto cuando tengas una razón explícita para no hacerlo (como cuando quieres reemplazar completamente la inicialización).
Sí, y hay dos formas. Con super(): super().metodo(args) — es la forma correcta para la mayoría de casos, sigue el MRO y funciona bien con herencia múltiple. Con nombre directo: Padre.metodo(self, args) — hardcodea el nombre del padre, no sigue el MRO y puede causar problemas en herencia múltiple. Usa siempre super() a menos que necesites llamar específicamente a un padre concreto en una jerarquía compleja, que es un caso raro.
@abstractmethod (con ABC) previene la instanciación de la clase si no se implementa el método — Python lanza TypeError en el momento de crear el objeto. raise NotImplementedError es solo convención: define el método en la clase base y lanza una excepción si se llama sin sobrescribir, pero no impide crear instancias de la clase base. La diferencia práctica: ABC protege antes de que el error ocurra en tiempo de ejecución, mientras que NotImplementedError solo falla cuando realmente se llama al método. Para interfaces formales usa ABC; para señalizar intención de sobrescritura en APIs internas, NotImplementedError puede ser suficiente.
Sí, y esta es una de las ventajas de las clases abstractas sobre las interfaces puras de otros lenguajes. Una clase abstracta puede tener métodos concretos con implementación completa, métodos abstractos (que obligan a la subclase a implementarlos), atributos de clase y propiedades abstractas. El patrón Template Method aprovecha esto: define el esqueleto de un algoritmo en la clase base con métodos concretos, y deja que las subclases implementen los pasos variables mediante métodos abstractos. Es un patrón de diseño muy útil para evitar duplicar lógica común.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Herencia en Python: super(), override y clases abstractas? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!