En la lección anterior viste que la mutabilidad de las listas es un arma de doble filo: potente para gestionar el stock cambiante de Papyrus, peligrosa cuando un alias modifica datos que creías a salvo. Python ofrece la contrapartida exacta: la tupla, una secuencia ordenada e inmutable. Además de proteger datos que no deben cambiar, la tupla es el mecanismo con el que Python devuelve varios valores desde una función — la promesa que dejamos apuntada en 03-02 y que hoy saldamos — y el primer paso serio para que cada libro de Papyrus sea una sola pieza de información en vez de una fila repartida en tres listas.
Contenido
- Crear tuplas (incluida la tupla de un elemento)
- Inmutabilidad: qué significa y para qué sirve
- Desempaquetado, también con
*para "el resto" - Retorno múltiple de funciones
- Tuplas como registros ligeros: el libro como tupla
- Aperitivo:
namedtuple - Tuplas vs listas: criterio de elección
Crear tuplas
Una tupla se escribe con paréntesis... aunque en realidad lo que la crea es la coma:
libro = ("La Odisea", 12.50, 4) # tupla de 3 elementos: la ficha de un libro
punto = 3, 5 # ¡también es una tupla! los paréntesis son opcionales
vacia = () # tupla vacía (aquí sí hacen falta los paréntesis)
tambien_vacia = tuple()
desde_lista = tuple([4, 6, 8]) # tuple() convierte cualquier iterableLa trampa clásica es la tupla de un solo elemento: sin coma final no hay tupla.
no_es_tupla = ("Fausto") # esto es solo un str entre paréntesis
si_es_tupla = ("Fausto",) # la coma final la convierte en tupla
tambien = "Fausto", # incluso sin paréntesis
print(type(no_es_tupla)) # <class 'str'>
print(type(si_es_tupla)) # <class 'tuple'>Como secuencias que son, las tuplas comparten casi toda la mecánica de la lección anterior: indexación positiva y negativa, slicing, len(), in/not in, count() e index():
libro = ("La Odisea", 12.50, 4)
print(libro[0]) # La Odisea
print(libro[-1]) # 4
print(libro[:2]) # ('La Odisea', 12.5) → el slice de una tupla es otra tupla
print(12.50 in libro) # TrueInmutabilidad: qué significa y para qué sirve
Una vez creada, la tupla no admite cambios: ni asignar por índice, ni append, ni remove, ni sort — esos métodos sencillamente no existen.
libro = ("La Odisea", 12.50, 4)
libro[2] = 3 # TypeError: 'tuple' object does not support item assignment¿Para qué querría Ana algo que "no se puede tocar"? Precisamente para eso:
- Protección frente a errores: los datos fijos de la tienda (nombre, coordenadas, la pareja descuento/IVA) no pueden corromperse por accidente. Si algún código intenta modificarlos, Python avisa al instante.
- Comunicación de intención: quien lee
("La Odisea", 12.50, 4)entiende que es un registro cerrado, no una colección que crece. - Claves de diccionario y elementos de conjunto: al ser inmutables (y por tanto hashables), las tuplas pueden usarse donde las listas no — lo aprovecharás en 04-03 y 04-04.
- Ligereza: Python puede optimizarlas mejor que a las listas.
Un matiz honesto: la inmutabilidad es superficial. Si una tupla contiene una lista, la tupla no puede cambiar qué objetos contiene, pero la lista interior sigue siendo mutable:
config = ("Papyrus", [4, 6, 8]) # tupla con una lista dentro
config[1].append(0) # legal: mutamos la lista, no la tupla
print(config) # ('Papyrus', [4, 6, 8, 0])Desempaquetado, también con *
El desempaquetado asigna los elementos de una tupla a varias variables de golpe. Ya lo usaste sin nombrarlo con enumerate() y zip(); ahora lo vemos como ciudadano de primera:
libro = ("La Odisea", 12.50, 4)
titulo, precio, stock = libro # tres variables en una línea
print(f"{titulo}: {precio:.2f} EUR ({stock} uds.)")
# Intercambiar variables sin auxiliar: el idiom más famoso de Python
a, b = 10, 20
a, b = b, a # (b, a) es una tupla que se desempaquetaEl número de variables debe coincidir con el de elementos... salvo que uses * para capturar "el resto" en una lista:
ventas_semana = (3, 1, 0, 2, 5, 7, 4) # unidades vendidas de lunes a domingo
lunes, *entre_semana, domingo = ventas_semana
print(lunes) # 3
print(entre_semana) # [1, 0, 2, 5, 7] ← siempre una LISTA, aunque venga de una tupla
print(domingo) # 4
primero, *resto = ("La Odisea", "Hamlet", "El Quijote", "Fausto")
print(primero) # La Odisea
print(resto) # ['Hamlet', 'El Quijote', 'Fausto']Solo puede haber un * por desempaquetado. Reconocerás la idea de *args (03-02): es el mismo mecanismo, ahora en el lado de la asignación.
Retorno múltiple de funciones
Aquí saldamos la deuda de 03-02. Cuando una función "devuelve varios valores", en realidad devuelve una tupla que luego se desempaqueta. Añadamos a nuestro repertorio una función de ticket para Papyrus:
IVA_LIBROS = 0.04 # IVA ficticio del curso
DESCUENTO_SOCIO = 0.05
def desglose_venta(base, socio=False):
"""Devuelve (base, descuento, iva, total) de una venta, en euros."""
descuento = round(base * DESCUENTO_SOCIO, 2) if socio else 0.0
subtotal = base - descuento
iva = round(subtotal * IVA_LIBROS, 2)
total = round(base * (1 - DESCUENTO_SOCIO) * (1 + IVA_LIBROS), 2) if socio \
else round(base * (1 + IVA_LIBROS), 2)
return base, descuento, iva, total # sin paréntesis: la coma crea la tupla
# Luis (socio) compra "El Quijote" (15.90 EUR de base)
resultado = desglose_venta(15.90, socio=True)
print(resultado) # (15.9, 0.8, 0.63, 15.71) → una única tupla
print(type(resultado)) # <class 'tuple'>
base, dto, iva, total = desglose_venta(15.90, socio=True) # desempaquetado directo
print(f"Total con carnet de socio: {total:.2f} EUR") # 15.71 EUR, como siempreObserva las dos caras: quien llama puede quedarse la tupla entera (resultado) o desempaquetarla al vuelo. Si solo te interesa una parte, el guion bajo es la convención para "esto lo ignoro":
*_, total = desglose_venta(12.50, socio=True)
print(total) # 12.35 → el precio de socio de "La Odisea", verificado en el módulo 1Tuplas como registros ligeros: el libro como tupla
Y aquí llega el movimiento estratégico del módulo. Las tres listas paralelas pueden convertirse en una sola lista de tuplas, donde cada tupla es la ficha completa de un libro:
# Antes: tres listas que sincronizar a mano
# catalogo = ["La Odisea", "Hamlet", "El Quijote"]; precios = [...]; stocks = [...]
# Ahora: una lista de registros (titulo, precio, stock)
catalogo = [
("La Odisea", 12.50, 4),
("Hamlet", 9.95, 6),
("El Quijote", 15.90, 8),
("Fausto", 21.00, 0),
]
for titulo, precio, stock in catalogo: # desempaquetado en el for: adiós zip()
estado = "AGOTADO" if stock == 0 else f"{stock} uds."
print(f"{titulo:<12} {precio:>6.2f} EUR {estado}")Compara con el zip(catalogo, precios, stocks) del módulo 3: el resultado es el mismo, pero ya no hay tres estructuras que puedan desincronizarse — dar de alta "Fausto" es un único catalogo.append(("Fausto", 21.00, 0)). La lista exterior es mutable (el catálogo crece y mengua); cada ficha es inmutable (los datos de un libro viajan juntos y protegidos). ¿Y si baja el stock? Al ser inmutable, no se modifica la tupla: se reemplaza por otra:
Ese reemplazo completo es incómodo, y además libro[1] sigue siendo un índice mudo: nada grita "soy el precio". Nos quedan dos mejoras por delante.
Aperitivo: namedtuple
La primera mejora existe ya en la biblioteca estándar (módulo collections, del que hablamos en 03-05): tuplas cuyos campos tienen nombre.
from collections import namedtuple
Libro = namedtuple("Libro", ["titulo", "precio", "stock"])
odisea = Libro("La Odisea", 12.50, 4)
print(odisea.precio) # 12.5 → por nombre, no por índice mudo
print(odisea[1]) # 12.5 → sigue siendo una tupla normal
titulo, precio, stock = odisea # y se desempaqueta igualQuédate con la idea; la desarrollaremos a fondo en 04-06. La segunda mejora — buscar un libro por título sin recorrer nada — es la lección 04-03.
Tuplas vs listas: criterio de elección
| Criterio | Lista | Tupla |
|---|---|---|
| Mutabilidad | Mutable: crece, mengua, se reordena | Inmutable: se crea de una pieza |
| Uso típico | Colección homogénea de tamaño variable (títulos, cola de pedidos) | Registro heterogéneo de estructura fija (título + precio + stock) |
| Métodos | Muchos (append, sort, remove...) |
Solo count() e index() |
| ¿Clave de dict / elemento de set? | No (no es hashable) | Sí |
| Retorno múltiple de funciones | Posible pero inusual | El idiom estándar |
| Sintaxis | [1, 2, 3] |
(1, 2, 3) o 1, 2, 3 |
Regla mnemotécnica: lista = "muchas cosas del mismo tipo"; tupla = "una cosa con varias partes". El catálogo (muchos libros) es una lista; cada libro (título, precio, stock) es una tupla.
Errores Comunes y Consejos
- Olvidar la coma en la tupla de un elemento:
("Fausto")es unstr. Si una función espera una tupla y le pasas eso, los errores aparecen lejos del origen. Escribe("Fausto",). - Intentar mutar una tupla (
libro[2] -= 1) lanzaTypeError. No es un defecto: es la tupla haciendo su trabajo. Si necesitas cambiar valores a menudo, esa pieza pedía ser otra estructura (spoiler: un diccionario). - Desempaquetado descuadrado:
a, b = (1, 2, 3)lanzaValueError: too many values to unpack. Cuenta las variables, o usa*restosi la longitud es variable. - Creer que
*restodevuelve una tupla: siempre produce una lista, venga de donde venga. - Tupla "inmutable" con listas dentro: la inmutabilidad no se propaga al interior. Si necesitas un registro de verdad congelado, evita meter listas en él.
- Consejo: cuando una función tuya empiece a devolver 4-5 valores, plantéate si no está pidiendo un
namedtuple(04-06) o un diccionario (04-03) — desempaquetar 5 posiciones de memoria es frágil.
Ejercicios
- Ticket con retorno múltiple. Escribe
linea_ticket(titulo, base, socio=False)que devuelva la tupla(titulo, total, ahorro)dondetotales el precio final (convención de Papyrus:base * (1 - DESCUENTO_SOCIO) * (1 + IVA_LIBROS)si es socio,base * (1 + IVA_LIBROS)si no, redondeado a 2 decimales) yahorrola diferencia entre no ser socio y serlo. Desempaqueta el resultado para "Hamlet" (9.95) siendo socio e imprime una línea de ticket con f-string. - Migración a lista de tuplas. Parte de las tres listas paralelas clásicas de Papyrus y construye con
zip()la lista de tuplascatalogo_fichas. Después añade "Fausto" (21.00, 0) con un soloappende imprime el catálogo con desempaquetado en elfor, marcando los agotados. - El más caro y el más barato. Sobre
catalogo_fichasdel ejercicio 2, usamax()ymin()conkey=lambda libro: libro[1](repaso de 03-03) para obtener la ficha del libro más caro y la del más barato, y desempaquétalas para imprimir solo sus títulos.
Soluciones
# Ejercicio 1
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def linea_ticket(titulo, base, socio=False):
"""Devuelve (titulo, total, ahorro) de una línea de venta."""
total_normal = round(base * (1 + IVA_LIBROS), 2)
total_socio = round(base * (1 - DESCUENTO_SOCIO) * (1 + IVA_LIBROS), 2)
total = total_socio if socio else total_normal
ahorro = round(total_normal - total_socio, 2)
return titulo, total, ahorro
titulo, total, ahorro = linea_ticket("Hamlet", 9.95, socio=True)
print(f"{titulo}: {total:.2f} EUR (ahorras {ahorro:.2f} EUR por ser socio)")
# Hamlet: 9.83 EUR (ahorras 0.52 EUR por ser socio)# Ejercicio 2
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
catalogo_fichas = list(zip(catalogo, precios, stocks)) # zip une; list materializa
catalogo_fichas.append(("Fausto", 21.00, 0)) # un solo alta, imposible desincronizar
for titulo, precio, stock in catalogo_fichas:
estado = "AGOTADO" if stock == 0 else f"{stock} uds."
print(f"{titulo:<12} {precio:>6.2f} EUR {estado}")Consejo: zip() devuelve tuplas de por sí — la migración de listas paralelas a lista de tuplas es literalmente un list(zip(...)). Es la última vez que necesitaremos zip() para el catálogo.
# Ejercicio 3
mas_caro = max(catalogo_fichas, key=lambda libro: libro[1])
mas_barato = min(catalogo_fichas, key=lambda libro: libro[1])
titulo_caro, *_ = mas_caro
titulo_barato, *_ = mas_barato
print(f"Más caro: {titulo_caro} | Más barato: {titulo_barato}")
# Más caro: Fausto | Más barato: HamletError común aquí: escribir key=libro[1] sin lambda — libro no existe fuera de la lambda; el key necesita una función, no un valor.
Conclusión
Las tuplas completan a las listas con la pieza que les faltaba: inmutabilidad. Sabes crearlas (recordando la coma del elemento único), desempaquetarlas — también con *resto — y has visto que el "retorno múltiple" de las funciones de Python siempre fue una tupla disfrazada, saldando la promesa de 03-02. Sobre todo, el catálogo de Papyrus ha dado su primer gran paso: de tres listas paralelas a una lista de tuplas donde cada libro viaja como una unidad, con namedtuple apuntada como mejora para 04-06. Pero sigue habiendo dos molestias: los campos se identifican por índices mudos y encontrar un libro exige recorrer la lista entera. La próxima lección presenta la estructura reina de Python, el diccionario, y con ella ejecutaremos el gran refactor que el módulo 3 dejó anunciado: cada libro accesible directamente por su título.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
