🧪 Problem-Driven Systems Lab

🐍 Python#

Versión fijada: 3.12 · Imagen base: python:3.12-alpine · Hub: :8200 · Casos operativos: 20 / 20

⬅️ Volver a los perfiles de lenguaje · 🗺️ Mapa de stacks · 🔄 Protocolo de actualización


🪪 Identidad#

Python es un lenguaje interpretado, de tipado dinámico y fuerte, diseñado alrededor de la legibilidad. Su biblioteca estándar es de las más amplias del set —"pilas incluidas" es un lema, no una metáfora— y su ecosistema científico no tiene competencia real.

Para qué se usa en la industria: análisis de datos, machine learning, automatización y scripting, backend web (Django, FastAPI), DevOps y herramientas internas. Es el lenguaje que más frecuentemente se elige por velocidad de la persona que escribe, no por velocidad del programa — y en la mayoría de los contextos esa es la decisión correcta.

Por qué está en este laboratorio: por el GIL. Python es el único stack del set donde el paralelismo de CPU está limitado por diseño, y eso convierte al caso 11 en una demostración honesta de un problema real que ningún otro runtime del laboratorio tiene. También porque su logging de stdlib es, según el veredicto del caso 03, la API más difícil de violar por accidente de los siete stacks.


⚙️ Modelo de ejecución#

Threads reales del sistema operativo, serializados por el GIL para la ejecución de bytecode.

El Global Interpreter Lock permite que solo un thread ejecute bytecode de Python a la vez. No impide la concurrencia de I/O —el GIL se libera durante las esperas— pero sí impide el paralelismo de CPU.

ConsecuenciaDónde se nota
Concurrencia de I/O sí, paralelismo de CPU noDos threads esperando a la base avanzan en paralelo; dos threads calculando, no — caso 11
El worker en thread compite con los lectoresEn el caso 01 el worker que refresca la tabla resumen serializa trabajo de CPU con los handlers — caso 01
La biblioteca estándar cubre casi todosqlite3, logging, threading, gc, tracemalloc, re — ninguno de los doce casos necesita un paquete de PyPItransversal
Sin tipos en runtimeLas anotaciones no se verifican al ejecutar. El error de firma llega con el request — caso 07

🧰 Primitivas que usa el laboratorio#

CasoPrimitiva centralPor qué esta y no otra
01 · API lentasqlite3 stdlib + threading.RLock + worker en threadSin dependencias; el GIL hace visible la contención entre worker y lectores
02 · N+1sqlite3 stdlibDirecto y legible: el N+1 queda a la vista en el código, sin ORM que lo disimule
03 · Observabilidadlogging.LoggerAdapter + JsonFormatterLa API más difícil de violar por accidente del set: el adapter inyecta el contexto en cada registro
04 · Timeoutstimeouts de socket / signalWall-clock. Limitación: abandona el resultado sin liberar el trabajo
05 · Memoriagc + sys.getsizeof + tracemallocInstrumentación de memoria en la stdlib, sin agente externo
06 · Pipelinedict protegido por lockCorrecto; sin red de seguridad de tipos debajo
07 · Monolitodict[str, Callable]Registrar un módulo es una línea. El error de firma aparece en runtime
08 · Extraccióncallbacks en listaSimple y sin desacople: los subscribers corren en línea
09 · Integración externathreading.Semaphore + re.match para SKUs + set de idempotenciaSemáforo de stdlib, correcto y directo
10 · Sobre-arquitecturadict O(1)El "right-sized" del caso
11 · ReportesThreadPoolExecutor separadoAísla el reporting; el GIL limita el paralelismo real del trabajo CPU
12 · Punto únicoif x is None / dict.get()Disciplina pura, cero respaldo del lenguaje
13 · Cache stampededict de vuelos + threading.EventEvent es el «esperá a que otro termine»; no hace falta librería
14 · Pool de conexionesqueue.Queue + @contextmanagerLa stdlib trae la estructura; el finally del generador aporta la disciplina
15 · Backpressurequeue.Queue(maxsize=N)Las tres políticas están en la firma de put(): bloquear, Full, o esperar acotado
16 · Idempotenciadict.setdefault bajo LockUna operación en vez de dos; el Lock está porque la atomicidad del GIL no es contrato
17 · Migración sin downtimeRWLock construido sobre ConditionLa stdlib no lo trae; la bandera de escritor esperando evita la hambruna
18 · Arranque en fríosin JIT · imports diferidosCurva plana porque no hay nada que calentar, y sin ningún artefacto compilado al que escapar
19 · Deriva del índiceálgebra de conjuntos (-, &)El diagnóstico más corto de los siete — y except: pass, el bug más corto
20 · DLQ olvidadajerarquía de excepcionesClasifica en cuatro líneas — y except Exception está a una palabra de arruinarlo

💡 El patrón que solo se ve mirando la columna entera: ninguno de los doce casos necesita instalar nada. sqlite3, logging, threading, gc y re alcanzan. Es el argumento más fuerte de Python en este laboratorio y no tiene que ver con rendimiento: tiene que ver con cuánto código de terceros hay que auditar para llegar a producción.


📈 Rendimiento: qué mide el laboratorio y cómo reproducirlo#

⚠️ Este repositorio no publica benchmarks entre lenguajes. Se mide la pendiente dentro de cada stack: legacy contra optimized, mismo runtime, misma máquina. Comparar el tiempo absoluto de Python contra Go mediría el intérprete, no el criterio.

SeñalDe dónde saleQué caso la expone
avg_ms · p95_ms · p99_msmuestras en memoria01, 02, 10
objetos vivos y tamañogc.get_objects() + sys.getsizeof05
picos de asignacióntracemalloc05
threads del pool en usoThreadPoolExecutor11
db_hits por requestcontador alrededor de sqlite301, 02

Reproducir la medición del caso 11 (el GIL como techo):

bash
docker compose -f compose.python.yml up -d --build
curl -s localhost:8200/11/activity
for i in $(seq 1 8); do curl -s "localhost:8200/11/report-legacy?rows=200000" & done; wait
curl -s "localhost:8200/11/order-write"                  # degraded: true — el CPU-bound serializa
curl -s "localhost:8200/11/report-isolated?rows=200000"  # pool separado: aisla, pero el GIL sigue ahi
curl -s localhost:8200/11/diagnostics/summary

Especificación de rendimiento que este stack verifica y ningún otro puede: separar el pool de reporting mejora el aislamiento pero no multiplica el throughput de CPU. En Java o .NET el mismo cambio sí lo multiplica. La comparación entre esos dos resultados es el argumento completo del caso 11, y solo existe porque Python está en el laboratorio.


🚧 Límites, problemas sin solución y desafíos#

LímitePor qué importaDónde se ve
El GIL impide el paralelismo de CPUSeparar pools aísla, pero no acelera el trabajo CPU-bound. Es el techo estructural del runtimecaso 11
Timeouts sin cancelación realEl wall-clock abandona el resultado; el trabajo del otro lado sigue corriendocaso 04
Ausencia sin respaldo del lenguajeis None y dict.get() son disciplina. Nada obliga a manejar el caso vacíocaso 12
Sin verificación de tipos en runtimeLas anotaciones documentan; no ejecutan. El error de contrato llega con el requestcaso 07
Callbacks síncronos sin desacopleLa lista de callbacks del caso 08 notifica en línea: no hay buffer ni consumidor independientecaso 08
Costo del intérpretePara trabajo CPU-bound puro, el orden de magnitud contra un compilado es real y no se cierra optimizando el códigotransversal

Desafío abierto del stack en este laboratorio: el caso 11 está construido sobre el supuesto de que el GIL serializa el trabajo de CPU. Python 3.13 introduce free-threading (PEP 703) como build opcional sin GIL. Si el laboratorio adoptara ese build, el argumento del caso 11 se invierte por completo — dejaría de ser "el techo del runtime" para pasar a ser "una decisión de configuración". Está anotado como disparador explícito en scripts/language_drift.py.


🏆 Dónde gana y dónde pierde en el laboratorio#

Agregado de los veredictos de las 19 comparativas que rankean: 0 primeros puestos, media 5.0.

  • 🥉 Tercero en 03LoggerAdapter + JsonFormatter: la API más difícil de violar por accidente del set completo. Es el mejor resultado de Python en el laboratorio y no tiene nada que ver con rendimiento.
  • 4º en 02, 06, 09, 14 y 15
  • 6º en 20 — clasifica en cuatro líneas y encadena causas desde la 3.0, pero except Exception está a una palabra de mandar a la DLQ los bugs del propio consumidor, y nada en el lenguaje lo señala.
  • 6º en 20 — clasifica en cuatro líneas y encadena causas desde la 3.0, pero except Exception está a una palabra de mandar a la DLQ los bugs del propio consumidor, y nada en el lenguaje lo señala.
  • 4º en 19 — el álgebra de conjuntos da el diagnóstico más corto de los siete, y except: pass el bug más corto: Python hace fácil el diagnóstico y también el error.
  • 6º en 18 — no hay curva porque no hay JIT, pero tiene el arranque más lento de los siete y es el único stack sin ninguna salida compilada: la única palanca es rediseñar los imports.
  • 5º en 16 y 17 — en el 17 la stdlib directamente no trae read-write lock, con la ventaja didáctica de tener que entenderlo por dentro. — setdefault expresa bien la reserva, pero su atomicidad viene del GIL y no del contrato del lenguaje. — sqlite3 y threading.Semaphore de stdlib, correctos y directos.
  • 6º en 04, 05, 07, 08, 12 y 13 — los casos donde el lenguaje no respalda la corrección con nada. En el 13 se suma el GIL: sin una barrera explícita, la estampida ni siquiera se deja observar.

Lectura honesta: Python queda sexto en cinco casos, y el laboratorio no lo maquilla. Lo que aporta es distinto: es el único stack que expone un límite estructural real —el GIL— y el que llega más lejos sin instalar nada. En un repositorio que compara criterio y no velocidad, ambas cosas cuentan.


🔄 Ciclo de versiones#

Versión fijada hoy3.12 (python:3.12-alpine)
Cadencia upstreamUna release menor por año, en octubre
Política de soporte5 años por versión: 3.12 recibe parches de seguridad hasta octubre de 2028
Producto en endoflife.datepython

Qué revisar en el próximo salto:

  1. 🚨 Free-threading (3.13+, PEP 703) — si el laboratorio pasa a un build sin GIL, el argumento del caso 11 deja de ser cierto. Es el disparador de revisión más importante de este stack: no es un bump, es reescribir la narrativa del caso.
  2. Cambios en sqlite3 — es la base de los casos 01 y 02.
  3. Cambios en logging — el caso 03 depende de LoggerAdapter y de un formatter propio.
  4. Subinterpretes (PEP 684) — abren un modelo de aislamiento nuevo que el caso 11 podría contrastar contra el ThreadPoolExecutor actual.

El detalle del procedimiento está en docs/language-upgrade-protocol.md.


🚀 Levantar el stack#

bash
docker compose -f compose.python.yml up -d --build

Los 20 casos quedan servidos en http://localhost:8200/NN/. Cada caso trae además su propio compose.yml para correrlo aislado.

Ver esta carpeta en GitHub ↗