Módulo 2: Tu primer pipeline — correr pytest en CI
5. Instalar las dependencias en el runner
Descripción
Después de la lección 4, el runner tiene tu código (checkout) y el Python correcto (setup-python). Pero le falta algo para poder correr la suite: tus dependencias. El runner trae Python "pelón", sin pytest ni ninguna librería de terceros que tu proyecto use. Esta lección instala esos paquetes. Al terminar vas a entender por qué el runner empieza sin tus librerías, cómo requirements.txt es la lista canónica de lo que tu proyecto necesita, y cómo los dos comandos python -m pip install --upgrade pip y pip install -r requirements.txt la convierten en paquetes instalados sobre el Python que fijaste.
Esta es la primera lección del módulo donde una parte corre de verdad. Instalar dependencias con pip es algo que haces en tu propia terminal, así que la salida de pip que verás es real, capturada de una instalación auténtica. Sigue siendo cierto que el workflow completo es contenido que explicamos; pero el step de instalar dependencias lo puedes reproducir tú mismo y obtener lo mismo, lo cual conecta directamente con la paridad local que armarás en el mini-proyecto.
Conexión con el módulo: este step se apoya en el de la lección 4 (instala sobre el Python que setup-python activó) y prepara el de la lección 6 (pytest solo puede correr si pytest está instalado). Hay una frontera que tocamos con cuidado: por qué conviene pinnear las versiones exactas en requirements.txt —para que el CI instale hoy lo mismo que mañana— lo mencionamos como motivación, pero el tema a fondo (la brecha de entorno, las instalaciones deterministas, reproducir un fallo de CI que tu local no tiene) es el módulo 3. Aquí instalamos las dependencias; el módulo 3 se ocupa de que esa instalación sea idéntica en todas partes.
La receta que trae su propia lista de ingredientes
Volvamos a la receta de cocina de la lección 2, pero fijémonos en algo que dejamos fuera: los ingredientes. Una receta bien escrita no dice solo los pasos ("mezcla, hornea"); trae primero una lista de ingredientes con cantidades exactas: "2 tazas de harina, 3 huevos, 200 g de mantequilla". Esa lista existe porque quien va a cocinar no tiene por qué tener esos ingredientes ya en la despensa. La cocina del hotel —el runner— está limpia y equipada con lo básico, pero no tiene tu harina especial ni tus huevos; los traes tú, y la lista te dice exactamente cuáles y cuántos.
requirements.txt es esa lista de ingredientes. Es un archivo de texto, parte de tu proyecto, que enumera cada paquete de Python que tu código necesita para funcionar, con su versión. Cuando el runner llega al step de instalar, no adivina qué librerías hacen falta: lee la lista y trae exactamente lo que dice. Y la gracia de tener la lista dentro del proyecto es que cualquiera —el runner, tú en una máquina nueva, un compañero que acaba de clonar el repo— instala los mismos ingredientes en las mismas cantidades. La lista es lo que hace que "instalar las dependencias" sea una operación repetible en vez de una cacería de "¿qué me faltaba?".
Para Reservo la lista es corta, casi vergonzosamente corta, y eso es a propósito: Reservo es lógica pura de Python estándar, sin librerías externas para su código. Su único ingrediente es la herramienta con la que se prueba.
requirements.txt: la lista de lo que hace falta
Este es el requirements.txt de Reservo, entero:
# requirements.txt
pytest==9.1.1
Una línea. El código de Reservo no usa ninguna librería de terceros —solo la librería estándar de Python—, así que lo único que hay que instalar para probarlo es pytest, la herramienta que corre los tests. La línea pytest==9.1.1 dice "instala pytest, exactamente la versión 9.1.1".
Fíjate en el ==9.1.1. Ese doble igual fija (pinnea) la versión exacta. Podrías escribir solo pytest sin versión, y pip instalaría la más reciente que encuentre el día que corra —lo cual suena cómodo hasta que una versión nueva de pytest cambia algo y tu CI, que ayer estaba verde, hoy amanece rojo sin que tú tocaras nada—. Pinnear con ==9.1.1 garantiza que el runner instale hoy lo mismo que mañana: la versión que probaste, no "la que salga". Esa es la primera pincelada de un tema grande —las instalaciones deterministas— que el módulo 3 desarrolla a fondo; por ahora quédate con la intuición: una lista con versiones exactas es una lista reproducible.
Un proyecto real tendría más líneas —una por cada librería que use, cada una con su ==versión—, pero la forma es siempre esta: un paquete por renglón, con su versión pinneada. La lista puede crecer; la mecánica de instalarla no cambia.
Los dos comandos que instalan la lista
El step que instala las dependencias corre dos comandos, en este orden:
- run: python -m pip install --upgrade pip
- run: pip install -r requirements.txt
O, más común, los dos juntos en un solo step con la barra vertical | de YAML, que permite varias líneas de comando dentro de un mismo run:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
El | después de run: significa "lo que sigue son varias líneas, ejecútalas en orden". Es la forma limpia de agrupar comandos que van juntos conceptualmente ("instalar dependencias") en un solo paso con un solo nombre. Los dos comandos hacen esto:
Uno: python -m pip install --upgrade pip. Actualiza la propia herramienta pip a su última versión antes de usarla. El pip que trae el runner de fábrica puede ser viejo, y un pip viejo a veces tiene problemas resolviendo dependencias o muestra advertencias molestas. Actualizarlo primero es higiene: te aseguras de instalar tus paquetes con una versión reciente y sana de la herramienta. Es un paso barato que previene una clase de fallos difíciles de diagnosticar. (Usamos python -m pip en vez de solo pip por la misma razón que en la guía de fundamentos usamos python3 -m pytest: garantiza que actualizas el pip de este Python, el que setup-python activó, sin ambigüedad.)
Dos: pip install -r requirements.txt. Aquí ocurre la instalación de verdad. La bandera -r significa "read requirements from this file" —lee la lista de este archivo—: pip abre requirements.txt, lee cada línea, y descarga e instala cada paquete con la versión indicada. Es la traducción directa de "trae los ingredientes de la lista". Después de este comando, pytest (y cualquier otra dependencia que la lista tuviera) está instalado sobre el Python del runner, listo para el step siguiente.
Ejemplo trabajado: la salida real de pip install -r requirements.txt
Aquí la parte que corre de verdad. En una máquina limpia con Python 3.14 —el mismo estado en que setup-python deja al runner—, corrimos los dos comandos. Esta es la salida real de instalar requirements.txt:
pip install -r requirements.txt
Qué esperar:
Collecting pytest==9.1.1 (from -r requirements.txt (line 1))
Using cached pytest-9.1.1-py3-none-any.whl.metadata (7.6 kB)
Collecting iniconfig>=1.0.1 (from pytest==9.1.1->-r requirements.txt (line 1))
Using cached iniconfig-2.3.0-py3-none-any.whl.metadata (2.5 kB)
Collecting packaging>=22 (from pytest==9.1.1->-r requirements.txt (line 1))
Using cached packaging-26.2-py3-none-any.whl.metadata (3.5 kB)
Collecting pluggy<2,>=1.5 (from pytest==9.1.1->-r requirements.txt (line 1))
Using cached pluggy-1.6.0-py3-none-any.whl.metadata (4.8 kB)
Collecting pygments>=2.7.2 (from pytest==9.1.1->-r requirements.txt (line 1))
Using cached pygments-2.20.0-py3-none-any.whl.metadata (2.5 kB)
Installing collected packages: pygments, pluggy, packaging, iniconfig, pytest
Successfully installed iniconfig-2.3.0 packaging-26.2 pluggy-1.6.0 pygments-2.20.0 pytest-9.1.1
Léela por capas, porque cada parte cuenta algo:
- Las líneas
Collectingson pip resolviendo la lista. Pedistepytest==9.1.1(la primera), pero pytest depende de otras librerías para funcionar —iniconfig,packaging,pluggy(su motor de plugins),pygments(para colorear la salida)—, y pip las trae solas. Fíjate que en cada una dice de dónde salió:from pytest==9.1.1->-r requirements.txt (line 1)significa "esta la necesita pytest, que tú pediste en la línea 1 del archivo". Tú listaste un ingrediente; pip trajo los cinco que hacen falta. Using cachedsignifica que pip encontró el paquete ya descargado en su caché local y no tuvo que bajarlo de internet. En una máquina totalmente fresca diríaDownloadingen su lugar. Es un detalle de velocidad; el resultado es el mismo. (Cachear dependencias en el CI para acelerar corridas es, por cierto, justo el tema del módulo 5 —aquí solo estás viendo el caché local de pip, otra cosa—.)Installing collected packages: ...lista, en orden de instalación, todo lo que pip va a poner. Nota que instala las dependencias antes que pytest (pytest va al final), porque pytest las necesita ya presentes.Successfully installed ...es el veredicto, la línea que importa. Confirma qué quedó instalado y con qué versión:pytest-9.1.1, la que pediste, más sus cuatro dependencias. Si ves esta línea, el runner ya tiene todo lo necesario para correr la suite.
Los números exactos de las dependencias (packaging-26.2, pygments-2.20.0) pueden variar un poco según el día, porque las pedimos con >= (pytest acepta "esta o más nueva" para sus dependencias). Lo que no varía es pytest-9.1.1, porque esa la pinneamos con ==. Ahí ves, en vivo, la diferencia entre una versión fija y una flexible: pytest sale siempre igual; sus dependencias, "la más nueva compatible". Y ahí también ves por qué pinnear lo que te importa da reproducibilidad.
Esta salida la puedes reproducir en tu máquina hoy: crea un entorno virtual con Python 3.14, pon pytest==9.1.1 en un requirements.txt, y corre el comando. Obtendrás esencialmente esto. Como el runner llega al step de instalar en ese mismo estado (Python limpio, dependencias por instalar), lo que ves en local es lo que el CI vería.
Por qué instalar desde un archivo y no a mano
Podrías, en teoría, escribir run: pip install pytest==9.1.1 directo en el workflow, sin archivo. Para Reservo, con una sola dependencia, casi da igual. Pero instalar desde requirements.txt es mejor práctica por una razón que escala:
La lista vive en un solo lugar, y todos la usan. Tu workflow instala desde requirements.txt; tú, en tu máquina, instalas desde el mismo requirements.txt; un compañero que clona el repo, también. Hay una única fuente de verdad sobre "qué necesita este proyecto", y está versionada junto con el código. Cuando agregas una dependencia, la pones en el archivo una vez, y automáticamente el CI, tú y todo el equipo la instalan. Si en cambio listaras los paquetes a mano dentro del YAML, tendrías dos listas (la del workflow y la que instalas en local) que hay que mantener sincronizadas —y que tarde o temprano divergen, produciendo el clásico "en el CI falta un paquete que yo tengo en local"—.
Esa divergencia entre el entorno del CI y el tuyo es, precisamente, la fuente número uno de "en mi máquina funciona pero en CI no", y es el corazón del módulo 3. Instalar desde un requirements.txt compartido es la primera defensa contra ella: si los dos instalan de la misma lista, empiezan del mismo lugar. No resuelve todo (el módulo 3 verá que hay más matices), pero es el hábito base.
De dónde sale un requirements.txt
Una pregunta natural: si la lista tiene que enumerar cada dependencia con su versión, ¿la escribes a mano una por una? Para Reservo, con una línea, sí, es trivial. Para un proyecto con veinte dependencias, escribirlas a mano sería tedioso y propenso a errores. La herramienta que ayuda es pip freeze:
pip freeze
Qué esperar (en el entorno de Reservo, tras instalar pytest):
iniconfig==2.3.0
packaging==26.2
pluggy==1.6.0
Pygments==2.20.0
pytest==9.1.1
pip freeze imprime todo lo que hay instalado en el entorno actual, cada paquete con su versión exacta, en el formato de requirements.txt. Fíjate en algo interesante: lista los cinco paquetes —pytest y sus cuatro dependencias—, no solo el que tú pediste. Eso es a propósito: capturar el estado completo del entorno, incluidas las dependencias de tus dependencias, es lo que hace una instalación de verdad reproducible. Puedes volcar esa salida a un archivo con pip freeze > requirements.txt para "congelar" (de ahí el nombre, freeze) el entorno exacto que probaste.
Aquí hay una tensión que conviene nombrar, aunque su resolución sea del módulo 3: un requirements.txt escrito a mano suele listar solo lo que tú pides directamente (pytest==9.1.1), dejando que pip resuelva el resto con rangos; un requirements.txt generado con pip freeze lista todo, pinneado al detalle, para que el entorno se reconstruya idéntico hasta en la última dependencia transitiva. El primero es más legible; el segundo, más reproducible. Cuál conviene, y las herramientas más nuevas que separan "lo que pido" de "lo que se resuelve" (los archivos de bloqueo o lock files), es justamente el terreno de las instalaciones deterministas del módulo 3. Por ahora quédate con el mecanismo: pip freeze te muestra —y, redirigido a un archivo, te guarda— el estado exacto de tu entorno, y esa foto es la base de la reproducibilidad.
Errores comunes
Correr pytest sin instalar las dependencias y toparse con "no module named pytest" (de dependencia ausente). Qué pasa: alguien escribe un workflow con checkout y setup-python, salta directo a run: pytest, y falla porque pytest no está instalado —el runner trae Python pelón, sin pytest—. Por qué pasa: en tu máquina pytest "siempre está" porque lo instalaste una vez hace tiempo y lo olvidaste; el runner limpio no. Cómo detectarlo: "no module named pytest" (o cualquier otra librería) en CI casi siempre significa que falta el step de instalar. Cómo corregirlo: incluye pip install -r requirements.txt (con pytest en la lista) antes del step de pytest. El runner solo tiene lo que le instalas explícitamente.
No pinnear las versiones y ver el CI romperse "solo" (de dependencia flotante). Qué pasa: alguien pone pytest sin ==versión en requirements.txt; durante semanas todo va bien, y un día el CI amanece rojo sin que nadie tocara el código —salió una versión nueva de pytest que cambió un comportamiento—. Por qué pasa: sin pin, pip instala "la más reciente" cada vez, así que el entorno cambia bajo tus pies. Cómo detectarlo: un CI que se rompe sin cambios tuyos, y un diff que no explica nada, apunta a una dependencia que se movió. Cómo corregirlo: pinnea las versiones que importan con == (como pytest==9.1.1), para que el CI instale siempre lo mismo. Es la intuición base; el módulo 3 profundiza en instalaciones totalmente deterministas.
Mantener dos listas de dependencias que divergen (de fuente duplicada). Qué pasa: alguien lista los paquetes a mano en el YAML (run: pip install pytest requests) y, por separado, en un requirements.txt que usa en local; con el tiempo agregan una librería a uno pero no al otro, y el CI falla por un paquete que "en mi máquina sí está". Por qué pasa: dos listas que deberían decir lo mismo pero se editan por separado siempre terminan discrepando. Cómo detectarlo: si tu workflow instala paquetes con nombres escritos a mano en vez de -r requirements.txt, tienes una segunda lista escondida. Cómo corregirlo: una sola fuente de verdad —el requirements.txt— y que tanto el workflow como tú instalen desde ella con -r. Cuando cambie, cambia en un solo lugar y todos quedan sincronizados.
Ejercicios
Ejercicio 1 — Lee la salida de pip. Mira esta línea final de una instalación y responde: ¿cuántos paquetes se instalaron en total?, ¿cuál pediste tú explícitamente en requirements.txt y cuáles son dependencias que pip trajo solo?, y ¿cuál de todos tiene la versión garantizada de ser exactamente esa?
Successfully installed iniconfig-2.3.0 packaging-26.2 pluggy-1.6.0 pygments-2.20.0 pytest-9.1.1
Ver solución
- Total instalado: cinco paquetes (iniconfig, packaging, pluggy, pygments, pytest).
- Pedido explícitamente: solo pytest, que es la única línea de tu
requirements.txt(pytest==9.1.1). Los otros cuatro —iniconfig, packaging, pluggy, pygments— son dependencias de pytest que pip resolvió y trajo por su cuenta, porque pytest las necesita para funcionar. - Versión garantizada de ser exactamente esa: pytest 9.1.1, porque la pinneaste con
==9.1.1. Las otras cuatro las pide pytest con>=(una versión mínima o más nueva), así que sus números pueden variar día a día; por eso podrías verpackaging-26.2hoy y otro parche mañana, pero pytest siempre saldrá9.1.1.
La lección: un solo == en tu lista se propaga a un árbol de dependencias, y controlas con precisión lo que te importa (pytest) mientras dejas que pip resuelva el resto dentro de los rangos que cada paquete acepta.
Ejercicio 2 — Escribe el step de instalación. Un proyecto necesita, además de pytest, la librería httpx en su versión 0.28.1. Escribe (a) las dos líneas del requirements.txt y (b) el step del workflow, con nombre, que actualiza pip e instala desde el archivo usando el | de YAML.
Ver solución
(a) requirements.txt:
pytest==9.1.1
httpx==0.28.1
Un paquete por línea, cada uno con su versión pinneada con ==.
(b) El step del workflow:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
Fíjate en lo que no cambió respecto al de Reservo: el step es idéntico. Aunque el proyecto ahora tiene dos dependencias en vez de una, el step de instalación no se toca —solo cambió el requirements.txt—. Esa es la ventaja de instalar desde el archivo: la lista crece, la mecánica no. El | agrupa los dos comandos bajo un solo step nombrado "Install dependencies", que se leerá bonito en el log de CI (lección 7).
Ejercicio 3 — Diagnostica el "en mi máquina sí". Un compañero tiene este step en su workflow y el CI falla con "no module named requests", aunque en su máquina los tests corren perfecto. Su requirements.txt contiene solo pytest==9.1.1. ¿Qué pasó y cómo se arregla?
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- run: pytest
Ver solución
Lo que pasó: el código del compañero usa la librería requests, pero requests no está en su requirements.txt (que solo lista pytest). En su máquina los tests corren porque él instaló requests a mano hace tiempo y lo tiene "suelto" en su entorno —lo olvidó—. El runner, en cambio, instala solo lo que dice la lista, así que no trae requests, y al correr los tests el import falla con "no module named requests".
Este es el "en mi máquina funciona" del módulo 1 en su forma más pura: el entorno local del compañero tiene un paquete que su lista de dependencias no declara, así que el CI —que parte de la lista— revela la omisión. El CI no está roto; está haciendo su trabajo, exponiendo una dependencia no declarada.
La corrección es agregar la dependencia faltante a la lista:
pytest==9.1.1
requests==2.32.5
Ahora el requirements.txt declara todo lo que el proyecto necesita, y el runner (y cualquier máquina nueva) instalará requests también. La moraleja: si un paquete hace falta para que el código corra, tiene que estar en requirements.txt, no solo "instalado por ahí" en tu máquina. El módulo 3 se dedica entero a cerrar esta clase de brechas entre tu entorno y el del CI.
Resumen y siguiente paso
En esta lección instalaste las dependencias en el runner —el paso que convierte "tengo Python" en "tengo con qué correr la suite"—. Entendiste que el runner trae Python pelón, sin tus librerías, y que requirements.txt es la lista de ingredientes de tu proyecto: un paquete por línea, con la versión pinneada con == para que el CI instale hoy lo mismo que mañana. Viste los dos comandos —python -m pip install --upgrade pip para tener una herramienta sana, y pip install -r requirements.txt para traer la lista— y leíste la salida real de pip: cómo un solo pytest==9.1.1 arrastra sus cuatro dependencias, cómo el Successfully installed es el veredicto, y cómo lo pinneado sale siempre igual mientras lo flexible varía. Y entendiste por qué instalar desde un archivo compartido —una sola fuente de verdad— es la primera defensa contra el "en mi máquina funciona".
Antes de avanzar deberías poder: explicar por qué el runner necesita un step de instalación; escribir un requirements.txt con versiones pinneadas y el step que lo instala; leer una salida de pip y distinguir lo que pediste de lo que pip trajo solo; y reconocer una dependencia no declarada como causa de un fallo de CI.
El runner ya está completamente equipado: código, Python correcto, y dependencias instaladas. Solo falta el paso por el que todo esto existe. En la lección 6 llegamos al corazón del módulo: run: pytest. Vas a ver cómo el CI descubre y corre tu suite exactamente igual que en local —con la salida verde real, 11 passed, corrida de verdad—, y vas a entender el exit code: el número que pytest devuelve al terminar (0 si todo pasó, 1 si algo falló, 5 si no encontró tests) y que GitHub lee para pintar el job de verde o de rojo. Es la lección que conecta "corrí los tests" con "el pipeline sabe cómo me fue".
Recursos
- Instalar paquetes de Python (Python Packaging User Guide) — el tutorial oficial de pip y
requirements.txt: qué es la lista, cómo se escribe, y cómo se instala con-r. La referencia de la parte que corre de verdad en esta lección. - Formato de requirements.txt (documentación de pip) — la referencia exacta del formato del archivo: cómo pinnear versiones (
==), rangos (>=), comentarios, y más. Consúltala cuando tu lista crezca más allá de una línea. - Build and test Python (documentación de GitHub Actions) — la guía oficial de GitHub, que muestra este mismo step de instalar dependencias dentro del workflow completo de Python. Útil para ver dónde encaja este paso en el conjunto.