Cuando escribes class Perro(Animal):, le estás diciendo a Python algo muy concreto: Perro es un tipo de Animal. No se parece a uno, no lo contiene — literalmente lo es. Esa distinción importa mucho más de lo que parece al principio.
La herencia es el mecanismo por el que una clase (la subclase o clase hija) recibe automáticamente todos los atributos y métodos de otra clase (la superclase o clase padre). No hay copia de código: Python simplemente sube por la cadena de clases buscando el atributo o método que le pides, empezando por la subclase y ascendiendo hasta encontrarlo. Esa cadena se llama MRO (Method Resolution Order) y para herencia simple es completamente lineal.
¿Por qué funciona así? Porque Python implementa clases como objetos con un atributo __dict__ y un puntero al padre (__bases__). Cuando haces perro.hablar(), Python busca hablar primero en Perro.__dict__, luego en Animal.__dict__, y así hacia arriba. Esto hace que sobrescribir un método sea tan simple como definirlo de nuevo en la subclase — Python lo encontrará ahí antes que en el padre.
Usas herencia cuando la relación entre clases es genuinamente “es-un”: un Perro es un Animal, un AdminUser es un User. Esa relación no es solo filosófica — tiene consecuencias directas en el código. isinstance(perro, Animal) devolverá True, lo que significa que cualquier función que espere un Animal puede recibir un Perro sin quejarse. Si la relación es “tiene-un” o “usa-un” (un Coche tiene un Motor, un Informe usa un FormateadorTexto), lo correcto es composición, no herencia. Herencia por conveniencia — para reutilizar tres métodos sin que exista una relación semántica real — es una deuda de diseño que cobra intereses.
Lo que se rompe cuando lo haces mal: la subclase hereda todo, incluyendo lo que no tiene sentido para ella. Si fuerzas herencia donde no hay relación “es-un”, terminas con métodos que no aplican, o peor, que aplican pero hacen algo inesperado porque los sobrescribiste a medias.
# Herencia simple en Python
class Animal:
def __init__(self, nombre: str, edad: int) -> None:
self.nombre = nombre
self.edad = edad
def describir(self) -> str:
return f"{self.nombre}, {self.edad} años"
def hablar(self) -> str:
# Método genérico; las subclases deberían especializarlo
return "..."
class Perro(Animal):
def __init__(self, nombre: str, edad: int, raza: str) -> None:
# super() delega la inicialización al padre antes de añadir lo propio
super().__init__(nombre, edad)
self.raza = raza
def hablar(self) -> str:
# Sustitución completa: no necesitamos nada del método padre
return "¡Guau!"
def describir(self) -> str:
# Extensión: reutilizamos el resultado del padre y añadimos información
base = super().describir()
return f"{base}, raza {self.raza}"
class Gato(Animal):
def hablar(self) -> str:
return "¡Miau!"
# describir() no se sobrescribe → hereda la del padre tal cual
def presentar(animal: Animal) -> None:
# Esta función no sabe ni le importa si recibe un Perro, Gato u otro Animal
print(f"{animal.describir()} dice: {animal.hablar()}")
perro = Perro("Rex", 3, "Labrador")
gato = Gato("Misi", 5)
presentar(perro)
presentar(gato)
print(isinstance(perro, Animal)) # True — Perro ES un Animal
print(isinstance(perro, Perro)) # True — y también ES un Perro
print(issubclass(Perro, Animal)) # True — la clase Perro extiende Animal
print(issubclass(Animal, Perro)) # False — la relación no es bidireccional
Lo que hace cada decisión y por qué importa
super().__init__ en Perro.__init__ no es opcional. Si lo omites, self.nombre y self.edad nunca se crean — Animal.__init__ es quien los define. La regla práctica: cuando la subclase extiende el constructor (añade parámetros propios), llama a super().__init__ primero con los parámetros del padre, luego inicializa lo tuyo.
hablar en Perro es una sustitución completa: el “¡Guau!” no tiene ninguna relación con el "..." del padre, así que no hay razón para llamar a super().hablar(). Llamarlo sería ruido. Cuando sobrescribes para reemplazar la lógica por completo, super() no aporta nada.
describir en Perro es lo contrario: una especialización que extiende. Llamamos a super().describir() para reutilizar el texto base y añadimos la raza encima. Esto es el patrón más común y más sano de herencia — no duplicas código, solo lo enriqueces.
Gato no sobrescribe describir. No tiene que hacerlo. La herencia hace exactamente lo que esperamos: gato.describir() sube hasta Animal y ejecuta el método de allí. Heredar sin sobrescribir es perfectamente válido.
presentar(animal: Animal) muestra el beneficio real del diseño. Esa función acepta cualquier subclase de Animal porque todas ellas son animales — isinstance lo confirma y Python lo garantiza estructuralmente. Si añades mañana class Loro(Animal), presentar funciona sin ningún cambio.
issubclass(Animal, Perro) devuelve False y eso vale la pena leerlo despacio: la herencia tiene dirección. Todo Perro es un Animal, pero no todo Animal es un Perro. Si tu diseño requiere que la relación funcione en ambos sentidos, no estás modelando herencia — estás modelando algo distinto.
Errores que debes conocer
Error: olvidar super().__init__() en la subclase cuando el padre inicializa atributos, dejando la instancia incompleta.
# ❌ Wrong
class Perro(Animal):
def __init__(self, nombre: str, edad: int, raza: str) -> None:
self.raza = raza # nombre y edad nunca se crean
# ✅ Right
class Perro(Animal):
def __init__(self, nombre: str, edad: int, raza: str) -> None:
super().__init__(nombre, edad) # el padre inicializa lo suyo
self.raza = raza
Sin la llamada a super().__init__, cualquier acceso a self.nombre lanzará AttributeError porque ese atributo solo lo crea Animal.__init__.
Error: usar herencia cuando la relación es “tiene-un”, porque así la subclase hereda métodos que no tienen sentido para ella.
# ❌ Wrong — Motor no ES un Coche, lo contiene
class Coche(Motor):
pass
# ✅ Right — composición: Coche TIENE un Motor
class Coche:
def __init__(self, motor: Motor) -> None:
self.motor = motor
Con composición, Coche solo expone lo que decide exponer y no arrastra toda la interfaz de Motor; el acoplamiento queda bajo control.
Error: llamar a super() en una sobrescritura de sustitución completa porque “parece lo correcto”, introduciendo comportamiento inesperado del padre.
# ❌ Wrong — el "..." del padre se ejecuta y luego lo ignoramos
class Perro(Animal):
def hablar(self) -> str:
super().hablar() # ejecuta código del padre sin usar el resultado
return "¡Guau!"
# ✅ Right — si reemplazas completamente, no hay nada que delegar
class Perro(Animal):
def hablar(self) -> str:
return "¡Guau!"
super() tiene sentido cuando quieres el resultado o los efectos secundarios del padre; si los descartas, solo añades confusión y potencial para bugs cuando el padre cambie.
N° 96