Con una cuenta desbloqueas el cuestionario, el foro de dudas y el tutor con IA de esta lección, además del registro de progreso y el certificado verificable al terminar «Python para Ingeniería Espacial».
En los ejercicios de un curso, los datos están limpios porque los escribió el autor. En una estación terrena real no: un archivo de telemetría de una noche de operación tiene líneas truncadas porque el enlace se cortó a mitad de trama, campos con N/A donde el sensor no respondió, valores absurdos porque el conversor analógico-digital saturó, líneas vacías al final del archivo y, con suerte, alguna coma de más.
Eso no es un accidente que se pueda evitar aguas arriba. Es la condición normal de trabajo. PiStation, la estación que decodifica APT de los satélites NOAA en el ecosistema, no procesa una señal limpia: procesa lo que llega por una antena barata con ruido, desvanecimientos y pérdidas de sincronía. La cadena que va del SDR al producto final está hecha, en su mayor parte, de código defensivo.
De ahí el principio que gobierna esta lección: un script que muere con un traceback en la línea 3 de 40 000 es peor que uno que descarta la línea 3, la registra y sigue. La robustez no es un extra de calidad: es lo que separa una herramienta de una demostración.
Leer un traceback sin pánico
El traceback asusta porque es largo y está en inglés, pero tiene una estructura rígida y se lee en un orden concreto. Este lo produce un parser ingenuo de telemetría:
Traceback (most recent call last):
File "telemetria.py", line 10, in <module>
print(procesar_lote(lote))
~~~~~~~~~~~~~^^^^^^
File "telemetria.py", line 6, in procesar_lote
return [leer_temperatura(linea.split(",")[1]) for linea in lineas]
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "telemetria.py", line 2, in leer_temperatura
return float(campo)
ValueError: could not convert string to float: 'N/A'
Se lee en tres movimientos:
La última línea primero. Ahí está todo lo que importa: el tipo de error (ValueError) y el mensaje (could not convert string to float: 'N/A'). Con eso solo ya sabes que alguien intentó convertir a número la cadena N/A.
El marco de abajo hacia arriba. La frase most recent call last significa que el último bloque es donde realmente reventó: float(campo) en la línea 2. Los bloques de arriba son quién llamó a quién.
El primer marco de tu código. Los marcos intermedios te dan el camino: <module> línea 10 llamó a procesar_lote, que en la línea 6 llamó a leer_temperatura. Si en la pila aparecen archivos de librerías de terceros, salta a tu archivo más profundo: el error casi siempre está en cómo tú llamaste, no en la librería.
Los símbolos ~~~^^^ bajo el código señalan la subexpresión exacta que falló. Es una ayuda de las versiones modernas de Python que ahorra minutos cuando una línea tiene cuatro llamadas anidadas.
Catálogo de errores que verás esta semana
Error
Qué significa
Ejemplo típico
SyntaxError
Python no pudo leer el archivo; nada se ejecutó
Falta el : tras un if, o un paréntesis sin cerrar
IndentationError
Caso especial del anterior: la indentación no cuadra
Mezclar tabuladores y espacios
NameError
Usas un nombre que no existe
periodo_min escrito periodo_mim
TypeError
Operación entre tipos incompatibles, o faltan argumentos
"21.4" + 1, o llamar fspl_db() sin argumentos
ValueError
Tipo correcto, valor imposible
float("N/A"), int("")
IndexError
Índice fuera del rango de una secuencia
partes[1] cuando split() devolvió un solo elemento
KeyError
Clave inexistente en un diccionario
datos["mean_motion"] cuando la clave se llama mean_motion_rev_dia
ZeroDivisionError
División entre cero
1440 / n con n = 0 en un TLE corrupto
La distinción TypeError / ValueError es la que más cuesta al principio y la que más rendimiento da. Regla mnemotécnica: TypeError es "no sé hacer eso con esa clase de cosa"; ValueError es "sé hacerlo, pero ese valor concreto no vale". float("N/A") es ValueError porque float sí sabe procesar cadenas: es esa cadena la que no representa un número.
try / except: capturar lo que se espera
La estructura completa tiene cuatro bloques y cada uno tiene un papel:
try:
temperatura = float(campo) # código que puede fallar
except ValueError as error:
registrar(f"campo no numérico: {error}") # qué hacer si falla
else:
guardar(temperatura) # solo si NO hubo excepción
finally:
contador += 1 # siempre, haya fallado o no
El try debe envolver lo mínimo imprescindible. Si metes veinte líneas dentro y capturas ValueError, no sabrás cuál de las veinte lo lanzó, y el except acabará escondiendo errores que no esperabas. El else existe justamente para eso: lo que va después del intento arriesgado, pero que no debe estar protegido por el mismo except.
La regla de oro: excepciones específicas
Escribir except Exception: pass es la forma más eficaz de convertir un error de cinco minutos en un misterio de tres días. Captura absolutamente todo — incluidos tus propios errores de tecleo, que se manifestarán como resultados silenciosamente erróneos — y no deja rastro. Compara:
# Mal: se traga cualquier fallo, incluidos los tuyos, y no deja rastro.
try:
t = float(campo)
except Exception:
pass
# Bien: captura solo lo previsto y deja constancia de qué se descartó.
try:
t = float(campo)
except ValueError:
descartadas.append((numero_linea, f"valor no numérico: {campo}"))
Se pueden capturar varios tipos a la vez con una tupla — except (ValueError, IndexError) as error: — y encadenar bloques except distintos para dar tratamientos distintos. Y cuando un error no debe ser tolerado, lo correcto es dejarlo pasar: si el archivo de configuración de la estación no existe, no hay nada sensato que hacer y el programa debe morir ruidosamente.
raise: fallar a propósito y con un motivo
La otra mitad del oficio es lanzar errores propios. Una función que valida debe rechazar lo inválido con un mensaje que sirva para corregir, no con un return None que el llamante ignorará:
if not MIN_C <= temperatura <= MAX_C:
raise ValueError(f"{temperatura} C fuera del rango [{MIN_C}, {MAX_C}]")
Ese mensaje contiene el valor recibido y el rango esperado. Cuando aparezca en un registro a las tres de la madrugada, quien lo lea sabrá en diez segundos qué pasó.
Validación defensiva de telemetría
Un validador de telemetría comprueba, en este orden, cuatro cosas — de la más barata a la más cara:
¿La línea tiene contenido? Una línea vacía o de solo espacios se descarta antes de tocarla.
¿Tiene la estructura esperada?split(",") debe dar exactamente el número de campos previsto. Ni más (una coma extra dentro de un valor) ni menos (la línea se cortó).
¿El valor es un centinela conocido?N/A, NaN, NULL, -, cadena vacía. Son valores que el instrumento emite deliberadamente para decir "no tengo dato", y hay que reconocerlos como tales en lugar de dejar que float() reviente.
¿El número está en un rango físicamente posible? Un radiador externo no está a 850°C ni a −999°C. Estos son los peores datos sucios de todos, porque son numéricamente válidos: pasan la conversión sin protestar y luego envenenan cualquier media, desviación o umbral que calcules después.
Ejemplo resuelto: el lote de las 03:14
Un lote de 12 líneas llega del receptor. Al pasarlo por el validador, seis se descartan: una con N/A (centinela), una vacía, una con −999.0 (fuera de rango), una truncada sin coma (un solo campo), una con coma decimal en vez de punto — 21,9 da tres campos al partir — y una con 850.0 (fuera de rango). Quedan seis medidas válidas: 21.4, 21.6, 21.5, 21.7, 21.8 y 22.0°C.
Ahora observa el contraste. Si el validador solo hubiera filtrado lo que hace fallar a float() — el N/A, la vacía y las malformadas — habrían entrado −999.0 y 850.0 en el cálculo, y la media habría sido (130.0−999.0+850.0)/8=−2.375°C. Dos valores numéricamente válidos destruyen por completo una media de ocho. Ese es el argumento entero a favor del paso 4: los datos que no revientan son los peligrosos.
Una tasa de aceptación del 50 % es alarmante y merece investigación aguas arriba; en operación normal se esperan cifras del 95 % o mejores. Por eso el validador no solo descarta: cuenta y registra el motivo. Sin ese registro nunca sabrás si tu enlace se degradó.
Escribe el parser robusto completo:
Playground · python
Tres retos. Primero: comenta la comprobación de rango y observa cómo la media se desploma a −2.375; es el experimento que fija el punto 4 para siempre. Segundo: cambia except ValueError por except Exception e introduce una errata a propósito (por ejemplo partes[9]) para ver cómo el IndexError desaparece sin dejar rastro y el programa reporta felizmente cero problemas. Tercero: añade finally con un contador de líneas procesadas y comprueba que también suma en las descartadas.
1 de 7
Comprueba la distinción que más rendimiento da al depurar:
Tu parser ejecuta float(campo) y campo vale la cadena "N/A". ¿Qué excepción se lanza?
Con datos limpios y contados, ya podemos hacer estadística de verdad. En la lección 9 llega NumPy y la técnica con la que PiStation detecta que algo va mal antes de que un humano lo note.