El decorador @property es una de las herramientas más elegantes de Python para el encapsulamiento y control de acceso a atributos. Permite transformar métodos en atributos que se pueden acceder directamente, sin paréntesis, mientras mantiene toda la lógica de validación y cálculo interna. Si buscas escribir código Python más limpio y profesional, dominar @property es esencial.

En esta guía completa, aprenderás desde lo básico hasta técnicas avanzadas de propiedades en Python, con ejemplos prácticos probados y las mejores prácticas del mercado.

El Problema del Acceso Directo a Atributos

En muchos lenguajes orientados a objetos, como Java y C++, el encapsulamiento se logra con atributos privados y métodos getters y setters públicos. Python tiene una filosofía diferente: todo es público por defecto. Esto puede causar problemas cuando un atributo necesita validación o lógica adicional en el futuro.

class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.saldo = saldo

Uso directo -- sin ninguna protección

cuenta = CuentaBancaria("Juan", 1000) cuenta.saldo = -500 # Saldo negativo sin aviso

El problema es evidente: cualquiera puede asignar un valor negativo a saldo, algo que una cuenta bancaria real nunca debería permitir. Podríamos resolverlo con métodos getter y setter tradicionales, pero la sintaxis sería más verbosa y menos intuitiva.

Creando tu Primera Property con @property

El decorador @property convierte un método en una propiedad que se puede acceder como un atributo normal:

class Temperatura:
    def __init__(self, celsius):
        self._celsius = celsius
@property
def celsius(self):
    """Devuelve la temperatura en Celsius."""
    return self._celsius

@property
def fahrenheit(self):
    """Convierte Celsius a Fahrenheit automáticamente."""
    return (self._celsius * 9 / 5) + 32

Uso elegante

temp = Temperatura(25) print(temp.celsius) # 25 print(temp.fahrenheit) # 77.0

Observa cómo temp.celsius y temp.fahrenheit se acceden como atributos, sin paréntesis. Internamente, sin embargo, son métodos que pueden contener cualquier lógica. Ese es el poder de @property: sintaxis de atributo con comportamiento de método.

Según la documentación oficial de Python, la función property() devuelve un atributo de propiedad y puede recibir hasta cuatro argumentos: fget, fset, fdel y doc. El decorador @property es simplemente azúcar sintáctico para usar property(fget).

Añadiendo Validación con @property.setter

El @property.setter permite definir un método setter para la propiedad, ejecutando lógica de validación cada vez que se cambia el valor:

class CuentaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = saldo
@property
def saldo(self):
    return self._saldo

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

def depositar(self, cantidad):
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser positiva")
    self.saldo += cantidad  # Usa el setter automáticamente

def retirar(self, cantidad):
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser positiva")
    if cantidad > self.saldo:
        raise ValueError("Saldo insuficiente")
    self.saldo -= cantidad  # Usa el setter automáticamente

Uso seguro

cuenta = CuentaBancaria("María", 1000) cuenta.depositar(500) print(cuenta.saldo) # 1500 cuenta.retirar(200) print(cuenta.saldo) # 1300

cuenta.saldo = -100 # ValueError

El setter se invoca automáticamente cada vez que asignas un valor a la propiedad, incluso dentro de otros métodos de la clase como depositar y retirar. Toda la lógica de validación queda centralizada en el setter, evitando duplicación de código.

El tutorial de Real Python sobre @property demuestra cómo este enfoque simplifica el mantenimiento del código y previene errores en producción.

Usando @property.deleter

El decorador @property.deleter define el comportamiento cuando usamos del en una propiedad. Es útil para liberar recursos o reiniciar valores:

class Sesion:
    def __init__(self, usuario):
        self._usuario = usuario
        self._tokens = ["token123"]
@property
def usuario(self):
    return self._usuario

@property
def tokens(self):
    return self._tokens

@tokens.deleter
def tokens(self):
    print("Limpiando tokens de la sesión...")
    self._tokens = []

sesion = Sesion("admin") print(sesion.tokens) # ['token123'] del sesion.tokens print(sesion.tokens) # []

El deleter permite ejecutar código de limpieza cuando se elimina un atributo. Es especialmente útil para gestionar recursos como conexiones de bases de datos, archivos abiertos o cachés.

Propiedades Computadas (Atributos Derivados)

Las propiedades computadas son atributos cuyo valor se calcula dinámicamente a partir de otros atributos. No necesitan un setter si son de solo lectura:

class Rectangulo:
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
@property
def area(self):
    return self.ancho * self.alto

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

@property
def diagonal(self):
    return (self.ancho ** 2 + self.alto ** 2) ** 0.5

r = Rectangulo(10, 5) print(r.area) # 50 print(r.perimetro) # 30 print(f"{r.diagonal:.2f}") # 11.18

Si el ancho cambia, las propiedades reflejan el cambio

r.ancho = 20 print(r.area) # 100 (calculado dinámicamente)

Las propiedades computadas son una excelente alternativa a métodos como get_area() o get_perimetro(). La sintaxis de atributo hace que el código sea más natural y legible. La guía de Descriptores en Python explica en detalle cómo funciona internamente el mecanismo de property.

Este concepto se usa ampliamente en POO. Si estás estudiando Programación Orientada a Objetos en Python, entender las properties es fundamental para aplicar el encapsulamiento correctamente.

Property vs Getters y Setters Tradicionales

En lenguajes como Java, es común ver:

// Java
public class Persona {
    private String nombre;
public String getNombre() {
    return nombre;
}

public void setNombre(String nombre) {
    this.nombre = nombre;
}

}

persona.getNombre(); persona.setNombre("Juan");

En Python, el mismo patrón con @property queda mucho más limpio:

class Persona:
    def __init__(self, nombre):
        self._nombre = nombre
@property
def nombre(self):
    return self._nombre

@nombre.setter
def nombre(self, valor):
    if not valor.strip():
        raise ValueError("El nombre no puede estar vacío")
    self._nombre = valor.strip()

@nombre.deleter
def nombre(self):
    print("Eliminando nombre...")
    self._nombre = None

p = Persona(" María ") print(p.nombre) # María (acceso directo) p.nombre = "Ana" print(p.nombre) # Ana

La diferencia clave es que el código cliente nunca necesita saber si está accediendo a un atributo o a un método. Puedes empezar con un atributo simple y, si más adelante necesitas validación, lo conviertes en una property sin romper la API pública. Este principio está recomendado por la PEP 8 -- Guía de Estilo para Código Python, que promueve el uso de properties para mantener la consistencia.

Encapsulamiento y Validación con Setters Inteligentes

Uno de los casos de uso más comunes para @property es la validación de datos en el setter. Creemos una clase Usuario con validaciones robustas:

import re

class Usuario: def init(self, email, edad): self.email = email # Usa el setter en la inicialización self.edad = edad # Usa el setter en la inicialización self._activo = True

@property
def email(self):
    return self._email

@email.setter
def email(self, valor):
    if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', valor):
        raise ValueError("Email inválido")
    self._email = valor.lower()

@property
def edad(self):
    return self._edad

@edad.setter
def edad(self, valor):
    if not isinstance(valor, int):
        raise TypeError("La edad debe ser un número entero")
    if valor < 0 or valor > 150:
        raise ValueError("La edad debe estar entre 0 y 150")
    self._edad = valor

@property
def activo(self):
    return self._activo

Uso con validación automática

usuario = Usuario("[email protected]", 25) print(usuario.email) # [email protected] (convertido a minúsculas) print(usuario.edad) # 25

usuario.email = "invalido" # ValueError

usuario.edad = 200 # ValueError

Observa cómo el constructor también pasa por los setters, garantizando que incluso los objetos recién creados tengan datos válidos. La comunidad de Stack Overflow explica detalladamente el funcionamiento del decorador property y sus matices.

Propiedades con Caché (Lazy Evaluation)

Cuando una propiedad computada es costosa de calcular, podemos usar caché para almacenar el resultado y recalcular solo cuando sea necesario:

class Informe:
    def __init__(self, datos):
        self.datos = datos
        self._cache = {}
@property
def analisis_completo(self):
    if "analisis" not in self._cache:
        print("Calculando análisis completo... (operación costosa)")
        resultado = {
            "total": sum(self.datos),
            "media": sum(self.datos) / len(self.datos),
            "maximo": max(self.datos),
            "minimo": min(self.datos),
            "tamano": len(self.datos)
        }
        self._cache["analisis"] = resultado
    return self._cache["analisis"]

def invalidar_cache(self):
    self._cache = {}

informe = Informe([10, 20, 30, 40, 50]) print(informe.analisis_completo["media"]) # Calcula la primera vez print(informe.analisis_completo["total"]) # Usa caché la segunda vez informe.invalidar_cache() print(informe.analisis_completo) # Recalcula

Este patrón se conoce como lazy evaluation y es especialmente útil para operaciones de E/S, consultas a bases de datos o procesamiento intensivo de datos, como se muestra en la Guía de Estilo de Python (Hitchhiker's Guide).

Herencia y Properties

Las properties se comportan como métodos en la herencia: se pueden sobrescribir en subclases para extender o modificar el comportamiento:

class Animal:
    def __init__(self, nombre):
        self._nombre = nombre
@property
def nombre(self):
    return self._nombre

@property
def sonido(self):
    return "..."

class Perro(Animal): @property def sonido(self): return "Guau!"

class Gato(Animal): @property def sonido(self): return "Miau!"

class Loro(Animal): def init(self, nombre): super().init(nombre) self._palabras = []

def aprender(self, palabra):
    self._palabras.append(palabra)

@property
def sonido(self):
    if self._palabras:
        return ", ".join(self._palabras)
    return "..."

animales = [Perro("Rex"), Gato("Mimi"), Loro("Loro")] for animal in animales: if isinstance(animal, Loro): animal.aprender("¡Hola!") animal.aprender("Python es genial") print(f"{animal.nombre}: {animal.sonido}")

La sobrescritura de properties funciona igual que la sobrescritura de métodos convencionales. Puedes redeclarar @property en la subclase o usar super() para extender la implementación original. La guía de GeeksforGeeks sobre @property ofrece ejemplos adicionales de herencia con properties.

Properties como Atributos de Solo Lectura

Para crear atributos que se puedan leer pero no modificar después de la creación, basta con definir solo el getter sin el setter:

class Configuracion:
    def __init__(self):
        self._version = "2.0.1"
        self._fecha_creacion = "2026-01-15"
@property
def version(self):
    return self._version

@property
def fecha_creacion(self):
    return self._fecha_creacion

config = Configuracion() print(config.version) # 2.0.1 print(config.fecha_creacion) # 2026-01-15

config.version = "3.0.0" # AttributeError

Las propiedades de solo lectura son ideales para exponer metadatos, constantes de configuración o información del sistema sin riesgo de modificación accidental. Para una guía más profunda sobre encapsulamiento, consulta nuestro artículo sobre Decoradores en Python, que muestra el uso avanzado de decoradores como @property.

Property con Type Hints

Python moderno admite type hints en properties, mejorando la legibilidad y permitiendo la verificación estática con herramientas como mypy:

class Producto:
    def __init__(self, nombre: str, precio: float) -> None:
        self._nombre = nombre
        self._precio = precio
@property
def nombre(self) -> str:
    return self._nombre

@nombre.setter
def nombre(self, valor: str) -> None:
    if not valor.strip():
        raise ValueError("El nombre no puede estar vacío")
    self._nombre = valor.strip()

@property
def precio(self) -> float:
    return self._precio

@precio.setter
def precio(self, valor: float) -> None:
    if valor < 0:
        raise ValueError("El precio no puede ser negativo")
    self._precio = round(valor, 2)

@property
def precio_con_impuesto(self) -> float:
    """Devuelve el precio con 10% de impuesto."""
    return round(self._precio * 1.1, 2)

p = Producto("Laptop", 3500.00) print(f"{p.nombre}: ${p.precio:.2f} (con impuesto: ${p.precio_con_impuesto:.2f})")

Los type hints en properties ayudan a los IDE a proporcionar autocompletado y detectar errores en tiempo de desarrollo, además de servir como documentación viva del código. Aprende más sobre type hints en el módulo typing de la documentación oficial.

Cuándo Evitar @property

A pesar de su poder, el uso excesivo de properties puede hacer que el código sea confuso. Evita usar @property cuando:

  • El método realiza una operación costosa que no debería parecer engañosamente rápida (prefiere un método explícito como .calcular_informe())
  • El método tiene efectos secundarios significativos (las properties deben ser seguras y predecibles)
  • El método acepta parámetros (las properties no aceptan argumentos además de self)
  • El acceso a la propiedad lanza excepciones frecuentemente (esto rompe la expectativa de que los atributos son seguros)
# MAL: operación costosa disfrazada de atributo
class BaseDeDatos:
    @property
    def todos_usuarios(self):
        # Consulta SQL pesada -- mejor como método explícito
        return self._ejecutar_consulta("SELECT * FROM usuarios")

BIEN: método explícito para operaciones pesadas

class BaseDeDatos: def listar_usuarios(self): return self._ejecutar_consulta("SELECT * FROM usuarios")

El principio es simple: las properties deben ser "baratas", predecibles y sin efectos secundarios. Para operaciones costosas o que requieren parámetros, usa métodos convencionales.

Conclusión

El decorador @property es una herramienta indispensable en el kit del desarrollador Python. Permite escribir código limpio, seguro y que sigue el principio de encapsulamiento sin sacrificar la simplicidad. Puedes empezar con atributos públicos simples y, según la necesidad, añadir validación y lógica sin romper la API pública de tu clase.

Dominar @property es un paso importante para escribir código Python profesional. Cuando se combina con type hints, herencia y buenas prácticas de validación, creates clases robustas, probables y fáciles de mantener.

Continúa aprendiendo: explora nuestra guía completa de Magic Methods en Python para profundizar aún más tus conocimientos de POO en Python.