Un set es una colección de elementos únicos y no ordenados respaldada internamente por una hash table, la misma estructura que usa dict para sus claves. Eso tiene una consecuencia directa: verificar si algo está en un set con in cuesta O(1) en promedio, sin importar si el set tiene 10 o 10 millones de elementos. Una lista haría ese mismo trabajo en O(n), recorriendo elemento por elemento.
El diseño de la sintaxis tiene una trampa que vale la pena conocer desde el principio: {} sin contenido crea un dict vacío, no un set. Si quieres un set vacío, obligatoriamente debes escribir set(). Con elementos sí puedes usar la sintaxis literal: {1, 2, 3} es un set. Esto no es inconsistencia caprichosa — dict existía antes que la sintaxis de sets en Python, y el {} ya estaba reservado.
Un frozenset es exactamente lo mismo pero inmutable: una vez creado, no puedes añadir ni eliminar elementos. Esa inmutabilidad lo hace hashable, lo que significa que puedes usarlo como clave de un dict o meterlo dentro de otro set. Un set normal no puede ser elemento de otro set porque es mutable y por tanto no hashable.
¿Cuándo preferir un set sobre una lista? Cuando la unicidad importa (registrar qué usuarios ya visitaron una página, qué IDs ya procesaste) o cuando harás búsquedas frecuentes. Meter cien mil elementos en una lista y hacer x in lista repetidamente es un antipatrón clásico; convertirla en set primero cambia el perfil de rendimiento completamente.
El único riesgo real está en remove vs discard: remove lanza KeyError si el elemento no existe, discard lo ignora silenciosamente. Confundirlos cuando no tienes garantía de que el elemento está presente es el error más frecuente con esta estructura.
from typing import FrozenSet
# ── Creación ─────────────────────────────────────────────────────────────
tags_a: set[str] = {"python", "backend", "api"}
tags_b: set[str] = {"python", "frontend", "css"}
# set() es la única forma de crear un set vacío
visited: set[int] = set()
# Duplicados se eliminan en construcción — no hay error, simplemente colapsan
unique_ids: set[int] = set([101, 202, 101, 303, 202]) # → {101, 202, 303}
# ── Operaciones de conjunto ───────────────────────────────────────────────
# Unión: todos los tags de ambos artículos
all_tags = tags_a | tags_b # {'python', 'backend', 'api', 'frontend', 'css'}
# Intersección: tags compartidos
shared = tags_a & tags_b # {'python'}
# Diferencia: tags en A que no están en B
only_in_a = tags_a - tags_b # {'backend', 'api'}
# Diferencia simétrica: los que están en uno u otro, pero no en ambos
exclusive = tags_a ^ tags_b # {'backend', 'api', 'frontend', 'css'}
# ── Mutación ─────────────────────────────────────────────────────────────
active_users: set[str] = {"alice", "bob", "carol"}
active_users.add("dave") # añade si no existe; no-op si ya estaba
# remove lanza KeyError — úsalo cuando la ausencia sería un bug
active_users.remove("bob")
# discard nunca lanza — úsalo cuando la ausencia es un caso normal
active_users.discard("unknown_user")
# pop extrae un elemento arbitrario (no ordenado = no predecible cuál)
# Útil para procesar un set elemento a elemento sin importar el orden
while active_users:
user = active_users.pop()
print(f"Processing {user}")
# ── Búsqueda O(1) en la práctica ─────────────────────────────────────────
processed_order_ids: set[int] = set()
incoming_orders = [1001, 1002, 1001, 1003, 1002, 1004]
for order_id in incoming_orders:
if order_id in processed_order_ids: # O(1) — no recorre nada
print(f"Duplicate order {order_id}, skipping")
continue
# … procesar el pedido …
processed_order_ids.add(order_id)
# ── frozenset: inmutable y hashable ──────────────────────────────────────
REQUIRED_PERMISSIONS: FrozenSet[str] = frozenset({"read", "write", "execute"})
# Porque es hashable, puede ser clave de dict
role_cache: dict[FrozenSet[str], str] = {
frozenset({"read"}): "viewer",
frozenset({"read", "write"}): "editor",
frozenset({"read", "write", "execute"}): "admin",
}
user_perms = frozenset({"read", "write"})
print(role_cache.get(user_perms, "unknown")) # → 'editor'
# Un set normal lanzaría TypeError: unhashable type: 'set'
# role_cache[{"read"}] = "viewer" ← esto falla
Qué significa cada decisión en el código
El bloque de deduplicación con set([101, 202, 101, 303, 202]) muestra algo importante: la unicidad no es una operación que aplicas después, ocurre en el momento de construcción. No hay duplicados “latentes” que debas limpiar más tarde.
Los operadores |, &, -, ^ devuelven nuevos sets sin modificar los originales. Son la forma idiomática de trabajar con operaciones matemáticas de conjuntos. Cada uno también tiene una versión de método (union, intersection, difference, symmetric_difference) que acepta iterables, no solo otros sets — útil cuando recibes datos de una fuente que no es necesariamente un set.
El bucle con pop() es un patrón válido para consumir un set cuando el orden de procesamiento no importa. Fíjate que pop() en un set no tiene el comportamiento predecible de pop() en una lista (que siempre extrae el último); aquí extrae un elemento “cualquiera” — lo que en la práctica depende del orden interno de la hash table.
El ejemplo de frozenset como clave de diccionario ilustra exactamente por qué existe frozenset. El conjunto de permisos de un rol es conceptualmente inmutable — no tiene sentido que alguien modifique la clave mientras la usas como índice. Forzar inmutabilidad con frozenset hace que el modelo de datos sea más honesto con las intenciones del código.
Errores que debes conocer
Error: usar remove cuando no tienes garantía de que el elemento existe, provocando un KeyError inesperado.
# ❌ Wrong
pending = {"task_a", "task_b"}
pending.remove("task_c") # KeyError: 'task_c'
# ✅ Right
pending.discard("task_c") # silencioso si no existe
discard es la opción segura cuando la ausencia del elemento es un estado válido del programa; reserva remove para cuando su ausencia indicaría un bug que quieres detectar.
Error: intentar crear un set vacío con {}, obteniendo un dict en su lugar.
# ❌ Wrong
collection = {}
collection.add("item") # AttributeError: 'dict' object has no attribute 'add'
# ✅ Right
collection = set()
collection.add("item")
{} en Python significa “diccionario vacío” por razones históricas; set() es la única sintaxis correcta para un set sin elementos iniciales.
Error: poner un set dentro de otro set o usarlo como clave de dict.
# ❌ Wrong
matrix = {{"a", "b"}, {"c", "d"}} # TypeError: unhashable type: 'set'
# ✅ Right
matrix = {frozenset({"a", "b"}), frozenset({"c", "d"})}
Un set es mutable, por lo que no tiene hash definido. frozenset garantiza inmutabilidad y por eso Python le asigna un hash estable.
N° 45