La lección anterior terminó con una pregunta: ¿por qué consultar catalogo["Fausto"] es prácticamente instantáneo? La respuesta vive en una estructura hermana del diccionario: el conjunto (set). Un conjunto es una colección sin orden garantizado, sin duplicados y sin valores asociados — solo elementos, como las claves de un diccionario sin sus valores. A cambio de renunciar al orden y a la repetición, ofrece dos superpoderes: comprobar pertenencia a toda velocidad y operar con la lógica de la teoría de conjuntos (unión, intersección, diferencia) que en Papyrus resolverá preguntas como "¿qué socios compraron este mes pero no el pasado?" en una sola línea.

Contenido

  1. Crear conjuntos (y la trampa del {} vacío)
  2. Unicidad: los duplicados desaparecen
  3. Añadir y eliminar elementos
  4. Pertenencia ultrarrápida: por qué in vuela
  5. Operaciones de conjuntos: unión, intersección, diferencias
  6. Subconjuntos y superconjuntos
  7. frozenset: el conjunto inmutable
  8. Comprensiones de conjuntos

Crear conjuntos (y la trampa del {} vacío)

generos = {"novela", "teatro", "poesía"}          # literal: llaves con elementos sueltos
desde_lista = set(["novela", "teatro", "novela"]) # set() convierte iterables y DEDUPLICA
print(desde_lista)                                 # {'novela', 'teatro'} → el duplicado se fue

letras = set("papyrus")     # {'p', 'a', 'y', 'r', 'u', 's'} → la 'p' repetida, fuera

vacio = set()               # ÚNICA forma de crear un conjunto vacío
trampa = {}                 # ¡esto es un DICCIONARIO vacío, no un set!
print(type(vacio), type(trampa))   # <class 'set'> <class 'dict'>

La trampa del {} es histórica: los diccionarios llegaron antes a Python y se quedaron con las llaves vacías. Memorízalo: conjunto vacío = set(), siempre.

Los elementos deben ser inmutables (hashables), como las claves de un dict: cadenas, números y tuplas sí; listas y diccionarios no. Y no hay indexación: generos[0] es un TypeError, porque un conjunto no tiene posiciones.

Unicidad: los duplicados desaparecen

La unicidad no es una restricción molesta: es la funcionalidad estrella. El caso de uso más frecuente del mundo real — eliminar duplicados de una lista — es una línea:

# Pedidos del día en Papyrus: Luis ha pedido El Quijote dos veces por error
pedidos = ["El Quijote", "Hamlet", "El Quijote", "Fausto", "Hamlet"]

titulos_unicos = set(pedidos)
print(titulos_unicos)              # {'Fausto', 'Hamlet', 'El Quijote'} (orden no garantizado)
print(len(titulos_unicos))         # 3 títulos distintos, aunque hubo 5 pedidos

# Si necesitas conservar una lista (p. ej., ordenada), convierte de vuelta:
unicos_ordenados = sorted(set(pedidos))
print(unicos_ordenados)            # ['El Quijote', 'Fausto', 'Hamlet']

Cuidado: set() no conserva el orden original de la lista. Si el orden de aparición importa, hay un idiom con dict que verás en los ejercicios.

Añadir y eliminar elementos

Los conjuntos son mutables (existe versión inmutable: frozenset, más abajo):

generos = {"novela", "teatro"}

generos.add("poesía")            # añade un elemento (si ya está, no pasa nada)
generos.add("teatro")            # sin efecto ni error: ya estaba
generos.update(["ensayo", "novela"])   # añade varios de golpe (como extend en listas)

generos.remove("ensayo")         # elimina; KeyError si no existe
generos.discard("ciencia")       # elimina si está; SILENCIO si no está
elemento = generos.pop()         # saca un elemento ARBITRARIO (no "el último": no hay orden)
generos.clear()                  # vacía el conjunto
Método Si el elemento no está...
remove(x) Lanza KeyError
discard(x) No hace nada (la opción tranquila)

Pertenencia ultrarrápida: por qué in vuela

Con una lista, x in lista obliga a Python a comparar elemento a elemento, desde el principio: si la lista tiene un millón de entradas, en el peor caso hace un millón de comparaciones. Con un conjunto (o las claves de un dict), Python calcula el hash del elemento — una huella numérica — y salta directamente a la casilla interna donde estaría si existiera. Una comprobación, tenga el conjunto diez elementos o diez millones.

socios = {"LUIS-001", "MARTA-002", "PAU-003"}   # códigos de socio de Papyrus

def es_socio_valido(codigo):
    """La función de papyrus_utils, ahora respaldada por un set: validación inmediata."""
    return codigo.strip().upper() in socios

print(es_socio_valido("luis-001"))   # True
print(es_socio_valido("ANA-999"))    # False

Esta es también la respuesta a la pregunta de 04-03: catalogo["Fausto"] es instantáneo porque las claves de un diccionario usan exactamente el mismo mecanismo de hash. No necesitas la teoría formal de complejidad (llegará si algún día la buscas como "O(1) vs O(n)"); quédate con la regla práctica:

Muchas comprobaciones de "¿está o no está?" sobre la misma colección → conviértela en set primero.

Operaciones de conjuntos: unión, intersección, diferencias

Aquí los conjuntos brillan con luz propia. Cada operación existe como operador y como método (el método acepta cualquier iterable; el operador exige dos sets):

Operación Operador Método Resultado
Unión a | b a.union(b) Elementos en a o en b (o ambos)
Intersección a & b a.intersection(b) Elementos en a y en b
Diferencia a - b a.difference(b) En a pero no en b
Diferencia simétrica a ^ b a.symmetric_difference(b) En a o en b, pero no en ambos

Con datos de Papyrus: Ana quiere analizar la fidelidad de sus socios comparando los compradores de junio y julio:

compraron_junio = {"Luis", "Marta", "Pau", "Júlia"}
compraron_julio = {"Luis", "Júlia", "Omar"}

print(compraron_junio | compraron_julio)   # {'Luis','Marta','Pau','Júlia','Omar'} → clientela activa
print(compraron_junio & compraron_julio)   # {'Luis', 'Júlia'} → los fieles: repiten mes a mes
print(compraron_junio - compraron_julio)   # {'Marta', 'Pau'} → compraron en junio y julio NO: ¿recuperarlos?
print(compraron_julio - compraron_junio)   # {'Omar'} → cliente nuevo en julio
print(compraron_junio ^ compraron_julio)   # {'Marta','Pau','Omar'} → solo un mes de los dos

Cinco preguntas de negocio, cinco líneas, cero bucles. La diferencia no es conmutativa (a - b != b - a); unión, intersección y diferencia simétrica sí lo son.

Otro uso natural: los géneros de cada libro como conjuntos, para responder "¿qué libros comparten género?":

generos_libro = {
    "La Odisea": {"clásico", "épica"},
    "Hamlet": {"clásico", "teatro", "tragedia"},
    "Fausto": {"clásico", "teatro"},
}
comunes = generos_libro["Hamlet"] & generos_libro["Fausto"]
print(comunes)     # {'clásico', 'teatro'}

Subconjuntos y superconjuntos

tragedias = {"Hamlet", "Fausto"}
clasicos = {"La Odisea", "Hamlet", "El Quijote", "Fausto"}

print(tragedias <= clasicos)          # True: tragedias es SUBconjunto de clasicos
print(tragedias.issubset(clasicos))   # equivalente con método
print(clasicos >= tragedias)          # True: clasicos es SUPERconjunto
print(tragedias.isdisjoint({"Ilíada"}))  # True: no comparten ningún elemento

Útil para validaciones: "¿todos los títulos del pedido están en el catálogo?" es set(pedido) <= set(catalogo) — y sí, set(catalogo) sobre el dict de 04-03 extrae sus claves.

frozenset: el conjunto inmutable

La misma relación que tupla/lista: frozenset es un conjunto congelado. No admite add() ni remove(), y a cambio es hashable — puede ser clave de diccionario o elemento de otro conjunto:

PACK_CLASICOS = frozenset({"La Odisea", "Hamlet"})   # oferta fija de la tienda

ofertas = {
    PACK_CLASICOS: 19.90,        # un frozenset como clave de dict: legal
}
print(ofertas[frozenset({"Hamlet", "La Odisea"})])   # 19.9 → el orden no importa

Con un set normal como clave obtendrías TypeError: unhashable type: 'set'. Uso breve y ocasional, pero cuando lo necesitas, nada más lo sustituye.

Comprensiones de conjuntos

Tercera comprensión de la familia (lista → dict → set). Sintaxis: llaves sin dos puntos:

pedidos = ["  hamlet ", "FAUSTO", "Hamlet", "el quijote"]

# Títulos normalizados y únicos, de golpe (la normalización a fondo, en 04-05)
unicos = {p.strip().lower() for p in pedidos}
print(unicos)     # {'hamlet', 'fausto', 'el quijote'} → 4 pedidos, 3 títulos reales

# Sobre el catálogo canónico de 04-03: títulos con stock
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},
}
disponibles = {t for t, ficha in catalogo.items() if ficha["stock"] > 0}
print(disponibles)   # {'La Odisea', 'Hamlet', 'El Quijote'}
Comprensión Sintaxis Produce
De lista [x for x in it] Lista (ordenada, con duplicados)
De diccionario {k: v for ...} Dict (clave: valor)
De conjunto {x for x in it} Set (único, sin orden)

Errores Comunes y Consejos

  • {} no es un conjunto vacío: es un dict. El error no da mensaje — simplemente trampa.add(x) fallará con AttributeError más tarde. Conjunto vacío = set().
  • Esperar orden: los sets no garantizan orden de iteración. Si imprimes un set y "sale desordenado", no es un bug. Para presentar, sorted(mi_set).
  • Indexar un set: s[0] lanza TypeError. Sin orden no hay posiciones; si necesitas "el primero", te falta una lista.
  • Meter elementos mutables: {["a", "b"]} lanza TypeError: unhashable type: 'list'. Convierte a tupla si necesitas agrupar.
  • Confundir remove() con discard(): si la ausencia es normal, discard(); si es un bug, remove() (que falle pronto). El mismo criterio que corchetes vs get() en dicts.
  • Deduplicar con set cuando el orden importa: set(lista) pierde el orden de aparición. Idiom que lo conserva: list(dict.fromkeys(lista)) — los dicts deduplican claves y recuerdan el orden de inserción.
  • Consejo: si te sorprendes escribiendo bucles anidados para comparar dos listas ("los de aquí que no están allá"), casi seguro que era una operación de conjuntos.

Ejercicios

  1. Auditoría de pedidos. De la lista pedidos = ["Hamlet", "Fausto", "Hamlet", "Drácula", "El Quijote", "Fausto"], obtén: (a) el conjunto de títulos distintos pedidos; (b) cuántos pedidos duplicados hubo (total de pedidos menos títulos distintos); (c) usando el catálogo canónico de 04-03 y una diferencia de conjuntos, qué títulos pedidos no están en el catálogo.
  2. Campaña de fidelización. Con junio = {"Luis", "Marta", "Pau", "Júlia"} y julio = {"Luis", "Júlia", "Omar"}, calcula: los socios que hay que recuperar (compraron en junio pero no en julio), los nuevos de julio, y los fieles de ambos meses. Después comprueba con un operador de subconjunto si todos los fieles compraron en junio.
  3. Géneros disponibles. Con el dict generos_libro de la lección (La Odisea, Hamlet, Fausto) y el conjunto disponibles calculado sobre el catálogo canónico, construye mediante operaciones y/o una comprensión el conjunto de géneros realmente disponibles hoy (los géneros de los libros con stock > 0). Pista: la unión de varios conjuntos puede hacerse con set().union(...).

Soluciones

# Ejercicio 1
pedidos = ["Hamlet", "Fausto", "Hamlet", "Drácula", "El Quijote", "Fausto"]
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},
}

distintos = set(pedidos)                       # (a) {'Hamlet','Fausto','Drácula','El Quijote'}
duplicados = len(pedidos) - len(distintos)     # (b) 2
desconocidos = distintos - set(catalogo)       # (c) {'Drácula'} → set(dict) toma las claves
print(distintos, duplicados, desconocidos)
# Ejercicio 2
junio = {"Luis", "Marta", "Pau", "Júlia"}
julio = {"Luis", "Júlia", "Omar"}

recuperar = junio - julio        # {'Marta', 'Pau'}
nuevos = julio - junio           # {'Omar'}
fieles = junio & julio           # {'Luis', 'Júlia'}
print(fieles <= junio)           # True: todo fiel compró en junio (por definición)

Consejo: nombra los resultados con vocabulario de negocio (recuperar, fieles), no matemático (diferencia, interseccion): el código se explica solo.

# Ejercicio 3
generos_libro = {
    "La Odisea": {"clásico", "épica"},
    "Hamlet": {"clásico", "teatro", "tragedia"},
    "Fausto": {"clásico", "teatro"},
}
disponibles = {t for t, ficha in catalogo.items() if ficha["stock"] > 0}

generos_hoy = set().union(*(generos_libro[t] for t in disponibles if t in generos_libro))
print(generos_hoy)   # {'clásico', 'épica', 'teatro', 'tragedia'} → Fausto (agotado) no aporta

Fíjate: el * desempaqueta los conjuntos como argumentos de union() (el mismo * de 03-02), y el if t in generos_libro protege contra libros sin géneros fichados — "El Quijote" está disponible pero no tiene entrada, y sin ese filtro habría un KeyError.

Conclusión

Los conjuntos completan el trío de colecciones "con llaves": sabes crearlos (recordando que set() es la única forma del vacío), aprovechar la deduplicación automática, y expresar lógica de negocio con unión, intersección y diferencias en una línea donde antes habrías anidado bucles. Entiendes además por qué in es ultrarrápido en sets y dicts — la magia del hash — y conoces frozenset para cuando la inmutabilidad importa. En Papyrus ya deduplican pedidos, validan socios y comparan meses de ventas. Pero en casi todos los ejemplos ha asomado un actor que llevamos usando desde el módulo 1 sin estudiarlo en serio: la cadena de texto, con sus strip(), lower() y compañía. La próxima lección la trata por fin como lo que es — una secuencia con superpoderes propios — y sistematiza la normalización de entradas de usuario que buscar_libro() y es_socio_valido() llevan haciendo a medias.

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