TopologicalSorter: ordena dependencias sin ciclos

Publicado el: 06/09/2026
Tempo de leitura: 6 minutos
Grafo de dependencias y flujo de tareas con TopologicalSorter en Python

graphlib.TopologicalSorter es una clase de la biblioteca estándar de Python diseñada para ordenar dependencias en un grafo dirigido acíclico. Resuelve un problema habitual en sistemas reales: una tarea solo puede ejecutarse después de que todos sus requisitos previos hayan finalizado. Compilación de proyectos, pipelines de datos, migraciones de bases de datos, generación de informes, despliegues y procesamiento de archivos son ejemplos claros.

En lugar de mantener una secuencia manual y frágil, describes qué nodos dependen de cuáles predecesores. TopologicalSorter calcula un orden válido y también ofrece una API incremental para enviar tareas listas a workers paralelos sin romper las dependencias.

Qué es un orden topológico

Un orden topológico es una secuencia en la que cada dependencia aparece antes del elemento que depende de ella. Imagina cuatro pasos: descargar datos, validarlos, transformarlos y publicarlos. La validación depende de la descarga; la transformación depende de la validación; y la publicación depende de la transformación. En un grafo grande, varias tareas independientes pueden quedar disponibles al mismo tiempo.

La ordenación topológica solo funciona con grafos sin ciclos. Si A depende de B y B depende de A, no existe un orden válido. Python detecta esta situación y lanza CycleError.

Primer ejemplo con static_order

from graphlib import TopologicalSorter

grafo = {
    "publicar": {"transformar"},
    "transformar": {"validar"},
    "validar": {"descargar"},
    "descargar": set(),
}

orden = list(TopologicalSorter(grafo).static_order())
print(orden)

El diccionario relaciona cada tarea con su conjunto de predecesores. static_order() prepara el grafo, comprueba ciclos y produce una secuencia válida. Para scripts secuenciales, esta es la interfaz más sencilla.

No debes considerar el orden relativo de tareas independientes como una garantía de negocio. Si dos nodos no dependen entre sí, cualquiera puede aparecer primero y el resultado seguirá siendo correcto.

Construir el grafo con add

from graphlib import TopologicalSorter

ts = TopologicalSorter()
ts.add("descargar")
ts.add("validar", "descargar")
ts.add("transformar", "validar")
ts.add("publicar", "transformar")

print(tuple(ts.static_order()))

add(nodo, *predecesores) acumula dependencias. Si llamas de nuevo a add para el mismo nodo, se agregan nuevos predecesores. Esto resulta útil cuando plugins, módulos o archivos de configuración aportan partes diferentes del mismo flujo.

Ejecutar tareas en paralelo

La API incremental está pensada para concurrencia. Primero llama a prepare(). Después, get_ready() devuelve todos los nodos cuyas dependencias ya están satisfechas. Cuando un worker termina una tarea, llama a done(). Entonces pueden liberarse nuevos nodos.

from graphlib import TopologicalSorter
from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED

requisitos = {
    "extraer_clientes": set(),
    "extraer_pedidos": set(),
    "unir": {"extraer_clientes", "extraer_pedidos"},
    "informe": {"unir"},
}

def ejecutar(nombre):
    print("ejecutando", nombre)
    return nombre

ts = TopologicalSorter(requisitos)
ts.prepare()

with ThreadPoolExecutor(max_workers=4) as pool:
    futuros = {}
    while ts.is_active() or futuros:
        for tarea in ts.get_ready():
            futuros[pool.submit(ejecutar, tarea)] = tarea

        if not futuros:
            break

        terminados, _ = wait(futuros, return_when=FIRST_COMPLETED)
        for futuro in terminados:
            tarea = futuros.pop(futuro)
            futuro.result()
            ts.done(tarea)

Este patrón permite paralelismo solo entre tareas listas. future.result() propaga excepciones del worker. En producción debes definir la política de error: detener todo, reintentar, bloquear descendientes o ejecutar una compensación.

prepare, get_ready, done e is_active

prepare() finaliza la preparación y detecta ciclos. Después de iniciar la ejecución, el grafo debe considerarse fijo. get_ready() puede devolver varios nodos, y cada uno debe notificarse posteriormente mediante done() cuando termine correctamente.

Nunca marques una tarea como completada antes de que su trabajo real haya finalizado. Eso libera dependientes demasiado pronto. Tampoco llames dos veces a done() para el mismo nodo.

is_active() indica si todavía existe trabajo posible o pendiente. Es útil para controlar el bucle de un scheduler, especialmente cuando las tareas se envían a una cola externa.

Detectar ciclos

from graphlib import TopologicalSorter, CycleError

grafo = {
    "a": {"b"},
    "b": {"c"},
    "c": {"a"},
}

try:
    list(TopologicalSorter(grafo).static_order())
except CycleError as error:
    print("ciclo detectado", error.args)

Los ciclos normalmente indican un error de modelado o configuración. La excepción puede incluir nodos relacionados, pero no conviene depender de una representación exacta. Un diagnóstico sólido registra las dependencias cargadas y muestra una cadena comprensible.

Normalizar configuraciones

Los datos procedentes de YAML, JSON o una base de datos pueden contener duplicados, nombres vacíos o dependencias desconocidas. Normaliza antes de crear el sorter.

def normalizar(configuracion):
    grafo = {}
    for nombre, dependencias in configuracion.items():
        nombre = nombre.strip()
        if not nombre:
            raise ValueError("tarea sin nombre")
        grafo[nombre] = {d.strip() for d in dependencias if d.strip()}
    return grafo

TopologicalSorter acepta predecesores que no aparecen como claves y los incluye automáticamente como nodos. Esto puede ser útil, pero también puede ocultar un error tipográfico. En sistemas estrictos, valida cada dependencia contra un catálogo conocido.

Ejemplo de pipeline de archivos

from graphlib import TopologicalSorter

pipeline = {
    "comprimir": {"crear_csv", "crear_pdf"},
    "enviar": {"comprimir"},
    "crear_csv": {"consultar"},
    "crear_pdf": {"consultar", "cargar_plantilla"},
    "consultar": set(),
    "cargar_plantilla": set(),
}

for etapa in TopologicalSorter(pipeline).static_order():
    print(etapa)

consultar y cargar_plantilla pueden ejecutarse primero, posiblemente en paralelo. crear_pdf espera ambas; crear_csv solo depende de la consulta. comprimir espera los dos artefactos y enviar cierra el flujo.

Gestionar resultados

El sorter organiza nodos, pero no almacena resultados. Usa un diccionario o un almacenamiento externo:

resultados = {}

def ejecutar(tarea):
    if tarea == "consultar":
        resultados[tarea] = [1, 2, 3]
    elif tarea == "crear_csv":
        resultados[tarea] = crear_csv(resultados["consultar"])

Con threads, protege estructuras mutables si hay escrituras concurrentes. Con procesos o workers distribuidos, usa una base de datos, caché, broker de mensajes u object storage. Relaciona los resultados con un identificador de ejecución para evitar mezclas entre pipelines.

Idempotencia y recuperación

Un orquestador fiable debe soportar reinicios. Haz que las tareas sean idempotentes siempre que sea posible: ejecutarlas de nuevo debe llevar al mismo estado final. Registra estados como pendiente, ejecutando, completado y fallido. Al recuperar, reconstruye el grafo y marca como completas solo las tareas cuyo resultado haya sido verificado.

TopologicalSorter no persiste estado y no sustituye Airflow, Prefect, Celery ni un motor especializado. Es un componente ligero para aplicaciones que necesitan controlar dependencias localmente o implementar su propio scheduler.

Reintentos y propagación de fallos

Los reintentos pertenecen a la capa de ejecución, no al grafo. Limita intentos, usa backoff exponencial para errores transitorios y no repitas validaciones que fallan de forma permanente. Los descendientes de una tarea fallida deben permanecer bloqueados salvo que la política indique lo contrario.

Guarda la excepción original, el número de intento, la hora de inicio y la hora de fin. Estos datos facilitan el diagnóstico y permiten distinguir una dependencia lenta de un problema real del scheduler.

Memoria y escala

El grafo permanece en memoria. Miles o decenas de miles de nodos pueden ser razonables, pero debes medir. Grafos enormes y dinámicos pueden requerir particionado o un sistema especializado. La ordenación tampoco reduce el coste de las tareas; solo determina cuándo pueden empezar.

Prefiere identificadores inmutables y hashables como strings, enteros, enums o tuplas. Los nodos se usan en diccionarios y conjuntos, por lo que los valores mutables no son adecuados.

Pruebas correctas

Prueba ramas independientes, un requisito compartido, un nodo con varios predecesores, una dependencia desconocida y al menos un ciclo intencional. En ejecución concurrente, verifica que ningún descendiente empiece antes de que todos sus predecesores hayan terminado.

Una prueba robusta valida relaciones, no un orden total exacto. Para cada arista dependencia -> tarea, comprueba que el índice de la dependencia sea menor que el de la tarea. Así la prueba sigue siendo válida aunque cambie la posición de nodos independientes.

Buenas prácticas

Usa nombres estables, valida dependencias desconocidas, detecta ciclos antes de iniciar trabajo costoso, limita workers, propaga fallos, registra duraciones y resultados y no confundas “lista” con “completada”. Los pipelines importantes deben persistir eventos e incluir identificadores de correlación en los logs.

También puedes consultar los artículos de Academify sobre diccionarios en Python, conjuntos en Python, threading en Python y concurrent.futures. Como fuentes externas, revisa la documentación oficial de graphlib y la documentación de concurrent.futures.

Conclusión

graphlib.TopologicalSorter convierte una red de dependencias en un orden ejecutable y ofrece una base segura para paralelismo controlado. En flujos pequeños y medianos elimina secuencias manuales, detecta ciclos y hace explícito el contrato entre tareas. Combinado con validación, persistencia, idempotencia y tratamiento de errores, es una pieza práctica para construir pipelines fiables con la biblioteca estándar de Python.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Carpetas y directorios que representan os.fwalk en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    os.fwalk en Python: recorre directorios

    Aprende os.fwalk en Python para recorrer directorios con descriptores, reducir condiciones de carrera y manipular archivos con mayor seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    05/09/2026
    Desarrollador revisando código Python y métodos sobrescritos
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    typing.override: valida sobrescrituras de métodos

    Aprende typing.override en Python para validar sobrescrituras, firmas compatibles, herencia y refactorizaciones con análisis estático.

    Ler mais

    Tempo de leitura: 6 minutos
    05/09/2026
    Desarrollador trabajando con colas e hilos en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    queue.SimpleQueue: cola FIFO segura entre hilos

    Aprende queue.SimpleQueue en Python para crear colas FIFO seguras entre hilos, workers y diseños productor-consumidor.

    Ler mais

    Tempo de leitura: 5 minutos
    04/09/2026
    Desarrollador trabajando con enums y código Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    StrEnum en Python: enums como strings

    Aprende StrEnum en Python para crear enums como strings, validar entradas, serializar JSON y organizar APIs y configuraciones.

    Ler mais

    Tempo de leitura: 5 minutos
    04/09/2026
    Carpetas y directorios para contextlib.chdir en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    contextlib.chdir: restaura directorios automáticamente

    Aprende contextlib.chdir en Python para cambiar directorios temporalmente, restaurar rutas y crear pruebas confiables sin errores de estado global.

    Ler mais

    Tempo de leitura: 6 minutos
    03/09/2026
    Monitoreo de rendimiento y ejecución de código Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sys.monitoring: instrumentación de bajo overhead

    Aprende sys.monitoring en Python para instrumentar ejecución con bajo overhead, eventos selectivos, callbacks y observabilidad segura.

    Ler mais

    Tempo de leitura: 6 minutos
    03/09/2026