Lección 0.3.6 · 55 min

Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15

Un precio a cero, un ticker que no existe o una orden mal formada no deben tumbar un sistema, ni pasar en silencio. La lección enseña a leer, capturar y lanzar excepciones, a repartir el código en módulos y paquetes y la novedad de Python 3.15: los imports perezosos.

Fig. — Tiempo de importar cada librería del curso en un proceso nuevo (Python 3.14.8, mediana de siete arranques en un equipo de escritorio)
06/06
Módulo
0.3 · Fundamentos del lenguaje
Duración
55 min
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

Cuando algo sale mal

Hasta ahora, los errores de Python aparecían de pasada: dividir un Decimal entre un float (lección 0.3.1), pasar el riesgo por posición o modificar una variable global (lección 0.3.5). En un programa de trading, los errores no son una rareza: un proveedor manda un precio a cero, un ticker se da de baja, una orden llega con un campo mal escrito, el bróker no responde. La pregunta no es si pasarán, sino qué hace el programa cuando pasen.

Cuando Python no puede seguir, lanza una excepción: un objeto que describe el error y que interrumpe la ejecución. Si nadie la captura, el programa se para y muestra un traceback: la cadena de llamadas hasta la línea que falló y, en la última línea, el tipo de excepción y el mensaje. Esa última línea es lo primero que hay que leer.

Tres excepciones que aparecen en cualquier código de trading. Las celdas siguientes deben fallar; debajo de cada una, la última línea del traceback:

IN [01]python
cierre_anterior = 0.0      # un dato erróneo del proveedor
cierre = 142.22
rentabilidad = cierre / cierre_anterior - 1
OUT [01]Un precio a cero
ZeroDivisionError: division by zero
IN [02]python
precios = {"SINT": 142.22, "ORO": 171.44, "TECH": 77.82}
precios["BTC"]
OUT [02]Un ticker que no está
KeyError: 'BTC'
IN [03]python
float("1.426,71")
OUT [03]Un número con formato español
ValueError: could not convert string to float: '1.426,71'

ZeroDivisionError, KeyError y ValueError dicen exactamente qué pasó. El último es el problema inverso de la lección 0.3.2: un número escrito a la española no se convierte con float.

Que el programa se pare es, muchas veces, lo correcto. Un precio a cero no tiene una rentabilidad, y un sistema que siguiera operando con un inf haría algo mucho peor que pararse. Las excepciones se capturan solo cuando se sabe qué hacer con ellas.

Capturar con try y except

try ejecuta un bloque y, si dentro se lanza una excepción del tipo indicado en except, salta a ese bloque en lugar de parar el programa. Con as, el bloque recibe la excepción y puede usar su mensaje:

IN [04]python
cierres = [139.36, 139.32, 0.0, 139.56, 139.98]

rentabilidades = []
for c0, c1 in zip(cierres, cierres[1:]):
    try:
        r = c1 / c0 - 1
    except ZeroDivisionError:
        r = None                     # sin dato, no un cero
    rentabilidades.append(r)
print(rentabilidades)

for ticker in ["SINT", "BTC"]:
    try:
        print(ticker, precios[ticker])
    except KeyError as e:
        print(f"Sin precio para {e}: no se valora la posición")
OUT [04]Rentabilidades que no tumban el cálculo
[-0.0002870264064295247, -1.0, None, 0.003009458297506429]
SINT 142.22
Sin precio para 'BTC': no se valora la posición

El cierre a cero produce dos problemas en cadena: la rentabilidad de ese día (−100 %, aunque nadie perdió nada) y la del día siguiente, que se queda sin denominador y se marca con None. Un dato malo contamina más de un cálculo; lo correcto sería corregir o descartar el cierre antes de calcular nada (nivel 1, limpieza de datos). Marcar la ausencia con None, en lugar de con un 0, es la regla de la lección 0.3.3: un dato que falta no es un dato que vale cero.

Tres reglas para capturar bien:

  • Captura el tipo concreto. except ZeroDivisionError deja pasar cualquier otro error, que es lo que se quiere: un NameError por una variable mal escrita es un fallo del programa, no un dato malo.
  • Nunca except: a secas, ni except Exception: pass. Capturan todo, incluidos los fallos de programación, y los convierten en silencio. Una orden que no se envía y no deja rastro es el peor error posible en un sistema que opera.
  • Que el bloque try sea corto. Cuantas menos líneas tenga, más claro está qué excepción se espera y de dónde.

Las excepciones forman una jerarquía: ZeroDivisionError es un caso de ArithmeticError, y KeyError e IndexError son casos de LookupError. Un except captura su tipo y todos los que cuelgan de él.

ExcepciónCuelga deCuándo aparece en trading
ZeroDivisionErrorArithmeticErrorPrecio, volatilidad o distancia al stop a cero
KeyErrorLookupErrorTicker o campo que no está en un diccionario
IndexErrorLookupErrorPedir una barra más allá del final de la lista
ValueErrorExceptionTexto que no es un número, cantidad imposible
TypeErrorExceptionMezclar tipos: Decimal con float, texto con número
ConnectionErrorOSErrorEl bróker o el proveedor de datos no responde

else y finally

Un try puede tener dos bloques más. else se ejecuta solo si el try terminó sin excepciones: es el sitio para lo que solo debe pasar si todo fue bien. finally se ejecuta siempre, haya excepción o no, y aunque la excepción no se haya capturado: es el sitio para cerrar conexiones, liberar recursos y dejar constancia.

sínotry¿Excepción?exceptelsefinally
Fuente Mermaid
flowchart TB
  T[try] --> Q{¿Excepción?}
  Q -- sí --> X[except]
  Q -- no --> E[else]
  X --> F[finally]
  E --> F

El ejemplo simula el envío de órdenes a un bróker que falla con una de ellas. La función enviar es ficticia: lanza ConnectionError para las órdenes de ORO, como si la conexión se cortara:

IN [05]python
def enviar(orden):
    """Simula el envío: falla la conexión con las órdenes de ORO."""
    if orden["simbolo"] == "ORO":
        raise ConnectionError("tiempo de espera agotado")
    return f"ID-{orden['simbolo']}-{orden['cantidad']}"

registro = []
for orden in [{"simbolo": "SINT", "cantidad": 58},
              {"simbolo": "ORO", "cantidad": 20}]:
    try:
        id_orden = enviar(orden)
    except ConnectionError as e:
        registro.append(f"{orden['simbolo']}: NO ENVIADA ({e})")
    else:
        registro.append(f"{orden['simbolo']}: enviada {id_orden}")
    finally:
        registro.append(f"{orden['simbolo']}: intento registrado")
print(*registro, sep="\n")
OUT [05]Enviar órdenes con try, except, else y finally
SINT: enviada ID-SINT-58
SINT: intento registrado
ORO: NO ENVIADA (tiempo de espera agotado)
ORO: intento registrado

print(*registro, sep="\n") desempaqueta la lista en argumentos (lección 0.3.5) y los separa con saltos de línea. La orden de ORO no se envía y queda constancia de por qué; la de SINT se confirma en el else. En los dos casos, el finally registra el intento. Qué hacer después con la orden fallida (reintentar, avisar, cancelar la estrategia) es una decisión de diseño del nivel 6; lo que nunca se hace es olvidarla.

Lanzar excepciones: raise y OrdenInvalida

Las funciones propias también deben avisar cuando reciben algo que no tiene sentido. La función de tamaño de posición de la lección 0.3.5 devolvía un número negativo con un stop por encima de la entrada; con raise, se niega:

IN [06]python
def tamano_posicion(capital, entrada, stop, *, riesgo=0.01):
    """Acciones que arriesgan `riesgo` × `capital` hasta el stop."""
    if stop >= entrada:
        raise ValueError(f"stop {stop} no está por debajo de "
                         f"la entrada {entrada}")
    return int(capital * riesgo / (entrada - stop))

print(tamano_posicion(25_000, 142.22, 137.95))
try:
    tamano_posicion(25_000, 142.22, 145.00)
except ValueError as e:
    print("Rechazado:", e)
OUT [06]Validar con raise
58
Rechazado: stop 145.0 no está por debajo de la entrada 142.22

raise lanza una excepción con un mensaje que explica el problema con los valores concretos. Quien llama decide si la captura o deja que pare el programa.

Para los errores propios de un dominio, como una orden mal formada, conviene una excepción con nombre propio. Se define en dos líneas con class, indicando de qué excepción cuelga (las clases se explican en la lección 0.4.1; aquí basta con copiar la forma):

IN [07]python
class OrdenInvalida(ValueError):
    """Orden que no se puede enviar al bróker."""

LADOS = {"compra", "venta"}

def validar_orden(orden):
    """Devuelve la orden si es válida; si no, OrdenInvalida."""
    for campo in ("simbolo", "lado", "cantidad"):
        if campo not in orden:
            raise OrdenInvalida(f"falta el campo {campo!r}")
    if orden["lado"] not in LADOS:
        raise OrdenInvalida(f"lado desconocido {orden['lado']!r}")
    if not isinstance(orden["cantidad"], int) or orden["cantidad"] <= 0:
        raise OrdenInvalida(f"cantidad no válida {orden['cantidad']!r}")
    return orden

ordenes = [{"simbolo": "SINT", "lado": "compra", "cantidad": 58},
           {"simbolo": "ORO", "lado": "comprar", "cantidad": 20},
           {"simbolo": "TECH", "lado": "venta", "cantidad": 2.5},
           {"simbolo": "RFIJ", "cantidad": 10}]
for orden in ordenes:
    try:
        validar_orden(orden)
    except OrdenInvalida as e:
        print(f"RECHAZADA {orden['simbolo']}: {e}")
    else:
        print(f"VÁLIDA    {orden['simbolo']}")
OUT [07]Una excepción propia para las órdenes
VÁLIDA    SINT
RECHAZADA ORO: lado desconocido 'comprar'
RECHAZADA TECH: cantidad no válida 2.5
RECHAZADA RFIJ: falta el campo 'lado'

isinstance(valor, int) pregunta si un valor es de un tipo: 2,5 acciones no es una cantidad válida. Como OrdenInvalida cuelga de ValueError, un código que ya capturara ValueError también la captura; y quien quiera tratar solo los rechazos de órdenes puede capturar OrdenInvalida sin tocar los demás errores. El orden de las comprobaciones importa: primero que existan los campos, después sus valores. Si se comprobara el lado antes de saber que existe, una orden sin lado daría un KeyError en lugar de un rechazo claro.

Módulos: tu código en ficheros

Cuando un notebook crece, las funciones que ya funcionan (tamaño de posición, drawdown, validación de órdenes) se mueven a ficheros .py. Cada fichero es un módulo, y su nombre sin .py es el nombre con el que se importa. Normalmente el fichero se crea en el editor (lección 0.2.3); aquí la celda lo escribe para que el ejemplo se pueda ejecutar tal cual. El contenido es una versión corta de la función de antes:

IN [08]python
codigo = '''"""Gestión del riesgo: tamaño de posición."""

RIESGO_POR_DEFECTO = 0.01


def tamano_posicion(capital, entrada, stop, *,
                    riesgo=RIESGO_POR_DEFECTO):
    """Acciones que arriesgan `riesgo` × `capital`."""
    if stop >= entrada:
        raise ValueError("el stop debe estar bajo la entrada")
    return int(capital * riesgo / (entrada - stop))


if __name__ == "__main__":
    print(tamano_posicion(25_000, 142.22, 137.95))
'''
with open("riesgo.py", "w", encoding="utf-8") as f:
    f.write(codigo)

Con el fichero en la carpeta de trabajo, import lo carga. Hay tres formas, y las tres son habituales:

IN [09]python
import riesgo
print(riesgo.tamano_posicion(25_000, 142.22, 137.95))
print(riesgo.RIESGO_POR_DEFECTO, riesgo.__name__)

from riesgo import tamano_posicion as tamano
print(tamano(25_000, 142.22, 137.95, riesgo=0.02))

import statistics as st
print(st.mean([139.36, 139.32, 139.56]))
OUT [09]Tres formas de importar
58
0.01 riesgo
117
139.41333333333333
  • import riesgo carga el módulo y da acceso a su contenido con un punto: riesgo.tamano_posicion. Queda claro de dónde viene cada nombre.
  • from riesgo import tamano_posicion trae un nombre concreto; as le pone otro (por ejemplo, para no chocar con el tamano_posicion definido antes en el notebook).
  • import statistics as st es lo mismo con un alias. Las librerías tienen alias convencionales que todo el mundo usa: import numpy as np, import pandas as pd, import polars as pl.

Los módulos de la biblioteca estándar (statistics, decimal, datetime, csv…) vienen con Python; los de terceros (pandas, numpy) se instalan en el entorno del proyecto con uv add (lección 0.2.2). Python busca los módulos primero en la carpeta del script y después en el entorno; por eso un fichero propio llamado statistics.py o pandas.py en la carpeta de trabajo tapa a la librería real. Nunca llames a un fichero como una librería.

La última línea del módulo, if __name__ == "__main__":, separa dos usos del mismo fichero. Cuando se importa, __name__ vale el nombre del módulo ("riesgo", como muestra la celda) y el bloque no se ejecuta. Cuando se ejecuta como script desde la terminal, __name__ vale "__main__" y el bloque sí se ejecuta:

uv run python riesgo.py

Esa orden imprimiría el tamaño de posición del ejemplo, 58 acciones. Es el sitio para una prueba rápida o para la entrada de un script, sin que importar el módulo dispare nada.

Un módulo se ejecuta una sola vez por sesión: el segundo import riesgo reutiliza el que ya está cargado. En un notebook, si se edita el fichero, hay que reiniciar el kernel (o usar importlib.reload(riesgo)) para ver los cambios. Es una fuente habitual de confusión: el fichero dice una cosa y el notebook ejecuta la versión de antes.

Paquetes: varios módulos juntos

Un paquete es una carpeta de módulos con un fichero __init__.py, que puede estar vacío y marca la carpeta como paquete. Es la forma de organizar el código del curso: en tu repositorio, el paquete ppt vive en src/ppt/ (lecciones 0.1.3 y 0.2.2). La celda crea una versión mínima en la carpeta de trabajo:

IN [10]python
import os

os.makedirs("ppt", exist_ok=True)
ficheros = {
    "ppt/__init__.py": '"""Código del curso Python para Trading."""\n',
    "ppt/costes.py": (
        "def coste_total(*comisiones, minimo=1.0):\n"
        "    return round(sum(max(c, minimo) for c in comisiones), 2)\n"
    ),
    "ppt/riesgo.py": codigo,
}
for ruta, texto in ficheros.items():
    with open(ruta, "w", encoding="utf-8") as f:
        f.write(texto)

from ppt.costes import coste_total
from ppt import riesgo as ppt_riesgo
print(coste_total(5.14, 0.42, 7.10))
print(ppt_riesgo.tamano_posicion(25_000, 142.22, 137.95))
OUT [10]Un paquete con dos módulos
13.24
58
IN [11]
ppt/
    __init__.py
    costes.py
    riesgo.py

ppt.costes es el módulo costes dentro del paquete ppt: el punto recorre las carpetas. Dentro del paquete, un módulo puede importar a otro con un import relativo, from .costes import coste_total, donde el punto significa «de este mismo paquete». Así el paquete se puede mover o renombrar sin cambiar sus imports internos.

Novedad de Python 3.15: imports perezosos

Un import carga el módulo en el momento: busca el fichero, lo ejecuta entero y, si el módulo importa otros, también los carga. Con librerías grandes, eso tarda. La figura de la cabecera muestra cuánto tarda en importarse cada librería del curso en el entorno de curso-code (Python 3.14.8, mediana de siete arranques en un equipo de escritorio): pandas, unos 355 ms; matplotlib.pyplot, unos 430 ms. Las cifras cambian con cada máquina, pero el orden de magnitud no.

Para un notebook que se abre una vez al día da igual. Para una herramienta de línea de órdenes, no: un script que importa pandas y matplotlib al principio tarda más de medio segundo en responder incluso a --help, que no usa ninguna de las dos (comparten numpy, que solo se carga una vez). Python 3.15, publicado el 9 de octubre de 2026, añade una solución en el propio lenguaje: los imports perezosos explícitos (PEP 810)1Nota 1 · PEP 810, «Explicit lazy imports», estado Final, aceptada el 3 de noviembre de 2025 para Python 3.15 (peps.python.org/pep-0810). La referencia del lenguaje la documenta en docs.python.org/3.15, y las notas de la versión, publicada el 9 de octubre de 2026, en «What's new in Python 3.15».. La palabra lazy delante de un import lo aplaza: el nombre queda ligado a un objeto provisional y el módulo se carga la primera vez que se usa.

El curso usa Python 3.14 (lección 0.2.1), así que este bloque no se ejecuta en el curso; es sintaxis de 3.15 y en 3.14 da un SyntaxError:

IN [12]informe.py (Python 3.15)python
import sys
lazy import pandas as pd
lazy from matplotlib import pyplot as plt

if "--help" in sys.argv:
    print("uso: informe.py FICHERO.csv")   # ni pandas ni pyplot
    sys.exit(0)

datos = pd.read_csv(sys.argv[1])           # aquí se carga pandas
datos["cierre"].plot()                     # y aquí pyplot
plt.savefig("informe.png")

Lo que dice la especificación y conviene saber antes de usarlo:

  • lazy es una palabra clave blanda: solo tiene significado delante de import o from. En cualquier otro sitio sigue siendo un nombre normal, así que el código que ya usaba lazy como variable no se rompe.
  • Solo se admite en el nivel superior del módulo. Dentro de una función, de una clase o de un bloque try/except es un SyntaxError, y tampoco se permite con from módulo import *.
  • Si el módulo no existe o falla al cargarse, el error no aparece en la línea del import, sino en el primer uso, con un traceback que señala las dos líneas. Un script con un import perezoso roto puede funcionar durante semanas hasta que alguien pasa por la rama que lo usa.
  • Para código que tiene que funcionar también en versiones anteriores, un módulo puede declarar __lazy_modules__ = ["pandas"]: en 3.15 los import pandas normales de ese módulo pasan a ser perezosos, y las versiones anteriores ignoran la variable.
  • Hay un modo global, -X lazy_imports=all o la variable de entorno PYTHON_LAZY_IMPORTS, que hace perezosos todos los imports del nivel superior sin tocar el código. Es una herramienta avanzada: un módulo que hace algo al importarse (registrar un plugin, configurar el logging) deja de hacerlo en el momento esperado.

Ejercicios

  1. Escribe rentabilidad_segura(c0, c1), que devuelva la rentabilidad de un cierre respecto al anterior y lance ValueError con un mensaje claro si alguno de los dos precios no es positivo. Pruébala con (139.98, 142.22) y, capturando la excepción, con (0.0, 142.22).
Solución
IN [13]python
def rentabilidad_segura(c0, c1):
    """Rentabilidad de c0 a c1; los dos precios deben ser > 0."""
    if c0 <= 0 or c1 <= 0:
        raise ValueError(f"precios no positivos: {c0}, {c1}")
    return c1 / c0 - 1

print(f"{rentabilidad_segura(139.98, 142.22):+.2%}")
try:
    rentabilidad_segura(0.0, 142.22)
except ValueError as e:
    print("Error:", e)
OUT [13]Rentabilidad con validación
+1.60%
Error: precios no positivos: 0.0, 142.22

La función comprueba antes de dividir, así que el error dice qué dato es malo en lugar de un ZeroDivisionError genérico. Además rechaza los precios negativos, que no darían ningún error y producirían una rentabilidad absurda.

  1. Añade a validar_orden una comprobación: si la orden tiene la clave "precio", tiene que ser un número mayor que cero. Comprueba que rechaza {"simbolo": "SINT", "lado": "compra", "cantidad": 10, "precio": -1}.
Solución
IN [14]python
def validar_orden_con_precio(orden):
    """validar_orden más el precio límite, si lo hay."""
    validar_orden(orden)
    if "precio" in orden:
        precio = orden["precio"]
        if not isinstance(precio, (int, float)) or precio <= 0:
            raise OrdenInvalida(f"precio no válido {precio!r}")
    return orden

try:
    validar_orden_con_precio({"simbolo": "SINT", "lado": "compra",
                              "cantidad": 10, "precio": -1})
except OrdenInvalida as e:
    print("RECHAZADA:", e)
OUT [14]Validar el precio límite
RECHAZADA: precio no válido -1

La función nueva reutiliza validar_orden en lugar de copiar sus comprobaciones, y isinstance admite una tupla de tipos. Si validar_orden lanza la excepción, sale de validar_orden_con_precio sin pasar por el resto.

  1. Añade al paquete ppt un módulo rentabilidades.py con la función rentabilidades(cierres) de la lección 0.3.5, impórtalo y úsalo con [100, 110, 99].
Solución
IN [15]python
texto = (
    "def rentabilidades(cierres):\n"
    "    return [c1 / c0 - 1 for c0, c1 in zip(cierres, cierres[1:])]\n"
)
with open("ppt/rentabilidades.py", "w", encoding="utf-8") as f:
    f.write(texto)

from ppt.rentabilidades import rentabilidades
print([round(r, 4) for r in rentabilidades([100, 110, 99])])
OUT [15]Un módulo más en el paquete
[0.1, -0.1]

Basta con crear el fichero dentro de la carpeta del paquete. En tu repositorio, ese módulo iría en src/ppt/ con su test (lección 0.4.5).

Footnotes

  1. PEP 810, «Explicit lazy imports», estado Final, aceptada el 3 de noviembre de 2025 para Python 3.15 (peps.python.org/pep-0810). La referencia del lenguaje la documenta en docs.python.org/3.15, y las notas de la versión, publicada el 9 de octubre de 2026, en «What's new in Python 3.15». ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. ¿Qué excepción da precios["BTC"] si el diccionario precios no tiene esa clave?

    • AValueError
    • BIndexError
    • CKeyError
    • DDevuelve None
    Ver respuesta

    Respuesta C. KeyError

    Leer con corchetes una clave que no existe lanza KeyError con la clave en el mensaje. None es lo que devolvería precios.get("BTC").

  2. ¿Cuándo se ejecuta el bloque else de un try?

    • ASiempre
    • BSolo si el bloque try no lanzó ninguna excepción
    • CSolo si se capturó una excepción
    • DAntes de finally, haya o no excepción
    Ver respuesta

    Respuesta B. Solo si el bloque try no lanzó ninguna excepción

    else es para lo que solo debe pasar si todo fue bien (confirmar una orden enviada). finally es lo que se ejecuta siempre (registrar, cerrar la conexión).

  3. ¿Qué tiene de malo try: enviar(orden) seguido de except Exception: pass?

    • ANada: evita que el programa se pare
    • BQue es más lento
    • CQue oculta cualquier error, también los fallos de programación: la orden no se envía y nadie se entera
    • DQue no captura KeyError
    Ver respuesta

    Respuesta C. Que oculta cualquier error, también los fallos de programación: la orden no se envía y nadie se entera

    Capturar todo y no hacer nada convierte cualquier fallo en silencio. Se capturan solo las excepciones que se esperan y se sabe tratar; el resto debe parar el programa o, al menos, registrarse.

  4. En Python 3.15, ¿qué hace lazy import pandas as pd al principio de un script?

    • AImporta pandas en un hilo aparte
    • BCrea un objeto aplazado: pandas se carga la primera vez que se usa pd
    • CImporta solo las funciones de pandas que se usan
    • DNada en 3.15: es una propuesta rechazada
    Ver respuesta

    Respuesta B. Crea un objeto aplazado: pandas se carga la primera vez que se usa pd

    PEP 810, aceptada e incluida en Python 3.15: el nombre queda ligado a un objeto aplazado y el módulo se carga entero en el primer uso. Si el script nunca usa pd, no paga el tiempo de importarlo.

Ideas clave

  • Una excepción para el programa y deja un traceback: la última línea dice el tipo y el mensaje; las anteriores, dónde pasó.
  • Captura solo lo que esperas y sabes tratar (except KeyError), nunca except: a secas. else es lo que va si todo fue bien; finally, lo que va siempre.
  • Rechazar es mejor que adivinar: raise OrdenInvalida(...) con un mensaje claro en lugar de corregir la orden en silencio.
  • Un fichero .py es un módulo; una carpeta con __init__.py, un paquete. if __name__ == "__main__": separa lo que se ejecuta como script de lo que se importa.
  • Python 3.15 añade lazy import (PEP 810): el módulo se carga en el primer uso. El curso usa 3.14; en 3.15, sirve para que las herramientas de línea de órdenes arranquen rápido.

Recursos