Cuando ves self por primera vez, parece que Python hace algo misterioso detrás de escena. En realidad es todo lo contrario: self existe precisamente para que no haya misterio.
self no es una palabra reservada. Es simplemente el nombre que la comunidad acordó darle al primer parámetro de cualquier método de instancia. Podrías llamarlo esto, yo, o x, y Python no se quejaría. Lo que sí importa es que ese primer parámetro exista, porque Python lo usa para pasarte la instancia sobre la que estás trabajando.
Cómo funciona el mecanismo real
Cuando defines una clase, los métodos son funciones ordinarias guardadas en el espacio de nombres de la clase. Nada especial todavía. La magia sucede en el momento en que llamas al método:
instancia.metodo(arg)
Python transforma esa línea en:
Clase.metodo(instancia, arg)
Automáticamente. Sin que escribas nada. Esta transformación es lo que se llama bound method (método enlazado): cuando accedes a instancia.metodo, Python crea un objeto especial que ya tiene instancia cosida como primer argumento. Por eso puedes llamarlo sin pasarle self explícitamente.
¿Por qué este diseño? Porque en Python “explicit is better than implicit” (El Zen de Python, ítem 2). Otros lenguajes como Java o C++ tienen this implícito: el compilador lo inyecta sin que lo declares. Python prefiere que veas que el método recibe la instancia, porque eso hace que el modelo sea predecible y que los métodos sean inspeccionables como funciones normales. Si en algún momento necesitas la función sin enlazar, puedes acceder a ella directamente como Clase.metodo y pasarle lo que quieras.
Si omites self como primer parámetro y luego llamas el método desde una instancia, Python pasará la instancia como primer argumento y tu código recibirá un TypeError porque los parámetros no cuadran. Es uno de los errores más comunes al empezar con OOP en Python.
class Thermometer:
def __init__(self, brand: str, celsius: float) -> None:
self.brand = brand # 'self' recibe la instancia recién creada
self.celsius = celsius
def to_fahrenheit(self) -> float:
return self.celsius * 9 / 5 + 32
def set_temperature(self, celsius: float) -> None:
self.celsius = celsius
# Creamos dos instancias completamente independientes
kitchen = Thermometer("Acme", 100.0)
freezer = Thermometer("Polar", -18.0)
# Llamada normal — Python la convierte en Thermometer.to_fahrenheit(kitchen)
print(kitchen.to_fahrenheit()) # 212.0
print(freezer.to_fahrenheit()) # -0.4
# Un bound method es un objeto real: lleva 'self' ya cosido
method_ref = kitchen.to_fahrenheit
print(method_ref) # <bound method Thermometer.to_fahrenheit of <Thermometer ...>>
print(method_ref()) # 212.0 — no necesita argumentos
# La misma función, sin enlazar: necesita la instancia explícita
unbound = Thermometer.to_fahrenheit
print(unbound(freezer)) # -0.4
Lo que este código demuestra
__init__ recibe self igual que cualquier otro método. Cuando escribes Thermometer("Acme", 100.0), Python crea el objeto y lo pasa automáticamente como self. Dentro de __init__, self.brand = brand simplemente añade el atributo brand a ese objeto concreto, no a la clase. kitchen y freezer son objetos distintos en memoria; cada uno tiene su propio celsius.
Cuando llamas kitchen.to_fahrenheit(), Python resuelve to_fahrenheit en la clase Thermometer, ve que es una función, y construye un bound method que envuelve tanto la función como la instancia kitchen. El resultado es que self.celsius dentro del método apunta a 100.0, no a -18.0.
La línea method_ref = kitchen.to_fahrenheit captura ese bound method. Fíjate en el repr: te dice explícitamente a qué instancia está enlazado. Si luego kitchen.celsius cambia (con set_temperature), method_ref() devolverá el nuevo valor, porque el enlace apunta al objeto, no a una copia.
La última parte, Thermometer.to_fahrenheit, es la función pura tal como vive en la clase. Es accesible, inspeccionable y llamable, pero necesita que le pases la instancia tú mismo. Esto confirma que no hay nada oculto: self es solo el primer argumento.
Errores que debes conocer
Error: Olvidar self como primer parámetro hace que Python pase la instancia al argumento que sí declaraste, y los tipos no cuadran.
# ❌ Wrong
class Counter:
def __init__(self):
self.count = 0
def increment(amount): # falta self — 'amount' recibirá la instancia
self.count += amount
c = Counter()
c.increment(1) # TypeError: increment() takes 1 positional argument but 2 were given
# ✅ Right
class Counter:
def __init__(self):
self.count = 0
def increment(self, amount):
self.count += amount
c = Counter()
c.increment(1)
Añadir self como primer parámetro hace que la transformación automática de Python coloque la instancia donde debe ir.
Error: Usar un nombre distinto a self no rompe nada funcionalmente, pero rompe la convención y confunde a linters, herramientas de documentación y a cualquier persona que lea tu código.
# ❌ Wrong
class Box:
def __init__(this, width):
this.width = width # funciona, pero nadie lo espera
# ✅ Right
class Box:
def __init__(self, width):
self.width = width
Python acepta cualquier nombre, pero el ecosistema entero asume self; rompiendo esa convención pierdes soporte de IDEs y aumentas la carga cognitiva sin ningún beneficio.
N° 94