Módulo 4: La matriz — múltiples versiones y entornos
7. Cuándo la matriz paga y cuándo es ruido
Descripción
Este módulo te ha enseñado a construir una matriz, esculpirla y leerla. Todo eso es el cómo. Esta lección es el cuándo, y es la más importante, porque una matriz mal dimensionada no es un error técnico —el YAML funciona igual— sino un error de juicio que se paga cada día en minutos facturados y en ruido que entrena a tu equipo a ignorar rojos. La pregunta no es "¿puedo hacer una matriz de nueve celdas?" —sí puedes, es trivial—, sino "¿mi proyecto merece nueve celdas, o le sobran seis?".
Al terminar vas a poder decidir, con criterio y un modelo de costo, qué matriz corresponde a un proyecto según lo que es. Vas a distinguir el caso donde la matriz es obligatoria —una librería que muchos instalan, cuyo trabajo es funcionar en cada versión y sistema que promete— del caso donde es ruido y costo —una app interna que despliegas a una sola versión en un solo sistema, donde ocho de nueve celdas prueban entornos donde tu código jamás correrá—. Vas a ver el modelo de costo (N celdas = N× minutos por cada push), la regla que resuelve casi todos los casos —"prueba lo que envías más lo que prometes soportar, y nada más"— y cómo recortar una matriz inflada sin perder cobertura real.
Conexión con el módulo: las lecciones 3 a 6 te dieron las herramientas; esta te da el juicio para usarlas. Se apoya en todo lo anterior: el catálogo de diferencias de la lección 2 (para saber si tu código toca alguna), la multiplicación de la lección 4 (para calcular el costo), y el exclude de la lección 5 (para recortar). Es la penúltima lección, y prepara el mini-proyecto: cuando en la lección 8 configures la matriz de Reservo, la pregunta "¿qué matriz merece Reservo?" será la que decida el diseño, no un tutorial copiado.
El seguro que compras según lo que arriesgas
Nadie contrata el mismo seguro para una bicicleta que para una flota de camiones de carga. No porque el seguro de camiones sea "mejor" —es más caro y más completo—, sino porque lo que arriesgas es distinto. Para la bicicleta, un seguro básico contra robo basta; pagar el de flota sería tirar dinero por una cobertura que nunca vas a usar. Para los camiones, escatimar en cobertura es una imprudencia: un accidente sin seguro te quiebra. El seguro correcto no es el más grande ni el más chico: es el que corresponde a lo que de verdad arriesgas.
La matriz es un seguro contra "se rompe en un entorno que no probé". Su tamaño correcto depende de cuántos entornos de verdad arriesgas. Una librería publicada para el mundo es la flota de camiones: la instalan miles de personas en versiones y sistemas que tú no controlas, y si se rompe en Python 3.11 en Windows, es un problema real de un usuario real. Ahí la matriz grande no es lujo, es responsabilidad. Una app interna que solo tú despliegas, a un servidor Linux con Python 3.12 clavado, es la bicicleta: solo hay un entorno que de verdad importa —el de producción—, y probar en ocho más es pagar el seguro de flota para una bici. El tamaño de tu matriz debería seguir tu riesgo, no tu ansiedad.
La matriz correcta es la que cubre los entornos que de verdad arriesgas: los que tu código va a pisar en manos de otros o en producción. Ni más (ruido y costo), ni menos (un entorno prometido sin probar).
El modelo de costo: por qué cada celda cuesta
Antes de decidir, hay que ver el precio, porque una matriz no es gratis y su costo es fácil de subestimar. Cada celda de la matriz es una corrida completa de tu suite en un runner, y eso cuesta en tres monedas:
- Minutos de cómputo. Los runners de GitHub se facturan por minuto (con una cuota gratis que se agota). Una matriz de nueve celdas consume, en cada push, nueve veces los minutos de un solo job. Si tu suite tarda dos minutos, un solo job cuesta dos minutos por push; la matriz de nueve cuesta dieciocho. Multiplicado por cada push de cada persona del equipo, cada día, se vuelve real. Y ojo: los runners de macOS y Windows suelen costar más por minuto que los de Linux (a menudo varias veces más), así que una columna de macOS no cuesta lo mismo que una de Linux.
- Tiempo de espera. Aunque las celdas corren en paralelo, esperar a que las nueve terminen —y a que se liberen runners si hay cola— alarga el ciclo de feedback. Una matriz enorme puede convertir un feedback de dos minutos en uno de diez.
- Ruido y atención. Este es el costo escondido y el más caro a la larga. Si tienes celdas que fallan por razones que no te importan —una versión que no soportas, un entorno donde tu app nunca corre—, tu equipo aprende a ver rojos y encogerse de hombros. Y una vez que el equipo ignora rojos, la matriz dejó de proteger nada: es un semáforo que nadie mira.
La cuenta mental es simple y hay que hacerla siempre: número de celdas = producto de las listas, y costo por push ≈ celdas × duración de la suite (ponderado, porque no todos los runners cuestan igual). Nueve celdas no son "un poco más" que una; son nueve veces. Ese factor es el que justifica preguntarse por cada celda si aporta.
Ejemplo trabajado: la misma suite, una celda contra nueve
Corramos la suite de Reservo una vez en local para tener el número base, y de ahí razonemos el costo de la matriz:
python -m pytest tests/
Qué esperar. En Python 3.14.0, medido de verdad:
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /private/tmp/reservo-m4
collected 9 items
tests/test_pricing.py ... [ 33%]
tests/test_refunds.py ... [ 66%]
tests/test_version_features.py ..s [100%]
========================= 8 passed, 1 skipped in 0.01s =========================
Una corrida: 8 passed, 1 skipped in 0.01s. Esa es una celda. Ahora piensa en matrices sobre ese mismo trabajo:
- 1 celda (un job, Linux, una versión): corres la suite una vez por push. Es lo que Reservo, siendo lógica pura, casi necesita.
- 3 celdas (Linux, tres versiones): corres la suite tres veces. Justificado si prometes soportar tres versiones y usas features que varían entre ellas —como el
report_pagesde la lección 1—. - 9 celdas (tres sistemas × tres versiones): corres la suite nueve veces. Justificado solo si tu código de verdad se comporta distinto en los tres sistemas y en las tres versiones —lo cual, para la lógica pura de Reservo, en su mayoría no ocurre—.
El punto no es que nueve sea malo y uno bueno. Es que cada salto —de 1 a 3 a 9— multiplica el costo, y ese costo solo se justifica si las celdas nuevas cazan algo que las viejas no cazarían. Para Reservo, saltar a nueve porque "se ve más completo" sería pagar el seguro de flota para una bici: nueve corridas de 8 passed, 1 skipped donde ocho no descubren nada que la primera no descubriera ya.
La regla que resuelve casi todos los casos
Hay una regla que decide bien la enorme mayoría de las matrices:
Prueba lo que ENVÍAS más lo que PROMETES SOPORTAR, y nada más.
Desglosada:
- Lo que envías — el entorno donde tu código va a correr de verdad. Para una app que despliegas, es la versión y el sistema exactos de producción. Si producción es Python 3.12 en Linux, esa celda tiene que estar, porque es el único entorno que de verdad te importa: si falla ahí, tu app está rota de verdad.
- Lo que prometes soportar — para una librería, cada versión y sistema que tu README, tu
pyproject.tomlo tu documentación afirman soportar. Si prometes "Python 3.11+", cada una de esas versiones es una promesa que la matriz debe verificar. Prometer y no probar es mentir con confianza. - Y nada más — la parte que la gente olvida. No pruebes versiones que no prometes ni sistemas donde tu código nunca correrá. Cada celda de más es costo sin cobertura, y ruido que erosiona la confianza en la matriz.
Aplica la regla a los dos casos canónicos:
Una librería (reservo publicada en PyPI para el mundo). La instalan personas que tú no controlas, en las versiones y sistemas que ellas tengan. Tu pyproject.toml dice requires-python = ">=3.11" y tu README promete Linux, macOS y Windows. Entonces "lo que prometes soportar" es grande: 3.11, 3.12, 3.13 × Linux, macOS, Windows = las nueve celdas, y todas pagan, porque cada una es un usuario real con ese entorno. Aquí la matriz completa no es exceso, es exactamente la promesa. Recortarla sería prometer un soporte que no verificas.
Una app interna (Reservo corriendo como servicio, desplegado por tu equipo). Solo tú la despliegas, a un entorno que tú eliges: Python 3.12 en un contenedor Linux, y nada más. Nadie la instala en Windows; nadie la corre en 3.11. "Lo que envías" es una celda —(ubuntu, 3.12)—, y "lo que prometes soportar" es vacío (no es una librería, no le prometes versiones a nadie). Entonces la matriz correcta es una celda, quizá dos si estás por migrar de versión y quieres probar la nueva antes. Las otras siete celdas de una matriz 3×3 probarían entornos donde tu app jamás correrá: puro costo y ruido.
La misma base de código —Reservo— merece matrices opuestas según cómo se distribuye. No es una propiedad del código; es una propiedad de dónde va a vivir.
Casos intermedios y cómo pensarlos
Pocos proyectos son "librería para el mundo" o "app de una sola celda" puros. Para los intermedios, unas guías:
- Una app que corre en varios entornos de despliegue. Si despliegas la misma app a Linux y a Windows (raro, pero pasa), entonces "lo que envías" son esos dos, y la matriz debe cubrir ambos —pero solo esos dos, no macOS, que no despliegas—.
- Una librería con un piso de versión reciente. Si tu librería usa
itertools.batchedsin fallback y declararequires-python = ">=3.12", entonces 3.11 no es una promesa: no va en la matriz. Subir el piso de versión es una forma legítima de recortar la matriz —menos promesas, menos celdas—. - Foco en las fronteras. Cuando una feature cambia de comportamiento en una versión (la lección 2), asegúrate de tener celdas a ambos lados de esa frontera, y no te obsesiones con las del medio. Si el cambio está entre 3.11 y 3.12, esas dos importan más que agregar 3.13 y 3.14 "por completitud".
- La representativa, no la exhaustiva. Para la dimensión de sistema, si tu código toca rutas pero no te importa cada sistema por igual, a veces basta "un POSIX (Linux) y Windows", saltándote macOS —que comparte con Linux casi todo el comportamiento POSIX—. Dos celdas cubren la diferencia real (POSIX vs. Windows) sin la tercera casi redundante.
Cómo recortar una matriz inflada
Si heredas o detectas una matriz que es puro reflejo —nueve celdas copiadas de un tutorial para una app de una sola celda—, así la recortas sin perder cobertura real:
- Lista los entornos que de verdad arriesgas (envías + prometes). Para la app interna:
(ubuntu, 3.12). Punto. - Compara con las celdas actuales. Todo lo que esté en la matriz pero no en tu lista de riesgo es candidato a irse.
- Quita dimensiones enteras si no aportan. Si tu código es lógica pura (no toca el sistema), elimina la dimensión de
oscompleta —déjala en[ubuntu-latest]—. Si solo despliegas a una versión, reducepython-versiona esa. - Usa
excludepara las esquinas puntuales que sobran pero cuyo resto de la fila/columna sí quieres (lección 5). - Documenta el porqué en un comentario del YAML:
# solo Linux 3.12: es el unico entorno de despliegue. Así el próximo que lo lea no vuelve a inflarla "por si acaso".
Recortar no es ser descuidado; es ser honesto sobre qué arriesgas. Una matriz de una celda bien justificada protege más que una de nueve que el equipo aprendió a ignorar.
Errores comunes
Copiar una matriz de nueve celdas de un tutorial sin preguntarse si aplica. Qué pasa: un tutorial muestra una matriz 3×3 "profesional", alguien la pega en una app interna de una sola versión, y ahora cada push gasta nueve corridas para probar ocho entornos donde el código nunca correrá. Por qué pasa: la matriz grande se ve más seria y "completa". Cómo detectarlo: por cada celda pregúntate "¿alguien va a correr mi código aquí de verdad?". Si la respuesta honesta es no para la mayoría, la matriz está inflada. Cómo corregirlo: aplica la regla —envías + prometes, nada más— y recorta con el procedimiento de arriba.
Prometer soporte que la matriz no verifica. Qué pasa: el README dice "soporta Python 3.9+", pero la matriz solo prueba 3.12 y 3.13. Un usuario de 3.9 instala y truena; le prometiste algo que nunca probaste. Por qué pasa: la promesa y la matriz se editan en momentos distintos y se desincronizan. Cómo detectarlo: compara la lista de versiones de tu pyproject.toml/README con la lista de la matriz; deben coincidir. Cómo corregirlo: o subes el piso de versión a lo que de verdad pruebas (y ajustas el README), o agregas las versiones prometidas a la matriz. La matriz es la promesa hecha verificable; que digan lo mismo.
Confundir "más celdas" con "más calidad". Qué pasa: alguien agrega versiones y sistemas "para estar más seguro", y la matriz crece a quince celdas que tardan y cuestan, sin que ninguna de las nuevas cace un bug real (el código no las toca). Por qué pasa: se equipara cantidad de cobertura con calidad de cobertura. Cómo detectarlo: pregúntate por cada celda nueva "¿qué bug caza esta que las otras no cazarían?". Si no tienes respuesta, no aporta. Cómo corregirlo: mide la cobertura por lo que arriesgas, no por el tamaño de la cuadrícula. Una celda que caza algo real vale más que cinco que dan el mismo verde.
Ejercicios
Ejercicio 1 — Dimensiona la matriz por el caso. Para cada proyecto, di qué matriz corresponde (cuántas celdas y de qué) y justifícalo con la regla "envías + prometes": (a) una librería de utilidades publicada en PyPI, con requires-python = ">=3.11" y README que promete Linux/macOS/Windows; (b) una app web interna desplegada solo a un contenedor Linux con Python 3.12; (c) un script de línea de comandos que tu equipo corre en sus laptops, unas con Mac y otras con Windows, siempre en Python 3.12.
Ver solución
- (a) Librería PyPI, 3.11+ en tres sistemas → matriz completa, 3 × 3 = 9 celdas (o más, si soporta 3.14). "Lo que prometes soportar" es explícito y grande: cada versión desde 3.11 y cada uno de los tres sistemas es una promesa a usuarios que no controlas. Las nueve celdas pagan porque cada una es un entorno real de un usuario real. Recortarla sería prometer sin verificar.
- (b) App interna, un solo despliegue Linux/3.12 → una celda,
(ubuntu-latest, 3.12). "Lo que envías" es exactamente un entorno —producción—, y no le prometes versiones a nadie (no es librería). Las demás celdas de una 3×3 probarían entornos donde la app nunca correrá: costo y ruido. Quizá dos celdas si estás por migrar a 3.13 y quieres probarla antes. - (c) Script corrido en laptops Mac y Windows, Python 3.12 → dos celdas,
(macos-latest, 3.12)y(windows-latest, 3.12). "Lo que envías/corres" son esos dos sistemas (no Linux, que nadie del equipo usa), en una sola versión. La dimensión de sistema paga (Mac y Windows difieren, sobre todo en rutas); la de versión no (todos usan 3.12). Dos celdas, no seis.
La regla resuelve los tres: la matriz sigue a los entornos que de verdad arriesgas, que dependen de cómo se distribuye el código, no de cuánto código hay.
Ejercicio 2 — Calcula el costo y decide. Una app interna tiene una suite que tarda 3 minutos por corrida. El equipo hace en promedio 20 pushes al día. Compara el costo mensual (30 días) en minutos de runner entre una matriz de 1 celda y una de 9 celdas, y di si el salto se justifica sabiendo que la app solo se despliega a (ubuntu, 3.12).
Ver solución
Cálculo (asumiendo, para simplificar, runners de igual costo; en la práctica macOS/Windows cuestan más):
- 1 celda: 3 min × 20 pushes × 30 días = 1 800 minutos/mes.
- 9 celdas: 3 min × 9 celdas × 20 pushes × 30 días = 16 200 minutos/mes.
La diferencia es 14 400 minutos/mes —nueve veces el costo— por una matriz de 3×3. Y la pregunta decisiva: la app solo se despliega a (ubuntu, 3.12). Eso significa que ocho de las nueve celdas prueban entornos donde la app jamás correrá (Windows, macOS, 3.11, 3.13). Esas ocho celdas gastan 14 400 minutos al mes para cazar bugs en entornos que no existen para esta app.
Veredicto: el salto a 9 celdas no se justifica en absoluto. La matriz correcta es 1 celda, (ubuntu, 3.12), que gasta 1 800 minutos y cubre el único entorno que importa. Los 14 400 minutos extra no compran cobertura real: compran ruido. Este es exactamente el "seguro de flota para una bici" de la analogía, con números.
Ejercicio 3 — Recorta la matriz inflada. Heredas este workflow de una app interna que solo se despliega a Linux con Python 3.12. Explica qué está mal y reescribe la sección strategy.matrix a lo que de verdad merece, con un comentario que justifique el recorte.
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
Ver solución
Qué está mal: la matriz es 3 × 4 = 12 celdas para una app que solo se despliega a un entorno, (ubuntu, 3.12). Once de las doce celdas prueban entornos donde la app nunca correrá —macOS, Windows, y las versiones 3.10, 3.11, 3.13—. Es puro costo (doce corridas por push) y ruido (rojos en entornos irrelevantes que el equipo aprenderá a ignorar). No es una librería: no le promete versiones ni sistemas a nadie.
Recorte a lo que merece:
strategy:
matrix:
# La app solo se despliega a Linux con Python 3.12 (produccion).
# No es una libreria: no promete otras versiones ni sistemas a nadie.
# Por eso: una sola celda, la de despliegue.
os: [ubuntu-latest]
python-version: ["3.12"]
Queda una celda, (ubuntu-latest, 3.12), que es exactamente "lo que envías" —el entorno de producción— y "lo que prometes soportar" —nada, no es librería—. El comentario documenta el porqué para que el próximo que lo lea no vuelva a inflarla "por si acaso". Si el equipo estuviera evaluando migrar a 3.13, se podría agregar "3.13" temporalmente para probar la nueva versión antes de mover producción —una segunda celda con justificación, no doce por reflejo—.
Resumen y siguiente paso
En esta lección aprendiste el juicio que gobierna toda matriz: su tamaño correcto sigue a lo que de verdad arriesgas, como el seguro sigue a lo que aseguras. Una librería para el mundo merece la matriz completa —cada versión y sistema que promete es un usuario real—; una app interna de un solo despliegue merece una celda —las demás prueban entornos que no existen para ella—. La regla que resuelve casi todo: prueba lo que envías más lo que prometes soportar, y nada más.
Viste el modelo de costo —cada celda es una corrida completa, N celdas = N× minutos por push, con macOS y Windows más caros— y lo aplicaste con números: nueve celdas de 8 passed, 1 skipped para una app de una celda son ocho corridas que no cazan nada, puro costo y ruido que entrena al equipo a ignorar rojos. Y aprendiste a recortar una matriz inflada: lista lo que arriesgas, quita dimensiones que no aportan, usa exclude para las esquinas, y documenta el porqué.
Antes de avanzar deberías poder: dimensionar la matriz de un proyecto según cómo se distribuye; calcular su costo por push; aplicar la regla "envías + prometes, nada más"; y recortar una matriz de reflejo a la que de verdad merece, justificando cada celda.
Lo que sigue, en la lección 8, es el mini-proyecto que junta todo el módulo: configuras la matriz de tres versiones de Python para la suite de Reservo —el YAML con strategy.matrix, la feature con skipif que se comporta distinto por versión, la corrida local que muestra 8 passed, 1 skipped, la lectura de los tres resultados esperados, y —con lo que aprendiste aquí— una nota que justifica qué matriz merece Reservo de verdad—.
Recursos
- About billing for GitHub Actions — GitHub Docs — cómo se facturan los minutos de runner, incluidos los multiplicadores de macOS y Windows respecto a Linux. El número real detrás del "modelo de costo" de esta lección.
requires-pythonenpyproject.toml— Python Packaging User Guide — dónde una librería declara qué versiones soporta, la fuente de verdad de "lo que prometes". La matriz debería reflejar exactamente este valor.- Using a matrix for your jobs: example matrices — GitHub Actions — la referencia para expresar la matriz que decidas, grande o de una celda. El tamaño lo eliges tú con el criterio de esta lección; la sintaxis la da esta página.
- Supported Python versions y su ciclo de vida — Python Developer's Guide — qué versiones de Python siguen con soporte oficial y cuáles ya no. Útil para decidir el piso de versión de tu matriz: probar una versión que ya llegó a su fin de vida rara vez paga.