Programación orientada a objetos en Python: clases, objetos y herencia

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

La programación orientada a objetos no es una complicación innecesaria: es una forma de organizar el código que imita cómo pensamos sobre el mundo. Un usuario tiene nombre, email y contraseña — y puede iniciar sesión, cambiar su perfil o darse de baja. Esos datos y esas acciones van juntos. Una clase los agrupa en un solo lugar.

Python es un lenguaje multiparadigma: puedes programar con funciones, con clases, o mezclar ambas según lo que cada parte del proyecto necesite. Pero entender la POO es indispensable porque toda la biblioteca estándar y los frameworks más usados están construidos sobre clases.

🏗️ Tu primera clase: atributos y métodos

class Persona:
    """Representa a una persona con nombre y edad."""

    def saludar(self):
        return "Hola, soy una persona."

# Crear instancias (objetos)
p1 = Persona()
p2 = Persona()

print(p1.saludar())    # "Hola, soy una persona."
print(p1 is p2)        # False — son objetos distintos
print(type(p1))        # 

🔑 self y __init__: la identidad del objeto

__init__ es el constructor: se ejecuta automáticamente al crear cada instancia. self es la referencia al objeto concreto — permite que cada instancia tenga sus propios datos.

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre    # atributo de instancia
        self.edad   = edad

    def presentarse(self):
        return f"Soy {self.nombre}, tengo {self.edad} años."

    def cumpleanos(self):
        self.edad += 1
        return f"¡Ahora tengo {self.edad}!"

ana  = Persona("Ana",  28)
luis = Persona("Luis", 35)

print(ana.presentarse())    # "Soy Ana, tengo 28 años."
print(luis.presentarse())   # "Soy Luis, tengo 35 años."
ana.cumpleanos()
print(ana.edad)             # 29 — solo afecta a ana
print(luis.edad)            # 35 — luis no cambia
Diagrama de anatomía de una clase Python con atributo de clase, constructor __init__, self, método de instancia y herencia a dos clases hija Perro y Gato
Anatomía de una clase Python: atributo de clase (compartido), constructor __init__ con self, método de instancia. La flecha amarilla indica herencia — Perro y Gato heredan todo de Animal y pueden añadir o sobreescribir. Abajo, las instancias concretas. Infografía: Ciberaula.

📊 Atributos de clase vs atributos de instancia

class Producto:
    iva = 0.21              # atributo de CLASE: compartido por todas las instancias

    def __init__(self, nombre, precio_base):
        self.nombre       = nombre        # atributo de INSTANCIA
        self.precio_base  = precio_base

    def precio_final(self):
        return round(self.precio_base * (1 + Producto.iva), 2)

p1 = Producto("Teclado", 50)
p2 = Producto("Ratón",   25)

print(p1.precio_final())    # 60.5
print(p2.precio_final())    # 30.25

# Cambiar el IVA para TODOS:
Producto.iva = 0.10
print(p1.precio_final())    # 55.0

# Sobrescribir solo para una instancia (crea atributo propio):
p1.iva = 0.04               # solo p1 tiene su propio iva
print(p1.iva)               # 0.04
print(p2.iva)               # 0.10 — sigue usando el de la clase
Mano dibujando planos arquitectónicos sobre papel con detalles técnicos
Un plano arquitectónico es la clase: define la estructura, las habitaciones y las conexiones. Cada edificio construido a partir de ese plano es una instancia — comparten el diseño pero son objetos independientes con su propia dirección. Fuente: Pexels (licencia libre).

🌳 Herencia y super(): reutilizar sin copiar

La herencia permite que una clase hija reutilice todo el código de la clase padre y añada o modifique lo que necesite. super() llama al método del padre sin hardcodear su nombre.

class Animal:
    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})"


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

    def ladrar(self):
        return "¡Guau!"

    def respirar(self):                          # sobrescribe el método padre
        return f"{self.nombre} jadea feliz."


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

    def maullar(self):
        return "¡Miau!"


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

print(rex.info())       # "Rex (Canis lupus)"    ← heredado
print(rex.ladrar())     # "¡Guau!"               ← propio
print(rex.respirar())   # "Rex jadea feliz."     ← sobrescrito
print(luna.maullar())   # "¡Miau!"
print(luna.vivo)        # True                   ← heredado

# Comprobar herencia:
isinstance(rex, Perro)   # True
isinstance(rex, Animal)  # True  ← también es Animal
issubclass(Perro, Animal)# True
Bosque de pinos envuelto en niebla sobre una ladera de montaña
Todos los pinos del bosque comparten la misma naturaleza — raíces, tronco, capacidad de fotosíntesis. Como las clases hija que heredan atributos y métodos del padre. Cada árbol es distinto, pero todos son árboles. Fuente: Pexels (licencia libre).

🎭 Polimorfismo: misma interfaz, comportamientos distintos

class Forma:
    def area(self):
        raise NotImplementedError("Las subclases deben implementar area()")

    def describir(self):
        return f"Soy un(a) {type(self).__name__} con área {self.area():.2f}"


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

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


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

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


class Triangulo(Forma):
    def __init__(self, base, altura):
        self.base   = base
        self.altura = altura

    def area(self):
        return 0.5 * self.base * self.altura


# Polimorfismo: la misma función trabaja con cualquier Forma
formas = [Circulo(5), Rectangulo(4, 6), Triangulo(3, 8)]
for f in formas:
    print(f.describir())    # cada una calcula su propia área

# Área total — no importa qué tipo de forma sea:
total = sum(f.area() for f in formas)
print(f"Área total: {total:.2f}")

🔒 @property: encapsulación con estilo Python

En lugar de métodos get_algo() y set_algo() al estilo Java, Python usa el decorador @property para acceder a atributos con validación como si fueran atributos normales.

class CuentaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self._saldo  = saldo_inicial    # _ indica "uso interno"

    @property
    def saldo(self):                    # getter: cuenta.saldo
        return self._saldo

    @saldo.setter
    def saldo(self, valor):             # setter: cuenta.saldo = 500
        if valor < 0:
            raise ValueError("El saldo no puede ser negativo.")
        self._saldo = valor

    def ingresar(self, cantidad):
        if cantidad <= 0:
            raise ValueError("La cantidad debe ser positiva.")
        self._saldo += cantidad
        return self._saldo

    def retirar(self, cantidad):
        if cantidad > self._saldo:
            raise ValueError("Saldo insuficiente.")
        self._saldo -= cantidad
        return self._saldo

    def __str__(self):
        return f"Cuenta de {self.titular}: {self._saldo:.2f} €"


cuenta = CuentaBancaria("Ana", 1000)
print(cuenta.saldo)        # 1000    ← usa el getter
cuenta.saldo = 1500        # ← usa el setter (valida)
cuenta.ingresar(200)
cuenta.retirar(300)
print(cuenta)              # "Cuenta de Ana: 1400.00 €"

try:
    cuenta.saldo = -100    # ValueError
except ValueError as e:
    print(e)
Referencia rápida de POO en Python: métodos dunder, @property con getter y setter, @classmethod y @staticmethod, herencia múltiple y MRO
Referencia rápida — Métodos especiales (dunder) más usados con ejemplos; @property con getter y setter validado; @classmethod como factory y @staticmethod como función utilitaria; herencia múltiple y MRO; isinstance/issubclass/vars/hasattr. Ficha: Ciberaula.

✨ Métodos especiales: hacer objetos nativos de Python

class Vector:
    """Vector 2D con operaciones aritméticas."""

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

    def __repr__(self):
        return f"Vector(x={self.x}, y={self.y})"

    def __add__(self, otro):
        return Vector(self.x + otro.x, self.y + otro.y)

    def __sub__(self, otro):
        return Vector(self.x - otro.x, self.y - otro.y)

    def __mul__(self, escalar):
        return Vector(self.x * escalar, self.y * escalar)

    def __eq__(self, otro):
        return self.x == otro.x and self.y == otro.y

    def __len__(self):
        import math
        return int(math.sqrt(self.x**2 + self.y**2))

    def __abs__(self):
        import math
        return math.sqrt(self.x**2 + self.y**2)


v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1)           # "Vector(1, 2)"    ← __str__
print(v1 + v2)      # "Vector(4, 6)"   ← __add__
print(v2 - v1)      # "Vector(2, 2)"   ← __sub__
print(v1 * 3)       # "Vector(3, 6)"   ← __mul__
print(v1 == v2)     # False            ← __eq__
print(abs(v2))      # 5.0              ← __abs__

⚙️ @classmethod y @staticmethod

class Fecha:
    def __init__(self, dia, mes, anno):
        self.dia   = dia
        self.mes   = mes
        self.anno  = anno

    def __str__(self):
        return f"{self.dia:02d}/{self.mes:02d}/{self.anno}"

    @classmethod
    def desde_string(cls, cadena):
        """Constructor alternativo desde 'DD/MM/AAAA'."""
        dia, mes, anno = map(int, cadena.split("/"))
        return cls(dia, mes, anno)   # cls = Fecha

    @classmethod
    def hoy(cls):
        """Devuelve la fecha actual."""
        from datetime import date
        d = date.today()
        return cls(d.day, d.month, d.year)

    @staticmethod
    def es_bisiesto(anno):
        """No necesita acceso a la instancia ni a la clase."""
        return anno % 4 == 0 and (anno % 100 != 0 or anno % 400 == 0)


# @classmethod — constructores alternativos
f1 = Fecha(6, 3, 2026)
f2 = Fecha.desde_string("15/08/2025")
f3 = Fecha.hoy()
print(f1)    # "06/03/2026"
print(f2)    # "15/08/2025"

# @staticmethod — utilidad sin estado
Fecha.es_bisiesto(2024)   # True
Fecha.es_bisiesto(2026)   # False

🛠️ Programa completo: sistema de biblioteca

from datetime import date, timedelta

class Libro:
    def __init__(self, titulo, autor, isbn):
        self.titulo    = titulo
        self.autor     = autor
        self.isbn      = isbn
        self._prestado = False

    @property
    def disponible(self):
        return not self._prestado

    def __str__(self):
        estado = "disponible" if self.disponible else "prestado"
        return f'"{self.titulo}" — {self.autor} [{estado}]'

    def __repr__(self):
        return f"Libro(isbn={self.isbn!r})"


class Prestamo:
    DIAS_PRESTAMO = 14

    def __init__(self, libro, socio):
        if not libro.disponible:
            raise ValueError(f"{libro.titulo} ya está prestado.")
        self.libro        = libro
        self.socio        = socio
        self.fecha_inicio = date.today()
        self.fecha_limite = self.fecha_inicio + timedelta(days=self.DIAS_PRESTAMO)
        libro._prestado   = True

    def devolver(self):
        self.libro._prestado = False
        dias_retraso = (date.today() - self.fecha_limite).days
        if dias_retraso > 0:
            return f"Devuelto con {dias_retraso} días de retraso."
        return "Devuelto a tiempo. ¡Gracias!"

    def __str__(self):
        return (f"Préstamo: {self.libro.titulo} → {self.socio} "
                f"(hasta {self.fecha_limite.strftime('%d/%m/%Y')})")


class Biblioteca:
    def __init__(self, nombre):
        self.nombre  = nombre
        self._libros = {}    # isbn → Libro

    def registrar(self, libro):
        self._libros[libro.isbn] = libro
        print(f"✓ Registrado: {libro}")

    def buscar(self, termino):
        termino = termino.lower()
        return [l for l in self._libros.values()
                if termino in l.titulo.lower() or termino in l.autor.lower()]

    def disponibles(self):
        return [l for l in self._libros.values() if l.disponible]

    def __len__(self):
        return len(self._libros)

    def __str__(self):
        return f"Biblioteca {self.nombre} ({len(self)} libros)"


# ── Uso del sistema ─────────────────────────────────
bib = Biblioteca("Ciberaula")

b1 = Libro("Python Crash Course", "Eric Matthes", "978-1593279288")
b2 = Libro("Clean Code", "Robert C. Martin", "978-0132350884")
b3 = Libro("Automate the Boring Stuff", "Al Sweigart", "978-1593275990")

for libro in [b1, b2, b3]:
    bib.registrar(libro)

print(bib)           # "Biblioteca Ciberaula (3 libros)"

p1 = Prestamo(b1, "Ana García")
print(p1)

resultados = bib.buscar("python")
for r in resultados:
    print(r)

print(f"\nDisponibles: {len(bib.disponibles())}/3")
print(p1.devolver())

🐛 Errores clásicos con clases

1. Olvidar self en los métodos

# ❌
class Contador:
    def incrementar():    # falta self
        self.n += 1       # NameError: name 'self' is not defined

# ✅
class Contador:
    def __init__(self):
        self.n = 0
    def incrementar(self):
        self.n += 1

2. Atributo mutable compartido como atributo de clase

# ❌ Todos los objetos comparten la misma lista
class Alumno:
    notas = []    # atributo de clase — ¡peligro!
    def agregar(self, nota):
        self.notas.append(nota)

a1 = Alumno(); a2 = Alumno()
a1.agregar(9)
print(a2.notas)   # [9] — a2 también tiene la nota de a1

# ✅ Lista como atributo de instancia
class Alumno:
    def __init__(self):
        self.notas = []    # cada instancia tiene su propia lista

3. Llamar al padre sin super()

# ❌ Rompe con herencia múltiple y es frágil
class Perro(Animal):
    def __init__(self, nombre, raza):
        Animal.__init__(self, nombre)   # hardcoded

# ✅
class Perro(Animal):
    def __init__(self, nombre, raza):
        super().__init__(nombre)        # flexible y correcto

4. Modificar atributos "privados" directamente desde fuera

# ❌ Salta la validación del setter
cuenta._saldo = -9999    # no usa el setter, rompe la lógica interna

# ✅ Usar siempre la interfaz pública
cuenta.saldo = 500       # usa el setter con validación

✅ Resumen y próximos pasos

Una clase agrupa datos (atributos) y comportamiento (métodos). __init__ inicializa cada instancia. self es la referencia al objeto concreto. La herencia reutiliza código sin copiarlo — usa siempre super(). El polimorfismo permite tratar objetos distintos con la misma interfaz. @property añade validación manteniendo la sintaxis limpia. Los métodos especiales (dunder) hacen que tus objetos funcionen de forma nativa con operadores, len(), str() y más.

La siguiente lección: iteradores y generadores — cómo crear objetos iterables propios, la palabra clave yield y por qué los generadores son más eficientes que las listas para datos grandes.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Programación orientada a objetos en Python: clases, objetos y herencia

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

Las clases tienen sentido cuando modelas entidades con estado propio que cambia a lo largo del tiempo (un usuario, una conexión de BD, un carrito de compra) y cuando tienes múltiples instancias del mismo tipo de cosa que deben comportarse de forma independiente. Las funciones simples son mejores para transformaciones sin estado: tomar datos de entrada y devolver un resultado. No todo tiene que ser una clase. Python permite mezclar ambos estilos: clases donde encajan, funciones donde son suficientes.
__str__ define lo que se muestra cuando haces print(objeto) o str(objeto) — pensado para usuarios finales, tiene que ser legible. __repr__ define la representación técnica para depuración, lo que se muestra en el intérprete interactivo o repr(objeto) — idealmente debería ser una expresión Python que permita recrear el objeto. La regla práctica: __repr__ es para desarrolladores, __str__ es para usuarios. Si solo implementas uno, implementa __repr__ porque Python lo usa como fallback.
Python sigue la filosofía de "consenting adults": confía en que los programadores saben lo que hacen. La convención de un guión bajo (_atributo) comunica "este atributo es de uso interno, úsalo bajo tu responsabilidad". El doble guión bajo (__atributo) aplica name mangling real para evitar colisiones en herencia, no para impedir el acceso. Es una decisión filosófica deliberada: la legibilidad y la confianza en el programador importan más que la restricción forzada.
super() devuelve un objeto proxy que delega las llamadas a métodos a la clase padre (o a la siguiente en el MRO). En herencia simple, super().__init__() llama al __init__ del padre. En herencia múltiple, sigue el MRO (Method Resolution Order) que Python calcula con el algoritmo C3. Siempre usa super() en lugar de llamar directamente al padre por nombre (Padre.__init__(self)) porque super() funciona correctamente con herencia múltiple y es más flexible si la jerarquía de clases cambia.
Duck typing viene del dicho "if it walks like a duck and quacks like a duck, it's a duck". En Python no importa la clase de un objeto sino que tenga los métodos o atributos que necesitas. Si una función espera un objeto con .read(), cualquier objeto que tenga ese método funcionará, sea un archivo real, un StringIO, o cualquier clase que lo implemente. Esto da mucha flexibilidad. El polimorfismo en Python se basa en duck typing, no en herencia obligatoria como en Java o C#.
@classmethod recibe la clase como primer argumento (cls) en lugar de la instancia (self). Se usa principalmente para constructores alternativos: métodos de fábrica que crean instancias desde distintos tipos de entrada. @staticmethod no recibe ni self ni cls — es una función normal que vive en el namespace de la clase por organización lógica, no por necesidad técnica. La regla: si el método necesita acceder o modificar el estado de la instancia → método normal. Si necesita la clase pero no la instancia → @classmethod. Si no necesita ninguno de los dos → @staticmethod.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Programación orientada a objetos en Python: clases, objetos y herencia? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

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