Skip to content
This page has been auto-translated and may contain errors.View in English

Diccionarios

Las listas te permiten buscar cosas por posición. Pero a menudo querés buscar algo por nombre. No "dame el elemento 3", sino "dame el puntaje de Sofía". Un diccionario almacena datos como pares clave-valor: buscás un valor por su clave, no por su posición.

Cuando el índice posicional de una lista no es significativo, un diccionario es la estructura adecuada. Los diccionarios mapean claves arbitrarias a valores, dándote búsqueda por nombre en tiempo O(1). Una tabla de posiciones, una respuesta JSON, un archivo de configuración: todos se expresan naturalmente como mapeos clave-valor.

dict es un almacén clave-valor respaldado por una tabla hash con búsqueda, inserción y eliminación promedio en O(1). Las claves deben ser hashables; los valores pueden ser cualquier objeto. Desde Python 3.7, los diccionarios preservan el orden de inserción. dict es la base para los espacios de nombres de Python, los atributos __dict__ de los objetos y los argumentos por palabra clave.

Crear un diccionario

Llaves con dos puntos entre cada clave y valor, y comas entre los pares. Las claves son casi siempre cadenas. Los valores pueden ser cualquier cosa: números, cadenas, otras listas, incluso otros diccionarios.

Los literales de diccionario usan llaves con la sintaxis key: value. Las claves pueden ser de cualquier tipo inmutable (hashable): cadenas, enteros, tuplas. Los valores pueden ser cualquier objeto de Python. Los diccionarios preservan el orden de inserción, así que cuando iterás, obtenés los elementos en el orden en que se agregaron.

Los literales de diccionario se evalúan de izquierda a derecha. Las claves deben ser hashables: str, int, tuple funcionan; list y dict no. Los valores no tienen restricciones. El orden de inserción está garantizado a partir de Python 3.7 (implementado como una tabla hash compacta desde 3.6). Las claves duplicadas en un literal usan silenciosamente el último valor.

python
player = {
    "name":  "Sofía",
    "score": 87,
    "level": 5,
    "alive": True,
}

Acceder a los valores

Usá corchetes con la clave para obtener el valor. Si la clave no existe, Python lanza un KeyError. Usá .get() cuando no estés seguro de que una clave esté: devuelve None en lugar de fallar, o un valor por defecto que especifiques.

El acceso con corchetes lanza KeyError con una clave faltante. .get(key) devuelve None ante una clave ausente. .get(key, default) devuelve el valor por defecto en su lugar. Usá .get() cuando la presencia de la clave sea incierta; es más seguro y más legible que envolver el acceso en un try/except.

d[key] llama a __getitem__, que hashea la clave y sondea la tabla: O(1) promedio. Ante una clave ausente lanza KeyError. .get(key, default=None) realiza el mismo sondeo pero devuelve el valor por defecto ante una clave ausente en lugar de lanzar una excepción. La verificación key in d (que llama a __contains__) es O(1) y es la forma idiomática de protegerse antes del acceso.

python
player = {"name": "Sofía", "score": 87}

player["name"]    # "Sofía"
player["score"]   # 87
player["lives"]   # KeyError (key doesn't exist)
python
player.get("score")          # 87
player.get("lives")          # None (no error, returns None by default)
player.get("lives", 3)       # 3   (use this default if key is absent)

.get() es más seguro siempre que una clave pueda faltar:

python
count = inventory.get("arrows", 0)   # 0 if "arrows" isn't in the dict

Agregar y actualizar

Asigná a una clave con corchetes. Si la clave ya existe, el valor se reemplaza. Si todavía no existe, se crea una nueva entrada. Usá .update() para combinar otro diccionario entero de una sola vez.

La asignación a una clave llama a __setitem__: O(1) promedio, crea o reemplaza. .update() acepta otro diccionario o un iterable de pares clave-valor y llama a __setitem__ por cada entrada, sobrescribiendo las claves existentes.

d[key] = value llama a __setitem__, que hashea la clave e inserta o sobrescribe en la tabla: O(1) promedio. .update(other) equivale a llamadas repetidas a __setitem__. El operador | (Python 3.9+) combina diccionarios sin mutación y devuelve un nuevo diccionario; |= muta en su lugar.

python
player = {"name": "Sofía", "score": 87}

player["score"] = 92        # update existing
player["level"] = 5         # add new key
python
extras = {"level": 5, "alive": True}
player.update(extras)   # adds/overwrites with keys from extras

Eliminar elementos

Cuatro formas de eliminar entradas. .pop() elimina una clave y te devuelve el valor. .pop() con un valor por defecto es seguro cuando la clave podría no estar. del elimina una clave sin valor de retorno. .clear() vacía todo el diccionario.

.pop(key) lanza KeyError ante una clave ausente. .pop(key, default) devuelve el valor por defecto en su lugar, convirtiéndolo en el método de eliminación seguro. del d[key] llama a __delitem__ y lanza KeyError ante una clave ausente. .clear() elimina todas las entradas pero mantiene el objeto diccionario en sí.

.pop(key, default) es un único sondeo hash: O(1) promedio. del d[key] llama a __delitem__, mismo sondeo, lanza ante clave ausente. Después de la eliminación, la tabla hash puede reducirse para liberar memoria. .clear() restablece el tamaño de la tabla. Iterar un diccionario y mutarlo en el mismo bucle lanza RuntimeError; primero armá una lista de claves a eliminar.

python
player = {"name": "Sofía", "score": 87, "level": 5}

player.pop("level")            # removes "level" and returns 5
player.pop("lives", None)      # safe pop, returns None if key absent
del player["score"]            # removes "score", no return value
player.clear()                 # removes everything

.pop() con un valor por defecto es la forma más segura de eliminar una clave que podría no existir.

Iterar

Tres vistas te permiten recorrer diferentes partes de un diccionario. Iterar solo el diccionario te da las claves. .values() da los valores. .items() da ambos a la vez y es lo que más vas a usar: desempaquetá cada par en dos nombres para bucles limpios y legibles.

.keys(), .values() e .items() devuelven objetos vista, no listas. Las vistas reflejan el estado actual del diccionario de forma dinámica: si modificás el diccionario, la vista se actualiza inmediatamente. .items() es la más útil para la mayoría de los bucles porque el desempaquetado de tuplas for k, v in d.items() se lee con claridad.

.keys(), .values() e .items() devuelven objetos vista dict_keys, dict_values y dict_items. Las vistas son perezosas: no copian datos y se actualizan cuando el diccionario subyacente cambia. dict_keys soporta álgebra de conjuntos (&, |, -) ya que las claves son únicas y hashables. Mutar un diccionario durante la iteración lanza RuntimeError; usá list(d.items()) para hacer una instantánea si es necesario.

python
player = {"name": "Sofía", "score": 87, "level": 5}

for key in player:               # iterate keys (most common)
    print(key)

for key in player.keys():        # same, explicit keys view
    print(key)

for value in player.values():    # just the values
    print(value)

for key, value in player.items():   # both, most useful
    print(f"{key}: {value}")

.items() es lo que más vas a usar. Desempaquetar cada par en dos nombres hace que el bucle sea legible.

Verificar pertenencia

in verifica si una clave existe en el diccionario. No verifica los valores, solo las claves. Para verificar si algo no está presente, usá not in.

in y not in llaman a __contains__, que es O(1) para diccionarios. Verifica solo las claves. Para verificar valores, usarías in d.values(), pero eso es O(n) ya que los valores no están indexados.

key in d llama a dict.__contains__, que hashea la clave y sondea la tabla: O(1) promedio. value in d.values() itera la vista de valores: O(n). Esta asimetría es una razón fundamental para preferir las claves de diccionario para búsqueda en vez de escanear valores.

python
player = {"name": "Sofía", "score": 87}

"name"  in player      # True
"lives" in player      # False
"lives" not in player  # True

in solo verifica las claves. Para verificar valores, usá in player.values(), aunque eso rara vez se necesita.

Diccionarios anidados

Los valores pueden ser diccionarios en sí mismos. Así es como representás datos estructurados con múltiples niveles: un jugador que tiene una sección de estadísticas, un archivo de configuración con subsecciones. Dos pares de corchetes acceden a un valor anidado: el primero elige la clave externa, el segundo elige la clave interna.

Los diccionarios anidados son diccionarios donde los valores son a su vez diccionarios. Se accede con subíndices encadenados. Mutar un diccionario interno afecta al diccionario externo porque el externo guarda una referencia al mismo objeto. Mantené el anidamiento poco profundo cuando sea posible: el anidamiento profundo se vuelve rápidamente difícil de leer y navegar.

Los diccionarios anidados almacenan referencias a objetos, no copias. La copia superficial del diccionario externo (d.copy()) no copia los diccionarios internos; las mutaciones a los diccionarios internos son visibles tanto a través del original como de la copia. Para estructuras profundamente anidadas, copy.deepcopy() crea copias totalmente independientes. Las llamadas encadenadas a __getitem__ son cada una O(1), por lo que la profundidad de acceso no tiene costo asintótico.

python
users = {
    "sofia":   {"score": 87, "level": 5},
    "mateo":   {"score": 74, "level": 3},
}

users["sofia"]["score"]   # 87
users["mateo"]["level"]   # 3

Se accede con corchetes encadenados. Para estructuras profundamente anidadas, esto puede volverse poco manejable, así que mantené el anidamiento poco profundo donde puedas.

setdefault

.setdefault() lee una clave si existe, o la establece a un valor por defecto si no, y luego devuelve el valor. Es útil cuando necesitás que una clave exista pero no querés sobrescribirla si ya está ahí.

.setdefault(key, default) es una operación atómica de leer-o-crear: si la clave existe, devuelve su valor actual sin cambiar nada; si no, inserta el valor por defecto y lo devuelve. El caso de uso común es construir estructuras agrupadas sin una verificación de existencia separada.

.setdefault(key, default) es un único sondeo hash: O(1) promedio. Si la clave está ausente, se inserta default y se devuelve. Si está presente, se devuelve el valor existente y default se ignora (nunca se evalúa después de la verificación). Para el patrón común de "agrupar elementos en listas", esta es la alternativa estándar a verificar key in d antes de hacer append.

python
inventory = {}

inventory.setdefault("arrows", 0)    # sets "arrows": 0, returns 0
inventory.setdefault("arrows", 10)   # "arrows" already exists, no change, returns 0

Es útil para construir estructuras agrupadas sin verificar primero la existencia de la clave:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)

collections.defaultdict y Counter

La biblioteca estándar tiene dos subclases de diccionario que manejan automáticamente patrones comunes. defaultdict crea un valor por defecto para las claves faltantes para que nunca tengas un KeyError. Counter cuenta con qué frecuencia aparece cada elemento en una secuencia y te da los resultados como un diccionario.

defaultdict toma un callable que produce el valor por defecto para nuevas claves, eliminando la necesidad de .setdefault(). Counter es un diccionario especializado para contar frecuencias con un método .most_common(). Ambos son subclases de dict, así que todas las operaciones estándar de diccionario funcionan en ellos.

defaultdict.__missing__ llama a la fábrica y almacena el resultado, haciéndolo thread-safe para el caso común. Counter hereda de dict y agrega .most_common(n) (O(n log n) vía heapq), .subtract() y operadores aritméticos para combinar conteos. Ambos están en collections; las importaciones se cubren en el capítulo Modules.

importación de collections

defaultdict y Counter necesitan importarse desde la biblioteca estándar. Las importaciones se cubren en el capítulo Modules.

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # no KeyError if team is new
python
from collections import Counter

words  = ["cat", "dog", "cat", "bird", "cat", "dog"]
counts = Counter(words)
# Counter({'cat': 3, 'dog': 2, 'bird': 1})

counts.most_common(2)   # [('cat', 3), ('dog', 2)]

Counter ahorra un montón de código repetitivo de "contar cosas en un bucle".

En la práctica

Construir un registro de puntajes e imprimir un resumen con todas las entradas:

python
scores = {"Sofía": 87, "Mateo": 74, "Camila": 92, "Diego": 55}

total   = sum(scores.values())
average = total / len(scores)

print(f"Players:  {len(scores)}")
print(f"Average:  {average:.1f}")
print(f"Highest:  {max(scores.values())}")
print(f"Lowest:   {min(scores.values())}")
print()

for name, score in scores.items():
    print(f"  {name}: {score}")

Construir un diccionario de resultados por archivo en un bucle, luego resumir todas las entradas:

python
job_results = {}
files       = ["report_jan.csv", "report_feb.csv", "report_mar.csv"]

for filename in files:
    size = len(filename) * 100   # placeholder for real file size
    if size < 2000:
        status = "ok"
    else:
        status = "large"
    job_results[filename] = {"size": size, "status": status}

ok_count    = 0
large_count = 0

for result in job_results.values():
    if result["status"] == "ok":
        ok_count += 1
    else:
        large_count += 1

print(f"Processed {len(job_results)} file(s): {ok_count} ok, {large_count} large")

Validar un diccionario de solicitud anidado iterando los campos requeridos, luego normalizar un diccionario de importancia de características en su lugar:

python
request = {
    "method":  "POST",
    "path":    "/users",
    "headers": {"Content-Type": "application/json"},
    "body":    {"username": "sofia", "email": "[email protected]"},
}

body   = request["body"]
errors = []

for field in ["username", "email"]:
    if not body.get(field):
        errors.append(f"Missing required field: {field}")

if "email" in body and "@" not in body["email"]:
    errors.append("Invalid email format")

print(f"Method: {request['method']} {request['path']}")
if errors:
    print(f"Errors: {errors}")
else:
    print("Validation passed")

# Normalise feature importance values to sum to 1
feature_importance = {"age": 0.34, "income": 0.28, "region": 0.15, "purchases": 0.23}
total = sum(feature_importance.values())

for key in feature_importance:
    feature_importance[key] = round(feature_importance[key] / total, 3)

print(f"Normalised: {feature_importance}")