Cuando Python evalúa x = x + 1, no está modificando el número que tenía x — está creando un número nuevo y haciendo que x apunte a él. Para algunos tipos esto es todo lo que puede ocurrir. Para otros, el objeto en sí puede cambiar internamente sin que ninguna variable “se mueva” a ningún lado. Esa distinción es mutabilidad vs inmutabilidad, y es uno de los conceptos más prácticos del modelo de objetos de Python.
Un objeto mutable puede cambiar su estado interno manteniendo la misma identidad (la misma dirección de memoria, el mismo id()). Un objeto inmutable no puede: cualquier operación que parezca modificarlo produce un objeto completamente nuevo. Los tipos mutables fundamentales del lenguaje son list, dict y set. Los inmutables son int, float, str, tuple, frozenset y bytes.
La razón de diseño es intencional. Los tipos que actúan como contenedores de datos en evolución (listas, diccionarios) son mutables porque eso es lo que necesitas cuando acumulas o transformas datos. Los tipos que representan valores atómicos o claves de búsqueda (números, cadenas) son inmutables porque su identidad es su valor: no tiene sentido que el número 42 se convierta en 43 en mitad de tu programa.
Aquí aparece el requisito de hashabilidad. Para que un objeto pueda usarse como clave de diccionario o elemento de set, Python necesita calcular su hash() una vez y confiar en que no cambiará nunca. Si el hash de un objeto pudiera cambiar después de insertarlo, Python no podría encontrarlo de vuelta — el índice interno donde lo guardó ya no correspondería. Por eso solo los objetos inmutables son hashables por defecto: su valor no puede cambiar, así que su hash tampoco.
La inmutabilidad además interactúa directamente con las referencias compartidas: cuando dos variables apuntan al mismo objeto mutable y una lo modifica, la otra también ve el cambio. Con objetos inmutables eso no puede ocurrir, porque cualquier “modificación” genera un nuevo objeto y deja intacto el original.
# Python 3.x — no se requieren imports
# ── Inmutables: cada operación produce un objeto nuevo ──────────────────
name = "ada"
original_id = id(name)
name = name.upper() # str.upper() devuelve un str nuevo
print(name) # ADA
print(id(name) == original_id) # False — objeto completamente distinto
counter = 0
print(id(counter)) # algún id, e.g. 140234567890
counter += 1
print(id(counter)) # id diferente: 1 y 0 son objetos distintos
# ── Mutables: el mismo objeto cambia internamente ───────────────────────
scores = [10, 20, 30]
original_id = id(scores)
scores.append(40) # modifica la lista en su lugar
print(scores) # [10, 20, 30, 40]
print(id(scores) == original_id) # True — mismo objeto, estado distinto
# ── Referencias compartidas: el peligro real ────────────────────────────
team_a = [1, 2, 3]
team_b = team_a # ambas apuntan al MISMO objeto
team_b.append(4) # modificamos a través de team_b...
print(team_a) # [1, 2, 3, 4] ← team_a también cambió
# Con inmutables esto no puede ocurrir
x = "hola"
y = x
y = y + "!" # y apunta a un objeto nuevo; x no se toca
print(x) # hola
# ── Hashabilidad: por qué las listas no pueden ser claves ───────────────
lookup = {}
lookup["python"] = "lenguaje" # str es inmutable → hashable ✓
lookup[(1, 2)] = "coordenada" # tuple de inmutables → hashable ✓
try:
lookup[[1, 2]] = "error" # list es mutable → no hashable ✗
except TypeError as e:
print(e) # unhashable type: 'list'
# frozenset es la versión inmutable (y hashable) de set
fs = frozenset({1, 2, 3})
lookup[fs] = "conjunto fijo" # funciona sin problema
Qué está ocurriendo en cada bloque
El primer bloque con name y counter muestra que incluso una operación tan cotidiana como += sobre un entero produce un objeto nuevo. Comprobarlo con id() lo hace concreto: no estás “añadiendo uno” al objeto 0, estás consiguiendo el objeto 1 y redirigiendo la variable. Esto es siempre verdad para int, float y str sin excepción.
El bloque de scores demuestra la cara opuesta: append no crea nada nuevo — manda al objeto lista que crezca. El id() idéntico antes y después lo confirma. Esto es lo que significa “mutar”: la identidad permanece, el contenido cambia.
El bloque de referencias compartidas es donde la mutabilidad deja de ser teórica y se convierte en bug real. team_b = team_a no copia la lista, copia la referencia. Ahora tienes dos nombres para un mismo objeto. Cualquier mutación visible desde uno es visible desde el otro. Si necesitas una copia independiente, usa team_b = team_a.copy() o team_b = list(team_a). Con cadenas o enteros este problema no existe porque nunca puedes mutar el objeto subyacente.
El bloque de hashabilidad conecta todo: el TypeError que lanza Python al intentar usar una lista como clave no es una limitación arbitraria — es la consecuencia directa de que una lista podría cambiar después de insertarse, rompiendo la estructura interna del diccionario. frozenset existe exactamente para el caso en que necesitas un conjunto de valores como clave: es un set que renunció a la mutabilidad a cambio de poder participar en diccionarios y otros sets.
Errores que debes conocer
Error: Asumir que asignar una lista a otra variable la copia, y luego mutar “la copia” afectando el original sin darte cuenta.
# ❌ Wrong defaults = [1, 2, 3] config = defaults config.append(99) print(defaults) # [1, 2, 3, 99] — modificaste lo que creías intacto # ✅ Right defaults = [1, 2, 3] config = defaults.copy() # objeto nuevo con el mismo contenido config.append(99) print(defaults) # [1, 2, 3] — defaults no se tocó
.copy() crea una lista nueva con los mismos elementos; a partir de ese punto las dos variables viven vidas independientes.
Error: Intentar usar una tupla que contiene una lista como clave de diccionario, creyendo que “si está en una tupla es inmutable”.
# ❌ Wrong
key = (1, [2, 3]) # tuple que contiene una list
d = {}
d[key] = "valor" # TypeError: unhashable type: 'list'
# ✅ Right
key = (1, (2, 3)) # tuple de inmutables → completamente hashable
d = {}
d[key] = "valor" # funciona
Una tuple solo es hashable si todos sus elementos también lo son; la inmutabilidad de la tupla no “contagia” a los objetos mutables que contiene.
N° 32