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.
- Módulo
- 0.3 · Fundamentos del lenguaje
- Duración
- 55 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
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:
cierre_anterior = 0.0 # un dato erróneo del proveedor
cierre = 142.22
rentabilidad = cierre / cierre_anterior - 1ZeroDivisionError: division by zeroprecios = {"SINT": 142.22, "ORO": 171.44, "TECH": 77.82}
precios["BTC"]KeyError: 'BTC'float("1.426,71")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:
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")[-0.0002870264064295247, -1.0, None, 0.003009458297506429]
SINT 142.22
Sin precio para 'BTC': no se valora la posiciónEl 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 ZeroDivisionErrordeja pasar cualquier otro error, que es lo que se quiere: unNameErrorpor una variable mal escrita es un fallo del programa, no un dato malo. - Nunca
except:a secas, niexcept 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
trysea 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ón | Cuelga de | Cuándo aparece en trading |
|---|---|---|
ZeroDivisionError | ArithmeticError | Precio, volatilidad o distancia al stop a cero |
KeyError | LookupError | Ticker o campo que no está en un diccionario |
IndexError | LookupError | Pedir una barra más allá del final de la lista |
ValueError | Exception | Texto que no es un número, cantidad imposible |
TypeError | Exception | Mezclar tipos: Decimal con float, texto con número |
ConnectionError | OSError | El 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.
Fuente Mermaid
flowchart TB
T[try] --> Q{¿Excepción?}
Q -- sí --> X[except]
Q -- no --> E[else]
X --> F[finally]
E --> FEl 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:
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")SINT: enviada ID-SINT-58
SINT: intento registrado
ORO: NO ENVIADA (tiempo de espera agotado)
ORO: intento registradoprint(*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:
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)58
Rechazado: stop 145.0 no está por debajo de la entrada 142.22raise 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):
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']}")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:
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:
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]))58
0.01 riesgo
117
139.41333333333333import riesgocarga 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_posiciontrae un nombre concreto;asle pone otro (por ejemplo, para no chocar con eltamano_posiciondefinido antes en el notebook).import statistics as stes 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.pyEsa 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:
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))13.24
58ppt/
__init__.py
costes.py
riesgo.pyppt.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:
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:
lazyes una palabra clave blanda: solo tiene significado delante deimportofrom. En cualquier otro sitio sigue siendo un nombre normal, así que el código que ya usabalazycomo 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/exceptes unSyntaxError, y tampoco se permite confrom 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 losimport pandasnormales de ese módulo pasan a ser perezosos, y las versiones anteriores ignoran la variable. - Hay un modo global,
-X lazy_imports=allo la variable de entornoPYTHON_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
- Escribe
rentabilidad_segura(c0, c1), que devuelva la rentabilidad de un cierre respecto al anterior y lanceValueErrorcon 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
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)+1.60%
Error: precios no positivos: 0.0, 142.22La 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.
- Añade a
validar_ordenuna 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
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)RECHAZADA: precio no válido -1La 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.
- Añade al paquete
pptun módulorentabilidades.pycon la funciónrentabilidades(cierres)de la lección 0.3.5, impórtalo y úsalo con[100, 110, 99].
Solución
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])])[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
-
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
¿Qué excepción da
precios["BTC"]si el diccionariopreciosno tiene esa clave?- A
ValueError - B
IndexError - C
KeyError - DDevuelve
None
Ver respuesta
Respuesta C.
KeyErrorLeer con corchetes una clave que no existe lanza
KeyErrorcon la clave en el mensaje.Nonees lo que devolveríaprecios.get("BTC").- A
¿Cuándo se ejecuta el bloque
elsede untry?- ASiempre
- BSolo si el bloque
tryno 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
tryno lanzó ninguna excepciónelsees para lo que solo debe pasar si todo fue bien (confirmar una orden enviada).finallyes lo que se ejecuta siempre (registrar, cerrar la conexión).¿Qué tiene de malo
try: enviar(orden)seguido deexcept 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.
En Python 3.15, ¿qué hace
lazy import pandas as pdal 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
pdPEP 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), nuncaexcept:a secas.elsees 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
.pyes 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.