Saltar al contenido
Inicio 100 Días de Código en Python — Día 55: Rutas con variables, HTML desde Flask y el juego "Mayor o menor" en la web

100 Días de Código en Python — Día 55: Rutas con variables, HTML desde Flask y el juego "Mayor o menor" en la web

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 55 (nivel intermedio+).

Este tutorial adapta pedagógicamente el Día 55. Las secciones 📝 Contenido original del curso y 📝 Ejercicio original del curso provienen de las lecciones 394 a 399. La solución oficial del proyecto final está en el gist enlazado en la lección 399. El ejercicio de decoradores con argumentos se menciona al final de la lección 398, pero su enunciado no está entre los archivos suministrados; aquí se propone uno equivalente, marcado como tal. Los retos 🟢🟡🟠🔴, la sección de seguridad, el proyecto extra y la etapa 2 del proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14 y Flask 3.1.3, se ejecutaron los ejemplos con el cliente de pruebas de Flask. Se comprobó: el comportamiento de los convertidores int y path, que una ruta con variable inserta HTML sin escapar si se devuelve con una f-string, que apilar decoradores propios bajo @app.route produce un AssertionError sin functools.wraps, que el decorador del curso con args[0] falla si el argumento se pasa por nombre, y que los cuatro GIF del proyecto final siguen disponibles.


Tabla de contenidos

  1. Antes de empezar: el juego que vas a construir
  2. Rutas con variables y el modo de depuración
  3. Devolver HTML desde Flask
  4. 🔒 Seguridad: nunca confíes en la URL
  5. Reto del curso: decoradores que dan estilo
  6. Decoradores avanzados con *args y **kwargs
  7. 🚀 Proyecto del día: "Mayor o menor" con URLs
  8. 🌟 Proyecto extra: la calculadora por URL
  9. 🏗️ Proyecto integrador del bloque Flask — Etapa 2
  10. 🧩 Comprueba lo aprendido
  11. 🤖 Programar con apoyo de IA
  12. 🛠️ Correcciones técnicas al material original
  13. 📋 Matriz de trazabilidad
  14. 📦 Evidencias de aprendizaje del Día 55

0. Antes de empezar: el juego que vas a construir

(Fuente: 394 - Day 55 Goals- what you will make by the end of the day.md)

📝 Contenido original del curso

Hoy se profundiza en Flask: cómo devolver HTML, cómo leer lo que el usuario escribe en la URL y cómo usar decoradores avanzados que reciben argumentos posicionales y de palabra clave.

Todo termina en un juego de adivinar un número en la web: la ruta de inicio / pide adivinar un número entre 0 y 9; si visitas /3, la página responde si es muy bajo, muy alto o correcto, con un título de color y un GIF distinto en cada caso.

Prerequisitos: Flask básico y decoradores (Día 54), HTML y CSS en línea (Días 41 a 44), clases (Días 16 a 20), *args y **kwargs (Día 27), el juego de adivinar el número (Día 12) y "Mayor o menor" (Día 14).


1. Rutas con variables y el modo de depuración

(Fuente: 395 - Working Flask URL Paths and the Flask Debugger.md)

🎯 Objetivo de aprendizaje

Capturar partes de la URL como variables, convertirlas al tipo correcto y usar el depurador de Flask para encontrar errores.

📝 Contenido original del curso

Reglas variables. Para capturar lo que el usuario escribe en una parte de la ruta, se encierra entre corchetes angulares. Flask pasa ese valor a la función como un argumento con el mismo nombre:

Python
@app.route("/username/<name>")
def greet(name):
    return f"Hello there {name}!"

Visitar /username/Angela muestra "Hello there Angela!". Se puede agregar texto antes y después de la variable (/username/<name>/1).

Modo de depuración. Cada cambio en el servidor exige detenerlo y volver a ejecutarlo; si no, aparece un 404 aunque la ruta ya exista en el código. Con el modo de depuración:

Python
if __name__ == "__main__":
    app.run(debug=True)
  • se activa el recargador automático: al guardar el archivo, el servidor se reinicia solo;
  • se activa el depurador en el navegador cuando hay un error.

El depurador en acción. La profesora provoca dos errores:

  1. Cambia la ruta a <username> pero deja la función con el parámetro name → TypeError: greet() got an unexpected keyword argument 'username'.
  2. Hace name + 12 → TypeError: can only concatenate str (not "int") to str. El depurador señala el archivo y la línea del error.

Al pulsar el ícono de la consola en la página de error, Flask pide el PIN que aparece en la terminal. Con él se abre una consola en ese punto del código, donde puedes escribir name y ver su valor. El PIN existe para que, si el sitio estuviera en Internet con el modo de depuración activo, un extraño no pudiera usar esa consola.

Convertidores. Por defecto la variable es un texto sin barras (string). Se puede especificar otro tipo:

Python
@app.route("/username/<path:name>")          # acepta barras: /username/Angela/1 → "Angela/1"
@app.route("/username/<name>/<int:number>")  # número entero: /username/Angela/12
def greet(name, number):
    return f"Hello there {name}, you are {number} years old!"

📖 Explicación conceptual

Los convertidores más usados (verificado con Flask 3.1):

Convertidor Acepta Ruta de ejemplo Visitar .../15 .../abc .../-3 .../4.5
string (por defecto) Texto sin / /valor/<v> "15" (¡texto!) "abc" "-3" "4.5"
int Enteros no negativos /valor/<int:v> 15 404 404 404
float Decimales con punto y no negativos /valor/<float:v> 404 404 404 4.5
path Texto con / /archivo/<path:ruta> — — — —

Tres consecuencias importantes:

  • Sin int:, el valor es texto: "15" + 1 lanza TypeError y "15" == 15 es False.
  • Con int:, Flask rechaza lo que no sea un entero con un 404 antes de llamar a tu función. Es validación gratis. Si necesitas negativos: <int(signed=True):v> (verificado: /valor/-3 → -3).
  • float no acepta 15 sin punto decimal (verificado). Si quieres aceptar 15 y 4.5, registra dos rutas (una int y una float) sobre la misma función, o recibe texto y conviértelo tú con float() dentro de un try.

🛠️ Nota técnica: hoy también puedes activar la depuración sin tocar el código, al ejecutar: flask --app server run --debug. Y nunca publiques un sitio con debug=True: la consola del depurador permite ejecutar código Python en el servidor, y el PIN es solo una barrera, no una garantía.

🛠️ Nota técnica (verificado): la barra final importa. Con @app.route("/username/<name>"), la URL /username/Ana/ (con barra al final) da 404.

⚠️ Errores frecuentes

Error Causa Solución
TypeError: greet() got an unexpected keyword argument 'username' El nombre en <...> y el parámetro no coinciden Usa el mismo nombre en ambos
TypeError: greet() missing 1 required positional argument La función espera un parámetro que la ruta no tiene Agrega la variable a la ruta o quita el parámetro
404 con una ruta que sí escribiste Servidor sin reiniciar, o barra final de más Modo de depuración; revisa la URL exacta
TypeError: can only concatenate str (not "int") to str La variable llegó como texto str(...) al concatenar, o <int:...> si debe ser número

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: una ruta /cuadrado/<int:n> que devuelva el cuadrado de n.
  • 🟡 Reto 2 — Ampliación: /saludo/<nombre>/<int:edad> que diga cuántos años le faltan para cumplir 18 (o que ya es mayor de edad).
  • 🟠 Reto 3 — Desafío: provoca a propósito un TypeError en una ruta, abre la consola del depurador con el PIN e inspecciona tres variables. Toma captura.
  • 🔴 Reto 4 — Pensamiento computacional: diseña las rutas (solo las URL, sin código) de una biblioteca escolar: ver un libro por código, ver los libros de un autor, ver los préstamos de un estudiante por su número de lista. ¿Qué convertidor usa cada variable y por qué?

2. Devolver HTML desde Flask

(Fuente: 396 - Rendering HTML Elements with Flask.md)

🎯 Objetivo de aprendizaje

Devolver etiquetas HTML con atributos y estilos desde una función de Flask, manejando bien las comillas.

📝 Contenido original del curso

Cuando una función devuelve un texto simple, Flask lo mete en el <body> sin más estructura. Pero Flask acepta HTML en el return:

Python
@app.route("/")
def hello_world():
    return '<h1 style="text-align: center">Hello, World!</h1>' \
           '<p>This is a paragraph.</p>' \
           '<img src="https://media.giphy.com/media/hvS1eKlR75hMr0l7VJ/giphy.gif" width=200>'
  • Puedes usar cualquier etiqueta, atributos y CSS en línea.
  • Para varias líneas, PyCharm corta la cadena al presionar Enter y agrega una barra invertida \ al final de la línea.
  • Un <img> no necesita etiqueta de cierre, pero sí src. También sirven los GIF (por ejemplo, de Giphy), y puedes ajustar su width.
  • Comillas: si el return usa comillas simples por fuera, los atributos van con comillas dobles por dentro, y viceversa.

📖 Tres formas de escribir HTML largo (adaptación)

Forma Ejemplo Comentario
Barra invertida (la del curso) '<h1>..</h1>' \ + salto de línea Funciona, pero un espacio después de \ rompe el código
Paréntesis return ('<h1>..</h1>'
'<p>..</p>')
Python une las cadenas dentro de los paréntesis; más seguro
Triple comilla return """<h1>..</h1>
<p>..</p>"""
Cómodo para bloques grandes; admite ambas comillas adentro

Aun así, escribir páginas enteras dentro de Python es incómodo: no tienes el resaltado ni el autocompletado de un archivo HTML. El Día 56 moverás el HTML a plantillas.

⚠️ Errores frecuentes

Error Causa Solución
SyntaxError: invalid syntax en la línea del HTML Comillas del mismo tipo dentro y fuera Alterna simples y dobles, o usa triple comilla
SyntaxError: unexpected character after line continuation character Un espacio o comentario después de \ Quita lo que haya después de \, o usa paréntesis
La imagen no aparece URL de la página de Giphy, no del archivo .gif Clic derecho → "Copiar dirección de imagen"

3. 🔒 Seguridad: nunca confíes en la URL

(Sección creada para esta adaptación a partir de los ejemplos de las lecciones 395 y 396.)

🎯 Objetivo de aprendizaje

Reconocer que lo que llega en la URL lo controla el usuario y escaparlo antes de ponerlo en HTML.

📖 Explicación conceptual

Junta las dos lecciones anteriores: una ruta con variable y un return que se interpreta como HTML.

Python
@app.route("/username/<name>")
def greet(name):
    return f"Hello there {name}!"

¿Qué pasa si alguien visita /username/<h1 style='color:red'>HACKEADO<h1>? Verificado con Flask 3.1, la respuesta es:

HTML
Hello there <h1 style='color:red'>HACKEADO<h1>!

El navegador dibuja ese HTML. Con etiquetas más peligrosas, un atacante podría ejecutar JavaScript en la página de otra persona que abra un enlace preparado. Este ataque se llama XSS (Cross-Site Scripting) y está entre los más comunes en la web.

La solución: escapar. Escapar convierte los caracteres especiales en texto inofensivo (< pasa a &lt;). Flask incluye la función escape (del paquete markupsafe, que se instala con Flask). De hecho, el propio Quickstart de Flask la usa en su ejemplo de rutas variables:

Python
from markupsafe import escape

@app.route("/username/<name>")
def greet(name):
    return f"Hello there {escape(name)}!"

Resultado verificado: Hello there &lt;h1 style=&#39;color:red&#39;&gt;HACKEADO&lt;h1&gt;! El navegador lo muestra como texto; no lo ejecuta.

💡 El Día 56 usarás plantillas con render_template, que escapan automáticamente las variables. Mientras construyas HTML con f-strings, el responsable eres tú.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: prueba la ruta sin escape con <b>hola</b> y con <i>hola</i>. Luego agrega escape y compara el código fuente de la página (Ctrl + U).
  • 🟡 Reto 2: ¿las variables con <int:...> necesitan escape? Justifica tu respuesta con lo que sabes de los convertidores.
  • 🟠 Reto 3: escribe una lista de verificación de 4 puntos para revisar la seguridad de cualquier ruta que devuelva HTML.

4. Reto del curso: decoradores que dan estilo

(Fuente: 397 - Challenge- Use Python Decorators to Style HTML Tags.md)

📝 Ejercicio original del curso

Envolver un texto en <b>, <em> y <u> a mano es propenso a errores tipográficos y difícil de editar. El reto: crear tres decoradores, make_bold, make_emphasis y make_underlined, que se ponen debajo de @app.route(...) y envuelven el texto que devuelve la función en la etiqueta correspondiente:

Python
@app.route("/bye")
@make_bold
@make_emphasis
@make_underlined
def bye():
    return "Bye!"

Resultado esperado: el texto "Bye!" en negrita, cursiva y subrayado. Tiempo sugerido: 10 minutos.

🔍 Antes de programar

  • La función decorada devuelve un texto. ¿Qué debe devolver el wrapper? (Pista: no basta con llamar a la función, como en el Día 54.)
  • Si apilas tres decoradores, ¿en qué orden se aplican? ¿Cuál queda por fuera?
  • ¿Qué recibe @app.route: la función original o la ya envuelta?

📖 Explicación conceptual

La diferencia con el Día 54: allí el wrapper solo llamaba a la función. Aquí debe usar su resultado y devolverlo modificado:

Python
def make_bold(function):
    def wrapper():
        return f"<b>{function()}</b>"
    return wrapper

Los decoradores se aplican de abajo hacia arriba: el más cercano a la función la envuelve primero. @app.route queda arriba, así que registra la versión ya decorada.

🛠️ Nota técnica (verificado): con un solo uso funciona, pero si decoras dos rutas con make_bold, Flask se detiene al arrancar:

Texto
AssertionError: View function mapping is overwriting an existing endpoint function: wrapper

Flask identifica cada ruta por el nombre de la función (su endpoint). Ambas rutas le entregan una función llamada wrapper, y Flask no permite dos con el mismo nombre. La solución estándar es functools.wraps, que copia el nombre (y la documentación) de la función original al wrapper:

Python
import functools

def make_bold(function):
    @functools.wraps(function)
    def wrapper():
        return f"<b>{function()}</b>"
    return wrapper

Es buena práctica ponerlo en todo decorador que escribas.

⚠️ Errores frecuentes

Error Causa Solución
La página sale vacía o dice None El wrapper llama a la función pero no hace return return f"<b>{function()}</b>"
<b><function bye at 0x...></b> Olvidaste los paréntesis: {function} {function()}
Los decoradores no tienen efecto Pusiste tus decoradores encima de @app.route @app.route siempre arriba
AssertionError: ... endpoint function: wrapper Mismo decorador en varias rutas @functools.wraps(function)

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: agrega make_heading que envuelva en <h1>.
  • 🟡 Reto 2: cambia el orden de los decoradores y mira el código fuente (Ctrl + U). Explica el orden de las etiquetas.
  • 🟠 Reto 3: escribe una sola función envolver_en(etiqueta) que fabrique decoradores: @envolver_en("b"), @envolver_en("mark"). (Pista: una función que devuelve un decorador, que devuelve un wrapper: tres niveles. Es lo mismo que hace @app.route("/").)
  • 🔴 Reto 4: ¿make_bold sirve para una ruta con variable como greet(name)? Pruébalo, explica el error y resuélvelo con lo que aprenderás en la sección 5.

5. Decoradores avanzados con *args y **kwargs

(Fuente: 398 - Advanced Decorators with -args and --kwargs.md)

🎯 Objetivo de aprendizaje

Escribir decoradores que funcionen con funciones que reciben argumentos, y usar esos argumentos dentro del wrapper.

📝 Contenido original del curso

Hasta ahora los wrappers llamaban a la función sin argumentos. ¿Y si la función los necesita, o si el decorador debe revisar algo antes de llamarla?

La profesora simula un blog donde solo los usuarios que iniciaron sesión pueden publicar:

Python
class User:
    def __init__(self, name):
        self.name = name
        self.is_logged_in = False


def is_authenticated_decorator(function):
    def wrapper(*args, **kwargs):
        if args[0].is_logged_in == True:
            function(args[0])
    return wrapper


@is_authenticated_decorator
def create_blog_post(user):
    print(f"This is {user.name}'s new blog post.")


new_user = User("angela")
new_user.is_logged_in = True
create_blog_post(new_user)

Por qué *args y **kwargs: sin ellos, create_blog_post(new_user) falla con "wrapper() takes 0 positional arguments but 1 was given": la función que realmente se llama es el wrapper, y no aceptaba argumentos. Con *args (argumentos posicionales ilimitados) y **kwargs (de palabra clave ilimitados), el wrapper recibe lo que le pasen. args[0] es el primer argumento posicional: el usuario.

Si is_logged_in es False, no se imprime nada; al cambiarlo a True, se publica.

📖 Explicación conceptual

Mejora 1: reenviar todo. El código del curso llama function(args[0]), que solo sirve para funciones con un argumento. La forma general reenvía todo lo que llegó y devuelve el resultado:

Python
def wrapper(*args, **kwargs):
    ...
    return function(*args, **kwargs)

Mejora 2: no depender de la posición. Verificado: si alguien llama create_blog_post(user=new_user) (por nombre), args está vacío y args[0] lanza IndexError: tuple index out of range. Una versión más robusta busca el usuario en los dos lugares:

Python
import functools

def is_authenticated_decorator(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        user = args[0] if args else kwargs.get("user")
        if user is not None and user.is_logged_in:
            return function(*args, **kwargs)
        print("Acceso denegado: inicia sesión primero.")
    return wrapper
  • if user.is_logged_in: es más claro que == True.
  • Informar "acceso denegado" es mejor que no hacer nada en silencio.

💡 Este patrón es exactamente el de la vida real: en el Día 68 del curso usarás @login_required, un decorador que protege rutas de Flask para que solo las vean usuarios autenticados.

📝 Ejercicio de práctica: decorador que registra llamadas

(El video anuncia "un ejercicio de codificación para probar esta nueva forma avanzada de decoradores". Su enunciado no está entre los archivos suministrados; este es un ejercicio equivalente propuesto por esta adaptación.)

Crea logging_decorator que, al llamar a la función decorada, imprima su nombre, los argumentos con que se llamó y el valor que devolvió:

Python
@logging_decorator
def a_function(a, b, c):
    return a * b * c

a_function(1, 2, 3)
# Salida esperada (el formato exacto es tuyo):
# Llamaste a_function(1, 2, 3)
# Devolvió: 6

Condiciones: debe funcionar con cualquier número de argumentos, debe devolver el resultado (no solo imprimirlo) y debe usar functools.wraps.

⚠️ Errores frecuentes

Error Causa Solución
TypeError: wrapper() takes 0 positional arguments but 1 was given El wrapper no acepta argumentos def wrapper(*args, **kwargs):
IndexError: tuple index out of range Se usó args[0] y el argumento llegó por nombre Revisa kwargs también
La función decorada devuelve None El wrapper no hace return return function(*args, **kwargs)
function(args, kwargs) Pasa una tupla y un diccionario como dos argumentos Desempaqueta: function(*args, **kwargs)

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: agrega a User un atributo es_docente y crea @solo_docentes para una función calificar(user, nota).
  • 🟡 Reto 2: aplica logging_decorator a una función con argumentos por nombre (precio(valor, descuento=0.1)) y muestra también los kwargs.
  • 🟠 Reto 3: reescribe make_bold (sección 4) con *args y **kwargs para que funcione con la ruta greet(name).
  • 🔴 Reto 4 — Pensamiento computacional: termina el decorador @reintentar que diseñaste el Día 54: que reenvíe los argumentos y reintente hasta 3 veces si hay una excepción. Pruébalo con una función que falle al azar el 50 % de las veces (random.random() < 0.5).

6. 🚀 Proyecto del día: "Mayor o menor" con URLs

(Fuente: 399-final-project-higher-or-lower-urls.md)

📝 Ejercicio original del curso

Es el juego de adivinar del Día 14, pero ahora como sitio web.

  1. Crea un proyecto higher-lower con un archivo server.py.
  2. Una aplicación Flask cuya ruta de inicio muestre un <h1> que diga "Guess a number between 0 and 9" y un GIF de tu elección de giphy.com (o el que propone el curso: https://media.giphy.com/media/3o7aCSPqXE5C6T8tBC/giphy.gif).
  3. Genera un número aleatorio entre 0 y 9 (u otro rango que elijas).
  4. Crea una ruta que detecte el número que el usuario escribe en la URL (por ejemplo URL/3 o URL/9) y lo compare con el número aleatorio: muy bajo, muy alto o correcto. Intenta que el <h1> tenga un color distinto en cada caso. Por ejemplo, si el número fuera 5: 3 es muy bajo, 7 es muy alto y 5 es correcto.

GIF del curso (verificados, siguen disponibles): muy alto …/3o6ZtaO9BZHcOjmErm/giphy.gif, muy bajo …/jD4DwBtqPXRXa/giphy.gif, correcto …/4T7e4DmcrP9du/giphy.gif (todos en https://media.giphy.com/media/). "Es mucho más divertido buscar los tuyos".

Problema y contexto

El juego de consola del Día 14 solo lo podía jugar quien estaba frente al computador. Como sitio web, cualquier persona en la red del salón puede jugar desde su celular escribiendo una URL.

🔍 Antes de programar

  • ¿Qué información tenemos? El número que el usuario escribe en la URL.
  • ¿Dónde se genera el número secreto: dentro de la función de la ruta o fuera? ¿Qué pasaría en cada caso? (Pista: si se genera dentro, ¡cambia en cada visita!)
  • ¿Qué convertidor usarás para que /abc no llegue a tu función?
  • ¿Qué salidas hay? Tres páginas, cada una con color y GIF propios.

Requisitos funcionales

  1. / con el <h1> y el GIF de inicio.
  2. /<int:intento> con tres respuestas posibles, cada una con color y GIF distintos.
  3. Si el número está fuera del rango (por ejemplo, /15), un mensaje que lo diga.

Restricciones técnicas

  • Número secreto generado una sola vez, fuera de las funciones.
  • Convertidor int en la ruta.
  • HTML largo con paréntesis o triple comilla (sección 2).

Planificación

  1. Comprender. 2. Entrada: el número en la URL. 3. Proceso: comparar con el secreto. 4. Salidas: tres páginas. 5. Dividir: ruta de inicio, ruta de intento. 6. Diseñar la tabla de colores y GIF. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: la ruta / con el título y el GIF.
  • Versión 2: la ruta de intento que solo repite el número recibido.
  • Versión 3: la comparación con un número secreto fijo (por ejemplo, 5) para poder probarla.
  • Versión 4: el número aleatorio.
  • Versión final: colores, GIF y el mensaje de fuera de rango.

Pruebas

(Fija temporalmente el número secreto en 5 para probar.)

URL Resultado esperado
/ "Guess a number between 0 and 9" y el GIF
/3 Muy bajo, con su color y GIF
/7 Muy alto, con su color y GIF
/5 ¡Correcto!
/15 Mensaje de fuera de rango
/abc y /-1 404 (el convertidor int los rechaza, verificado)

🛠️ Nota técnica: con el modo de depuración activo, cada vez que guardas el archivo el servidor se reinicia y el número secreto cambia. No es un error: el módulo se vuelve a ejecutar. Además, el número es el mismo para todos los jugadores de la red: el servidor tiene una sola variable. Para que cada jugador tenga su propio número harían falta cookies o sesiones (Día 68 del curso).

🧪 Prueba y depuración

Python
import random
from flask import Flask
app = Flask(__name__)

@app.route("/<intento>")
def adivinar(intento):
    secreto = random.randint(0, 9)
    if intento < secreto:
        return "<h1 style="color: red">Muy bajo</h1>"
    elif intento > secreto:
        return "<h1 style='color: purple'>Muy alto</h1>"

Encuentra cuatro problemas: falta int: (comparar texto con número lanza TypeError), el número secreto se genera en cada visita, las comillas dobles dentro y fuera dan SyntaxError, y falta el caso correcto (la función devolvería None y Flask mostraría un error).

⭐ Extensión del proyecto

  • Una ruta /nuevo que genere otro número secreto (pista: la palabra clave global, Día 12).
  • Contar los intentos y mostrarlos al acertar.
  • Un enlace "Volver a intentar" en cada página.

7. 🌟 Proyecto extra: la calculadora por URL

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

El docente de matemáticas quiere una herramienta rápida para verificar operaciones desde el celular, escribiendo una URL como /sumar/12/30.

Requisitos

  1. Rutas para al menos cuatro operaciones: /sumar/<a>/<b>, /restar/..., /multiplicar/..., /dividir/..., con el convertidor adecuado (decide si int o float, y cómo aceptar negativos).
  2. Un diccionario de funciones (Día 54, sección 4) en lugar de cuatro funciones de ruta repetidas: una sola ruta /<operacion>/<a>/<b>.
  3. La división entre cero muestra un mensaje amable, no el depurador.
  4. Un decorador @registrar (con *args, **kwargs y functools.wraps) que guarde cada operación en historial.txt.
  5. Una ruta /historial que muestre las últimas 10 operaciones, con escape sobre lo que se lee del archivo.

Decisiones que debes tomar (y justificar)

  • ¿/dividir/7/2 debe dar 3.5 o 3? ¿Y cómo aceptas /sumar/2.5/3, si el convertidor float rechaza 3 y el int rechaza 2.5? (Revisa la tabla de la sección 1.)
  • ¿Qué respuesta das a /potencia/2/3 si no implementaste potencia: un 404 o un mensaje propio?

Casos de prueba

  • /sumar/12/30 → 42.
  • /dividir/1/0 → mensaje "No se puede dividir entre cero".
  • /restar/5/-3 → 8 (solo si aceptas negativos).
  • Tras 3 operaciones, /historial muestra 3 líneas.

8. 🏗️ Proyecto integrador del bloque Flask — Etapa 2

(Proyecto creado para esta adaptación. Descripción completa en el tutorial del Día 58.)

El Portal del Grupo — Etapa 2 (hoy):

  1. Una ruta /integrante/<nombre> que muestre la ficha de un integrante (datos ficticios en un diccionario) o un mensaje "No encontrado" si el nombre no existe. Usa escape.
  2. Una ruta /actividad/<int:numero> que muestre la actividad número n de una lista.
  3. Los decoradores de estilo de la sección 4 con functools.wraps, aplicados a al menos dos rutas.
  4. Ejecuta con --debug y documenta en cuaderno_errores.md un error que hayas encontrado con el depurador.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué hacen los corchetes angulares en @app.route("/usuario/<nombre>")?
  2. ¿Qué dos ventajas da el modo de depuración? ¿Por qué nunca debe usarse en un sitio público?
  3. ¿Para qué sirve el convertidor int además de convertir el tipo?
  4. ¿Qué es XSS y cómo lo evita escape?
  5. ¿Por qué el decorador de autenticación necesita *args y **kwargs?

Análisis de código

Python
@app.route("/doble/<n>")
def doble(n):
    return f"{n * 2}"

¿Qué muestra /doble/21? ¿Por qué no es 42? ¿Cómo lo corriges?

Predicción

Python
def make_bold(f):
    def w():
        return "<b>" + f() + "</b>"
    return w

def make_underlined(f):
    def w():
        return "<u>" + f() + "</u>"
    return w

@make_underlined
@make_bold
def texto():
    return "Hola"

print(texto())

Depuración

Python
def is_authenticated(function):
    def wrapper():
        if user.is_logged_in:
            function(user)
    return wrapper

¿Qué errores aparecen al usarlo con create_blog_post(new_user)? Corrígelos.

Modificación

Cambia el proyecto "Mayor o menor" para que el rango sea de 1 a 100 y el mensaje diga "muy bajo" o "un poco bajo" según la distancia al número secreto (más o menos de 10).

Creación

Escribe desde cero un decorador @contar_llamadas que lleve la cuenta de cuántas veces se ha llamado a la función y la imprima en cada llamada.


10. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — ¿Es seguro? Pide a una IA una ruta de Flask que salude al usuario por su nombre desde la URL. ¿Usa escape? Pruébala con <b>hola</b> en la URL. Si no escapa, corrígela y explica a la IA el problema.

Actividad 2 — Comprobar una afirmación. Pregunta a una IA si <int:n> acepta números negativos en Flask. Verifícalo tú (/ruta/-3) y consulta la sección "Variable Rules" y la de convertidores de la documentación de Flask/Werkzeug.

Actividad 3 — El error del endpoint. Muestra a una IA el AssertionError: View function mapping is overwriting an existing endpoint function: wrapper y pide una explicación. Compárala con la nota técnica de la sección 4 y con la documentación de functools.wraps.


11. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
395, 396 return f"Hello there {name}!" con una variable de la URL Inserta HTML del usuario sin escapar: vulnerable a XSS (verificado) Sección 3 con markupsafe.escape
395 Activar debug=True Correcto en desarrollo; nunca en producción. Hoy también existe flask run --debug Nota técnica de la sección 1
397 Apilar make_bold, make_emphasis y make_underlined bajo @app.route Si el mismo decorador se usa en dos rutas, Flask lanza AssertionError por el endpoint wrapper (verificado) Nota técnica de la sección 4 con functools.wraps
398 if args[0].is_logged_in == True: function(args[0]) Falla con IndexError si el usuario se pasa por nombre (verificado); solo sirve para funciones de un argumento; no devuelve el resultado Sección 5: versión con *args, **kwargs, return y wraps
398 Ejercicio de codificación de decoradores avanzados El enunciado no está entre los archivos suministrados Ejercicio equivalente en la sección 5, marcado como propio
399 Número aleatorio del juego Con el recargador cambia al guardar y es compartido por todos los jugadores Nota técnica de la sección 6
396 Cortar cadenas largas con \ Funciona, pero es frágil (un espacio después de \ rompe el código) Tabla de alternativas de la sección 2

12. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Objetivos: HTML, lectura de la URL, decoradores avanzados, juego de adivinar en la web 394 Sección 0 — — Sección 6
Rutas /bye y /username/<name>, reglas variables, reiniciar el servidor, modo de depuración, recargador, depurador y PIN, errores de nombre y de tipo, texto antes y después de la variable, convertidores path e int, varias variables 395 Sección 1 Ejemplos de rutas variables Retos 1–4 de la sección 1; tabla de convertidores Secciones 6, 7 y 8
Devolver HTML, CSS en línea, varios elementos, \ para cortar líneas, <img> con GIF, ancho, comillas simples y dobles 396 Sección 2 Página con título, párrafo e imagen Tres formas de escribir HTML largo Sección 6
(Consecuencia de 395 + 396; no se trata en el curso) — Sección 3 — XSS y escape; retos 1–3 Secciones 7 y 8
Reto de decoradores make_bold, make_emphasis, make_underlined bajo @app.route 397 Sección 4 Reto de los tres decoradores Retos 1–4 de la sección 4; functools.wraps Sección 8
Clase User, is_logged_in, is_authenticated_decorator, *args y **kwargs, args[0], ejercicio de decoradores avanzados 398 Sección 5 Decorador de autenticación; ejercicio (equivalente propuesto) Retos 1–4 de la sección 5 Sección 7
Proyecto "Higher or Lower URLs": server.py, ruta de inicio con <h1> y GIF, número aleatorio, ruta con el número, colores y GIF por caso 399 Sección 6 Proyecto final del día Extensiones de la sección 6 Sección 6
(No existe en el curso original) — — — Calculadora por URL Sección 7
(No existe en el curso original) — — — Portal del Grupo, etapa 2 Sección 8

13. 📦 Evidencias de aprendizaje del Día 55

  • "Mayor o menor" funcionando, con capturas de las cuatro páginas (inicio, bajo, alto, correcto) y de un 404.
  • La captura de la consola del depurador con el PIN (Reto 3 de la sección 1).
  • La comparación "sin escape" y "con escape" (código fuente de la página).
  • Los tres decoradores de estilo con functools.wraps aplicados a dos rutas.
  • logging_decorator funcionando con funciones de distinto número de argumentos.
  • La calculadora por URL (proyecto extra) con su historial.
  • La etapa 2 del Portal del Grupo.
  • Cuaderno de errores: nombre de la variable distinto del parámetro, texto contra número en la URL, barra final, comillas mezcladas, \ con espacio, HTML sin escapar, wrapper sin return, endpoint wrapper repetido, args[0] con argumentos por nombre, número secreto dentro de la función.