Tuplas en Python: inmutabilidad como contrato de diseño
- Qué es una tupla y por qué existe
- Crear tuplas: sintaxis y casos especiales
- Acceso e iteración
- Inmutabilidad: qué significa realmente
- Desempaquetado: la superpotencia de las tuplas
- Retorno múltiple de funciones
- NamedTuple: tuplas con campos con nombre
- Rendimiento: tupla vs lista
- Cuándo usar tupla y cuándo lista
- Errores clásicos
- Preguntas frecuentes
(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
🔒 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
↩️ 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)
⚡ 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
🧭 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.
💬 Foro de discusión
¿Tienes dudas sobre Tuplas en Python: inmutabilidad como contrato de diseño? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!