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

  1. Crear tuplas (incluida la tupla de un elemento)
  2. Inmutabilidad: qué significa y para qué sirve
  3. Desempaquetado, también con * para "el resto"
  4. Retorno múltiple de funciones
  5. Tuplas como registros ligeros: el libro como tupla
  6. Aperitivo: namedtuple
  7. 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 iterable

La 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) # True

Inmutabilidad: 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 desempaqueta

El 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 siempre

Observa 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 1

Tuplas 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:

catalogo[2] = ("El Quijote", 15.90, 7)   # Luis compra un ejemplar

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 igual

Qué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)
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 un str. 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) lanza TypeError. 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) lanza ValueError: too many values to unpack. Cuenta las variables, o usa *resto si la longitud es variable.
  • Creer que *resto devuelve 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

  1. Ticket con retorno múltiple. Escribe linea_ticket(titulo, base, socio=False) que devuelva la tupla (titulo, total, ahorro) donde total es 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) y ahorro la 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.
  2. Migración a lista de tuplas. Parte de las tres listas paralelas clásicas de Papyrus y construye con zip() la lista de tuplas catalogo_fichas. Después añade "Fausto" (21.00, 0) con un solo append e imprime el catálogo con desempaquetado en el for, marcando los agotados.
  3. El más caro y el más barato. Sobre catalogo_fichas del ejercicio 2, usa max() y min() con key=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: Hamlet

Error común aquí: escribir key=libro[1] sin lambdalibro 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados