Un dict en Python no es solo “un mapa de clave a valor”. Por debajo es una hash table — una estructura que te da acceso, inserción y borrado en tiempo O(1) promedio, independientemente de cuántos elementos tenga. Entender cómo funciona eso cambia la manera en que diseñas código.
Cuando escribes d["nombre"] = "Ana", Python no recorre una lista buscando dónde guardar ese dato. Llama a hash("nombre"), obtiene un número entero, y usa ese número para calcular directamente el índice dentro de un array interno. Al leer d["nombre"], hace exactamente lo mismo: calcula el hash, va al índice, devuelve el valor. Sin búsqueda lineal.
El problema que toda hash table tiene que resolver es el de colisiones: dos claves distintas que producen el mismo índice. CPython las resuelve con un esquema de perturbación — cuando una celda está ocupada, recalcula el siguiente candidato usando bits del hash original en lugar de simplemente avanzar al siguiente hueco. Eso evita que las colisiones se concentren en zonas del array y mantiene la distribución uniforme.
Para que todo esto funcione, las claves deben ser hashables: tipos inmutables como str, int, float, tuple (si su contenido también es inmutable). Una list no puede ser clave porque es mutable; si cambiaras la lista después de insertarla, su hash cambiaría y el dict perdería el dato.
Desde Python 3.7 el orden de inserción es parte del contrato del lenguaje — no un detalle de implementación. Si insertas "b" antes que "c", iterar el dict siempre te los devuelve en ese orden. Antes de 3.7 no podías contar con eso.
¿Cuándo usas un dict? Cuando necesitas asociar datos por una clave significativa (IDs, nombres, configuración), cuando buscas por clave frecuentemente, o cuando construyes índices sobre colecciones. Lo que rompe si te equivocas: usar una clave mutable (obtienes TypeError), asumir orden en Python < 3.7, o confundir que el O(1) es promedio — un hash muy malo puede degradar a O(n) en casos patológicos.
from __future__ import annotations
# ── Construcción ──────────────────────────────────────────────
# Constructor con keyword arguments: claro cuando las claves son identificadores válidos
config = dict(host="localhost", port=5432, debug=False)
# Literal: preferido en general
inventory = {"apples": 50, "bananas": 30, "cherries": 120}
# ── Acceso y escritura ────────────────────────────────────────
inventory["apples"] += 10 # actualiza en O(1)
# get() devuelve None (o el default) si la clave no existe, sin lanzar KeyError
stock = inventory.get("mangoes", 0)
# setdefault() inserta la clave con el default solo si no existe, y la devuelve
inventory.setdefault("mangoes", 0)
inventory["mangoes"] += 5
# ── Iteración: keys(), values(), items() ──────────────────────
# items() devuelve vistas ligeras, no copias — eficiente en memoria
for product, qty in inventory.items():
if qty < 20:
print(f"⚠️ Stock bajo: {product} ({qty} unidades)")
# ── pop() elimina y devuelve el valor ─────────────────────────
removed_qty = inventory.pop("bananas", None) # None evita KeyError si no existe
# ── update() fusiona otro dict (o iterable de pares) ─────────
restock = {"apples": 100, "grapes": 60}
inventory.update(restock) # sobreescribe "apples", añade "grapes"
# ── Operador | para merge (Python 3.9+) ──────────────────────
# | crea un dict NUEVO; |= actualiza en sitio
defaults = {"timeout": 30, "retries": 3}
overrides = {"timeout": 60}
final_config = defaults | overrides # {"timeout": 60, "retries": 3}
# ── Dict comprehensions ───────────────────────────────────────
# Construir un índice inverso: de cantidad a producto
# Útil cuando transformas o filtras datos al construir el dict
price_list = {"apples": 1.2, "cherries": 3.5, "grapes": 2.0}
# Solo productos que cuestan más de 1.5 €, con el precio redondeado
expensive = {
product: round(price, 2)
for product, price in price_list.items()
if price > 1.5
}
# {"cherries": 3.5, "grapes": 2.0}
# Invertir clave↔valor (asumiendo valores únicos)
inverted = {v: k for k, v in price_list.items()}
Lo que hace interesante cada decisión
El dict(host="localhost", port=5432) es azúcar sintáctico elegante, pero solo funciona cuando las claves son strings que también son identificadores Python válidos. En cuanto una clave tiene un espacio o empieza por número, necesitas el literal {}.
get() y setdefault() parecen similares pero tienen contratos distintos. get("mangoes", 0) es de solo lectura: consulta sin tocar el dict. setdefault("mangoes", 0) es una operación atómica de “inserta si no existe y devuélveme el valor” — muy útil cuando construyes estructuras anidadas o acumuladores, porque evita el patrón verbose de comprobar-y-luego-insertar.
Las vistas que devuelven keys(), values() e items() no son listas: son objetos que reflejan el estado actual del dict en tiempo real. Si añades una clave al dict después de obtener la vista, la vista la incluirá. Por eso iterar y modificar el mismo dict simultáneamente lanza RuntimeError — la vista detecta que el tamaño cambió.
El operador | (Python 3.9+) es deliberadamente no destructivo: siempre crea un dict nuevo. Eso lo hace seguro para combinar configuraciones sin mutar las originales. |= existe cuando sí quieres la mutación in-place, equivalente semántico de update() pero más expresivo.
Las dict comprehensions siguen el mismo modelo mental que las list comprehensions que ya conoces, con la diferencia de que produces pares clave: valor. La clave no tiene por qué ser el elemento original — puedes transformarla, extraerla de una tupla, o calcularla. El ejemplo de invertir clave↔valor ({v: k for k, v in ...}) es un patrón clásico para construir índices de búsqueda inversa.
Errores que debes conocer
Error: usar una clave mutable como list provoca TypeError inmediatamente porque las listas no implementan __hash__.
# ❌ Wrong
d = {}
d[[1, 2, 3]] = "valor" # TypeError: unhashable type: 'list'
# ✅ Right
d = {}
d[(1, 2, 3)] = "valor" # tuple es inmutable y hashable
Convierte la lista a tuple antes de usarla como clave; si necesitas ordenación variable, considera usar frozenset.
Error: acceder directamente con [] cuando la clave puede no existir, lo que lanza KeyError y rompe el flujo.
# ❌ Wrong user_input = "mangoes" qty = inventory[user_input] # KeyError si no está # ✅ Right qty = inventory.get(user_input, 0) # devuelve 0 en lugar de explotar
get() con un valor por defecto es la forma idiomática de manejar claves opcionales sin try/except.
Error: asumir que | modifica el dict original cuando en realidad crea uno nuevo.
# ❌ Wrong
settings = {"debug": False}
settings | {"debug": True} # el resultado se descarta; settings no cambia
# ✅ Right
settings |= {"debug": True} # o: settings = settings | {"debug": True}
Si necesitas la mutación, usa |= o update(); si necesitas preservar el original, usa | y asigna el resultado.
El hecho de que hash() sea determinista dentro de una ejecución pero no entre ejecuciones distintas (Python aleatoriza el seed por seguridad desde 3.3) es la razón por la que nunca debes serializar el orden de un dict basándote en sus hashes — usa sorted() explícito si el orden importa fuera del proceso.
N° 43