---
title: "0.3.6 Excepciones, módulos y paquetes"
description: "Errores con un precio a cero, un ticker que falta o una orden inválida: try/except, excepciones propias, módulos, paquetes y los imports perezosos de 3.15."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/excepciones-modulos-y-paquetes/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/excepciones-modulos-y-paquetes.png"
---

# 0.3.6 · 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.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.3 · Fundamentos del lenguaje
- Duración: 55 min
- Requisitos: [0.3.5 · Funciones: argumentos, *args y **kwargs, lambdas y ámbito](https://pythonparatrading.com/curso-python-trading/nivel-0/funciones-argumentos-y-ambito/)

## Al terminar esta lección

- Leer un traceback e identificar el tipo de excepción (`ZeroDivisionError`, `KeyError`, `ValueError`) y la línea que la causa.
- Capturar excepciones concretas con `try`/`except`/`else`/`finally` sin ocultar errores.
- Lanzar excepciones con `raise` y definir una propia, `OrdenInvalida`, para rechazar órdenes.
- Escribir un módulo y un paquete propios, importarlos y explicar qué cambia con `lazy import` en Python 3.15.

## 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:

```python raises
cierre_anterior = 0.0      # un dato erróneo del proveedor
cierre = 142.22
rentabilidad = cierre / cierre_anterior - 1
```
```output
ZeroDivisionError: division by zero
```

```python raises
precios = {"SINT": 142.22, "ORO": 171.44, "TECH": 77.82}
precios["BTC"]
```
```output
KeyError: 'BTC'
```

```python raises
float("1.426,71")
```
```output
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:

```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")
```
```output
[-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ó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.

```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:

```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")
```
```output
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:

```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)
```
```output
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):

```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']}")
```
```output
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.

<aside class="callout" data-kind="riesgo">

Rechazar es mejor que adivinar. Una función que «arregla» una orden (redondea 2,5 a 2, interpreta «comprar» como
«compra») acaba enviando algo que nadie decidió. La lección 0.4.2 hace estas validaciones de forma declarativa con
pydantic.

</aside>

## 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:

```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:

```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]))
```
```output
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:

```bash
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:

```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))
```
```output
13.24
58
```

```text
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)[^1]. 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`:

```python skip title="informe.py (Python 3.15)"
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.

<aside class="callout" data-kind="nota">

Antes de 3.15, el truco para lo mismo era mover el `import` dentro de la función que usa la librería. Funciona, y se
verá en código existente, pero esconde las dependencias del módulo en mitad del código. En 3.15, `lazy import` deja
los imports arriba, donde se leen, y aplaza solo la carga.

</aside>

## 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.**

```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)
```
```output
+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.

2. 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.**

```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)
```
```output
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.

3. 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.**

```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])])
```
```output
[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).

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

## Autoevaluación

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

   - A. `ValueError`
   - B. `IndexError`
   - C. `KeyError`
   - D. Devuelve `None`

   *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`?

   - A. Siempre
   - B. Solo si el bloque `try` no lanzó ninguna excepción
   - C. Solo si se capturó una excepción
   - D. Antes de `finally`, haya o no excepción

   *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`?

   - A. Nada: evita que el programa se pare
   - B. Que es más lento
   - C. Que oculta cualquier error, también los fallos de programación: la orden no se envía y nadie se entera
   - D. Que no captura `KeyError`

   *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?

   - A. Importa pandas en un hilo aparte
   - B. Crea un objeto aplazado: pandas se carga la primera vez que se usa `pd`
   - C. Importa solo las funciones de pandas que se usan
   - D. Nada en 3.15: es una propuesta rechazada

   *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

### Documentación

- [Python: errores y excepciones (tutorial oficial)](https://docs.python.org/3/tutorial/errors.html) · docs.python.org
- [Python: excepciones integradas y su jerarquía (documentación oficial)](https://docs.python.org/3/library/exceptions.html) · docs.python.org
- [Python: módulos y paquetes (tutorial oficial)](https://docs.python.org/3/tutorial/modules.html) · docs.python.org
- [PEP 810: Explicit lazy imports (aceptada; Python 3.15)](https://peps.python.org/pep-0810/) · peps.python.org
- [Python 3.15: imports perezosos (referencia del lenguaje)](https://docs.python.org/3.15/reference/simple_stmts.html#lazy-imports) · docs.python.org

Lección anterior: [0.3.5 · Funciones: argumentos, *args y **kwargs, lambdas y ámbito](https://pythonparatrading.com/curso-python-trading/nivel-0/funciones-argumentos-y-ambito/)

Lección siguiente: [0.4.1 · Programación orientada a objetos: clases, herencia, composición y dataclasses](https://pythonparatrading.com/curso-python-trading/nivel-0/clases-herencia-y-dataclasses/)
