Este es el momento que el curso lleva dos módulos preparando. Las listas paralelas de Papyrus obligaron a malabares con zip() e índices; la lista de tuplas de la lección anterior unió los datos de cada libro, pero seguía identificando los campos por posición y exigía recorrer todo el catálogo para encontrar un título. El diccionario (dict) resuelve ambas cosas: asocia claves con valores y permite acceder a cualquier valor directamente por su clave, sin bucles ni índices. Al final de esta lección ejecutaremos el gran refactor: el catálogo de Papyrus será, por fin, una sola estructura.
Contenido
- Crear diccionarios
- Acceder a valores: corchetes y
get() - Añadir, modificar y eliminar (
del,pop) - Las tres vistas:
keys(),values(),items() - Iterar sobre diccionarios
- Diccionarios anidados
- Comprensiones de diccionarios
- El gran refactor: el catálogo de Papyrus como
dict
Crear diccionarios
Un diccionario es una colección de pares clave → valor. Se escribe entre llaves, con clave: valor separados por comas:
precios = {
"La Odisea": 12.50,
"Hamlet": 9.95,
"El Quijote": 15.90,
}
vacio = {} # dict vacío (¡ojo: NO es un set vacío!)
tambien_vacio = dict()
desde_pares = dict([("Fausto", 21.00)]) # a partir de una lista de tuplas (clave, valor)
con_kwargs = dict(lunes=3, martes=1) # claves como keywords (solo si son identificadores)Reglas del juego:
- Las claves deben ser únicas e inmutables (hashables): cadenas, números, tuplas... nunca listas. Si repites una clave, la última asignación gana.
- Los valores pueden ser cualquier cosa: números, cadenas, listas, otros diccionarios.
- Desde Python 3.7 los diccionarios conservan el orden de inserción — recorrerlos devuelve las claves en el orden en que se añadieron.
Acceder a valores: corchetes y get()
print(precios["Hamlet"]) # 9.95 → acceso directo por clave, sin recorrer nada
print(precios["Drácula"]) # KeyError: 'Drácula' → la clave no existe, el programa se detieneLos corchetes son tajantes: clave inexistente, KeyError. Para consultas donde la ausencia es un caso normal (¿tenemos este libro?), get() devuelve None — o el valor por defecto que tú elijas — en vez de fallar:
print(precios.get("Drácula")) # None: no está, pero no explota
print(precios.get("Drácula", 0.0)) # 0.0: valor por defecto a tu medida
print(precios.get("Hamlet", 0.0)) # 9.95: si existe, get() devuelve el valor real
if "Fausto" in precios: # in comprueba CLAVES, no valores
print("Fichado")¿Recuerdas el centinela None de 03-02? get() es la misma filosofía integrada en el lenguaje.
| Situación | Usa |
|---|---|
| La clave debe existir; su ausencia es un bug | d[clave] (que falle pronto y claro) |
| La ausencia es un caso normal del negocio | d.get(clave) o d.get(clave, defecto) |
| Solo necesitas saber si está | clave in d |
Añadir, modificar y eliminar
Los diccionarios son mutables, como las listas — y con las mismas consecuencias de alias vs copia (d2 = d1 no copia; d1.copy() sí, superficialmente).
precios = {"La Odisea": 12.50, "Hamlet": 9.95, "El Quijote": 15.90}
precios["Fausto"] = 21.00 # AÑADIR: asignar a una clave nueva la crea
precios["Hamlet"] = 10.50 # MODIFICAR: asignar a una clave existente la sobreescribe
precios["Hamlet"] = 9.95 # (la dejamos como estaba)
del precios["Fausto"] # ELIMINAR: del borra el par; KeyError si no existe
precio = precios.pop("El Quijote") # pop: borra Y devuelve el valor
print(precio) # 15.9
rescate = precios.pop("Drácula", None) # con 2º argumento, no falla si no existe
print(rescate) # NoneFíjate en la simetría con las listas: pop() también "saca y devuelve", pero aquí por clave. La misma sintaxis de asignación sirve para crear y para modificar: no hay append() porque no hay posiciones, hay claves.
Las tres vistas: keys(), values(), items()
precios = {"La Odisea": 12.50, "Hamlet": 9.95, "El Quijote": 15.90}
print(precios.keys()) # dict_keys(['La Odisea', 'Hamlet', 'El Quijote'])
print(precios.values()) # dict_values([12.5, 9.95, 15.9])
print(precios.items()) # dict_items([('La Odisea', 12.5), ('Hamlet', 9.95), ...])
print(list(precios.keys())) # convertibles a lista cuando lo necesites
print(sum(precios.values())) # 38.35 → valor total del catálogo (a 1 ud.)
print(max(precios.values())) # 15.9Son vistas dinámicas, no copias: si el diccionario cambia, las vistas lo reflejan. Y observa items(): devuelve pares como tuplas (clave, valor) — las tuplas de la lección anterior aparecen por todas partes.
Iterar sobre diccionarios
# 1) Iterar directamente recorre las CLAVES
for titulo in precios:
print(titulo, "->", precios[titulo])
# 2) La forma idiomática: items() + desempaquetado de tupla
for titulo, precio in precios.items():
print(f"{titulo:<12} {precio:>6.2f} EUR")
# 3) Solo valores
total = 0.0
for precio in precios.values():
total += precio
# 4) Ordenado por precio: sorted + lambda sobre los items (03-03 en acción)
for titulo, precio in sorted(precios.items(), key=lambda par: par[1]):
print(titulo, precio)La forma 2 es la que verás (y escribirás) el 90 % de las veces: items() te da la tupla y el for la desempaqueta, exactamente como hacías con zip() — pero sin listas paralelas detrás.
Diccionarios anidados
Los valores pueden ser a su vez diccionarios. Es la pieza que nos faltaba para que la ficha de un libro tenga campos con nombre:
ficha_fausto = {"precio": 21.00, "stock": 0}
print(ficha_fausto["precio"]) # 21.0 → "precio" grita lo que es; libro[1] no decía nada
catalogo = {
"La Odisea": {"precio": 12.50, "stock": 4},
"Hamlet": {"precio": 9.95, "stock": 6},
}
print(catalogo["Hamlet"]["stock"]) # 6 → primer corchete: libro; segundo: campoEl acceso encadenado catalogo[titulo][campo] se lee de fuera hacia dentro. Con get() puedes encadenar con red: catalogo.get("Drácula", {}).get("stock", 0) devuelve 0 sin explotar.
Comprensiones de diccionarios
Las presentamos como aperitivo en el módulo 2; ahora son herramienta oficial. Sintaxis: {clave: valor for elemento in iterable}.
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
precios = {"La Odisea": 12.50, "Hamlet": 9.95, "El Quijote": 15.90, "Fausto": 21.00}
# Tarifa de socio calculada de golpe (convención de Papyrus)
tarifa_socio = {t: round(p * (1 - DESCUENTO_SOCIO) * (1 + IVA_LIBROS), 2)
for t, p in precios.items()}
print(tarifa_socio)
# {'La Odisea': 12.35, 'Hamlet': 9.83, 'El Quijote': 15.71, 'Fausto': 20.75}
# Con filtro: solo los libros de menos de 15 EUR
economicos = {t: p for t, p in precios.items() if p < 15}
# De dos listas paralelas a dict en una línea (nos servirá en el refactor)
titulos = ["La Odisea", "Hamlet"]
stocks = [4, 6]
inventario = {t: s for t, s in zip(titulos, stocks)} # {'La Odisea': 4, 'Hamlet': 6}El gran refactor: el catálogo de Papyrus como dict
Ha llegado el momento. Reunimos todas las piezas y reescribimos el corazón de Papyrus. Antes (módulo 3):
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
# Buscar = recorrer; mostrar = zip(); alta/baja = tocar 3 listas sin equivocarseDespués — la nueva estructura canónica del catálogo:
NOMBRE_TIENDA = "Papyrus"
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
catalogo = {
"La Odisea": {"precio": 12.50, "stock": 4},
"Hamlet": {"precio": 9.95, "stock": 6},
"El Quijote": {"precio": 15.90, "stock": 8},
"Fausto": {"precio": 21.00, "stock": 0},
}Cada libro es una sola pieza de información: la clave es el título y el valor, su ficha con campos nombrados. Veamos qué les pasa a las funciones de papyrus_utils.py y menu.py. buscar_libro() conserva su contrato del módulo 3 en espíritu — búsqueda sin distinguir mayúsculas, None si no está — pero ahora devuelve la clave canónica en lugar de un índice:
def buscar_libro(catalogo, buscado):
"""Devuelve el título tal y como figura en el catálogo, o None si no está.
La comparación ignora mayúsculas/minúsculas y espacios de los extremos.
"""
buscado = buscado.strip().lower()
for titulo in catalogo: # iterar un dict recorre sus claves
if titulo.lower() == buscado:
return titulo # la clave "oficial", lista para indexar
return None
def mostrar_catalogo(catalogo):
"""Imprime el catálogo alineado. Ya no necesita zip(): adiós, listas paralelas."""
print(f"— Catálogo de {NOMBRE_TIENDA} —")
for titulo, ficha in catalogo.items():
estado = "AGOTADO" if ficha["stock"] == 0 else f"{ficha['stock']} uds."
print(f"{titulo:<12} {ficha['precio']:>6.2f} EUR {estado}")Y las operaciones del día a día de Ana quedan así de directas:
# Consulta exacta: acceso directo, sin recorrer nada
print(catalogo["Fausto"]["precio"]) # 21.0
# Consulta tolerante (lo que teclea Luis en el menú)
clave = buscar_libro(catalogo, " el quijote ")
if clave is not None:
print(f"{clave}: {catalogo[clave]['precio']:.2f} EUR")
# Venta: un solo dato que actualizar (antes: buscar el índice y tocar la lista correcta)
if catalogo["El Quijote"]["stock"] > 0:
catalogo["El Quijote"]["stock"] -= 1
# Alta y baja: una operación, imposible desincronizar
catalogo["Ilíada"] = {"precio": 13.75, "stock": 2}
del catalogo["Ilíada"]graph TD
subgraph "Antes: 3 listas paralelas"
A["catalogo[2] = 'El Quijote'"] -.índice 2.- B["precios[2] = 15.90"]
A -.índice 2.- C["stocks[2] = 8"]
end
subgraph "Ahora: 1 diccionario"
D["'El Quijote'"] --> E["{'precio': 15.90, 'stock': 8}"]
end
Solo buscar_libro() sigue recorriendo, porque su tolerancia a mayúsculas lo exige; la consulta exacta es instantánea (en 04-04 verás por qué esa búsqueda por clave es tan rápida). precio_final(base, socio=False, iva=IVA_LIBROS) no cambia ni una línea: opera sobre el precio base, venga de donde venga — esa es la recompensa de haber separado negocio y datos en el módulo 3.
Errores Comunes y Consejos
KeyErrorpor confiar en los corchetes: si la clave puede no existir (entrada de usuario), usaget()o comprueba coninantes. Reservad[clave]para claves que garantizas.- Usar una lista como clave:
TypeError: unhashable type: 'list'. Las claves deben ser inmutables; si necesitas una clave compuesta, usa una tupla:("Hamlet", "tapa dura"). {}crea un diccionario, no un conjunto: lo veremos en la próxima lección, pero grábatelo ya.- Modificar un dict mientras lo recorres (
deldentro delfor) lanzaRuntimeError. Recorre una copia de las claves:for t in list(catalogo):. - Confundir
insobre claves coninsobre valores:"12.50" in preciosmira claves. Para valores:12.50 in precios.values(). - Olvidar que
d2 = d1es un alias: idéntico a las listas. Ycopy()es superficial: con fichas anidadas,copia["Hamlet"]sigue siendo la misma ficha interior. - Consejo de diseño: claves = identidad (el título), valores = atributos (precio, stock). Si te descubres buscando por valor constantemente, quizá elegiste mal la clave.
Ejercicios
- Migración completa. Escribe una función
montar_catalogo(titulos, precios, stocks)que reciba las tres listas paralelas históricas de Papyrus y devuelva el diccionario anidado canónico (clave = título →{"precio": ..., "stock": ...}). Úsala con los datos clásicos, añade después "Fausto" (21.00, stock 0) con una sola asignación e imprime el resultado. - Informe de reposición. Con el catálogo canónico completo (los 4 libros), construye mediante una comprensión de diccionario el dict
a_reponercon los títulos cuyostocksea menor que 5 y como valor cuántas unidades faltan hasta 5. Resultado esperado:{'La Odisea': 1, 'Fausto': 5}. - Venta segura. Escribe
vender(catalogo, buscado)que usebuscar_libro()para localizar el título (tolerante a mayúsculas), devuelvaNonesi el libro no existe o está agotado y, si hay stock, lo decremente y devuelva el precio de socio (convención de Papyrus, conDESCUENTO_SOCIOeIVA_LIBROS). Prueba con"hamlet"(debe devolver 9.83 y dejar el stock en 5) y con"fausto"(debe devolverNonesin tocar nada).
Soluciones
# Ejercicio 1
def montar_catalogo(titulos, precios, stocks):
"""Convierte las tres listas paralelas en el catálogo canónico."""
return {t: {"precio": p, "stock": s}
for t, p, s in zip(titulos, precios, stocks)} # zip, en su último servicio
catalogo = montar_catalogo(
["La Odisea", "Hamlet", "El Quijote"], [12.50, 9.95, 15.90], [4, 6, 8]
)
catalogo["Fausto"] = {"precio": 21.00, "stock": 0}
print(catalogo)Fíjate: la comprensión con zip() de tres listas es la ceremonia de despedida de las listas paralelas — se ejecuta una vez en la migración y no vuelven a verse.
# Ejercicio 2
a_reponer = {titulo: 5 - ficha["stock"]
for titulo, ficha in catalogo.items()
if ficha["stock"] < 5}
print(a_reponer) # {'La Odisea': 1, 'Fausto': 5}# Ejercicio 3
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def vender(catalogo, buscado):
"""Vende 1 ud. a precio de socio; None si no existe o está agotado."""
clave = buscar_libro(catalogo, buscado)
if clave is None:
return None
ficha = catalogo[clave]
if ficha["stock"] == 0:
return None
ficha["stock"] -= 1 # muta la ficha compartida: el catálogo lo refleja
return round(ficha["precio"] * (1 - DESCUENTO_SOCIO) * (1 + IVA_LIBROS), 2)
print(vender(catalogo, "hamlet")) # 9.83
print(catalogo["Hamlet"]["stock"]) # 5
print(vender(catalogo, "fausto")) # None (agotado)Error común: devolver False en vez de None para "no vendido". Sé consistente con el centinela None de 03-02: es la convención de todo el curso.
Conclusión
El gancho lanzado al final del módulo 3 queda cerrado: el catálogo de Papyrus ya no son tres listas que sincronizar a mano ni una lista que recorrer buscando títulos, sino un diccionario anidado — {título: {"precio": ..., "stock": ...}} — donde cada libro es una sola pieza de información con campos nombrados y acceso directo. Dominas la creación, el acceso con corchetes y get(), las altas/bajas con asignación, del y pop(), las vistas keys()/values()/items(), la iteración idiomática, el anidamiento y las comprensiones. buscar_libro() y mostrar_catalogo() se han reescrito sobre la nueva estructura sin cambiar su contrato, y precio_final() ni se ha enterado. Queda una pregunta en el aire: hemos dicho que consultar catalogo["Fausto"] es "instantáneo" — ¿por qué? La respuesta la comparte otra estructura basada en la misma magia, especializada en unicidad y pertenencia ultrarrápida: el conjunto, protagonista de la próxima lección.
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
