Prompts de revisión de código: 16 casos listos

Dieciséis fragmentos con un defecto real cada uno, listos para una revisión de código con IA. Cada tarjeta fija el lenguaje, el enfoque y el entorno donde corre: cambia el ejemplo por el tuyo y pulsa aplicar.

Errores y lógica

Código que compila y aun así miente: encontrar el error lógico de un argumento mutable por defecto, de un cero tomado por valor ausente, de dos horas comparadas sin zona y de una regresión colada en un pull request.

Argumento mutable por defecto

La trampa clásica de Python: la lista de la firma sobrevive a la llamada y acumula datos de otros.

Prompt

Revisa esta función del carrito: los clientes encuentran en su pedido artículos que nunca añadieron.def add_item(item, cart=[]): cart.append(item) return cart

Resultado

Línea 1 · crítico · errorcart=[] se crea una sola vez, al importar el módulo, y no en cada llamada: el segundo cliente sigue llenando el carrito del primero, y así hasta que el proceso se reinicia.Corrección: cart: list | None = None y, como primera línea del cuerpo, if cart is None: cart = [].

Un cero tomado por ausencia

La comprobación de verdad esconde un cero legítimo y el cliente sin saldo ve un texto de relleno.

Prompt

Mira esta comprobación: ¿distingue de verdad un valor ausente de un cero?def render_balance(user): balance = user.get("balance") if not balance: return "sin datos" return format_money(balance)

Resultado

Línea 3 · grave · errorif not balance atrapa por igual None, 0 y 0.0: quien ha gastado hasta el último céntimo ve "sin datos" en lugar de un cero honesto.Corrección: if balance is None — la ausencia se comprueba de forma explícita y el cero pasa al formateador como cualquier otro número.

Comparar horas sin zona

datetime.now() devuelve la hora local del servidor mientras la caducidad del token se guarda en UTC.

Prompt

Revisa el cálculo de caducidad del token. El servicio corre en servidores de varias zonas horarias.def is_expired(token): return datetime.now() > token.expires_at

Resultado

Línea 2 · crítico · errordatetime.now() es hora local naive y expires_at llega de la base en UTC: en un servidor de Madrid los tokens viven dos horas de más y, frente a un valor aware, la comparación revienta directamente con TypeError.Corrección: datetime.now(timezone.utc) y una regla para todo el proyecto — lo que se guarda es aware y en UTC.

Regresión en un pull request

Una revisión del cambio y no del archivo: qué se lleva a producción este commit.

Prompt

Revisa este diff de un pull request: ¿qué entra exactamente en producción junto con él?@@ -12,8 +12,7 @@ def apply_discount(order, percent): if order.paid: raise ValueError("order already paid")- if percent > 90:- raise ValueError("discount too large") order.total = order.total * (100 - percent) / 100+ order.discount_percent = percent

Resultado

Líneas 15-16 · crítico · errorLa comprobación eliminada era el único límite de percent: con 150 el total del pedido se vuelve negativo y la caja lo convierte en una devolución.Corrección: devolver la comprobación o moverla a la validación de la petición, y añadir el caso percent=150 a los tests.

Seguridad

Las vulnerabilidades que más se repiten: una inyección SQL en el buscador del sitio, un token de producción escrito dentro del script y una ruta de subida que se escapa de su carpeta.

Inyección SQL en la búsqueda

La dirección del formulario se concatena dentro del SQL: quien rellena el formulario pasa a mandar en la base.

Prompt

Revisa esta búsqueda de usuario. El email llega directamente de un formulario de la web.def find_user(conn, email): query = "SELECT * FROM users WHERE email = '" + email + "'" return conn.execute(query).fetchone()

Resultado

Línea 2 · crítico · seguridademail se pega al texto SQL: un valor con una comilla cierra la condición y todo lo que va detrás lo ejecuta la base como consulta tuya, DROP TABLE incluido.Corrección: un parámetro en vez de la concatenación — conn.execute("SELECT id, email FROM users WHERE email = %s", [email]); de paso desaparece el SELECT * que hoy se trae también el hash de la contraseña.

Token dentro del script

Una clave de producción vive en el repositorio y de paso se imprime en el log de despliegue.

Prompt

Revisa este script de despliegue: ¿hay algo peligroso desde el punto de vista de la seguridad?#!/usr/bin/env bashAPI_TOKEN="sk_live_9f3c1ad84b22"echo "deploying with $API_TOKEN"curl -H "Authorization: Bearer $API_TOKEN" -X POST https://api.example.com/deploy

Resultado

Líneas 2-3 · crítico · seguridadUn token de producción está escrito en un archivo versionado en git: lo tiene en su historial todo el que haya clonado el repositorio, y echo lo copia además al log de despliegue, que lee mucha más gente.Corrección: leerlo del entorno ($API_TOKEN sin valor por defecto), quitar el echo y dar la clave por filtrada — hay que rotarla.

Salida de la carpeta de subidas

El nombre del archivo se toma tal cual de la petición y dos puntos con barra llevan a cualquier archivo del servidor.

Prompt

Revisa este endpoint de descarga: el nombre del archivo llega en la query.def download(request): name = request.args.get("file") path = os.path.join("/var/app/uploads", name) return send_file(path)

Resultado

Línea 3 · crítico · seguridados.path.join sale sin problemas de /var/app/uploads cuando el nombre es ../../etc/passwd, y una ruta absoluta descarta directamente el primer argumento: se descarga cualquier archivo que el proceso pueda leer.Corrección: os.path.basename(name), después comparar el os.path.realpath del resultado con la carpeta de subidas y servir el archivo solo si está dentro.

Rendimiento

Optimizar el rendimiento antes de comprar más servidor: consultas a la base lanzadas dentro de un bucle, una consulta que ignora el índice y recorre la tabla entera, y un log enorme leído en memoria.

Consultas dentro del bucle

El informe va dos veces a la base por cada usuario: mil filas se convierten en dos mil consultas.

Prompt

Mira este informe: con mil usuarios tarda un minuto. ¿Dónde se va el tiempo?def orders_report(user_ids): rows = [] for user_id in user_ids: user = db.query("SELECT name FROM users WHERE id = %s", user_id) orders = db.query("SELECT total FROM orders WHERE user_id = %s", user_id) rows.append((user.name, sum(o.total for o in orders))) return rows

Resultado

Líneas 3-5 · grave · rendimientoDos consultas por usuario: mil usuarios son dos mil idas y vueltas, y el tiempo se va en la red, no en los cálculos.Corrección: una consulta con JOIN y GROUP BY users.id, o dos consultas con IN y la unión hecha en memoria.

Consulta que ignora el índice

Una función sobre la columna y un porcentaje al principio del LIKE apagan los índices: la base lee la tabla entera.

Prompt

Revisa esta consulta: en una tabla de diez millones de filas tarda veinte segundos.SELECT *FROM ordersWHERE date_trunc('day', created_at) = '2026-09-01' AND lower(email) LIKE '%@example.com'ORDER BY created_at DESC

Resultado

Líneas 3-4 · grave · rendimientodate_trunc sobre created_at deja el índice inservible y un LIKE con porcentaje inicial no puede usarlo en absoluto: queda un seq scan de toda la tabla, y SELECT * arrastra además columnas que nadie mira.Corrección: comparar created_at contra un rango (desde la medianoche del 1 de septiembre y menor que la medianoche del día 2), indexar el dominio aparte o guardarlo en su propia columna, y seleccionar solo los campos que se usan.

El log entero en memoria

El archivo se lee de una sola vez, así que el tamaño del log pasa a ser el tamaño del proceso.

Prompt

Revisa este contador de errores de un log. Los ficheros llegan a varios gigabytes.def count_errors(path): lines = open(path).read().split("\n") return len([line for line in lines if "ERROR" in line])

Resultado

Línea 2 · grave · rendimientoread() sube el archivo completo a memoria, split lo duplica y la lista dentro de len() guarda una tercera copia: con un log de ocho gigas llega antes el OOM killer que el resultado. Además el archivo no se cierra nunca.Corrección: with open(path) as f y sum(1 for line in f if "ERROR" in line) — línea a línea y con memoria constante.

Legibilidad y estándares

Código limpio en la práctica: cuatro condiciones anidadas que se pueden aplanar, nombres de una letra que incumplen la guía de estilo y un mismo tipo de IVA duplicado en dos funciones.

Cuatro condiciones anidadas

La regla para enviar el correo se esconde en el quinto nivel de sangría y solo se entiende entera.

Prompt

Valora la legibilidad de esta función: hay que leerla hasta el final para saber la condición.def notify(user): if user is not None: if user.email is not None: if user.subscribed: if not user.banned: send_email(user.email) return True return False

Resultado

Líneas 2-5 · menor · legibilidadCuatro if anidados son una sola regla convertida en escalera: para saber a quién le llega el correo hay que sostener las cuatro condiciones a la vez, y PEP 8 pide la forma plana.Corrección: salidas tempranas — if user is None: return False y así sucesivamente — y el cuerpo se queda en un único nivel de sangría.

Nombres contra la guía de estilo

Un método con mayúscula inicial y variables de una letra: protesta RuboCop y protesta quien lo lea después.

Prompt

Revisa este método con RuboCop: ¿qué contradice aquí el estilo habitual de Ruby?def CalcTotal(o) t = 0 o.each do |i| t = t + i.price * i.qty end tend

Resultado

Líneas 1-4 · menor · estándaresNaming/MethodName: en Ruby los nombres de métodos van en snake_case y el CamelCase se lee aquí como una constante. Los nombres o, t e i no dicen nada de su contenido, y acumular a mano es justo el trabajo de sum.Corrección: def calc_total(items) e items.sum do |item| item.price * item.qty end — cuatro líneas se quedan en una.

El IVA en dos sitios

Una misma regla copiada en dos funciones y ya divergente: 20 por ciento en la factura, 19 en el recibo.

Prompt

Revisa estas dos funciones: ¿calculan lo mismo?def invoice_total(order): return round(order.subtotal * 1.2, 2)def receipt_total(order): return round(order.subtotal * 1.19, 2)

Resultado

Líneas 2 y 5 · grave · arquitecturaUna regla de negocio escrita dos veces ya se ha separado: la factura aplica el 20 por ciento y el recibo el 19, así que el cliente ve dos totales distintos para un mismo pedido.Corrección: una constante VAT_RATE y una función que usen las dos; así el tipo se cambia en un solo sitio.

Tests y fiabilidad

Qué pasa cuando algo falla: probar el manejo de errores y no solo el camino feliz, una excepción capturada y silenciada sin avisar y una copia de seguridad que nunca se ha restaurado.

Test solo del camino feliz

Un único caso en verde da una sensación de cobertura que no existe.

Prompt

Valora estos tests de apply_discount: ¿qué les falta?def test_apply_discount(): order = Order(total=100) apply_discount(order, 10) assert order.total == 90

Resultado

Línea 1 · grave · testsHay un caso cubierto: un descuento normal sobre un pedido sin pagar. Nada de cero, ni cien por cien, ni valores negativos, ni un pedido ya pagado, ni el redondeo sobre 33,33 — cualquiera de esas ramas puede romperse sin que nadie se entere.Corrección: parametrize con los valores límite y un test aparte con pytest.raises para percent=150 y para el pedido ya pagado.

Excepción tragada

except Exception: pass convierte una caída en "ok" y borra el rastro de los logs.

Prompt

Revisa este guardado de perfil: los usuarios dicen que a veces sus cambios desaparecen.def save_profile(user, data): try: db.update(user.id, data) search.reindex(user.id) except Exception: pass return "ok"

Resultado

Líneas 5-6 · crítico · errorexcept Exception: pass se traga todo y devuelve "ok" aunque la escritura no haya ocurrido: el usuario ve éxito, los datos no están y el log está vacío. Con dos operaciones bajo un mismo try, un fallo del reindexado tampoco se distingue de un fallo de la base.Corrección: capturar las excepciones concretas, registrar con logger.exception y devolver un estado honesto; sacar el reindexado aparte para que su fallo no anule el guardado.

Copia de seguridad sin comprobar

El script ignora los códigos de salida y borra las copias antiguas antes de que nadie vea que la nueva está vacía.

Prompt

Revisa este script de copia nocturna: restaurar desde la última copia no ha funcionado.#!/usr/bin/env bashpg_dump "$DATABASE_URL" > /backup/db.sqlgzip -f /backup/db.sqlfind /backup -name "db.sql.gz" -mtime +7 -delete

Resultado

Líneas 2-4 · crítico · errorFalta set -euo pipefail: si pg_dump falla el archivo se crea igual, vacío, gzip lo comprime tan tranquilo y find borra las copias buenas de más de una semana; a los siete días no queda ni una íntegra. Un $DATABASE_URL sin valor pasa igual de desapercibido.Corrección: set -euo pipefail en la primera línea, comprobar el tamaño del dump después de pg_dump y borrar las copias antiguas solo cuando la nueva haya salido bien.

Prompts de revisión de código: 16 casos listos | iBro