<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Juan Carlos Isaza</title>
    <description>The latest articles on DEV Community by Juan Carlos Isaza (@isazajuancarlos).</description>
    <link>https://dev.to/isazajuancarlos</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4059817%2F1ee73c68-4e15-4926-951a-2c512db40d71.png</url>
      <title>DEV Community: Juan Carlos Isaza</title>
      <link>https://dev.to/isazajuancarlos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isazajuancarlos"/>
    <language>en</language>
    <item>
      <title>De la idea al MVP: lanza tu producto en 2 semanas con Python</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:20:06 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/de-la-idea-al-mvp-lanza-tu-producto-en-2-semanas-con-python-50ok</link>
      <guid>https://dev.to/isazajuancarlos/de-la-idea-al-mvp-lanza-tu-producto-en-2-semanas-con-python-50ok</guid>
      <description>&lt;p&gt;Tienes una idea. La tentación es construirla "completa" antes de enseñarla a nadie. Es el error más caro que se puede cometer: inviertes meses en funciones que quizá nadie quiere. Un &lt;strong&gt;MVP&lt;/strong&gt; (Producto Mínimo Viable) le da la vuelta a eso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué es (y qué no es) un MVP
&lt;/h2&gt;

&lt;p&gt;Un MVP es la &lt;strong&gt;versión más pequeña&lt;/strong&gt; de tu producto que permite &lt;strong&gt;validar una hipótesis&lt;/strong&gt; con usuarios reales. No es un prototipo de mentira ni el producto final recortado: es lo justo para aprender si la gente lo quiere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que un MVP NO debe tener:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Panel de administración elaborado.&lt;/li&gt;
&lt;li&gt;Múltiples integraciones "por si acaso".&lt;/li&gt;
&lt;li&gt;Optimizaciones para una escala que aún no tienes.&lt;/li&gt;
&lt;li&gt;Todas las funciones de la competencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lo que sí:&lt;/strong&gt; el flujo central que resuelve el problema, y una forma de medir si funciona.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué Python acelera esto
&lt;/h2&gt;

&lt;p&gt;Un stack Python moderno permite construir un MVP funcional en &lt;strong&gt;1–2 semanas&lt;/strong&gt; en lugar de meses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; para la lógica y las APIs: rápido y con validación automática.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NiceGUI&lt;/strong&gt; para la interfaz sin escribir JavaScript (ver &lt;a href="https://dev.to/blog/nicegui-web-app-python-sin-javascript"&gt;este artículo&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; o PostgreSQL para los datos: empieza simple, migra cuando haga falta.&lt;/li&gt;
&lt;li&gt;Despliegue en un VPS modesto detrás de nginx.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un solo desarrollador puede cubrir backend, frontend y despliegue, sin coordinar tres especialistas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El proceso
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define la hipótesis:&lt;/strong&gt; "los usuarios pagarán por X para resolver Y".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recorta sin piedad:&lt;/strong&gt; ¿cuál es el flujo mínimo que prueba eso?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construye rápido&lt;/strong&gt; con herramientas que no te frenen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lanza a usuarios reales&lt;/strong&gt; y mide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Itera&lt;/strong&gt; con lo aprendido.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La meta no es el código perfecto: es &lt;strong&gt;aprender barato&lt;/strong&gt;. Un MVP que te dice "esto no funciona" en dos semanas te ahorra meses.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Tienes una idea que validar?
&lt;/h2&gt;

&lt;p&gt;En Xiliux desarrollo MVPs en Python de principio a fin, con precio y plazos cerrados. Cuéntame tu idea y te digo cómo llevarla a un producto probable en semanas: &lt;a href="https://dev.to/#contacto"&gt;cotiza tu MVP&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>RAG explicado: cómo darle a un LLM tu propia información</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Sat, 22 Aug 2026 18:25:06 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/rag-explicado-como-darle-a-un-llm-tu-propia-informacion-2a8b</link>
      <guid>https://dev.to/isazajuancarlos/rag-explicado-como-darle-a-un-llm-tu-propia-informacion-2a8b</guid>
      <description>&lt;p&gt;Un modelo de lenguaje sabe mucho del mundo, pero &lt;strong&gt;no sabe nada de tu empresa&lt;/strong&gt;: tus manuales, tus políticas, tus productos. Y si le preguntas por algo que no sabe, puede &lt;em&gt;inventar&lt;/em&gt; una respuesta que suena convincente. &lt;strong&gt;RAG&lt;/strong&gt; (Retrieval-Augmented Generation) resuelve las dos cosas.&lt;/p&gt;

&lt;h2&gt;
  
  
  La idea
&lt;/h2&gt;

&lt;p&gt;En lugar de esperar que el modelo "se sepa" tu información, se la &lt;strong&gt;das en el momento de la pregunta&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Indexas&lt;/strong&gt; tus documentos: los divides en fragmentos y los conviertes en &lt;em&gt;embeddings&lt;/em&gt; (vectores numéricos que capturan el significado).&lt;/li&gt;
&lt;li&gt;Cuando llega una pregunta, &lt;strong&gt;buscas&lt;/strong&gt; los fragmentos más parecidos a esa pregunta.&lt;/li&gt;
&lt;li&gt;Le pasas al LLM la pregunta &lt;strong&gt;junto con esos fragmentos&lt;/strong&gt; y le pides que responda usándolos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El modelo ya no adivina: responde a partir de un contexto real que tú controlas. Y puedes pedirle que &lt;strong&gt;cite&lt;/strong&gt; de dónde salió cada dato.&lt;/p&gt;

&lt;h2&gt;
  
  
  El patrón en Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Indexado (una vez): fragmentos -&amp;gt; embeddings -&amp;gt; base vectorial
#    (con librerías como sentence-transformers + FAISS, o un servicio gestionado)
&lt;/span&gt;
&lt;span class="c1"&gt;# 2. En cada pregunta: recuperar los fragmentos relevantes
&lt;/span&gt;&lt;span class="n"&gt;fragmentos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_vectorial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buscar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pregunta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;contexto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragmentos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Generar la respuesta con el contexto
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Responde SOLO con la información del contexto. Si no está, dilo.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Contexto:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contexto&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Pregunta: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pregunta&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fíjate en la instrucción del &lt;code&gt;system&lt;/code&gt;: pedirle que responda &lt;strong&gt;solo&lt;/strong&gt; con el contexto y que admita cuando no sabe es lo que reduce drásticamente las alucinaciones.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Para qué sirve?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asistentes de soporte&lt;/strong&gt; que responden con tu documentación real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Búsqueda interna&lt;/strong&gt; en lenguaje natural sobre tus manuales o wikis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding&lt;/strong&gt;: un chatbot que conoce tus procesos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Detalles que marcan la diferencia
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cómo divides los documentos&lt;/strong&gt; (chunking) afecta mucho a la calidad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buenos embeddings&lt;/strong&gt; y una base vectorial adecuada importan más que el modelo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citar las fuentes&lt;/strong&gt; genera confianza y permite verificar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RAG es hoy una de las formas más rentables de aplicar IA en un negocio: no reentrenas nada, y el asistente mejora con solo actualizar tus documentos.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Quieres un asistente con tus datos?
&lt;/h2&gt;

&lt;p&gt;En Xiliux construyo sistemas RAG en Python de principio a fin. Cuéntame qué información quieres poner a trabajar: &lt;a href="https://dev.to/#contacto"&gt;contacto&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Cifrado híbrido: por qué combinar criptografía clásica y post-cuántica</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Fri, 21 Aug 2026 18:25:21 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/cifrado-hibrido-por-que-combinar-criptografia-clasica-y-post-cuantica-3k69</link>
      <guid>https://dev.to/isazajuancarlos/cifrado-hibrido-por-que-combinar-criptografia-clasica-y-post-cuantica-3k69</guid>
      <description>&lt;p&gt;Cuando aparece un algoritmo criptográfico nuevo, surge una tensión: los algoritmos &lt;strong&gt;clásicos&lt;/strong&gt; (como X25519 o Ed25519) llevan años resistiendo ataques, pero son vulnerables a un futuro ordenador cuántico; los &lt;strong&gt;post-cuánticos&lt;/strong&gt; (como ML-KEM o ML-DSA) resisten a lo cuántico, pero son más recientes y menos probados. El cifrado híbrido resuelve la tensión: &lt;strong&gt;usa los dos a la vez&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  La idea en una frase
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Combina un algoritmo clásico y uno post-cuántico de forma que el sistema &lt;strong&gt;solo se rompe si ambos fallan simultáneamente&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Un atacante clásico tendría que romper el algoritmo post-cuántico; un atacante cuántico tendría que romper el clásico &lt;em&gt;y además&lt;/em&gt; el post-cuántico. Ganas seguridad frente al futuro sin apostar todo a un algoritmo joven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dos lugares donde aplicarlo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Intercambio de claves (cifrar para un destinatario).&lt;/strong&gt; Se combinan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;X25519&lt;/strong&gt; — intercambio de claves clásico, rápido y muy probado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML-KEM-1024&lt;/strong&gt; — el mecanismo de encapsulado post-cuántico del NIST, en su nivel más alto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Las dos claves resultantes se mezclan con una función de derivación (HKDF) ligada al contexto, de modo que ninguna por separado basta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firmas digitales (autenticidad).&lt;/strong&gt; Se combinan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ed25519&lt;/strong&gt; — firma clásica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML-DSA-87&lt;/strong&gt; — firma post-cuántica del NIST.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El mensaje se acepta solo si &lt;strong&gt;ambas&lt;/strong&gt; firmas verifican (un combinador tipo AND).&lt;/p&gt;

&lt;h2&gt;
  
  
  Un principio que nunca se rompe
&lt;/h2&gt;

&lt;p&gt;Hay una regla de oro en criptografía, el &lt;strong&gt;principio de Kerckhoffs&lt;/strong&gt;: un sistema debe ser seguro aunque el atacante conozca todo su diseño; la seguridad vive en la &lt;strong&gt;clave&lt;/strong&gt;, no en ocultar el formato. Un buen sistema híbrido usa primitivas públicas y auditadas (XChaCha20-Poly1305 para cifrar, Argon2id para derivar claves de contraseñas, HKDF para separar dominios) y &lt;strong&gt;nunca inventa criptografía propia&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo lo aplica Quipu
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/blog/quipu-cifrado-multilenguaje-rust-python-node-go"&gt;Quipu&lt;/a&gt; es una librería libre que implementa exactamente este enfoque para &lt;strong&gt;datos en reposo&lt;/strong&gt;: cifrado híbrido X25519 + ML-KEM-1024, firma híbrida Ed25519 + ML-DSA-87, y solo primitivas verificadas por debajo. Está en nivel de seguridad NIST 5 (CNSA 2.0) y es de código abierto, así que cualquiera puede revisar cómo funciona.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nota honesta: Quipu compone primitivas estándar, pero la composición aún no ha pasado una auditoría criptográfica &lt;strong&gt;independiente&lt;/strong&gt;. Para secretos reales de alto valor, esa auditoría externa es el sello que conviene esperar.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;¿Necesitas proteger datos en reposo con visión de futuro? &lt;a href="https://dev.to/#contacto"&gt;Hablemos&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>rust</category>
      <category>security</category>
      <category>postquantum</category>
    </item>
    <item>
      <title>¿Qué es la criptografía post-cuántica y por qué deberías migrar ya?</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:15:58 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/que-es-la-criptografia-post-cuantica-y-por-que-deberias-migrar-ya-1l45</link>
      <guid>https://dev.to/isazajuancarlos/que-es-la-criptografia-post-cuantica-y-por-que-deberias-migrar-ya-1l45</guid>
      <description>&lt;p&gt;La criptografía que protege hoy tus datos —claves, HTTPS, firmas— se apoya en problemas matemáticos que los ordenadores actuales no pueden resolver en un tiempo razonable. Un ordenador cuántico suficientemente grande &lt;strong&gt;sí&lt;/strong&gt; podría resolver algunos de ellos. Eso es la criptografía post-cuántica: algoritmos diseñados para resistir tanto a ordenadores clásicos como cuánticos.&lt;/p&gt;

&lt;h2&gt;
  
  
  La amenaza no es solo del futuro
&lt;/h2&gt;

&lt;p&gt;Podrías pensar: "no existe todavía un ordenador cuántico que rompa mi cifrado, así que puedo esperar". El problema es un ataque llamado &lt;strong&gt;"harvest now, decrypt later"&lt;/strong&gt; (cosecha ahora, descifra después):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Un adversario &lt;strong&gt;captura hoy&lt;/strong&gt; tu tráfico cifrado y lo guarda. Cuando dentro de unos años exista la capacidad cuántica, lo &lt;strong&gt;descifra&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Si tus datos deben seguir siendo secretos dentro de 10 años (historiales médicos, secretos industriales, datos personales), la amenaza es &lt;strong&gt;presente&lt;/strong&gt;, no futura. Los datos que cifras hoy con criptografía clásica podrían leerse mañana.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los nuevos estándares del NIST
&lt;/h2&gt;

&lt;p&gt;El NIST (el organismo estadounidense de estándares) finalizó en 2024 los primeros algoritmos post-cuánticos, tras años de análisis público:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ML-KEM&lt;/strong&gt; (antes CRYSTALS-Kyber): para &lt;strong&gt;intercambio de claves&lt;/strong&gt; — cómo dos partes acuerdan una clave secreta de forma segura.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML-DSA&lt;/strong&gt; (antes CRYSTALS-Dilithium): para &lt;strong&gt;firmas digitales&lt;/strong&gt; — cómo verificar que un mensaje es auténtico y no fue alterado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estos ya no son experimentos académicos: son estándares que gobiernos y grandes empresas están empezando a exigir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cifrado híbrido: el enfoque prudente
&lt;/h2&gt;

&lt;p&gt;Los algoritmos post-cuánticos son nuevos, y "nuevo" en criptografía significa "menos probado". Por eso la mejor práctica actual es &lt;strong&gt;híbrida&lt;/strong&gt;: combinar un algoritmo clásico bien establecido con uno post-cuántico. El resultado solo se rompe si &lt;strong&gt;ambos&lt;/strong&gt; caen a la vez. Ganas protección post-cuántica sin renunciar a décadas de confianza en la criptografía clásica.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por dónde empezar?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventaría&lt;/strong&gt; dónde usas criptografía y qué datos deben durar años.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioriza&lt;/strong&gt; los datos en reposo de larga vida (los más expuestos al "harvest now").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopta cifrado híbrido&lt;/strong&gt; en vez de reemplazos de golpe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usa librerías que compongan primitivas verificadas&lt;/strong&gt;, no criptografía casera.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este último punto es clave, y es la filosofía detrás de &lt;a href="https://dev.to/blog/quipu-cifrado-multilenguaje-rust-python-node-go"&gt;Quipu&lt;/a&gt;, una librería libre de cifrado híbrido post-cuántico para datos en reposo. Si te interesa proteger tus datos frente a la amenaza cuántica, &lt;a href="https://dev.to/#contacto"&gt;hablemos&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>rust</category>
      <category>security</category>
      <category>postquantum</category>
    </item>
    <item>
      <title>Automatiza tareas repetitivas con un bot en Python</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Wed, 19 Aug 2026 18:22:46 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/automatiza-tareas-repetitivas-con-un-bot-en-python-2p3b</link>
      <guid>https://dev.to/isazajuancarlos/automatiza-tareas-repetitivas-con-un-bot-en-python-2p3b</guid>
      <description>&lt;p&gt;Cada tarea manual y repetitiva que haces cada semana es tiempo (y dinero) que un script de Python puede recuperarte. La automatización no es solo para grandes empresas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primero: ¿qué vale la pena automatizar?
&lt;/h2&gt;

&lt;p&gt;Busca tareas &lt;strong&gt;repetitivas, basadas en reglas y frecuentes&lt;/strong&gt;. Algunos ejemplos habituales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Descargar y consolidar informes cada mañana.&lt;/li&gt;
&lt;li&gt;Copiar datos entre una web y una hoja de cálculo.&lt;/li&gt;
&lt;li&gt;Enviar recordatorios o alertas.&lt;/li&gt;
&lt;li&gt;Vigilar precios o cambios en una página.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una regla práctica: si puedes explicar la tarea como una lista de pasos sin excepciones, probablemente se puede automatizar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las herramientas del ecosistema Python
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;requests / httpx&lt;/strong&gt; para hablar con APIs y webs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BeautifulSoup / Playwright&lt;/strong&gt; para web scraping (Playwright cuando la página carga con JavaScript).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pandas&lt;/strong&gt; para transformar datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APScheduler&lt;/strong&gt; o &lt;strong&gt;cron&lt;/strong&gt; para ejecutarlo en un horario.&lt;/li&gt;
&lt;li&gt;Bots de &lt;strong&gt;Telegram o Discord&lt;/strong&gt; para recibir avisos donde ya estás.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Un ejemplo mínimo
&lt;/h2&gt;

&lt;p&gt;Vigilar el título de una página y avisar si cambia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;titulo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;anterior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;titulo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...ejecutado por cron cada hora...
&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;titulo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;anterior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;¡Cambió!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# aquí enviarías un mensaje de Telegram
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  De script a bot fiable
&lt;/h2&gt;

&lt;p&gt;Un script que corre en tu portátil es un buen comienzo, pero un bot &lt;strong&gt;fiable&lt;/strong&gt; vive en un servidor, registra lo que hace, maneja errores (reintentos, tiempos de espera) y te avisa si algo falla. Ese salto —de experimento a herramienta en la que confías— es donde más aporta un desarrollador.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Tienes una tarea que odias hacer a mano?
&lt;/h2&gt;

&lt;p&gt;Probablemente se pueda automatizar. Cuéntame cuál es y te digo cómo abordarla: &lt;a href="https://dev.to/#contacto"&gt;contacto&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>NiceGUI: crea una aplicación web en Python sin escribir JavaScript</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Tue, 18 Aug 2026 18:45:05 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/nicegui-crea-una-aplicacion-web-en-python-sin-escribir-javascript-3kef</link>
      <guid>https://dev.to/isazajuancarlos/nicegui-crea-una-aplicacion-web-en-python-sin-escribir-javascript-3kef</guid>
      <description>&lt;p&gt;Si sabes Python pero el frontend te frena, &lt;strong&gt;NiceGUI&lt;/strong&gt; es una de las mejores noticias de los últimos años: te permite construir aplicaciones web completas —con botones, formularios, gráficos y navegación— usando solo Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué es NiceGUI?
&lt;/h2&gt;

&lt;p&gt;Es un framework construido sobre &lt;strong&gt;FastAPI&lt;/strong&gt; (backend) y &lt;strong&gt;Quasar/Vue&lt;/strong&gt; (frontend). Tú escribes Python; NiceGUI genera la interfaz en el navegador y mantiene sincronizado el estado por ti. No necesitas HTML, CSS ni JavaScript para empezar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo simple que es
&lt;/h2&gt;

&lt;p&gt;Una app con un botón que muestra un mensaje son literalmente cuatro líneas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nicegui&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;

&lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Saludar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;¡Hola!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La jerarquía de la interfaz se expresa con &lt;em&gt;context managers&lt;/em&gt;, que reflejan cómo se anidan los elementos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;card&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Iniciar sesión&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text-xl font-bold&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;usuario&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Usuario&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;clave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Clave&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Entrar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;entrar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Añadir estado reactivo, formularios, tablas o rutas (&lt;code&gt;@ui.page('/panel')&lt;/code&gt;) es igual de directo.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Para qué es ideal?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MVPs y prototipos:&lt;/strong&gt; validar una idea en días, no semanas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards&lt;/strong&gt; internos y paneles de datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Herramientas internas&lt;/strong&gt; para tu equipo, sin montar un frontend aparte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demos&lt;/strong&gt; de modelos de IA o scripts que necesitan una interfaz.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ¿Cuándo NO es la mejor opción?
&lt;/h2&gt;

&lt;p&gt;NiceGUI renderiza en el cliente, así que para sitios donde el &lt;strong&gt;SEO del contenido&lt;/strong&gt; es crítico (un blog, una landing pública que debe posicionar) conviene complementarlo con buenas meta etiquetas y datos estructurados, o valorar renderizado en servidor. Para aplicaciones, dashboards y herramientas internas es una elección excelente y muy productiva.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendimiento y despliegue
&lt;/h2&gt;

&lt;p&gt;Una app NiceGUI se despliega como cualquier app de FastAPI/Uvicorn, normalmente detrás de &lt;strong&gt;nginx&lt;/strong&gt; con systemd. Sirve los estáticos desde nginx y usa &lt;code&gt;ui.run(show=False)&lt;/code&gt; en producción para no abrir un navegador en el servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Quieres una app así?
&lt;/h2&gt;

&lt;p&gt;Este mismo portafolio está hecho con NiceGUI. Si necesitas un MVP, un dashboard o una herramienta interna en Python, &lt;a href="https://dev.to/#contacto"&gt;escríbeme&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cómo integrar un LLM (Claude o GPT) en tu aplicación Python</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Mon, 17 Aug 2026 18:30:04 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/como-integrar-un-llm-claude-o-gpt-en-tu-aplicacion-python-42gm</link>
      <guid>https://dev.to/isazajuancarlos/como-integrar-un-llm-claude-o-gpt-en-tu-aplicacion-python-42gm</guid>
      <description>&lt;p&gt;Integrar un modelo de lenguaje (LLM) en una aplicación Python es hoy más sencillo de lo que parece, y abre la puerta a chatbots, asistentes internos, extracción de datos y automatización con lenguaje natural. En esta guía verás el patrón completo, con código real.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Elige el proveedor
&lt;/h2&gt;

&lt;p&gt;Los tres más usados son &lt;strong&gt;Anthropic (Claude)&lt;/strong&gt;, &lt;strong&gt;OpenAI (GPT)&lt;/strong&gt; y &lt;strong&gt;Google (Gemini)&lt;/strong&gt;. Todos exponen una API HTTP con un SDK de Python oficial, y la lógica de tu app apenas cambia entre ellos. En los ejemplos usaré Claude, pero el patrón es idéntico en los demás.&lt;/p&gt;

&lt;p&gt;Instala el SDK y guarda tu clave en una &lt;strong&gt;variable de entorno&lt;/strong&gt;, nunca en el código:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tu-clave"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. La llamada mínima
&lt;/h2&gt;

&lt;p&gt;El patrón es siempre el mismo: envías una lista de mensajes y recibes una respuesta.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# lee ANTHROPIC_API_KEY del entorno
&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Resume en una frase: la fotosíntesis...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dos detalles importantes: &lt;code&gt;resp.content&lt;/code&gt; es una &lt;strong&gt;lista de bloques&lt;/strong&gt; (comprueba &lt;code&gt;.type&lt;/code&gt; antes de leer &lt;code&gt;.text&lt;/code&gt;), y &lt;code&gt;max_tokens&lt;/code&gt; limita la longitud de la respuesta.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Streaming para una buena experiencia
&lt;/h2&gt;

&lt;p&gt;En una interfaz, esperar a que se genere todo el texto se siente lento. El &lt;strong&gt;streaming&lt;/strong&gt; muestra la respuesta token a token, como en ChatGPT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Escribe un email de bienvenida.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para salidas largas, el streaming además evita que la petición supere el tiempo de espera de la conexión.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Salida estructurada (JSON fiable)
&lt;/h2&gt;

&lt;p&gt;Si necesitas que el modelo devuelva datos en un formato exacto (por ejemplo para guardarlos en una base de datos), pide un esquema JSON en vez de parsear texto libre. Esto elimina la clase de bugs más común al integrar IA: respuestas con formato impredecible.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Buenas prácticas para producción
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Controla el coste:&lt;/strong&gt; fija un &lt;code&gt;max_tokens&lt;/code&gt; razonable y cachea respuestas repetidas. Los SDKs modernos soportan &lt;em&gt;prompt caching&lt;/em&gt;, que abarata mucho el contexto reutilizado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maneja errores y reintentos:&lt;/strong&gt; las APIs fallan; los SDKs oficiales ya reintentan errores 429 y 5xx con &lt;em&gt;backoff&lt;/em&gt; exponencial. Captura las excepciones tipadas (&lt;code&gt;RateLimitError&lt;/code&gt;, etc.) en lugar de comparar cadenas de texto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protege tus claves:&lt;/strong&gt; variables de entorno, rotación periódica y permisos mínimos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elige el modelo según la tarea:&lt;/strong&gt; un modelo grande para razonamiento complejo, uno más pequeño y rápido para clasificación de alto volumen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ¿Necesitas ayuda?
&lt;/h2&gt;

&lt;p&gt;En Xiliux integro LLMs en aplicaciones Python de principio a fin: chatbots, automatización con IA y asistentes internos. Si tienes una idea, &lt;a href="https://dev.to/#contacto"&gt;hablemos&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>i18n sin gettext: traducciones en JSON con claves de punto</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Sun, 16 Aug 2026 18:29:59 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/i18n-sin-gettext-traducciones-en-json-con-claves-de-punto-48fd</link>
      <guid>https://dev.to/isazajuancarlos/i18n-sin-gettext-traducciones-en-json-con-claves-de-punto-48fd</guid>
      <description>&lt;p&gt;Quieres que tu app hable español e inglés. Buscas cómo, y el ecosistema te empuja a &lt;code&gt;gettext&lt;/code&gt; o Babel: ficheros &lt;code&gt;.po&lt;/code&gt;, un paso de compilación a &lt;code&gt;.mo&lt;/code&gt;, herramientas de extracción. Potente, sí. Pero para una app pequeña o mediana es un peaje que no querías pagar — solo necesitabas un &lt;code&gt;t()&lt;/code&gt; honesto.&lt;/p&gt;

&lt;p&gt;Lo resolví tantas veces que lo empaqueté: &lt;a href="https://github.com/isazajuancarlos/dotkey-i18n" rel="noopener noreferrer"&gt;&lt;code&gt;dotkey-i18n&lt;/code&gt;&lt;/a&gt;, Python puro, sin dependencias. Tus traducciones son JSON que cualquiera puede editar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;locales/es.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hola, {name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Entrar"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"menu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Informes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ajustes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotkey_i18n&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Translator&lt;/span&gt;

&lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Translator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login.welcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Juan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# "Hola, Juan"
&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;menu.reports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# "Reports"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tres detalles que marcan la diferencia
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claves con notación de punto.&lt;/strong&gt; &lt;code&gt;t("login.submit")&lt;/code&gt; navega el JSON anidado. Agrupas las cadenas por pantalla o módulo sin claves planas kilométricas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback al idioma por defecto.&lt;/strong&gt; Si una clave falta en el idioma pedido, se busca en el idioma por defecto antes de rendirse. Tus traducciones pueden ir incompletas —la vida real— sin dejar huecos en blanco en la interfaz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nunca revienta la interfaz.&lt;/strong&gt; Una clave que no existe devuelve la propia clave (un marcador visible, no una excepción a mitad de render). Una interpolación con un campo que falta devuelve el texto sin formatear. Un JSON corrupto se trata como vacío. Nada de esto tumba la pantalla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agnóstico del framework
&lt;/h2&gt;

&lt;p&gt;El idioma actual entra por un &lt;code&gt;lang_getter&lt;/code&gt; inyectable, así el mismo &lt;code&gt;Translator&lt;/code&gt; sirve en NiceGUI, Flask, FastAPI o un script suelto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# NiceGUI: idioma desde la sesión del usuario
&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Translator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;lang_getter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idioma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Flask
&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Translator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang_getter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lang&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La prioridad es clara: &lt;code&gt;lang=&lt;/code&gt; explícito → &lt;code&gt;lang_getter()&lt;/code&gt; → idioma por defecto.&lt;/p&gt;

&lt;h2&gt;
  
  
  De dónde viene
&lt;/h2&gt;

&lt;p&gt;Salió del servicio i18n de un sistema de gestión de informes bilingüe. Estaba atado al framework —leía el idioma directamente de la sesión de NiceGUI—; al extraerlo se desacopló con el &lt;code&gt;lang_getter&lt;/code&gt;, y se le añadió el fallback al idioma por defecto que el original no tenía. Extraer para publicar suele mejorar el código original: lo obliga a no depender de su casa.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;dotkey-i18n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Código y tests (notación de punto, fallback, claves ausentes, JSON corrupto; verificados por mutación): &lt;strong&gt;&lt;a href="https://github.com/isazajuancarlos/dotkey-i18n" rel="noopener noreferrer"&gt;github.com/isazajuancarlos/dotkey-i18n&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Notificar a varios canales sin que un fallo tumbe al resto</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Sat, 15 Aug 2026 18:28:19 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/notificar-a-varios-canales-sin-que-un-fallo-tumbe-al-resto-3b5a</link>
      <guid>https://dev.to/isazajuancarlos/notificar-a-varios-canales-sin-que-un-fallo-tumbe-al-resto-3b5a</guid>
      <description>&lt;p&gt;Quieres mandar la misma notificación a varios sitios: Slack, Discord, un webhook, un email. La primera versión es un &lt;code&gt;for&lt;/code&gt; de tres líneas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;canal&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;canales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;canal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mensaje&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y funciona en las demos. Hasta que un día Discord devuelve un 500, &lt;code&gt;canal(mensaje)&lt;/code&gt; lanza, y &lt;strong&gt;el email y el Slack que iban detrás nunca salen&lt;/strong&gt;. Peor: te enteras por el usuario que no recibió la alerta, no por un log.&lt;/p&gt;

&lt;p&gt;Dos cosas fallan en ese &lt;code&gt;for&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No aísla.&lt;/strong&gt; La primera excepción corta el reparto entero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No reporta.&lt;/strong&gt; O cada canal se traga su error en un &lt;code&gt;try/except&lt;/code&gt; disperso, o el fallo se pierde.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  La forma correcta
&lt;/h2&gt;

&lt;p&gt;Aísla cada canal y recoge el resultado. Lo empaqueté como &lt;a href="https://github.com/isazajuancarlos/fanout-broadcast" rel="noopener noreferrer"&gt;&lt;code&gt;fanout-broadcast&lt;/code&gt;&lt;/a&gt; —Python puro, sin dependencias— porque lo reescribía en cada proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fanout_broadcast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Broadcaster&lt;/span&gt;

&lt;span class="n"&gt;bc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Broadcaster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;a_discord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a_telegram&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;a_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# apagado por ahora
&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;¡Nueva versión publicada!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s falló: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;broadcast&lt;/code&gt; llama a todos los canales habilitados, &lt;strong&gt;captura la excepción de cada uno por separado&lt;/strong&gt;, y sigue con el siguiente. Un Discord caído ya no impide que salga el email. Al final tienes un reporte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;           &lt;span class="c1"&gt;# ¿ningún canal falló?
&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivered&lt;/span&gt;    &lt;span class="c1"&gt;# los que entregaron
&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;       &lt;span class="c1"&gt;# los que lanzaron (cada uno con su .error)
&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skipped&lt;/span&gt;      &lt;span class="c1"&gt;# los que estaban deshabilitados
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Encender y apagar sin ramificar el código
&lt;/h2&gt;

&lt;p&gt;Cada canal tiene un interruptor, en runtime o por variable de entorno:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fanout_broadcast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;env_enabled&lt;/span&gt;

&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a_discord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;env_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# mira DISCORD_ENABLED
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto importa más de lo que parece: separa &lt;em&gt;qué canales existen&lt;/em&gt; de &lt;em&gt;cuáles están activos hoy&lt;/em&gt;, sin comentar código ni meter &lt;code&gt;if&lt;/code&gt; por todos lados. Apagas un canal problemático con una variable de entorno, no con un despliegue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalar, pero después de intentarlo todo
&lt;/h2&gt;

&lt;p&gt;A veces sí quieres que un fallo se propague. La diferencia está en &lt;em&gt;cuándo&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mensaje&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_failures&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# lanza DESPUÉS de haber intentado todos
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Al revés que el &lt;code&gt;for&lt;/code&gt; ingenuo, que aborta en el primero. Intentas todos los canales, y solo entonces decides si el conjunto fue un fracaso.&lt;/p&gt;

&lt;h2&gt;
  
  
  De dónde viene
&lt;/h2&gt;

&lt;p&gt;Salió de un agente que publicaba en varias redes a la vez. Extraje solo la fontanería: &lt;em&gt;qué&lt;/em&gt; se publica y &lt;em&gt;a quién&lt;/em&gt; es la lógica de cada aplicación; el &lt;em&gt;cómo&lt;/em&gt; repartir con aislamiento y reporte es genérico, y sirve igual para notificaciones, alertas o webhooks. Por eso es una librería, no un &lt;code&gt;for&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fanout-broadcast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Código y tests (centrados en el aislamiento de fallos, verificados por mutación): &lt;strong&gt;&lt;a href="https://github.com/isazajuancarlos/fanout-broadcast" rel="noopener noreferrer"&gt;github.com/isazajuancarlos/fanout-broadcast&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cuando tu clasificador parpadea: histéresis para señales que oscilan</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Sat, 08 Aug 2026 18:19:23 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/cuando-tu-clasificador-parpadea-histeresis-para-senales-que-oscilan-1499</link>
      <guid>https://dev.to/isazajuancarlos/cuando-tu-clasificador-parpadea-histeresis-para-senales-que-oscilan-1499</guid>
      <description>&lt;p&gt;Tienes una señal que a cada observación te dice en qué estado estás: un monitor de salud que dice &lt;code&gt;OK&lt;/code&gt; o &lt;code&gt;CAÍDO&lt;/code&gt;, un detector de conectividad, un clasificador de modo. Y cerca del umbral &lt;strong&gt;oscila&lt;/strong&gt;: &lt;code&gt;OK, CAÍDO, OK, CAÍDO, OK&lt;/code&gt;. Cada cambio dispara algo —una alerta, un failover, entrar o salir de una posición— y de repente tu sistema está temblando por ruido, no por una transición real.&lt;/p&gt;

&lt;p&gt;Es el mismo problema que resuelve el termostato de tu casa desde hace un siglo, y la solución tiene nombre: &lt;strong&gt;histéresis&lt;/strong&gt;. No cambies de estado hasta que el nuevo se haya sostenido.&lt;/p&gt;

&lt;h2&gt;
  
  
  La regla, en una frase
&lt;/h2&gt;

&lt;p&gt;Un estado nuevo solo se &lt;strong&gt;confirma&lt;/strong&gt; tras repetirse &lt;em&gt;N&lt;/em&gt; observaciones consecutivas. Si el candidato cambia o revierte antes de llegar a &lt;em&gt;N&lt;/em&gt;, la cuenta se reinicia. El estado vigente se mantiene estable; los parpadeos se ignoran.&lt;/p&gt;

&lt;p&gt;Lo empaqueté como librería —&lt;a href="https://github.com/isazajuancarlos/hysteresis-state" rel="noopener noreferrer"&gt;&lt;code&gt;hysteresis-state&lt;/code&gt;&lt;/a&gt;, Python puro, sin dependencias— porque lo reescribía una y otra vez:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hysteresis_state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HysteresisState&lt;/span&gt;

&lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HysteresisState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirmations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lectura&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="c1"&gt;# "OK" / "CAIDO"
&lt;/span&gt;    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lectura&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# solo cambia tras 3 lecturas seguidas
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="c1"&gt;# ¿esta lectura provocó la transición?
&lt;/span&gt;        &lt;span class="nf"&gt;alertar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aliméntalo con &lt;code&gt;OK, CAÍDO, OK, CAÍDO, OK&lt;/code&gt; y no pasa nada: ningún candidato se sostuvo. Hacen falta tres &lt;code&gt;CAÍDO&lt;/code&gt; seguidos para que el cambio se confirme.&lt;/p&gt;

&lt;h2&gt;
  
  
  El detalle que casi siempre falta: histéresis asimétrica
&lt;/h2&gt;

&lt;p&gt;Un umbral único tiene un problema sutil. Si exiges 3 confirmaciones para &lt;em&gt;entrar&lt;/em&gt; en fallo, también tardas 3 en &lt;em&gt;salir&lt;/em&gt; — y a veces quieres justo lo contrario: &lt;strong&gt;caer rápido a lo seguro, volver despacio a lo arriesgado&lt;/strong&gt;. Es el comportamiento de un disyuntor eléctrico: salta a la primera, se rearma con cautela.&lt;/p&gt;

&lt;p&gt;Se resuelve dejando que el umbral dependa de la transición:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1 confirmación para caer a "CAIDO", 5 para volver a "OK"
&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;desde&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hacia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hacia&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAIDO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HysteresisState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirmations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAIDO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# cae al instante
# ...ahora hacen falta 5 "OK" seguidos para volver
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Por qué no basta con "promediar"
&lt;/h2&gt;

&lt;p&gt;La tentación es suavizar con una media móvil y umbralizar. Funciona para señales numéricas, pero se rompe con &lt;strong&gt;estados discretos&lt;/strong&gt; (no promedias &lt;code&gt;OK&lt;/code&gt; y &lt;code&gt;CAÍDO&lt;/code&gt;), introduce retardo en &lt;em&gt;todas&lt;/em&gt; las transiciones por igual, y no te da la asimetría de arriba. La histéresis por confirmación es la herramienta correcta cuando lo que oscila es una &lt;em&gt;etiqueta&lt;/em&gt;, no un número.&lt;/p&gt;

&lt;h2&gt;
  
  
  De dónde salió
&lt;/h2&gt;

&lt;p&gt;Esto nació dentro de un bot de trading. Su cerebro de régimen clasificaba el mercado en &lt;em&gt;tendencia / rango / caos&lt;/em&gt;, y cerca de los umbrales parpadeaba: cada parpadeo congelaba o reactivaba la operativa, que es exactamente lo que no quieres que pase por ruido. La histéresis lo calmó. Pero el mecanismo no sabe nada de mercados —es anti-flapping para cualquier señal discreta—, así que lo liberé.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;hysteresis-state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Código y tests (con los casos límite del conteo y la asimetría, verificados por mutación): &lt;strong&gt;&lt;a href="https://github.com/isazajuancarlos/hysteresis-state" rel="noopener noreferrer"&gt;github.com/isazajuancarlos/hysteresis-state&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Dos formas en que un backtest te miente (y cómo evitarlas)</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Fri, 07 Aug 2026 18:23:26 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/dos-formas-en-que-un-backtest-te-miente-y-como-evitarlas-4c63</link>
      <guid>https://dev.to/isazajuancarlos/dos-formas-en-que-un-backtest-te-miente-y-como-evitarlas-4c63</guid>
      <description>&lt;p&gt;Pruebas una estrategia, o un modelo, sobre datos históricos. El backtest da un número bonito. Y luego, en real, no aparece. Casi siempre es una de estas dos ilusiones — y las dos se descartan con muy poco código.&lt;/p&gt;

&lt;p&gt;Empaqueté las dos correcciones como librería: &lt;a href="https://github.com/isazajuancarlos/honest-eval" rel="noopener noreferrer"&gt;&lt;code&gt;honest-eval&lt;/code&gt;&lt;/a&gt;, Python puro, sin dependencias. Salieron de un bot de trading, pero el rigor no tiene nada de específico al trading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ilusión 1: el modelo vio el futuro
&lt;/h2&gt;

&lt;p&gt;Partir los datos con el clásico &lt;code&gt;train_test_split&lt;/code&gt; aleatorio es correcto para datos independientes. En una &lt;strong&gt;serie temporal&lt;/strong&gt; es un desastre silencioso: mete muestras de mañana en el conjunto de entrenamiento, y el modelo "predice" en el test cosas que en producción todavía no habrían pasado. La métrica sale inflada, y confías en un edge que no existe.&lt;/p&gt;

&lt;p&gt;El test honesto es siempre el &lt;strong&gt;futuro&lt;/strong&gt;: el tramo más reciente en el tiempo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;honest_eval&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;temporal_split&lt;/span&gt;

&lt;span class="n"&gt;train_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;temporal_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_frac&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embargo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_tr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_te&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;train_idx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;test_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Devuelve índices, así lo aplicas a numpy, pandas o listas por igual.&lt;/p&gt;

&lt;p&gt;El &lt;strong&gt;embargo&lt;/strong&gt; cierra una fuga más sutil: si tu etiqueta mira &lt;code&gt;h&lt;/code&gt; pasos adelante, una muestra de entrenamiento a menos de &lt;code&gt;h&lt;/code&gt; del corte ya conoce parte del resultado del test. &lt;code&gt;embargo=h&lt;/code&gt; descarta ese borde. La métrica baja — pero por fin es la real &lt;em&gt;out-of-sample&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ilusión 2: la variante ganó por suerte
&lt;/h2&gt;

&lt;p&gt;Tienes varias variantes y quieres la mejor. Eliges la de mayor media. Error: con pocas muestras, &lt;strong&gt;eso premia la varianza, no la ventaja&lt;/strong&gt;. La variante más ruidosa suele quedar arriba por azar.&lt;/p&gt;

&lt;p&gt;Dos correcciones, ambas dentro de &lt;code&gt;select_best_variant&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aparear.&lt;/strong&gt; Mide variante y baseline sobre el &lt;em&gt;mismo&lt;/em&gt; ensayo y trabaja con &lt;code&gt;δ = variante − baseline&lt;/code&gt;. La varianza común del ensayo se cancela en la resta, y te quedas con la señal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exigir cota inferior de confianza &amp;gt; 0.&lt;/strong&gt; Gradúa una variante solo si &lt;code&gt;media − z·SE &amp;gt; 0&lt;/code&gt;: "incluso siendo pesimista dentro del margen de confianza, sigue por encima del baseline".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;honest_eval&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;select_best_variant&lt;/span&gt;

&lt;span class="n"&gt;variants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chandelier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;momentum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# media alta, muy dispersa
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;elegido&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_best_variant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.6449&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_effective_n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;politica&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elegido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elegido&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;momentum&lt;/code&gt; puede tener la media más alta y aun así &lt;strong&gt;no graduar&lt;/strong&gt;: su dispersión hunde la cota inferior. Eso es exactamente lo que quieres que pase — es el mecanismo que rechaza al ganador de suerte.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuando lo reciente pesa más
&lt;/h3&gt;

&lt;p&gt;Si el proceso cambia con el tiempo, pondera por recencia. El &lt;strong&gt;tamaño de muestra efectivo de Kish&lt;/strong&gt; &lt;code&gt;(Σw)² / Σw²&lt;/code&gt; evita que unas pocas muestras muy pesadas se hagan pasar por muchas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;honest_eval&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;halflife_weight&lt;/span&gt;

&lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;halflife_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;halflife&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exit_ts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;variants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chandelier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  No crean señal — dejan de fabricarla
&lt;/h2&gt;

&lt;p&gt;Ninguna de las dos inventa edge: eso son datos y buenas features. Lo que hacen es &lt;strong&gt;dejar de fabricarlo donde no lo hay&lt;/strong&gt;. Son el mínimo para no mentirte antes de arriesgar algo real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;honest-eval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo, tests (con la matemática anclada a mano y verificada por mutación) y detalle: &lt;strong&gt;&lt;a href="https://github.com/isazajuancarlos/honest-eval" rel="noopener noreferrer"&gt;github.com/isazajuancarlos/honest-eval&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Por qué tu bot recibe 403 de Cloudflare (y cómo endurecer un cliente ccxt)</title>
      <dc:creator>Juan Carlos Isaza</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:31:54 +0000</pubDate>
      <link>https://dev.to/isazajuancarlos/por-que-tu-bot-recibe-403-de-cloudflare-y-como-endurecer-un-cliente-ccxt-2nme</link>
      <guid>https://dev.to/isazajuancarlos/por-que-tu-bot-recibe-403-de-cloudflare-y-como-endurecer-un-cliente-ccxt-2nme</guid>
      <description>&lt;p&gt;Si automatizas un exchange con &lt;a href="https://github.com/ccxt/ccxt" rel="noopener noreferrer"&gt;ccxt&lt;/a&gt;, tarde o temprano lo verás en los logs: rachas cortas de &lt;strong&gt;&lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/strong&gt; que pegan a &lt;code&gt;fetch_balance&lt;/code&gt;, a los OHLCV o al saldo de earn, y que desaparecen solas a los pocos minutos. No es que tu API key esté mal. Es el WAF (Cloudflare) que muchos exchanges ponen delante de su REST, challengueando a algo que "parece un bot".&lt;/p&gt;

&lt;p&gt;Y tu bot &lt;em&gt;es&lt;/em&gt; un bot — pero uno &lt;strong&gt;legítimo&lt;/strong&gt;, operando tu propia cuenta contra la API oficial. El problema no es de permisos, es de reputación de cliente HTTP. Esto va de reducir los falsos positivos del WAF, no de evadir ningún control de acceso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dos capas que lo mitigan
&lt;/h2&gt;

&lt;p&gt;Saqué este patrón de un bot propio sobre OKX, tras varias rachas de 403, y lo publiqué como librería: &lt;a href="https://github.com/isazajuancarlos/ccxt-resilience" rel="noopener noreferrer"&gt;&lt;code&gt;ccxt-resilience&lt;/code&gt;&lt;/a&gt; (Apache-2.0).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Que el WAF challengue menos: &lt;code&gt;harden&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Un cliente ccxt por defecto se anuncia como lo que es. Ajustar un &lt;code&gt;User-Agent&lt;/code&gt; de navegador, la cabecera &lt;code&gt;Accept-Language&lt;/code&gt; y un timeout holgado hace que Cloudflare lo desafíe con menos frecuencia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ccxt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ccxt_resilience&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;harden&lt;/span&gt;

&lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;harden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ccxt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;okx&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;harden&lt;/code&gt; toca un cliente &lt;strong&gt;ya construido&lt;/strong&gt;, devuelve el mismo objeto (encadenable) y nunca rompe su construcción: si algo falla al fijar los atributos, los deja como estaban.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reintentar &lt;strong&gt;solo&lt;/strong&gt; lo que se debe: &lt;code&gt;with_retry&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;La tentación es envolver todo en un &lt;code&gt;try/except&lt;/code&gt; que reintente. Es una trampa: reintentar un error de credenciales o de fondos solo gasta tiempo, termina igual de mal, y &lt;strong&gt;esconde bugs de lógica&lt;/strong&gt; detrás de esperas.&lt;/p&gt;

&lt;p&gt;La clave es reintentar únicamente lo transitorio —403/Cloudflare, 429, timeouts— con backoff exponencial y jitter, y re-lanzar los errores reales en el acto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ccxt_resilience&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;with_retry&lt;/span&gt;

&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetch_balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ohlcv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetch_ohlcv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTC/USDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeframe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un error de autenticación se re-lanza inmediatamente, sin reintentar. Y si se agotan los intentos, se re-lanza la &lt;strong&gt;última&lt;/strong&gt; excepción, para que tu manejador de fail-safe siga gobernando (por ejemplo, devolver el último valor cacheado).&lt;/p&gt;

&lt;p&gt;La espera del intento &lt;code&gt;i&lt;/code&gt; es &lt;code&gt;min(max_s, base * 2**i) + rand()*base&lt;/code&gt;: crece de forma acotada y el jitter evita que varios clientes reintenten sincronizados.&lt;/p&gt;

&lt;h2&gt;
  
  
  La clasificación es explícita
&lt;/h2&gt;

&lt;p&gt;Lo que hace o no reintetable a una excepción no está escondido: es una función que puedes inspeccionar y sustituir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ccxt_resilience&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;is_transient_error&lt;/span&gt;

&lt;span class="c1"&gt;# ¿Un 403 de Cloudflare? Sí. ¿Una clave inválida? No.
&lt;/span&gt;&lt;span class="nf"&gt;is_transient_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;403 Forbidden Cloudflare&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="nf"&gt;is_transient_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid api key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;            &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con &lt;a href="https://github.com/ccxt/ccxt" rel="noopener noreferrer"&gt;ccxt&lt;/a&gt; instalado clasifica también por tipo de excepción (&lt;code&gt;DDoSProtection&lt;/code&gt;, &lt;code&gt;RequestTimeout&lt;/code&gt;…); sin él, por el mensaje. Por eso &lt;code&gt;ccxt&lt;/code&gt; es dependencia &lt;strong&gt;blanda&lt;/strong&gt;: la librería funciona con o sin él. Y si tu caso es otra API, &lt;code&gt;with_retry&lt;/code&gt; acepta tu propio predicado de reintento.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instalación
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ccxt-resilience
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El código, los tests y el detalle están en el repo: &lt;strong&gt;&lt;a href="https://github.com/isazajuancarlos/ccxt-resilience" rel="noopener noreferrer"&gt;github.com/isazajuancarlos/ccxt-resilience&lt;/a&gt;&lt;/strong&gt;. Es infraestructura pequeña y sin estado; úsala por partes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>algorithms</category>
    </item>
  </channel>
</rss>
