Tuplas en Python: inmutabilidad como contrato de diseño

📅 Actualizado en marzo 2026 📊 Nivel: Principiante ⏱️ 16 min de lectura
Τέτταρα τῶν πάντων ῥιζώματα πρῶτον ἄκουε
«Escucha primero las cuatro raíces de todo lo que existe»
Empédocles de Akragas · Filósofo presocrático · ~490 a.C. – ~430 a.C.
Empédocles propuso que toda la realidad está formada por cuatro elementos inmutables — fuego, tierra, agua, aire — que se combinan en distintas proporciones pero nunca se transforman en otra cosa. Son los datos fundamentales del universo: no cambian, no se destruyen, simplemente se organizan de maneras diferentes. Las tuplas de Python son exactamente eso: colecciones de valores que, una vez establecidos, son la fuente de verdad del programa. (40.4168, -3.7038) son las coordenadas de Madrid. No "probablemente". No "hasta que las actualicemos". Son Madrid. Empédocles habría elegido la tupla.

Una tupla es como una lista, pero inmutable: una vez creada no puedes cambiar, añadir ni eliminar sus elementos. Esa restricción no es una limitación — es una declaración de intención. Cuando devuelves una tupla de una función o la usas como clave de diccionario, el lector del código sabe que esos datos son fijos. La inmutabilidad es un contrato.

📌 Crear tuplas: sintaxis y casos especiales

# Formas de crear tuplas
vacia          = ()
un_elemento    = (42,)         # ← la coma es OBLIGATORIA
dos_elementos  = (1, 2)
sin_parentesis = 1, 2, 3       # los paréntesis son opcionales
punto          = (10, 20)
rgb            = (255, 128, 0)
registro       = ("Ana", 28, "Madrid", True)

# ⚠️ Sin coma no es una tupla
not_a_tuple = (42)     # esto es solo el entero 42
type((42))             # 
type((42,))            # 

# Construir desde otros iterables
desde_lista   = tuple([1, 2, 3])      # (1, 2, 3)
desde_string  = tuple("Python")       # ('P','y','t','h','o','n')
desde_range   = tuple(range(5))       # (0, 1, 2, 3, 4)
desde_set     = tuple({3, 1, 2})      # orden indefinido (set no garantiza orden)

# Longitud y pertenencia
punto = (10, 20, 30)
len(punto)         # 3
20 in punto        # True
99 in punto        # False

🔍 Acceso e iteración

El acceso a elementos de una tupla es idéntico al de las listas: por índice positivo o negativo, y con slicing. El resultado del slicing de una tupla es otra tupla.

t = (10, 20, 30, 40, 50)

# Acceso por índice
t[0]       # 10
t[-1]      # 50
t[1:3]     # (20, 30)  ← slicing devuelve tupla
t[::-1]    # (50, 40, 30, 20, 10)  ← invertida (nueva tupla)

# Iterar
for elemento in t:
    print(elemento)

# Índice + valor
for i, valor in enumerate(t):
    print(f"t[{i}] = {valor}")

# Métodos disponibles (solo dos, las tuplas son inmutables)
t.count(20)    # 1  ← cuántas veces aparece 20
t.index(30)    # 2  ← índice de la primera aparición de 30
Soldadura de metal fundido creando una unión permanente e irreversible entre dos piezas metálicas
Una soldadura: une elementos de forma permanente. No hay marcha atrás, no hay modificación. Exactamente la semántica de una tupla Python: datos que quedan fijados en el momento de su creación. Fuente: Pexels (licencia libre).

🔒 Inmutabilidad: qué significa realmente

La inmutabilidad de la tupla tiene matices importantes. La tupla no puede cambiar qué objetos referencia, pero si esos objetos son mutables (como listas), puedes modificar su contenido interno.

t = (1, 2, 3)

# Intentar modificar → TypeError
t[0] = 99           # TypeError: 'tuple' object does not support item assignment
t.append(4)         # AttributeError: 'tuple' object has no attribute 'append'

# Pero si contiene objetos mutables:
t = ([1, 2], [3, 4])
t[0].append(99)     # ✅ modifica la lista interna
print(t)            # ([1, 2, 99], [3, 4])  ← la tupla "parece" cambiar

# Consecuencia: una tupla con mutables no es hashable
t_solo_ints = (1, 2, 3)
hash(t_solo_ints)         # OK  ← es hashable, se puede usar como clave de dict

t_con_lista = ([1,2], [3,4])
hash(t_con_lista)         # TypeError: unhashable type: 'list'

# Las tuplas de valores simples son hashables → usables como claves
tablero = {}
tablero[(0, 0)] = "X"
tablero[(0, 1)] = "O"
tablero[(1, 0)] = "X"
print(tablero[(0, 0)])    # "X"

# En sets también:
puntos_visitados = {(0,0), (1,2), (3,4)}
(1, 2) in puntos_visitados    # True

📦 Desempaquetado: la superpotencia de las tuplas

El desempaquetado (unpacking) permite asignar los elementos de una tupla (o cualquier iterable) a variables individuales en una sola instrucción. Es uno de los patrones más expresivos de Python.

# Desempaquetado básico
punto = (10, 20)
x, y  = punto
print(x, y)    # 10 20

# Equivale a:
x = punto[0]
y = punto[1]

# Desempaquetado con * (captura el resto)
primero, *resto         = (1, 2, 3, 4, 5)
print(primero)          # 1
print(resto)            # [2, 3, 4, 5]  ← siempre es lista

*inicio, ultimo         = (1, 2, 3, 4, 5)
print(inicio)           # [1, 2, 3, 4]
print(ultimo)           # 5

primero, *medio, ultimo = (1, 2, 3, 4, 5)
print(primero)          # 1
print(medio)            # [2, 3, 4]
print(ultimo)           # 5

# Ignorar valores con _
_, segundo, _ = (10, 20, 30)   # solo nos interesa el segundo
print(segundo)                 # 20

# Intercambio sin variable temporal (icónico de Python)
a, b = 10, 20
a, b = b, a
print(a, b)    # 20 10

# Desempaquetado en bucles
coordenadas = [(0, 0), (1, 5), (2, 3), (3, 7)]
for x, y in coordenadas:
    print(f"Punto: ({x}, {y})")

# Con enumerate + desempaquetado
personas = [("Ana", 28), ("Luis", 22), ("Marta", 35)]
for i, (nombre, edad) in enumerate(personas):
    print(f"{i}: {nombre} tiene {edad} años")

# Desempaquetado anidado
(a, b), c = (1, 2), 3
print(a, b, c)    # 1 2 3

# Desempaquetar en argumentos de función
def distancia(x1, y1, x2, y2):
    return ((x2-x1)**2 + (y2-y1)**2) ** 0.5

p1 = (0, 0)
p2 = (3, 4)
dist = distancia(*p1, *p2)    # desempaqueta ambas tuplas
print(dist)    # 5.0
Infografía sobre tuplas en Python: inmutabilidad, desempaquetado con asterisco, retorno múltiple de funciones y uso como clave de diccionario
Las cuatro superpotencias de las tuplas Python: inmutabilidad como contrato, desempaquetado elegante, retorno múltiple de funciones y capacidad de actuar como clave de diccionario. Infografía: Ciberaula.

↩️ Retorno múltiple de funciones

Técnicamente, una función Python que devuelve varios valores no devuelve múltiples cosas — devuelve una tupla. Python crea la tupla automáticamente sin necesidad de escribir los paréntesis.

# Python crea una tupla automáticamente al retornar múltiples valores
def minmax(lista):
    return min(lista), max(lista)    # devuelve una tupla (min, max)

resultado = minmax([3, 1, 4, 1, 5, 9])
print(resultado)       # (1, 9)
print(type(resultado)) # 

# Con desempaquetado directo:
minimo, maximo = minmax([3, 1, 4, 1, 5, 9])
print(minimo)   # 1
print(maximo)   # 9

# Más ejemplos de retorno múltiple
def dividir(a, b):
    """Devuelve cociente y resto."""
    return a // b, a % b

cociente, resto = dividir(17, 5)
print(f"17 ÷ 5 = {cociente} resto {resto}")    # 17 ÷ 5 = 3 resto 2

def estadisticas(datos):
    """Devuelve media, mediana y desviación estándar."""
    n      = len(datos)
    media  = sum(datos) / n
    datos_ord = sorted(datos)
    mediana = datos_ord[n//2] if n % 2 else (datos_ord[n//2-1] + datos_ord[n//2]) / 2
    varianza = sum((x - media)**2 for x in datos) / n
    desv_std = varianza ** 0.5
    return media, mediana, desv_std

media, mediana, std = estadisticas([2, 4, 4, 4, 5, 5, 7, 9])
print(f"Media: {media:.2f}, Mediana: {mediana}, Std: {std:.2f}")

🏷️ NamedTuple: tuplas con campos con nombre

namedtuple crea subclases de tupla cuyos elementos son accesibles tanto por índice como por nombre. Combina la eficiencia de la tupla con la legibilidad del diccionario.

from collections import namedtuple

# Definir el tipo (nombre del tipo, nombres de campos)
Punto   = namedtuple('Punto', ['x', 'y'])
Color   = namedtuple('Color', ['r', 'g', 'b'])
Persona = namedtuple('Persona', ['nombre', 'edad', 'ciudad'])

# Crear instancias
p1 = Punto(10, 20)
p2 = Punto(x=5, y=8)       # argumentos con nombre
rojo = Color(255, 0, 0)
ana  = Persona("Ana", 28, "Madrid")

# Acceso por nombre (legible) Y por índice (compatible con tupla)
print(p1.x, p1.y)          # 10 20
print(p1[0], p1[1])        # 10 20  ← igual que una tupla
print(ana.nombre)           # "Ana"
print(rojo.r, rojo.g)       # 255 0

# Se puede desempaquetar como tupla
x, y = p1
nombre, edad, ciudad = ana

# Convertir a dict
print(ana._asdict())
# OrderedDict([('nombre', 'Ana'), ('edad', 28), ('ciudad', 'Madrid')])

# Crear una variante con un campo modificado (_replace)
ana2 = ana._replace(edad=29)
print(ana2)   # Persona(nombre='Ana', edad=29, ciudad='Madrid')
print(ana)    # Persona(nombre='Ana', edad=28, ciudad='Madrid')  ← original intacto

# Campos disponibles
print(Persona._fields)   # ('nombre', 'edad', 'ciudad')

# Alternativa moderna: typing.NamedTuple (Python 3.6+)
from typing import NamedTuple

class Coordenada(NamedTuple):
    latitud:  float
    longitud: float
    altitud:  float = 0.0    # campo con valor por defecto

madrid = Coordenada(40.4168, -3.7038)
print(madrid.latitud)    # 40.4168
print(madrid)            # Coordenada(latitud=40.4168, longitud=-3.7038, altitud=0.0)
Cajones de madera antiguos con etiquetas metálicas que identifican el contenido de cada uno
Cajones con etiquetas: acceso por nombre pero posición fija. La metáfora exacta de NamedTuple, que combina la velocidad de la tupla con la legibilidad del acceso por campo. Fuente: Pexels (licencia libre).

⚡ Rendimiento: tupla vs lista

import sys
import timeit

# Diferencia de memoria
lista = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
tupla = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

print(f"Lista: {sys.getsizeof(lista)} bytes")   # ~184 bytes
print(f"Tupla: {sys.getsizeof(tupla)} bytes")   # ~136 bytes
# La tupla ocupa ~25% menos memoria

# Diferencia de velocidad en creación
t_lista = timeit.timeit("x = [1, 2, 3, 4, 5]", number=10_000_000)
t_tupla = timeit.timeit("x = (1, 2, 3, 4, 5)", number=10_000_000)
print(f"Lista: {t_lista:.3f}s  |  Tupla: {t_tupla:.3f}s")
# La tupla se crea más rápido (Python la almacena en caché como literal)

# El acceso por índice es igual de rápido en ambas
# La diferencia real está en: creación, memoria y hashabilidad
Ficha comparativa: tupla vs lista vs NamedTuple en Python — sintaxis, mutabilidad, hashable, métodos y casos de uso
Comparativa completa: tupla, lista y NamedTuple. Propiedades, sintaxis, métodos disponibles, cuándo es cada una la elección correcta. Infografía: Ciberaula.

🧭 Cuándo usar tupla y cuándo lista

# ✅ Usa TUPLA cuando:

# 1. Los datos son fijos conceptualmente
coordenadas = (40.4168, -3.7038)   # Madrid — no cambia
pixel       = (255, 128, 0)         # color RGB

# 2. Quieres usar como clave de diccionario
cache = {}
cache[(1, 2, "suma")] = 3          # tupla como clave ✅

# 3. Retorno múltiple de función
def parse_fecha(s):
    partes = s.split("-")
    return int(partes[0]), int(partes[1]), int(partes[2])

año, mes, dia = parse_fecha("2026-03-09")

# 4. Datos que no deben modificarse por accidente
CONFIG = ("localhost", 5432, "mi_db")    # host, puerto, nombre

# ✅ Usa LISTA cuando:

# 1. La colección va a cambiar
carrito = ["manzana"]
carrito.append("pera")
carrito.remove("manzana")

# 2. Necesitas ordenar o filtrar
registros = [...]
registros.sort(key=lambda r: r["fecha"])

# 3. El tamaño varía en tiempo de ejecución
resultados = []
for item in fuente_de_datos:
    if criterio(item):
        resultados.append(item)

🐛 Errores clásicos con tuplas

1. Tupla de un elemento sin coma

t = (42)          # ← int, no tupla
t = (42,)         # ← tuple ✅
t = 42,           # ← tuple ✅ (sin paréntesis)

2. Intentar "modificar" una tupla con concatenación

t = (1, 2, 3)
t = t + (4,)      # ✅ válido, pero crea una NUEVA tupla (la original desaparece)
                  # no es modificación in-place como lista.append()
# Cada concatenación de tuplas es O(n) — evitar en bucles

3. Desempaquetado con número incorrecto de variables

a, b = (1, 2, 3)    # ValueError: too many values to unpack
a, b, c = (1, 2)    # ValueError: not enough values to unpack

# Solución: usar * para capturar el resto
a, *resto = (1, 2, 3)   # a=1, resto=[2,3]  ✅

4. Confundir hashabilidad

# Solo las tuplas que contienen SOLO objetos hashables son hashables
hash((1, 2, 3))       # ✅
hash((1, [2, 3]))     # TypeError: unhashable type ← contiene lista

d = {}
d[(1, 2)] = "ok"      # ✅ clave válida
d[(1, [2])] = "no"    # TypeError ← no se puede usar como clave

✅ Resumen y próximos pasos

Las tuplas son secuencias ordenadas e inmutables. Su valor no está en lo que permiten, sino en lo que prohíben: comunicar que esos datos son fijos. El desempaquetado las hace especialmente expresivas. namedtuple les añade legibilidad sin sacrificar rendimiento.

La siguiente lección: diccionarios, la estructura de datos más poderosa de Python para almacenar y recuperar información con acceso O(1) por clave.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Tuplas en Python: inmutabilidad como contrato de diseño

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

Sí. La inmutabilidad de la tupla se refiere a que no puedes cambiar qué objetos contiene (no puedes reasignar sus posiciones ni añadir/eliminar elementos), pero si uno de esos objetos es mutable (como una lista), puedes modificar el contenido de ese objeto. Ejemplo: t = ([1,2], [3,4]); t[0].append(99) funciona porque estás modificando la lista interna, no la tupla. Esto tiene una consecuencia importante: si una tupla contiene objetos mutables, no es hashable y no puede usarse como clave de diccionario.
Porque Python usa los paréntesis también para agrupar expresiones. Sin la coma, (42) es simplemente el número 42 entre paréntesis, no una tupla. La coma es lo que indica a Python "esto es una tupla": (42,) crea una tupla con un elemento. También puedes crearla sin paréntesis: t = 42, — aunque es menos legible. Comprobar: type((42)) devuelve int, type((42,)) devuelve tuple.
Usa namedtuple cuando los campos son fijos y conocidos de antemano, quieres acceso por nombre pero también por índice, te importa la memoria (namedtuple usa menos que dict), y los datos no deben cambiar. Usa diccionario cuando los campos pueden variar, necesitas añadir/eliminar campos dinámicamente, o el conjunto de claves no es conocido en tiempo de escritura del código. Para código más moderno, considera dataclasses (Python 3.7+) que ofrecen la legibilidad de namedtuple con mutabilidad opcional.
Sí. tuple(lista) convierte cualquier iterable (lista, string, set, etc.) en tupla. list(tupla) convierte una tupla en lista. Esto es útil cuando necesitas modificar temporalmente datos que conceptualmente deberían ser inmutables: convierte a lista, modifica, y vuelve a convertir a tupla. La conversión es O(n) en tiempo y crea un nuevo objeto, así que evítala en bucles internos de código crítico para el rendimiento.
Sí. El operador * en el desempaquetado puede aparecer en cualquier posición: primero, *resto = (1,2,3,4); *inicio, ultimo = (1,2,3,4); primero, *medio, ultimo = (1,2,3,4,5). Solo puede usarse un * en una asignación de desempaquetado. El resultado de * siempre es una lista, no una tupla. Esta sintaxis también funciona con listas, strings, rangos y cualquier iterable.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Tuplas en Python: inmutabilidad como contrato de diseño? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

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