Módulo 5: Rápido y en paralelo — caché y paralelismo
4. Paralelizar con `pytest-xdist` y `-n auto`
Descripción
La caché resolvió el primer sumidero: no reinstalar lo mismo. Pero queda el segundo, el que la lección 2 midió en la suite de Reservo —seis segundos de tests corriendo en fila, uno tras otro, en un solo proceso—. Cachear no ayuda ahí: los tests no se descargan, se ejecutan, y ejecutarlos en secuencia es lento por definición cuando hay muchos o cuando algunos tardan. La palanca para este sumidero es otra: paralelizar. En lugar de un proceso masticando la fila entera, varios procesos se la reparten y corren a la vez.
Al terminar vas a saber usar pytest-xdist, el plugin que reparte tus tests entre varios procesos worker, y vas a ver el speedup de verdad, ejecutado en local sobre la suite de Reservo. Vas a entender la diferencia entre -n auto (un worker por núcleo de tu CPU) y -n 4 (un número fijo de workers), vas a leer el header nuevo que pytest imprime en paralelo —created: 12/12 workers, 12 workers [23 items]—, y vas a medir con tus propios ojos cómo la misma suite baja de 6 segundos a poco más de uno sin cambiar una sola línea de test. También vas a entender por qué el speedup no es infinito: arrancar workers cuesta algo, y ese costo fija un piso. Esta es la lección más "ejecutable" del módulo: cada número que cite lo medí corriendo la suite en una máquina real.
Conexión con el módulo: esta lección ataca el sumidero 2 que la lección 2 diagnosticó, complementando la caché de la lección 3 (que atacaba el sumidero 1). Con las dos palancas encendidas tienes el módulo completo en lo técnico. Pero el paralelismo trae una condición que las lecciones 5 y 6 desarrollan: para repartir tests hay que saber cuáles repartir primero (dividir la suite, lección 5) y, sobre todo, los tests tienen que ser independientes (aislamiento, lección 6). Aquí encendemos el motor; esas dos lecciones se aseguran de que no se rompa. Y la lección 7 pondrá precio a este speedup: paralelizar consume CPU, y más no siempre es mejor.
Un solo cajero contra varias cajas
Retomemos el buffet de la lección 1, ahora en la caja. Con un solo cajero, cien comensales forman una fila y pagan de a uno: si cada pago toma seis segundos, la fila entera tarda diez minutos, y el cajero número cien espera todo ese rato aunque su pago sea idéntico al primero. El cuello de botella no es la velocidad del cajero —teclea a buen ritmo—, es que hay una sola fila y un solo ejecutor.
Abre cuatro cajas y reparte la fila entre ellas. Ahora cuatro pagos ocurren a la vez; la fila de cien avanza casi cuatro veces más rápido. No hiciste al cajero más veloz ni cambiaste a los clientes: cambiaste cuántas cosas pasan al mismo tiempo. Ese "al mismo tiempo" es el paralelismo, y su ganancia es proporcional a cuántas cajas abras —hasta un límite, porque abrir una caja cuesta (hay que traer una registradora, sentar a un cajero), y con más cajas que clientes, las de sobra quedan ociosas—.
pytest-xdist abre esas cajas para tus tests. Cada "caja" es un proceso worker: una copia de Python corriendo en paralelo, con su propia parte de la suite. Con doce workers en una máquina de doce núcleos, doce tests corren a la vez en lugar de uno. La fila de tests que tardaba seis segundos en serie se reparte, y el tiempo de pared —lo que tú esperas mirando la terminal— cae en picada. La condición, la misma que en el buffet: cada cliente tiene que poder pagar en cualquier caja sin depender del de al lado. Un test que necesita lo que otro test dejó no puede correr en una caja distinta —esa es la lección 6—.
pytest-xdistreparte tus tests entre varios procesos worker que corren a la vez.-n autoabre un worker por núcleo de tu CPU. El speedup es real y grande cuando la suite es lenta y los tests son independientes; tiene un piso, porque arrancar workers cuesta.
Instalarlo y encenderlo
pytest-xdist es un plugin de pytest: se instala con pip y pytest lo detecta solo. Se agrega a requirements.txt como cualquier otra dependencia de pruebas:
# requirements.txt
pytest==9.1.1
pytest-xdist
Y se instala igual que siempre:
pip install pytest-xdist
Una vez instalado, tienes una bandera nueva: -n, seguida del número de workers. La forma que casi siempre querrás es -n auto:
python -m pytest -n auto
auto significa "abre tantos workers como núcleos tenga esta máquina". En la máquina de esta guía, que tiene 12 CPUs, -n auto abre 12 workers. En una de 8 núcleos abriría 8. La gracia de auto es que no clavas un número: el mismo comando exprime todo el hardware disponible, sea cual sea, en tu laptop y en el runner del CI. También puedes pedir un número fijo con -n 4 (cuatro workers, sin importar cuántos núcleos haya), y más adelante veremos cuándo conviene cada uno.
Que pytest detectó el plugin lo confirma el header, en la línea plugins::
plugins: xdist-3.8.0
Ahí está xdist-3.8.0 listado entre los plugins activos. Sin instalarlo, -n no existiría y pytest te diría unrecognized arguments: -n. Con él, -n auto reparte la suite.
La demo real: serial contra -n auto
Aquí está el corazón de la lección, medido de verdad en Python 3.14.0 con pytest 9.1.1. Vamos a correr la parte lenta de la suite de Reservo —los doce tests del reporte mensual, cada uno con medio segundo de espera— de tres formas, y a comparar los tiempos.
Primero, en serie (sin -n), como corriste toda la guía:
python -m pytest -m slow
Qué esperar (salida real):
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /private/tmp/reservo-m5
configfile: pyproject.toml
testpaths: tests
plugins: xdist-3.8.0
collected 23 items / 11 deselected / 12 selected
tests/test_reports.py ............ [100%]
====================== 12 passed, 11 deselected in 6.11s =======================
Doce tests lentos, 6.11s. Son los doce sleep(0.5) corriendo uno tras otro: medio segundo, doce veces, en fila. (El -m slow selecciona solo los tests marcados como lentos; lo desmenuzamos en la lección 5. Por ahora fíjate solo en el tiempo.)
Ahora los mismos doce tests, agregando -n auto:
python -m pytest -m slow -n auto
Qué esperar (salida real):
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /private/tmp/reservo-m5
configfile: pyproject.toml
testpaths: tests
plugins: xdist-3.8.0
created: 12/12 workers
12 workers [12 items]
............ [100%]
============================== 12 passed in 1.17s ==============================
1.17s. Los mismos doce tests, todos verdes, en menos de una quinta parte del tiempo. De 6.11 s a 1.17 s. No tocamos ningún test: los repartimos entre doce workers, y como cada worker se quedó con uno o dos tests de medio segundo, todos terminaron casi a la vez. Seis segundos de fila se volvieron poco más de uno.
Y ahora, la suite completa —los 23 tests, rápidos y lentos— para ver la foto entera, primero en serie y luego con -n auto:
============================== 23 passed in 6.10s ==============================
created: 12/12 workers
12 workers [23 items]
............................ [100%]
============================== 23 passed in 1.15s ==============================
La suite entera baja de 6.10 s a 1.15 s. Ese es el speedup que persigue el módulo, y lo acabas de ver ejecutado, no descrito.
Cómo leer el header en paralelo
La corrida con -n cambia el header, y conviene leer las líneas nuevas con calma porque las vas a ver todo el resto del módulo.
created: 12/12 workers — pytest-xdist arrancó los workers antes de correr nada. 12/12 significa "pedí 12 y los 12 están listos". Estos son doce procesos de Python independientes, cada uno una copia del intérprete lista para recibir tests. Arrancarlos toma un instante (ese es el costo de "traer las registradoras"), y por eso aparece antes de la primera línea de resultados.
12 workers [23 items] — reemplaza a la línea collected 23 items de la corrida en serie. Se lee: "doce workers se van a repartir 23 tests". Es la confirmación de que el trabajo se dividió: en vez de un proceso con los 23, doce procesos con un puñado cada uno.
La línea de puntos ya no viene ordenada por archivo. En serie veías tests/test_reports.py ............ con los puntos agrupados por archivo, en orden. En paralelo ves una fila de puntos "sueltos" —............................— porque los resultados llegan de doce workers a la vez, en el orden en que cada uno termina su test, no en el orden del archivo. Un punto sigue siendo un test que pasó; lo que cambia es que ya no puedes leer el archivo de origen en la posición del punto. (Si necesitas ver qué test corrió en qué worker, existen banderas para eso, pero el resumen final —23 passed— es idéntico: mismos tests, mismo veredicto.)
El resto del header no cambió: misma plataforma, mismo Python, mismos plugins. Lo único nuevo es la evidencia de que el trabajo se repartió. Y el resumen final es la prueba de que repartir no alteró el resultado: 23 passed, exactamente los mismos 23 tests que en serie, solo que más rápido.
Por qué el speedup no es infinito
Podrías esperar que doce workers hicieran la suite doce veces más rápida. Bajó de 6.10 a 1.15 —unas cinco veces—, no doce. ¿Por qué no el máximo teórico? Por tres razones que conviene entender, porque explican el trade-off de la lección 7.
Arrancar workers cuesta. Cada worker es un proceso de Python que hay que iniciar, cargar y coordinar. Con doce workers, ese arranque —el created: 12/12 workers— toma una fracción de segundo que en serie no pagas. Para una suite de seis segundos, esa fracción es pequeña y vale la pena; para una suite que ya tarda medio segundo, el arranque podría costar más de lo que ahorras, y -n auto la haría más lenta. El paralelismo paga cuando hay bastante trabajo que repartir.
El reparto no es perfecto. Doce tests entre doce workers suena a "uno cada uno, todos terminan juntos", pero pytest-xdist reparte los tests a los workers según se van desocupando, y algunos workers acaban con dos tests mientras otros con uno. El worker que se llevó dos tarda el doble, y la corrida entera no termina hasta que el último worker acaba. Ese desbalance impide el speedup perfecto.
Hay un tope físico. Con doce núcleos, doce tests pueden correr de verdad a la vez; el treceavo tendría que esperar a que se libere un núcleo. Abrir más workers que núcleos no ayuda (las cajas de sobra quedan ociosas o se pelean el mismo núcleo). Por eso -n auto elige justo un worker por núcleo: es el punto donde exprimes el hardware sin pasarte.
La consecuencia práctica: el speedup es grande pero tiene un piso, y ese piso lo fija el arranque más el test más lento que no se puede partir. La lección 7 mide la curva completa —cómo el retorno se aplana según sumas workers— y te enseña a elegir cuántos sin desperdiciar.
En el workflow de CI
Llevar esto al pipeline es una línea. Donde el workflow del módulo 2 corría pytest, ahora corre pytest -n auto:
- name: Run the test suite
run: pytest -n auto
Con pytest-xdist en requirements.txt (para que el runner lo instale) y -n auto en el comando, el CI reparte la suite entre los núcleos del runner. Los runners de GitHub por defecto tienen varios núcleos, así que -n auto los aprovecha sin que tú sepas de antemano cuántos son —esa es, de nuevo, la ventaja de auto sobre un número fijo—. Recuerda la honestidad del módulo: este step es contenido (aquí no corre un runner), pero el speedup que produce lo acabas de medir en local, y el runner haría lo mismo con su propio número de núcleos.
Errores comunes
Usar -n sin instalar pytest-xdist. Qué pasa: alguien agrega -n auto al comando pero olvida poner pytest-xdist en requirements.txt, y pytest falla con error: unrecognized arguments: -n. Por qué pasa: -n no es una bandera de pytest, la aporta el plugin; sin el plugin, no existe. Cómo detectarlo: el mensaje unrecognized arguments: -n es inequívoco. Cómo corregirlo: agrega pytest-xdist a la lista de dependencias e instálalo; confírmalo viendo xdist-x.y.z en la línea plugins: del header.
Paralelizar una suite que ya es rápida y hacerla más lenta. Qué pasa: alguien pone -n auto en una suite de 20 tests instantáneos, y la corrida tarda más que en serie, porque arrancar doce workers cuesta más de lo que ahorra repartir 20 tests que ya volaban. Por qué pasa: "paralelo = más rápido" es cierto solo cuando hay bastante trabajo que repartir. Cómo detectarlo: compara el tiempo con y sin -n; si -n auto es más lento, tu suite no tiene suficiente trabajo para justificar los workers. Cómo corregirlo: usa -n auto donde duele (suites lentas o grandes) y déjalo fuera donde la suite ya es instantánea. La lección 7 da la regla completa.
Leer los puntos "desordenados" como un error. Qué pasa: alguien ve la fila de puntos sueltos de -n auto —sin agruparse por archivo como en serie— y cree que algo se corrompió o que los tests corrieron "mal". Por qué pasa: la salida en paralelo llega de varios workers a la vez, en el orden en que terminan, no en el del archivo, y eso desconcierta la primera vez. Cómo detectarlo: mira el resumen final: si dice 23 passed, corrieron los 23 y pasaron, sin importar el orden de los puntos. Cómo corregirlo: entiende que el orden de los puntos en paralelo refleja cuándo terminó cada worker, no un problema; el veredicto que importa es el conteo final, idéntico al de la corrida en serie.
Ejercicios
Ejercicio 1 — Calcula el speedup. La suite lenta de Reservo tarda 6.11 s en serie y 1.17 s con -n auto (12 workers). (a) ¿Cuántas veces más rápida es la versión paralela, redondeando? (b) Si el speedup fuera perfecto (12 veces), ¿cuánto tardaría? (c) ¿Por qué el número real (1.17 s) es mayor que ese ideal?
Ver solución
- (a) 6.11 ÷ 1.17 ≈ 5.2 veces más rápida. Un speedup grande, aunque no el máximo teórico.
- (b) Con speedup perfecto de 12×, tardaría 6.11 ÷ 12 ≈ 0.51 s —justo el tiempo de un solo test lento, que es el mínimo posible: doce tests de 0.5 s repartidos en doce workers darían medio segundo si cada worker corriera exactamente uno y arrancar fuera gratis—.
- (c) El real (1.17 s) es mayor que el ideal (0.51 s) por las tres razones de la lección: arrancar los doce workers cuesta una fracción de segundo que en serie no pagas; el reparto no es perfecto, así que algún worker se llevó dos tests (1 s de trabajo) mientras otros uno; y la corrida no termina hasta que el último worker acaba. La diferencia entre 0.51 y 1.17 es, justamente, el precio del paralelismo.
La lección: el speedup es real y grande (5×), pero el ideal teórico (12×) no se alcanza por el costo de arrancar y coordinar workers. Eso es normal y esperable.
Ejercicio 2 — Diagnostica el -n que no funciona. Un compañero corre python -m pytest -n auto y obtiene este error. ¿Qué pasó y cómo se arregla?
ERROR: usage: pytest [options] [file_or_dir] ...
pytest: error: unrecognized arguments: -n auto
Ver solución
Lo que pasó: pytest-xdist no está instalado. La bandera -n no pertenece a pytest; la aporta el plugin xdist. Sin el plugin, pytest no reconoce -n y aborta con unrecognized arguments: -n auto.
La corrección es instalar el plugin:
pip install pytest-xdist
Y, para que el CI también lo tenga, agregarlo a requirements.txt:
pytest==9.1.1
pytest-xdist
Después, python -m pytest -n auto funciona, y lo confirmas mirando el header: la línea plugins: ahora incluye xdist-x.y.z, y aparecen las líneas created: N/N workers y N workers [M items]. La pista para el futuro: si una bandera da unrecognized arguments, casi siempre falta el plugin que la aporta, no es que la escribiste mal.
Ejercicio 3 — auto o número fijo. Explica qué hace -n auto frente a -n 4, y da un caso donde preferirías auto y uno donde preferirías un número fijo como 4.
Ver solución
-n auto abre un worker por núcleo de la máquina donde corre: 12 en una de 12 núcleos, 8 en una de 8. -n 4 abre exactamente cuatro workers, sin importar cuántos núcleos haya.
- Preferirías
autocuando quieres exprimir todo el hardware disponible sin clavar un número, y el mismo comando corre en máquinas distintas —tu laptop, el runner del CI—. Comoautose adapta, un solopytest -n autoaprovecha 12 núcleos en una máquina y 8 en otra sin que edites nada. Es la opción por defecto para la mayoría de los casos. - Preferirías un número fijo como
-n 4cuando necesitas dejar núcleos libres para otra cosa —por ejemplo, en un runner compartido donde no quieres acaparar toda la CPU, o en tu laptop mientras trabajas en otra cosa y no quieres que se congele—, o cuando mediste que más de cuatro workers no aportan speedup para tu suite pero sí más costo (lección 7). Fijar el número te da control sobre cuánta máquina consumes.
La regla práctica: auto por defecto (exprime lo que haya); número fijo cuando quieres acotar el consumo de recursos a propósito.
Resumen y siguiente paso
En esta lección encendiste la segunda palanca del módulo, y la ejecutaste de verdad: paralelizar con pytest-xdist. Lo viste con las cajas del buffet: no hacer al cajero más rápido, sino abrir más cajas y repartir la fila. Instalaste el plugin, usaste -n auto (un worker por núcleo) y mediste el speedup real sobre la suite de Reservo: los doce tests lentos bajaron de 6.11 s en serie a 1.17 s en paralelo (unas 5 veces), y la suite completa de 6.10 s a 1.15 s, sin cambiar una sola línea de test —el resumen siguió diciendo 23 passed, mismos tests, mismo veredicto—.
Aprendiste a leer el header en paralelo: created: 12/12 workers (los procesos listos), 12 workers [23 items] (el reparto), y la fila de puntos sueltos que llega en el orden en que los workers terminan, no en el del archivo. Y entendiste por qué el speedup no es infinito: arrancar workers cuesta, el reparto no es perfecto, y hay un tope físico en el número de núcleos —tres razones que fijan un piso y que la lección 7 convertirá en una curva de costo—.
Antes de avanzar deberías poder: instalar pytest-xdist y correr pytest -n auto; leer el header en paralelo (N workers [M items]); explicar la diferencia entre -n auto y -n 4 y cuándo usar cada uno; y justificar por qué el speedup real (5×) queda por debajo del ideal teórico (12×).
Lo que sigue, en la lección 5, es afinar qué se paraleliza y en qué orden. No todo test es igual de caro: la mayoría de Reservo vuela, y solo los doce del reporte pesan. Vas a aprender a dividir la suite —marcar los lentos con @pytest.mark.slow, correr los rápidos primero para tener feedback en centésimas de segundo (11 passed in 0.01s) y los lentos aparte, y encontrar a los culpables con --durations—. Con la suite bien dividida, el paralelismo cae donde de verdad hace falta.
Recursos
- pytest-xdist en PyPI — la página oficial del plugin: instalación,
-n auto, y las opciones de distribución. La referencia de la palanca que ejecutamos aquí. pytest-xdisten GitHub — el repositorio con la documentación completa: cómo reparte los tests, los modos de distribución (--dist), y las advertencias sobre el estado compartido que la lección 6 desarrolla. Consúltalo cuando quieras control fino.- How to run tests in parallel — documentación de pytest-xdist — la guía oficial de los modos de distribución y cómo elegir el número de workers. Útil para ir más allá de
-n autocuando lo necesites. - Cómo invocar pytest (documentación de pytest) — la referencia de las banderas de pytest que combinas con
-n, incluida la selección por marcador (-m) que usamos aquí y desglosamos en la lección 5.