{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "bdc68087",
   "metadata": {},
   "source": [
    "[← Anterior](05_entrada_salida.ipynb) · [Índice](README.md) · [Siguiente →](07_clases.ipynb)\n",
    "\n",
    "# 06 · Errores y excepciones\n",
    "\n",
    "**Audiencia.** Personas que ya han visto algo de programación y quieren repasar Python de forma práctica.  \n",
    "**Prerrequisitos.** Funciones, condicionales y entrada/salida básica.  \n",
    "**Duración orientativa.** 60–90 minutos, incluida la práctica.\n",
    "\n",
    "## Objetivos de aprendizaje\n",
    "\n",
    "- Distinguir errores de sintaxis, de ejecución y de lógica.\n",
    "- Capturar únicamente las excepciones que se pueden gestionar.\n",
    "- Usar `else`, `finally`, `raise` y encadenamiento.\n",
    "- Definir excepciones propias y validar contratos.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72c2e782",
   "metadata": {},
   "source": [
    "## Itinerario\n",
    "\n",
    "1. Tipos de fallo\n",
    "2. `try`/`except`\n",
    "3. `else`/`finally`\n",
    "4. Lanzar errores\n",
    "5. Excepciones propias\n",
    "6. EAFP\n",
    "7. Práctica\n",
    "\n",
    "> **Cómo trabajar:** ejecuta las celdas en orden, predice el resultado antes de verlo y modifica los ejemplos. Cada bloque termina con un ejercicio, un punto de partida y una solución posible.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "796950bc",
   "metadata": {},
   "source": [
    "## 1. Tres clases de fallo\n",
    "\n",
    "- **Sintaxis:** el código no se puede analizar.\n",
    "- **Ejecución:** ocurre una excepción al ejecutar (`ValueError`, `TypeError`, etc.).\n",
    "- **Lógica:** el programa corre, pero produce un resultado equivocado; las pruebas ayudan a detectarlo.\n",
    "\n",
    "Capturamos una sintaxis inválida con `compile` para que el notebook continúe.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cf2d52e8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.402512Z",
     "iopub.status.busy": "2026-08-02T22:29:52.402390Z",
     "iopub.status.idle": "2026-08-02T22:29:52.406425Z",
     "shell.execute_reply": "2026-08-02T22:29:52.406127Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SyntaxError → invalid syntax\n",
      "ValueError → invalid literal for int() with base 10: 'doce'\n"
     ]
    }
   ],
   "source": [
    "codigo_invalido = \"if True print('hola')\"\n",
    "try:\n",
    "    compile(codigo_invalido, \"<ejemplo>\", \"exec\")\n",
    "except SyntaxError as error:\n",
    "    print(type(error).__name__, \"→\", error.msg)\n",
    "\n",
    "try:\n",
    "    int(\"doce\")\n",
    "except ValueError as error:\n",
    "    print(type(error).__name__, \"→\", error)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2944414",
   "metadata": {},
   "source": [
    "## 2. Captura específica con `try` y `except`\n",
    "\n",
    "Coloca en `try` solo la operación que puede fallar. Captura excepciones concretas, no un `except:` indiscriminado: ocultaría interrupciones y errores de programación.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "de04184c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.408227Z",
     "iopub.status.busy": "2026-08-02T22:29:52.408089Z",
     "iopub.status.idle": "2026-08-02T22:29:52.411087Z",
     "shell.execute_reply": "2026-08-02T22:29:52.410782Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('10', '2') → 5.0\n",
      "('x', '2') → entrada no numérica\n",
      "('10', '0') → división por cero\n"
     ]
    }
   ],
   "source": [
    "def dividir_textos(numerador, denominador):\n",
    "    try:\n",
    "        a = float(numerador)\n",
    "        b = float(denominador)\n",
    "        return a / b\n",
    "    except ValueError:\n",
    "        return \"entrada no numérica\"\n",
    "    except ZeroDivisionError:\n",
    "        return \"división por cero\"\n",
    "\n",
    "for caso in [(\"10\", \"2\"), (\"x\", \"2\"), (\"10\", \"0\")]:\n",
    "    print(caso, \"→\", dividir_textos(*caso))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eec9ebf1",
   "metadata": {},
   "source": [
    "Se pueden agrupar excepciones si comparten tratamiento. Guarda la excepción como `error` cuando el detalle sea útil; evita mostrar información sensible a usuarios finales.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "548fbff1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.412685Z",
     "iopub.status.busy": "2026-08-02T22:29:52.412555Z",
     "iopub.status.idle": "2026-08-02T22:29:52.415237Z",
     "shell.execute_reply": "2026-08-02T22:29:52.414808Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "b\n",
      "No disponible (IndexError)\n"
     ]
    }
   ],
   "source": [
    "def obtener_elemento(valores, indice):\n",
    "    try:\n",
    "        return valores[int(indice)]\n",
    "    except (ValueError, IndexError, TypeError) as error:\n",
    "        return f\"No disponible ({type(error).__name__})\"\n",
    "\n",
    "print(obtener_elemento([\"a\", \"b\"], \"1\"))\n",
    "print(obtener_elemento([\"a\", \"b\"], \"9\"))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "724b515d",
   "metadata": {},
   "source": [
    "## 3. `else` y `finally`\n",
    "\n",
    "`else` se ejecuta si no hubo excepción; evita ampliar innecesariamente el `try`. `finally` se ejecuta siempre y sirve para liberar recursos. Los gestores `with` suelen ser una alternativa más limpia para archivos y conexiones.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "4c23e058",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.416889Z",
     "iopub.status.busy": "2026-08-02T22:29:52.416763Z",
     "iopub.status.idle": "2026-08-02T22:29:52.419065Z",
     "shell.execute_reply": "2026-08-02T22:29:52.418788Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Intento terminado para '3.5'\n",
      "3.5\n",
      "Intento terminado para 'tres'\n",
      "formato inválido\n"
     ]
    }
   ],
   "source": [
    "def convertir_positivo(texto):\n",
    "    try:\n",
    "        valor = float(texto)\n",
    "    except ValueError:\n",
    "        resultado = \"formato inválido\"\n",
    "    else:\n",
    "        resultado = valor if valor > 0 else \"no es positivo\"\n",
    "    finally:\n",
    "        print(f\"Intento terminado para {texto!r}\")\n",
    "    return resultado\n",
    "\n",
    "print(convertir_positivo(\"3.5\"))\n",
    "print(convertir_positivo(\"tres\"))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1035870c",
   "metadata": {},
   "source": [
    "## 4. Rechazar estados inválidos con `raise`\n",
    "\n",
    "Una función debe fallar pronto y con un mensaje útil cuando se incumple su contrato. Usa tipos estándar cuando describen bien el problema: `TypeError` para tipo inadecuado y `ValueError` para valor fuera de dominio.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c178494f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.420566Z",
     "iopub.status.busy": "2026-08-02T22:29:52.420455Z",
     "iopub.status.idle": "2026-08-02T22:29:52.422753Z",
     "shell.execute_reply": "2026-08-02T22:29:52.422497Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "31.25\n",
      "Error controlado: total debe ser mayor que cero\n"
     ]
    }
   ],
   "source": [
    "def porcentaje(parte, total):\n",
    "    if not isinstance(total, (int, float)):\n",
    "        raise TypeError(\"total debe ser numérico\")\n",
    "    if total <= 0:\n",
    "        raise ValueError(\"total debe ser mayor que cero\")\n",
    "    return parte / total * 100\n",
    "\n",
    "print(porcentaje(25, 80))\n",
    "try:\n",
    "    porcentaje(2, 0)\n",
    "except ValueError as error:\n",
    "    print(\"Error controlado:\", error)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50bc5ce0",
   "metadata": {},
   "source": [
    "## 5. Excepciones propias y encadenamiento\n",
    "\n",
    "Una excepción de dominio comunica mejor que un error genérico. Se hereda de `Exception`. `raise ... from error` conserva la causa original y añade contexto.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "79c12e4b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.424329Z",
     "iopub.status.busy": "2026-08-02T22:29:52.424228Z",
     "iopub.status.idle": "2026-08-02T22:29:52.426748Z",
     "shell.execute_reply": "2026-08-02T22:29:52.426511Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Puerto: 8080\n",
      "Configuración rechazada: el puerto debe ser entero\n",
      "Configuración rechazada: falta la clave 'puerto'\n"
     ]
    }
   ],
   "source": [
    "class ConfiguracionError(Exception):\n",
    "    '''Configuración ausente o inválida.'''\n",
    "\n",
    "def leer_puerto(configuracion):\n",
    "    try:\n",
    "        puerto = int(configuracion[\"puerto\"])\n",
    "    except KeyError as error:\n",
    "        raise ConfiguracionError(\"falta la clave 'puerto'\") from error\n",
    "    except ValueError as error:\n",
    "        raise ConfiguracionError(\"el puerto debe ser entero\") from error\n",
    "    if not 1 <= puerto <= 65535:\n",
    "        raise ConfiguracionError(\"puerto fuera de rango\")\n",
    "    return puerto\n",
    "\n",
    "for config in [{\"puerto\": \"8080\"}, {\"puerto\": \"abc\"}, {}]:\n",
    "    try:\n",
    "        print(\"Puerto:\", leer_puerto(config))\n",
    "    except ConfiguracionError as error:\n",
    "        print(\"Configuración rechazada:\", error)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdf8414f",
   "metadata": {},
   "source": [
    "## 6. EAFP, LBYL y aserciones\n",
    "\n",
    "Python favorece a menudo EAFP (*es más fácil pedir perdón*): intenta la operación y captura el fallo esperado. LBYL (*mira antes de saltar*) comprueba primero. Las aserciones documentan invariantes internas, no validan datos de usuario: pueden desactivarse con optimización.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "423d2e29",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.428129Z",
     "iopub.status.busy": "2026-08-02T22:29:52.428030Z",
     "iopub.status.idle": "2026-08-02T22:29:52.430271Z",
     "shell.execute_reply": "2026-08-02T22:29:52.430035Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "UTC 9.0\n"
     ]
    }
   ],
   "source": [
    "perfil = {\"nombre\": \"Sam\", \"idioma\": \"es\"}\n",
    "\n",
    "# EAFP: una sola búsqueda, con alternativa explícita.\n",
    "try:\n",
    "    zona = perfil[\"zona_horaria\"]\n",
    "except KeyError:\n",
    "    zona = \"UTC\"\n",
    "\n",
    "def media(valores):\n",
    "    assert valores, \"la colección interna no debería estar vacía\"\n",
    "    return sum(valores) / len(valores)\n",
    "\n",
    "print(zona, media([8, 9, 10]))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76c2f6c5",
   "metadata": {},
   "source": [
    "## 7. Registrar sin ocultar\n",
    "\n",
    "En aplicaciones, `logging` es preferible a dispersar `print`: tiene niveles y destinos configurables. `logger.exception` se usa dentro de un `except` para incluir el *traceback*. Aquí mostramos un mensaje breve y controlado.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "7cf8e430",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.431588Z",
     "iopub.status.busy": "2026-08-02T22:29:52.431493Z",
     "iopub.status.idle": "2026-08-02T22:29:52.433689Z",
     "shell.execute_reply": "2026-08-02T22:29:52.433436Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "El ejemplo de registro está activo\n"
     ]
    }
   ],
   "source": [
    "import logging\n",
    "\n",
    "logger = logging.getLogger(\"curso\")\n",
    "logger.setLevel(logging.INFO)\n",
    "if not logger.handlers:\n",
    "    logger.addHandler(logging.StreamHandler())\n",
    "\n",
    "logger.info(\"El ejemplo de registro está activo\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8bee7467",
   "metadata": {},
   "source": [
    "## Errores habituales\n",
    "\n",
    "- Atrapar `Exception` y continuar como si nada.\n",
    "- Cubrir demasiadas líneas con un solo `try`.\n",
    "- Usar excepciones para flujo normal que se puede expresar claramente.\n",
    "- Exponer mensajes internos o datos sensibles.\n",
    "- Usar `assert` para validar entradas externas.\n",
    "\n",
    "**Extensión opcional:** investiga `contextlib` y la creación de gestores de contexto.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1020c267",
   "metadata": {},
   "source": [
    "## Práctica · Temperaturas tolerantes a errores\n",
    "\n",
    "Implementa `convertir_temperaturas(textos)` que convierta Celsius a Fahrenheit. Debe devolver `(validas, errores)`:\n",
    "\n",
    "- `validas`: lista de temperaturas convertidas;\n",
    "- `errores`: lista de diccionarios con entrada y motivo;\n",
    "- rechaza valores inferiores al cero absoluto (`-273.15 °C`).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "d4c655ad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.435080Z",
     "iopub.status.busy": "2026-08-02T22:29:52.434983Z",
     "iopub.status.idle": "2026-08-02T22:29:52.436490Z",
     "shell.execute_reply": "2026-08-02T22:29:52.436283Z"
    },
    "tags": [
     "ejercicio"
    ]
   },
   "outputs": [],
   "source": [
    "def convertir_temperaturas(textos):\n",
    "    validas = []\n",
    "    errores = []\n",
    "    # TODO: convierte cada entrada sin abortar todo el lote.\n",
    "    return validas, errores\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "16a0db27",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:52.437687Z",
     "iopub.status.busy": "2026-08-02T22:29:52.437620Z",
     "iopub.status.idle": "2026-08-02T22:29:52.440194Z",
     "shell.execute_reply": "2026-08-02T22:29:52.439985Z"
    },
    "tags": [
     "solucion"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Válidas: [32.0, 68.9]\n",
      "Errores: [{'entrada': 'frío', 'motivo': \"could not convert string to float: 'frío'\"}, {'entrada': -300, 'motivo': 'inferior al cero absoluto'}, {'entrada': None, 'motivo': \"float() argument must be a string or a real number, not 'NoneType'\"}]\n"
     ]
    }
   ],
   "source": [
    "def convertir_temperaturas(textos):\n",
    "    validas = []\n",
    "    errores = []\n",
    "    for texto in textos:\n",
    "        try:\n",
    "            celsius = float(texto)\n",
    "            if celsius < -273.15:\n",
    "                raise ValueError(\"inferior al cero absoluto\")\n",
    "        except (TypeError, ValueError) as error:\n",
    "            errores.append({\"entrada\": texto, \"motivo\": str(error)})\n",
    "        else:\n",
    "            validas.append(celsius * 9 / 5 + 32)\n",
    "    return validas, errores\n",
    "\n",
    "validas, errores = convertir_temperaturas([\"0\", \"20.5\", \"frío\", -300, None])\n",
    "print(\"Válidas:\", validas)\n",
    "print(\"Errores:\", errores)\n",
    "assert validas == [32.0, 68.9] and len(errores) == 3\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c75084ba",
   "metadata": {},
   "source": [
    "            ## Comprobación rápida\n",
    "\n",
    "            - [ ] Capturo excepciones concretas y cerca de su origen.\n",
    "- [ ] Sé cuándo usar `else`, `finally` y `raise`.\n",
    "- [ ] Puedo crear una excepción de dominio con una causa encadenada.\n",
    "\n",
    "            [← Anterior](05_entrada_salida.ipynb) · [Volver al índice](README.md) · [Siguiente →](07_clases.ipynb)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
