Mucha gente llega a Python pensando que tuple es “una lista que no puedes modificar”. Técnicamente no está del todo mal, pero esa definición te lleva a usarlos mal el 80% de las veces. La diferencia real está en la semántica: lo que cada estructura significa, no solo lo que hace.
Una list representa una colección homogénea de elementos del mismo tipo conceptual, con longitud variable. Una tuple representa un registro de campos con posición fija y significado heterogéneo. Piensa en ("Ana", 32, "ana@mail.com"): eso no es “una colección de tres cosas”, es una persona —nombre en la posición 0, edad en la 1, email en la 2. Cambiar el orden rompería el significado.
Esa distinción de diseño explica por qué tuple es hashable (siempre que sus elementos también lo sean), lo que la hace usable como clave de diccionario. Una list nunca puede serlo, porque al ser mutable Python no puede garantizar que su hash sea estable. Cuando creas un tuple, Python reserva un bloque de memoria contiguo y fijo; la creación es más rápida y el objeto ocupa menos espacio que una lista equivalente porque no necesita capacidad extra para futuras inserciones.
¿Cuándo usar cada uno? Usa tuple cuando los campos tienen nombres conceptuales implícitos en su posición y el conjunto no va a cambiar —coordenadas, resultados de funciones que devuelven múltiples valores, claves compuestas de diccionario. Usa list cuando tienes una cantidad variable de elementos del mismo tipo —líneas de un archivo, usuarios activos, tareas pendientes.
Lo que se rompe si ignoras esto: usar listas donde necesitas claves de diccionario falla con TypeError en tiempo de ejecución, y usar tuplas para colecciones que en realidad van a crecer te fuerza a reconstruirlas constantemente (t = t + (nuevo,)), que es lento y confuso.
# Coordenadas de ciudades: tuple porque (lat, lon) es un registro fijo
CITIES: dict[tuple[float, float], str] = {
(40.4168, -3.7038): "Madrid",
(41.3851, 2.1734): "Barcelona",
(37.3891, -5.9845): "Sevilla",
}
# Registro de usuario: tuple porque cada posición tiene un rol semántico
User = tuple[str, int, str] # (nombre, edad, email)
def create_user(name: str, age: int, email: str) -> User:
return (name, age, email)
def process_users(users: list[User]) -> None:
# list aquí porque es una colección homogénea de tamaño variable
for user in users:
# Unpacking idiomático: nombra los campos explícitamente
name, age, email = user
print(f"{name} ({age}) → {email}")
def classify_by_location(
events: list[tuple[float, float, str]]
) -> dict[tuple[float, float], list[str]]:
"""
Agrupa nombres de eventos por su coordenada (lat, lon).
La clave del dict ES una tuple porque lat+lon forman un registro fijo.
El valor ES una list porque la cantidad de eventos por punto varía.
"""
index: dict[tuple[float, float], list[str]] = {}
for lat, lon, event_name in events: # unpacking en el for, más limpio que [0],[1],[2]
key = (lat, lon)
if key not in index:
index[key] = []
index[key].append(event_name)
return index
# --- Demostración de la diferencia de tamaño en memoria ---
import sys
fixed_data_as_tuple = (1, 2, 3, 4, 5)
fixed_data_as_list = [1, 2, 3, 4, 5]
print(sys.getsizeof(fixed_data_as_tuple)) # ~80 bytes en CPython 3.12
print(sys.getsizeof(fixed_data_as_list)) # ~104 bytes — reserva capacidad extra
# --- Uso real ---
raw_events = [
(40.4168, -3.7038, "PyConES"),
(40.4168, -3.7038, "DjangoCon"),
(41.3851, 2.1734, "PyCat"),
]
grouped = classify_by_location(raw_events)
for coord, names in grouped.items():
city = CITIES.get(coord, "Ciudad desconocida")
print(f"{city}: {names}")
users = [
create_user("Ana", 32, "ana@mail.com"),
create_user("Luis", 28, "luis@mail.com"),
]
process_users(users)
Qué está pasando en cada decisión
La función classify_by_location es el ejemplo más claro del contraste: la clave del diccionario es (lat, lon) —una tuple— porque representa una ubicación, un registro con dos campos posicionales fijos. Si intentaras usar [lat, lon], Python lanzaría TypeError: unhashable type: 'list' en cuanto intentaras asignar index[key]. El valor del diccionario sí es una list porque la cantidad de eventos por coordenada no está definida de antemano.
El unpacking en el bucle for lat, lon, event_name in events no es solo azúcar sintáctico. Es la forma idiomática de comunicar que sabes exactamente cuántos campos hay y qué significa cada uno. Comparado con event[0], event[1], event[2], el unpacking hace que el código sea autoexplicativo y falla ruidosamente si la estructura cambia de forma inesperada —lo que es bueno: el error aparece donde está el problema.
sys.getsizeof revela algo interesante: la list reserva capacidad de sobra para futuros append. La tuple sabe desde el principio que nunca va a crecer, así que no necesita ese margen. Para datos que creas una vez y lees muchas veces —configuración, constantes, resultados de consultas que no vas a modificar— ese ahorro se multiplica.
Fíjate también en la anotación User = tuple[str, int, str]. Esto no es namedtuple ni dataclass, pero ya documenta la intención: hay tres campos, en ese orden, con esos tipos. Cuando el número de campos es pequeño y el código es interno, esta forma es suficiente. Cuando necesitas acceder a los campos por nombre en lugar de por índice y el tipo se va a usar en muchos sitios, el siguiente paso natural es collections.namedtuple o dataclasses.dataclass.
Errores que debes conocer
Error: intentar usar una lista como clave de diccionario porque “tiene los mismos datos que una tuple”.
# ❌ Wrong
cache = {}
key = [40.4168, -3.7038]
cache[key] = "Madrid" # TypeError: unhashable type: 'list'
# ✅ Right
cache = {}
key = (40.4168, -3.7038)
cache[key] = "Madrid"
Solo los objetos cuyo valor no puede cambiar después de crearse pueden ser hashables. Una lista puede mutar, así que Python se niega a usarla como clave.
Error: crear una tuple de un solo elemento sin la coma final, lo que da un tipo completamente diferente.
# ❌ Wrong single = (42) # Esto es int, no tuple — los paréntesis son solo agrupación print(type(single)) # <class 'int'> # ✅ Right single = (42,) # La coma es lo que hace la tuple, no los paréntesis print(type(single)) # <class 'tuple'>
Python necesita la coma para distinguir (expresión) de (elemento,). Es uno de los pocos casos donde la sintaxis de Python es ligeramente sorprendente.
Error: modificar “una tuple” reasignando la variable y pensar que es lo mismo que mutar una lista.
# ❌ Wrong — ilusión de mutación, en realidad creas objetos nuevos en cada paso
result = ()
for i in range(1000):
result = result + (i,) # O(n²): cada + crea una tuple completamente nueva
# ✅ Right — acumula en una list y convierte al final si necesitas una tuple fija
items = []
for i in range(1000):
items.append(i)
result = tuple(items) # Una sola conversión al terminar
Concatenar tuplas en un bucle tiene coste cuadrático porque cada operación copia todos los elementos anteriores; si necesitas construir una secuencia incremental, list + tuple() al final es la herramienta correcta.
N° 42