Depurar no es mirar el código con más intensidad. Es reducir un campo de sospechosos hasta que solo una explicación sobreviva. Este es el flujo de trabajo que uso cuando el mensaje de error es horrible, la fecha límite está cerca y adivinar resulta tentador.
🎙️ Publicado y grabado: ·
Un bug que no puedes provocar es un rumor. Escribe la ruta más corta y exacta desde un inicio limpio hasta el fallo: entrada, comando, entorno, resultado esperado, resultado real. Después ejecútala dos veces. Si la ruta no es fiable, registra qué cambia entre ejecuciones antes de tocar el código.
# Weak report: "upload is broken"
# Useful reproduction:
1. Start app with an empty data directory
2. Upload a file named report.final.csv, 0 bytes
3. Click Import once
Expected: "empty file" validation
Actual: TypeError: Cannot read properties of undefined (reading 'trim')Las trazas largas invitan al pánico y a la lectura de arriba abajo. Resiste ambas cosas. Las primeras líneas suelen describir el wrapper que detectó el crash. Empieza por la excepción final, luego sube hasta el primer frame que pertenece a tu código. Los internos de la librería son testigos, no suelen ser la escena del crimen.
Traceback (most recent call last):
File "app.py", line 41, in <module>
total = price * quantity
TypeError: can't multiply sequence by non-int of type 'float'
# Inspect the operands, not the multiplication operator:
print(repr(price), type(price), repr(quantity), type(quantity))
# '12.50' is text → convert at the input boundary:
price = float(raw_price)El mensaje dice que el runtime vio texto donde tu modelo mental veía un número. Corrige la conversión donde los datos entran al sistema, no con un cast aleatorio junto al crash.
Cuando una petición cruza un navegador, una API, un worker y una base de datos, revisar cada línea es un derroche. Pon una observación cerca del medio. ¿El valor ya está mal ahí? Quédate con la mitad culpable y descarta la inocente. Repite. Esto es búsqueda binaria aplicada a la causalidad.
# Boundary probes, not twenty random print statements
client payload ✓ {"count": 3}
API input ✓ count=3
queue message ✗ {"count": ""}
worker input {"count": ""}
# The defect lives between API input and queue message.
# Now bisect only that serializer path.Un log útil responde qué operación, con qué entradas, llegó a qué estado y cuánto tardó. "Llegué aquí" no responde ninguna de esas preguntas. Añade un ID de petición o job para poder seguir una operación a través de la salida intercalada. Registra decisiones en las fronteras; no narres cada línea.
# Bad
print("got here")
# Useful and searchable
logger.info("invoice_send", extra={
"invoice_id": invoice.id,
"customer_id": customer.id,
"attempt": attempt,
"elapsed_ms": elapsed_ms,
})Nunca registres contraseñas, tokens, cookies ni datos completos de pago. Más logs no son automáticamente más evidencia. Una inundación cambia tiempos, oculta el evento y puede crear un incidente de seguridad por sí sola.
Un breakpoint es valioso cuando tienes una pregunta. Detente justo antes de que se use el primer valor incorrecto, inspecciona las variables locales y la pila de llamadas, luego avanza paso a paso sobre la operación sospechosa más pequeña. Los breakpoints condicionales son mejores que detenerse dentro de un bucle diez mil veces.
# Break only on the failing order, not every order:
condition: order.id == "ord_8472"
# Watch the invariant you believe:
order.total >= 0
# Typical discovery:
subtotal = 18.00
credit = 25.00
total = -7.00Copia el camino que falla a un caso pequeño y desechable, luego elimina entradas, dependencias y configuración de una en una. Ejecuta después de cada eliminación. La última eliminación que hace desaparecer el fallo identifica un ingrediente necesario. Mínimo significa que cada línea restante se ha ganado su lugar.
# Production symptom:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x96 in position 14
# Minimal proof: the file is Windows-1252, not UTF-8
raw = b"July \x96 August"
raw.decode("utf-8") # reproduces
raw.decode("cp1252") # "July – August"
# Repair: detect/declare the source encoding at ingestion,
# then normalize to UTF-8 once.No pegues la aplicación entera en un issue y lo llames reproducción. La reducción es la investigación; el caso pequeño es su resultado.
Di lo que el código debe hacer, una operación a la vez, sin usar verbos vagos como "maneja" o "procesa". Nombra el valor y su tipo en cada frontera. En el momento en que tu explicación se salte un paso o se apoye en "obviamente", inspecciona ese paso.
# "It checks whether the cache has the user" hides the bug.
if cached_user:
return cached_user
# Spoken precisely:
# "An empty dictionary means a cached user with no fields,
# but this branch treats it as no cached value."
if cached_user is not None:
return cached_userUn Heisenbug cambia cuando lo observas. Añadir un log lo hace desaparecer; el modo debug lo hace fiable; una máquina nunca lo ve. Sospecha de races, estado no inicializado, relojes, caché, límites de recursos o código que depende accidentalmente del orden de iteración. Preserva los tiempos antes de añadir instrumentación pesada.
# Real intermittent symptom:
FileNotFoundError: [Errno 2] No such file or directory: 'result.tmp'
Thread A: write result.tmp → rename result.json
Thread B: delete result.tmp during cleanup
# Step-by-step repair:
1. Correlate both actions with file ID + monotonic timestamp
2. Reproduce under repeated/concurrent load
3. Make ownership explicit; cleanup ignores in-flight files
4. Rename atomically only after close/fsync
5. Add a stress test that ran thousands of iterationsMi postura: añadir sleeps no es una corrección de concurrencia. Solo mueve la ventana de la race y le envía el bug a un cliente más lento.
Antes de editar, haz que la reproducción falle. Después de editar, haz que la misma reproducción pase. Luego revierte o desactiva la edición y observa cómo falla de nuevo cuando sea práctico. Ese último paso atrapa el caso vergonzoso donde un reinicio, una caché obsoleta o datos de test cambiados arreglaron el síntoma por ti.
# The three observations
before patch → FAIL: duplicate invoice sent
with patch → PASS: one invoice sent
patch reverted → FAIL: duplicate returns
# Keep the smallest failing case as a regression test.
# Also test adjacent boundaries: zero, one, many; retry; timeout.Úsalo en orden. Es deliberadamente aburrido; aburrido le gana a ingenioso cuando estás cansado.
□ Can I reproduce it from a clean start?
□ What changed: code, config, data, dependency, environment?
□ What is the exact final error and first frame in my code?
□ Which value first differs from the expected value?
□ Can I halve the remaining search area?
□ Do logs include identity, state, and time without secrets?
□ Would a conditional breakpoint answer a specific question?
□ Can I remove half the reproduction and keep the failure?
□ Could observation be changing timing or order?
□ Did the same case fail before, pass after, and become a test?
□ Did I remove temporary logs, flags, sleeps, and debug access?La velocidad de depuración viene de negarse a adivinar. Reproduce, reduce, inspecciona, explica, demuestra. Las herramientas cambian; esa secuencia no.