Python no tiene modificadores de acceso como private o protected. No hay una barrera técnica que impida tocar los internos de una clase desde fuera. Esto es una decisión de diseño deliberada, no una omisión: la filosofía del lenguaje asume que los programadores son adultos responsables (“we’re all consenting adults here”). Lo que sí tiene Python es un sistema de convenciones y una transformación de nombres con consecuencias reales que vale la pena entender bien.
Tres prefijos, tres significados completamente distintos
Un guión bajo _nombre es pura convención social. El intérprete lo ignora por completo. Cuando ves _cache o _validate() en una clase, el autor te está diciendo: “este detalle de implementación puede cambiar sin previo aviso; si dependes de él desde fuera, el problema es tuyo”. No hay ningún mecanismo que lo enforece; el atributo es perfectamente accesible. Es el equivalente a poner un cartel de “no pasar” en una puerta sin cerrojo.
Dos guiones bajos __nombre son algo diferente en su totalidad. Aquí Python aplica name mangling: en el momento en que el intérprete procesa el cuerpo de la clase, cualquier identificador con dos guiones bajos al inicio (y como máximo uno al final) se transforma automáticamente en _NombreDeClase__nombre. Esto ocurre en tiempo de compilación del bytecode, antes de que el código se ejecute. El propósito no es privacidad —el atributo transformado sigue siendo accesible desde fuera si sabes el nombre real—, sino evitar colisiones accidentales en jerarquías de herencia.
Dunders __nombre__ como __init__, __str__ o __len__ pertenecen al protocolo del lenguaje. No invocan name mangling (tienen guiones al final también) y están reservados para que Python los llame internamente. Nunca deberías inventar un dunder propio; en versiones futuras del lenguaje podría significar algo que rompa tu código silenciosamente.
La pregunta de cuándo usar __ en lugar de _ tiene una respuesta concreta: cuando diseñas una clase pensada para ser extendida por otros, y tienes un atributo que las subclases no deben sobreescribir sin querer. No es para esconder datos; es ingeniería defensiva contra colisiones de nombres en árbol de herencia.
Si alguien hereda de tu clase y sin saber define un atributo con el mismo nombre que uno tuyo, puede romper tu lógica interna silenciosamente. El name mangling es exactamente el mecanismo que previene eso.
# Python 3.11+
# Demuestra _ convención, __ name mangling, y el problema de colisión
# que __ resuelve en herencia.
class Logger:
def __init__(self, name: str) -> None:
self.name = name
self._buffer: list[str] = [] # convención: detalle interno
self.__log_count: int = 0 # name mangling → _Logger__log_count
def log(self, message: str) -> None:
self._buffer.append(message)
self.__log_count += 1 # accede a _Logger__log_count
def stats(self) -> dict[str, int]:
return {"count": self.__log_count} # mismo nombre, mismo acceso
def _flush(self) -> list[str]:
# _ indica que esto es un detalle del mecanismo; no es API pública
result, self._buffer = self._buffer, []
return result
class TimestampLogger(Logger):
def __init__(self, name: str) -> None:
super().__init__(name)
# Si Logger usara _log_count (sin mangling), esta línea
# sobreescribiría silenciosamente el contador del padre.
# Con __ eso no ocurre: aquí se crea _TimestampLogger__log_count,
# un atributo completamente distinto.
self.__log_count: int = 0
def log(self, message: str) -> None:
import datetime
timestamped = f"[{datetime.datetime.now().isoformat()}] {message}"
super().log(timestamped)
self.__log_count += 1 # toca _TimestampLogger__log_count, no el del padre
def own_stats(self) -> dict[str, int]:
return {"timestamped_count": self.__log_count}
if __name__ == "__main__":
tl = TimestampLogger("app")
tl.log("Arranque del sistema")
tl.log("Conexión establecida")
print(tl.stats()) # {"count": 2} ← del padre, intacto
print(tl.own_stats()) # {"timestamped_count": 2} ← de la subclase
# Name mangling visible desde fuera si realmente lo necesitas:
print(tl._Logger__log_count) # 2 — accesible, no privado
print(tl._TimestampLogger__log_count) # 2 — atributo independiente
# _ accesible sin ningún obstáculo:
print(tl._buffer) # lista con los mensajes pendientes de flush
Qué está pasando realmente
Logger.__init__ registra __log_count como un entero. En el bytecode ya compilado, ese nombre vive como _Logger__log_count. Cuando log() lo incrementa, accede exactamente a _Logger__log_count, independientemente de desde qué subclase se llame.
TimestampLogger también declara self.__log_count. El mangling opera en el cuerpo de esa clase, así que genera _TimestampLogger__log_count. Son dos atributos en el mismo objeto tl, con nombres distintos, que no se pisan entre sí. Eso es exactamente lo que el mecanismo garantiza.
Fíjate en stats(): el padre reporta su propio contador sin importar que la subclase haya definido uno con el mismo nombre lógico. Si Logger hubiera usado _log_count, TimestampLogger.__init__ lo habría sobreescrito con 0 y stats() devolvería datos incorrectos desde el primer log(). Un bug silencioso y difícil de rastrear.
El atributo _buffer con un solo guión bajo es distinto: cualquier subclase puede leerlo, modificarlo o sobreescribirlo. El autor está diciendo que es un detalle de implementación, pero confía en que quien lo use sabe lo que hace. Para ese nivel de “por favor no toques esto”, _ es suficiente y semánticamente más honesto que __.
Errores que debes conocer
Error: usar __ pensando que crea privacidad real y que el atributo no es accesible desde fuera.
# ❌ Wrong
class Config:
def __init__(self, secret: str) -> None:
self.__secret = secret # "nadie podrá leerlo"
c = Config("mi_api_key")
# Esto funciona perfectamente:
print(c._Config__secret) # → "mi_api_key"
# ✅ Right
# Si necesitas datos sensibles, no los guardes en memoria como atributo.
# Usa variables de entorno, vaults, o módulos como `secrets`.
# El __ es para herencia, no para seguridad.
El name mangling transforma el nombre pero no bloquea el acceso; confundir ofuscación con protección es un error de diseño con consecuencias de seguridad.
Error: definir un dunder propio pensando que es solo una convención de nombre bonito.
# ❌ Wrong
class Event:
def __trigger__(self) -> None: # "se ve interno y especial"
...
# ✅ Right
class Event:
def trigger(self) -> None: # nombre normal, sin magia
...
Los dunders __x__ están reservados para el data model de Python; inventar uno podría colisionar con una adición futura del lenguaje sin ninguna advertencia.
Error: aplicar name mangling a una función de módulo (fuera de una clase), donde no tiene ningún efecto.
# ❌ Wrong
# utils.py
def __parse(data: str) -> dict: # el mangling solo opera dentro de clases
...
# La función se llama literalmente "__parse", confuso y sin beneficio.
# ✅ Right
def _parse(data: str) -> dict: # convención estándar para "interno al módulo"
...
El name mangling es estrictamente un mecanismo de cuerpo de clase; fuera de ese contexto, __nombre es simplemente un nombre con guiones que nadie espera ver.
N° 100