Herencia en Python
Hasta ahora, cada clase que escribiste fue independiente de las demás: si dos clases se parecen, tuviste que repetir el mismo __init__ y los mismos métodos en las dos. La herencia permite definir una clase "base" con lo que varias clases tienen en común, y que otras clases "hereden" todo eso sin volver a escribirlo.
Bienvenido
En Encapsulación viste cómo proteger el estado interno de una clase. Ahora, con _, __, @property y todo lo demás ya dominado, es momento de resolver un problema distinto: qué hacer cuando dos o más clases se parecen mucho entre sí, y no querés escribir el mismo código dos veces.
Al finalizar esta guía vas a saber declarar una clase que hereda de otra (class Hija(Padre):), usar super().__init__() para reutilizar el constructor de la clase base, sobrescribir un método para que una subclase se comporte distinto, extender un método heredado con super().metodo() en vez de reemplazarlo del todo, y usar isinstance()/issubclass() para preguntar por la relación entre clases.
Todos los bloques con el encabezado Python son de solo lectura (para que los escribas a mano en el editor de abajo). El panel editor-panel que sigue a cada uno es donde escribes y ejecutas tu propio código —usa Pyodide, un Python real compilado a WebAssembly. La primera ejecución tarda unos segundos porque descarga el intérprete.
Además del código, esta guía tiene 3 preguntas conceptuales cortas para chequear que entendiste bien el porqué de cada herramienta, no solo su sintaxis.
📖 Aprende
Por qué repetir código entre clases parecidas es un problema, y cómo class Hija(Padre): lo resuelve.
💻 Practica
Ejemplos editables y ejecutables en cada sección, construyendo la misma jerarquía de vehículos paso a paso.
🎯 Domina
super(), sobrescritura de métodos, y isinstance()/issubclass().
El problema: código repetido
Imagina dos clases parecidas: Perro y Gato. Ambas guardan un nombre y una edad, y ambas tienen un método para describirse. Sin herencia, ese código se escribe dos veces —casi idéntico.
class Perro:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def describir(self):
print(self.nombre + " tiene " + str(self.edad) + " años")
def hacer_sonido(self):
print("Guau")
class Gato:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def describir(self):
print(self.nombre + " tiene " + str(self.edad) + " años")
def hacer_sonido(self):
print("Miau")
perro = Perro("Rex", 3)
gato = Gato("Michi", 2)
perro.describir()
gato.describir()
Salida:
Rex tiene 3 años Michi tiene 2 años
Fíjate que __init__ y describir() son exactamente iguales en las dos clases —solo hacer_sonido() cambia de verdad. Si mañana agregás un tercer atributo (por ejemplo, raza) a todos los animales, hay que tocar cada clase por separado, y es fácil olvidarse de alguna o escribirla distinto por error. El resto de esta guía muestra cómo evitar esa duplicación con herencia.
Sintaxis básica de herencia
Una clase base (o "padre") reúne lo que varias clases tienen en común. Una clase que hereda de ella (o "hija", o "subclase") se declara con class Hija(Padre):, y automáticamente obtiene todos sus atributos y métodos, sin escribir nada más.
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def describir(self):
print(self.nombre + " tiene " + str(self.edad) + " años")
class Perro(Animal):
pass
perro = Perro("Rex", 3)
perro.describir()
Salida:
Rex tiene 3 años
class Perro(Animal): pass no define ningún atributo ni método propio —pass es solo un relleno sintáctico porque Python exige al menos una línea dentro del cuerpo de la clase. Aun así, Perro ya tiene __init__ y describir(), heredados enteros de Animal. Eso es lo que hace la herencia: nada que escribir, todo heredado.
super().__init__(): reutilizar el constructor
Una subclase casi siempre necesita agregar algo propio —no solo heredar todo tal cual. super() da acceso a la clase base desde adentro de la subclase, y super().__init__(...) llama al constructor del padre para no repetir su lógica.
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def describir(self):
print(self.nombre + " tiene " + str(self.edad) + " años")
class Perro(Animal):
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad)
self.raza = raza
perro = Perro("Rex", 3, "Labrador")
perro.describir()
print(perro.raza)
Salida:
Rex tiene 3 años Labrador
super().__init__(nombre, edad) ejecuta el __init__ de Animal, que es quien realmente asigna self.nombre y self.edad. Perro.__init__ no repite esas dos líneas: solo agrega lo que le falta, self.raza. Si Animal cambiara mañana (por ejemplo, agregando validación al nombre), Perro heredaría ese cambio automáticamente, sin tocar nada acá.
En la sección anterior, Perro(Animal): pass ni siquiera necesitó super() porque no definía su propio __init__ —Python usa directamente el de Animal. super() hace falta recién cuando la subclase sí define su propio __init__ y quiere reutilizar el del padre en vez de repetirlo entero.
Sobrescritura de métodos
Una subclase puede redefinir un método heredado con el mismo nombre para que se comporte distinto. A esto se lo llama sobrescribir (override): cuando se llama ese método sobre un objeto de la subclase, corre la versión nueva, no la del padre.
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def hacer_sonido(self):
print("Este animal no tiene un sonido definido")
class Perro(Animal):
def hacer_sonido(self):
print("Guau")
class Gato(Animal):
def hacer_sonido(self):
print("Miau")
perro = Perro("Rex", 3)
gato = Gato("Michi", 2)
perro.hacer_sonido()
gato.hacer_sonido()
Salida:
Guau Miau
perro.hacer_sonido() y gato.hacer_sonido() se llaman exactamente igual, pero cada uno ejecuta la versión definida en su propia clase, no la de Animal. Python busca el método primero en la clase del objeto, y solo sube a la clase base si no lo encuentra ahí. Esta idea —el mismo llamado comportándose distinto según el objeto— es la base de lo que en una guía futura se va a llamar polimorfismo.
Si Perro hereda de Animal y ambas clases definen un método hacer_sonido(), ¿qué versión se ejecuta al llamar perro.hacer_sonido() sobre un objeto Perro?
Extender un método con super()
Sobrescribir no siempre significa reemplazar del todo. Con super().metodo() dentro de la versión nueva, una subclase puede ejecutar primero la lógica del padre y después agregar la suya propia —extender en vez de reemplazar.
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def describir(self):
print(self.nombre + " tiene " + str(self.edad) + " años")
class Perro(Animal):
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad)
self.raza = raza
def describir(self):
super().describir()
print("Es de raza " + self.raza)
perro = Perro("Rex", 3, "Labrador")
perro.describir()
Salida:
Rex tiene 3 años Es de raza Labrador
Dentro de Perro.describir(), super().describir() llama a la versión de Animal, no se llama a sí misma. Si en cambio hubieras escrito self.describir() ahí adentro, sí sería recursión infinita, porque self es un Perro y volvería a entrar a la misma versión sobrescrita una y otra vez.
Dentro de Perro.describir(), ¿qué hace exactamente la línea super().describir()?
isinstance() e issubclass()
Con herencia de por medio, un objeto puede pertenecer a más de una clase a la vez —a la suya propia y a todas sus clases base. isinstance(objeto, Clase) pregunta por esa relación, y issubclass(Subclase, Clase) hace la misma pregunta pero entre dos clases.
class Animal:
pass
class Perro(Animal):
pass
perro = Perro()
print(isinstance(perro, Perro))
print(isinstance(perro, Animal))
print(isinstance(perro, str))
print(issubclass(Perro, Animal))
print(issubclass(Animal, Perro))
Salida:
True True False True False
isinstance(perro, Animal) da True aunque perro fue creado como Perro, no como Animal directamente —porque Perro hereda de Animal, todo objeto Perro "también es" un Animal. En cambio, type(perro) == Animal daría False, porque type() compara la clase exacta, sin mirar la herencia. Por eso isinstance() es casi siempre la forma correcta de preguntar "¿esto es de este tipo?" en Python.
Con class Perro(Animal): pass y perro = Perro(), ¿por qué isinstance(perro, Animal) da True si perro nunca fue creado como Animal("...") directamente?
Errores comunes
❌ Olvidar super().__init__()
Si la subclase define su propio __init__ sin llamar a super().__init__(...), los atributos que el padre debía asignar (como nombre o edad) simplemente no existen —cualquier método heredado que los use falla con AttributeError.
❌ Usar self.metodo() en vez de super().metodo()
Dentro de una versión sobrescrita, llamar self.metodo() en vez de super().metodo() vuelve a ejecutar la misma versión sobrescrita —una recursión infinita que termina en RecursionError, no una llamada a la del padre.
❌ Pensar que Python soporta sobrecarga de métodos
A diferencia de Java o C#, Python no permite dos métodos con el mismo nombre y distinta cantidad de parámetros dentro de la misma clase: la segunda definición simplemente reemplaza a la primera. No hay "sobrecarga" (overload), solo sobrescritura (override) entre clase base y subclase.
❌ Confundir type(x) == Clase con isinstance(x, Clase)
type(perro) == Animal exige que la clase sea exactamente Animal, y da False para un Perro. isinstance(perro, Animal) considera la herencia y da True. Casi siempre isinstance() es la forma correcta de preguntar por el tipo de un objeto.
Ejercicios Propuestos
Pon en práctica lo aprendido resolviendo los siguientes ejercicios directamente en el bloque editable.
- Escribe una clase
Personacon__init__(self, nombre, edad)y un métodopresentarse(self)que imprima"Hola, soy " + self.nombre. Escribe una claseEstudiante(Persona)que agreguecarreraen su propio__init__(usandosuper().__init__()para nombre y edad), y crea un objeto para probarla. - Escribe una clase
Figuracon un métodoarea(self)que imprima"Área no definida". EscribeRectangulo(Figura)yCirculo(Figura), cada una con su propio__init__(con los datos que necesite) y su propioarea(self)sobrescrito, que calcule e imprima el área real. - Escribe una clase
Empleadocon__init__(self, nombre, salario_base)y un métodocalcular_salario(self)que devuelvaself.salario_base. EscribeGerente(Empleado)que agreguebonoen su__init__(consuper().__init__()), y sobrescribacalcular_salario(self)para devolversuper().calcular_salario() + self.bono. - Escribe una clase
CuentaBancariacon__init__(self, saldo)y un métodoaplicar_interes(self)que sume un 1% al saldo. EscribeCuentaAhorro(CuentaBancaria)que sobrescribaaplicar_interes(self)llamando asuper().aplicar_interes()y sumando además un 2% extra —dos interés distintos combinados consuper(). - Escribe
isinstance()/issubclass()de práctica: crea objetos deEstudiante(ejercicio 1) y dePersonadirecta, e imprime seis resultados: si cada objeto es instancia dePersona, deEstudiante, y siEstudiantees subclase dePersona.
Reto Final
Completa una pequeña jerarquía de empleados que combine todo lo visto: herencia, super(), sobrescritura de métodos e isinstance().
Empleado:__init__(self, nombre, salario_base)guarda ambos datos. Métodocalcular_salario(self)devuelveself.salario_base.Vendedor(Empleado):__init__(self, nombre, salario_base, comision)llamasuper().__init__(nombre, salario_base)y guardaself.comision. Sobrescribecalcular_salario(self)para devolversuper().calcular_salario() + self.comision.Gerente(Empleado):__init__(self, nombre, salario_base, bono)llamasuper().__init__(nombre, salario_base)y guardaself.bono. Sobrescribecalcular_salario(self)para devolversuper().calcular_salario() + self.bono.- Una función
mostrar_nomina(lista_empleados)que recorra la lista con unfore imprima, para cada uno,nombre + ": " + str(calcular_salario())—el mismo llamado funcionando para los tres tipos de objeto, sin preguntar de qué clase es cada uno.
Pruébala con este código:
empleados = [
Empleado("Ana", 3000),
Vendedor("Beto", 2500, 800),
Gerente("Carla", 4000, 1500)
]
mostrar_nomina(empleados)
print(isinstance(empleados[1], Empleado))
print(isinstance(empleados[0], Vendedor))
Ana: 3000 Beto: 3300 Carla: 5500 True False
Fíjate que mostrar_nomina() llama calcular_salario() sin preguntar en ningún momento de qué clase es cada empleado —cada objeto sabe resolver ese llamado a su manera.
Cheat Sheet
| Uso | Ejemplo |
|---|---|
| Declarar una subclase | class Hija(Padre): ... |
| Heredar todo sin cambios | class Hija(Padre): pass |
| Reutilizar el constructor del padre | super().__init__(...) |
| Sobrescribir un método | Redefinirlo con el mismo nombre en la subclase |
| Extender un método heredado | super().metodo() dentro de la versión nueva |
| ¿Es esta instancia de esta clase (o de una base)? | isinstance(objeto, Clase) |
| ¿Esta clase hereda de esta otra? | issubclass(Subclase, Clase) |
| Clase exacta de un objeto (sin herencia) | type(objeto) |
¿Sabías que...?
🐍 Toda clase en Python hereda de object
Aunque nunca lo escribas, class Animal: es en realidad class Animal(object): por debajo. Métodos como __str__ o __eq__ que quizás uses más adelante ya existen en object —cuando los definís en tu propia clase, en el fondo también los estás sobrescribiendo.
🧬 Python sí permite herencia múltiple
A diferencia de Java o C#, en Python una clase puede heredar de más de una a la vez: class Hija(Padre1, Padre2):. Cuando dos padres definen el mismo método, Python decide cuál usar con un orden llamado MRO (Method Resolution Order) —un tema avanzado que no hace falta dominar todavía, pero que existe.
🧩 El name mangling de Encapsulación pensaba en esto
En Encapsulación viste que self.__atributo se renombra por dentro a _Clase__atributo. Ahora que ya sabés herencia, tiene más sentido por qué: evita que un __atributo de una clase base choque por accidente con otro del mismo nombre definido en una subclase que la hereda.