TaskGroup en Python: concurrencia estructurada

Publicado el: 28/08/2026
Tempo de leitura: 5 minutos
Detailed view of a resting reticulated python showcasing its textured scales and intricate patterns.

Crear tareas con asyncio.create_task() es sencillo, pero coordinar su ciclo de vida puede ser difícil. Una tarea puede fallar mientras otras continúan, las referencias pueden perderse y el cierre puede ocurrir antes de completar el cleanup. asyncio.TaskGroup coloca tareas relacionadas dentro de un ámbito estructurado: al salir del contexto, todas han terminado, fueron canceladas o aportaron un error.

Esta guía cubre creación de tareas, resultados, cancelación, ExceptionGroup, comparación con asyncio.gather(), grupos anidados, timeouts, límites de capacidad y limpieza confiable.

El problema de las tareas sueltas

Una tarea creada con create_task() empieza de forma independiente. El llamador debe conservar la referencia, esperarla y manejar sus errores.

import asyncio

async def trabajo(nombre: str) -> str:
    await asyncio.sleep(0.2)
    return nombre.upper()

async def main():
    tareas = [
        asyncio.create_task(trabajo("a")),
        asyncio.create_task(trabajo("b")),
    ]
    resultados = await asyncio.gather(*tareas)
    print(resultados)

asyncio.run(main())

El patrón funciona, pero una tarea puede sobrevivir a la operación lógica que la creó. TaskGroup hace explícita la propiedad.

Primer TaskGroup

async def main():
    async with asyncio.TaskGroup() as grupo:
        tarea_a = grupo.create_task(trabajo("a"))
        tarea_b = grupo.create_task(trabajo("b"))

    print(tarea_a.result())
    print(tarea_b.result())

El contexto no termina hasta que todas las tareas hijas finalizan. Después del bloque, los resultados están disponibles y no queda trabajo pendiente propiedad del grupo.

Concurrencia estructurada

La concurrencia estructurada significa que las tareas hijas viven dentro de un ámbito claro. El código que las crea también es responsable de su finalización. Esto simplifica el razonamiento sobre recursos, cancelación y errores.

El concepto amplía la guía de asyncio en Python. Las coroutines aportan concurrencia cooperativa; TaskGroup aporta estructura de ciclo de vida.

Cuando una tarea falla

Si una hija lanza una excepción distinta de cancelación, TaskGroup normalmente cancela las hermanas que siguen activas. Después de detenerlas, las fallas se propagan como ExceptionGroup.

async def fallo_rapido():
    await asyncio.sleep(0.1)
    raise ValueError("datos inválidos")

async def trabajo_lento():
    try:
        await asyncio.sleep(10)
    finally:
        print("cleanup lento")

async def main():
    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(fallo_rapido())
        grupo.create_task(trabajo_lento())

La tarea lenta recibe cancelación, ejecuta su finally y solo entonces el grupo propaga los errores.

Tratar ExceptionGroup con except*

async def main():
    try:
        async with asyncio.TaskGroup() as grupo:
            grupo.create_task(fallo_rapido())
            grupo.create_task(trabajo_lento())
    except* ValueError as errores:
        for error in errores.exceptions:
            print("error de validación:", error)

except* selecciona las excepciones compatibles dentro del grupo. Los tipos no tratados continúan propagándose.

Recoger resultados

TaskGroup.create_task() devuelve un objeto Task. Conserva las referencias cuando necesites los resultados individuales.

async def consultar(item_id: int) -> dict:
    await asyncio.sleep(0.1)
    return {"id": item_id}

async def cargar(ids: list[int]) -> list[dict]:
    tareas: list[asyncio.Task[dict]] = []
    async with asyncio.TaskGroup() as grupo:
        for item_id in ids:
            tareas.append(grupo.create_task(consultar(item_id)))

    return [tarea.result() for tarea in tareas]

La lista conserva el orden de creación aunque las tareas terminen en otro orden.

TaskGroup frente a gather

asyncio.gather() sigue siendo útil cuando ya tienes awaitables y quieres un resultado ordenado. Con return_exceptions=True, puede devolver errores como valores. TaskGroup se centra en crear y poseer tareas dentro de un ámbito y cancela las hermanas cuando una falla.

No sustituyas mecánicamente todo gather(). Usa TaskGroup cuando las operaciones deban vivir y morir juntas; usa gather cuando su semántica exacta sea la deseada.

Añadir tareas dinámicamente

Mientras el contexto esté activo, una hija puede recibir el grupo y programar trabajo adicional.

async def descubrir(grupo: asyncio.TaskGroup, pagina: int) -> None:
    await asyncio.sleep(0.1)
    if pagina < 3:
        grupo.create_task(descubrir(grupo, pagina + 1))

async def main():
    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(descubrir(grupo, 1))

No se pueden añadir tareas después del cierre. Limita siempre la recursión, las colas y el fan-out.

Cancelación externa

Si se cancela la tarea que contiene el grupo, TaskGroup cancela sus hijas y espera su cierre. Las coroutines deben liberar recursos en finally.

async def consumidor():
    recurso = await abrir_recurso()
    try:
        await procesar(recurso)
    finally:
        await recurso.cerrar()

No captures CancelledError para continuar indefinidamente. Realiza cleanup y normalmente vuelve a lanzar la cancelación.

No ocultar CancelledError

TaskGroup y otras herramientas asyncio usan cancelación internamente. Consumir CancelledError puede romper el cierre estructurado.

async def incorrecto():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        return  # oculta la cancelación

El patrón seguro limpia y usa raise, salvo que consumir la cancelación sea una decisión deliberada y documentada.

Timeout para todo el grupo

Envuelve el ámbito con asyncio.timeout().

async def main():
    try:
        async with asyncio.timeout(2.0):
            async with asyncio.TaskGroup() as grupo:
                grupo.create_task(operacion_a())
                grupo.create_task(operacion_b())
    except TimeoutError:
        print("plazo excedido")

Al expirar, se cancela la tarea contenedora; TaskGroup cancela y espera a las hijas.

Plazos individuales

Cuando cada operación necesita un límite propio, coloca el timeout dentro de la coroutine hija.

async def con_plazo(coro, segundos: float):
    async with asyncio.timeout(segundos):
        return await coro

Un timeout individual genera TimeoutError en esa hija y normalmente cancela a las hermanas. Si es recuperable, trátalo dentro de la hija y devuelve un resultado explícito.

Fallas esperadas como datos

No todo error debe terminar el grupo. En procesamiento por lotes, un registro inválido puede convertirse en un resultado mientras los demás continúan.

from dataclasses import dataclass

@dataclass
class Resultado:
    item_id: int
    valor: str | None = None
    error: str | None = None

async def procesar_seguro(item_id: int) -> Resultado:
    try:
        valor = await consultar_texto(item_id)
        return Resultado(item_id=item_id, valor=valor)
    except ErrorEsperado as exc:
        return Resultado(item_id=item_id, error=str(exc))

Reserva excepciones no tratadas para condiciones que invaliden toda la unidad concurrente.

Grupos anidados

TaskGroup puede representar suboperaciones.

async def procesar_cliente(cliente_id: int):
    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(cargar_perfil(cliente_id))
        grupo.create_task(cargar_pedidos(cliente_id))

async def main(ids: list[int]):
    async with asyncio.TaskGroup() as grupo:
        for cliente_id in ids:
            grupo.create_task(procesar_cliente(cliente_id))

Cada nivel posee sus hijas. Las fallas pueden producir grupos de excepciones anidados que conservan la estructura del trabajo.

Limitar concurrencia

TaskGroup no limita la cantidad de tareas simultáneas. Crear cientos de miles puede consumir mucha memoria. Usa un semáforo, una cola de workers o lotes.

limite = asyncio.Semaphore(20)

async def limitado(item):
    async with limite:
        return await procesar(item)

El grupo controla el ciclo de vida; el semáforo controla la capacidad.

Nombres de tareas y contexto

create_task() admite nombres y opciones de contexto según la versión de Python.

grupo.create_task(
    consultar(42),
    name="consulta-cliente-42",
)

Los nombres ayudan en logs y debugging. Las context variables suelen copiarse a las tareas hijas.

Errores comunes

  • Crear tareas relacionadas fuera del grupo: pueden escapar del ciclo de vida.
  • Ocultar CancelledError: rompe la cancelación estructurada.
  • Esperar una lista como retorno: conserva los Task para obtener resultados.
  • Programar trabajo ilimitado: usa semáforos, colas o lotes.
  • Tratar cada falla esperada como fatal: modela resultados recuperables como datos.
  • Omitir cleanup: usa finally y context managers asíncronos.

Ejemplo completo: agregador de servicios

import asyncio

async def obtener_usuario(usuario_id: int) -> dict:
    await asyncio.sleep(0.1)
    return {"id": usuario_id, "nombre": "Ana"}

async def obtener_pedidos(usuario_id: int) -> list[dict]:
    await asyncio.sleep(0.2)
    return [{"id": 1, "total": 99.0}]

async def construir_panel(usuario_id: int) -> dict:
    async with asyncio.timeout(3.0):
        async with asyncio.TaskGroup() as grupo:
            usuario = grupo.create_task(obtener_usuario(usuario_id), name="usuario")
            pedidos = grupo.create_task(obtener_pedidos(usuario_id), name="pedidos")

    return {
        "usuario": usuario.result(),
        "pedidos": pedidos.result(),
    }

El panel se devuelve solo cuando ambas operaciones terminan. Una falla cancela a la hermana y evita devolver un resultado parcial accidental.

Conclusión

asyncio.TaskGroup organiza tareas relacionadas en un ámbito con inicio y fin claros. Espera a las hijas, coordina cancelación y agrupa fallas, haciendo el código asíncrono más predecible.

La documentación oficial de TaskGroup en asyncio explica excepciones y cancelación. Úsalo para operaciones que pertenecen juntas, limita la capacidad por separado y maneja cleanup y fallas esperadas de forma explícita.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    A monochrome image of a lens on an open dictionary page, highlighting words.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    MappingProxyType: diccionario solo lectura

    Aprende MappingProxyType en Python para exponer diccionarios de solo lectura, crear vistas dinámicas y snapshots, y proteger invariantes sin copias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Retro fuel pump with rusty metal and vintage design, featuring a nozzle and liter meter.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Literal en Python: restringe valores

    Aprende typing.Literal en Python para restringir valores, crear overloads, discriminar TypedDict, usar match/case y mejorar APIs tipadas.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypedDict en Python: diccionarios tipados

    Aprende TypedDict en Python para diccionarios tipados, claves opcionales, NotRequired, Required, payloads de APIs y variantes discriminadas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Protocol en Python: tipado estructural

    Aprende Python Protocol para tipado estructural, contratos genéricos, callbacks, runtime_checkable, pruebas e inyección de dependencias.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview en Python: buffers sin copia

    Aprende memoryview en Python para buffers sin copia, slices, bytearray editable, cast, mmap, struct, sockets y control seguro del ciclo

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tracemalloc en Python: rastrea memoria

    Aprende tracemalloc en Python para medir picos, crear y comparar snapshots, filtrar asignaciones y diagnosticar crecimiento de memoria.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026