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

  1. Crear diccionarios
  2. Acceder a valores: corchetes y get()
  3. Añadir, modificar y eliminar (del, pop)
  4. Las tres vistas: keys(), values(), items()
  5. Iterar sobre diccionarios
  6. Diccionarios anidados
  7. Comprensiones de diccionarios
  8. 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 detiene

Los 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)                           # None

Fí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.9

Son 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: campo

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

Despué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

  • KeyError por confiar en los corchetes: si la clave puede no existir (entrada de usuario), usa get() o comprueba con in antes. Reserva d[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 (del dentro del for) lanza RuntimeError. Recorre una copia de las claves: for t in list(catalogo):.
  • Confundir in sobre claves con in sobre valores: "12.50" in precios mira claves. Para valores: 12.50 in precios.values().
  • Olvidar que d2 = d1 es un alias: idéntico a las listas. Y copy() 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

  1. 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.
  2. Informe de reposición. Con el catálogo canónico completo (los 4 libros), construye mediante una comprensión de diccionario el dict a_reponer con los títulos cuyo stock sea menor que 5 y como valor cuántas unidades faltan hasta 5. Resultado esperado: {'La Odisea': 1, 'Fausto': 5}.
  3. Venta segura. Escribe vender(catalogo, buscado) que use buscar_libro() para localizar el título (tolerante a mayúsculas), devuelva None si el libro no existe o está agotado y, si hay stock, lo decremente y devuelva el precio de socio (convención de Papyrus, con DESCUENTO_SOCIO e IVA_LIBROS). Prueba con "hamlet" (debe devolver 9.83 y dejar el stock en 5) y con "fausto" (debe devolver None sin 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

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