Ing. Bernardo Prado · UTH 2026-2 · DSM3
Aprenderás a manejar errores de forma elegante con excepciones, a verificar automáticamente que tu código funciona con pruebas unitarias (unittest), y a guardar y compartir tu trabajo con Git y GitHub.
El banco UTH necesita un sistema de cuentas bancarias robusto. Debe manejar errores correctamente cuando:
SaldoInsuficienteError).MontoInvalidoError).CuentaCerradaError).Además, se deben escribir pruebas unitarias que verifiquen automáticamente que todas estas situaciones se manejan correctamente, y se versiona todo el proyecto con Git.
Una excepción es un error que ocurre durante la ejecución del programa. Si no la manejas, el programa se detiene bruscamente. Si la manejas, puedes mostrar un mensaje amigable y continuar.
| Excepción de Python | ¿Cuándo ocurre? | Ejemplo |
|---|---|---|
ValueError | Tipo correcto pero valor inválido | int("hola") |
TypeError | Tipo de dato incorrecto | "2" + 2 |
ZeroDivisionError | División entre cero | 10 / 0 |
FileNotFoundError | El archivo no existe | open("x.txt") |
IndexError | Índice fuera de rango | [1,2][5] |
KeyError | Clave no existe en diccionario | {"a":1}["b"] |
AttributeError | El objeto no tiene ese atributo/método | None.upper() |
try:
# Código que PUEDE fallar
resultado = 10 / 0
except ZeroDivisionError:
# Se ejecuta si ocurre ZeroDivisionError
print("❌ No puedes dividir entre cero.")
except (ValueError, TypeError) as e:
# Captura múltiples excepciones. 'e' contiene el mensaje de error.
print(f"❌ Error de valor o tipo: {e}")
except Exception as e:
# Captura CUALQUIER excepción (úsalo como último recurso)
print(f"❌ Error inesperado: {e}")
else:
# Se ejecuta SOLO si NO hubo ninguna excepción
print(f"✅ Resultado: {resultado}")
finally:
# Se ejecuta SIEMPRE, haya o no excepción
# Ideal para cerrar archivos, conexiones, etc.
print("🔚 Operación terminada.")
| Bloque | ¿Cuándo se ejecuta? | Uso típico |
|---|---|---|
try: | Siempre, primero | El código que puede fallar |
except: | Solo si hubo excepción | Manejo del error |
else: | Solo si NO hubo excepción | Código que depende del éxito |
finally: | Siempre, al final | Limpieza (cerrar BD, archivos) |
raise)Además de las excepciones de Python, podemos crear las nuestras. Una excepción personalizada es simplemente una clase que hereda de Exception. Crea el archivo excepciones.py:
# =====================================================
# excepciones.py
# Excepciones personalizadas para el sistema bancario
# =====================================================
class ErrorBancario(Exception):
"""
Clase base para todas las excepciones del sistema bancario.
Hereda de Exception (la clase base de todas las excepciones de Python).
"""
pass
class SaldoInsuficienteError(ErrorBancario):
"""Se lanza cuando se intenta retirar más dinero del disponible."""
def __init__(self, saldo_actual: float, monto_retirar: float):
self.saldo_actual = saldo_actual
self.monto_retirar = monto_retirar
self.faltante = monto_retirar - saldo_actual
mensaje = (
f"Saldo insuficiente: tienes ${saldo_actual:,.2f}, "
f"intentas retirar ${monto_retirar:,.2f}. "
f"Faltan ${self.faltante:,.2f}."
)
# Llama al constructor de Exception con el mensaje
super().__init__(mensaje)
class MontoInvalidoError(ErrorBancario):
"""Se lanza cuando el monto es 0 o negativo."""
def __init__(self, monto: float):
mensaje = f"Monto inválido: ${monto:,.2f}. Debe ser mayor a 0."
super().__init__(mensaje)
class CuentaCerradaError(ErrorBancario):
"""Se lanza cuando se opera sobre una cuenta cerrada."""
def __init__(self, numero_cuenta: str):
mensaje = f"La cuenta {numero_cuenta} está cerrada. No se pueden realizar operaciones."
super().__init__(mensaje)
| Concepto | Explicación |
|---|---|
class SaldoInsuficienteError(ErrorBancario): | Nuestra excepción hereda de ErrorBancario, que a su vez hereda de Exception. Esto crea una jerarquía de excepciones. |
super().__init__(mensaje) | Pasa el mensaje de error al constructor de Exception. Así, cuando se imprime la excepción, muestra ese mensaje. |
Atributos propios (self.faltante) | Las excepciones pueden tener atributos. Quien captura la excepción puede acceder a error.faltante para obtener datos adicionales. |
CuentaBancaria con raiseLa palabra raise lanza (dispara) una excepción. Es como decir "algo salió mal, detente y avisa". Crea el archivo cuenta_bancaria.py:
# =====================================================
# cuenta_bancaria.py
# Clase CuentaBancaria con manejo de excepciones
# =====================================================
from excepciones import (SaldoInsuficienteError,
MontoInvalidoError,
CuentaCerradaError)
class CuentaBancaria:
"""Cuenta bancaria con manejo robusto de errores."""
def __init__(self, numero: str, titular: str, saldo_inicial: float = 0.0):
if saldo_inicial < 0:
# Lanzamos MontoInvalidoError si el saldo inicial es negativo
raise MontoInvalidoError(saldo_inicial)
self.__numero = numero
self.__titular = titular
self.__saldo = saldo_inicial
self.__abierta = True
self.__movimientos = [] # historial de movimientos
# ─── Properties ───
@property
def saldo(self): return self.__saldo
@property
def numero(self): return self.__numero
@property
def titular(self): return self.__titular
@property
def esta_abierta(self): return self.__abierta
# ─── Método de validación interna ───
def _validar_cuenta_abierta(self):
"""Verifica si la cuenta está abierta. Si no, lanza excepción."""
if not self.__abierta:
raise CuentaCerradaError(self.__numero)
def _validar_monto(self, monto: float):
"""Verifica que el monto sea positivo."""
if monto <= 0:
raise MontoInvalidoError(monto)
# ─── Operaciones ───
def depositar(self, monto: float) -> float:
"""
Deposita dinero en la cuenta.
Lanza MontoInvalidoError si el monto es <= 0.
Lanza CuentaCerradaError si la cuenta está cerrada.
Retorna el nuevo saldo.
"""
self._validar_cuenta_abierta()
self._validar_monto(monto)
self.__saldo += monto
self.__movimientos.append(f"+${monto:,.2f} (Depósito)")
return self.__saldo
def retirar(self, monto: float) -> float:
"""
Retira dinero de la cuenta.
Lanza MontoInvalidoError, CuentaCerradaError o SaldoInsuficienteError.
Retorna el nuevo saldo.
"""
self._validar_cuenta_abierta()
self._validar_monto(monto)
if monto > self.__saldo:
raise SaldoInsuficienteError(self.__saldo, monto)
self.__saldo -= monto
self.__movimientos.append(f"-${monto:,.2f} (Retiro)")
return self.__saldo
def transferir(self, destino: 'CuentaBancaria', monto: float):
"""
Transfiere dinero a otra cuenta.
Usa try/except para capturar errores de retirar/depositar.
"""
try:
self.retirar(monto) # puede lanzar excepción
destino.depositar(monto) # puede lanzar excepción
self.__movimientos.append(f"→ Transferencia a {destino.numero}: ${monto:,.2f}")
print(f"✅ Transferencia de ${monto:,.2f} realizada a cuenta {destino.numero}.")
except SaldoInsuficienteError as e:
print(f"❌ Transferencia fallida: {e}")
except CuentaCerradaError as e:
print(f"❌ Cuenta cerrada: {e}")
def cerrar_cuenta(self):
"""Cierra la cuenta. Solo se puede hacer si el saldo es 0."""
if self.__saldo != 0:
raise ValueError(
f"No se puede cerrar la cuenta con saldo ${self.__saldo:,.2f}. "
"Retire todo el dinero primero."
)
self.__abierta = False
print(f"🔒 Cuenta {self.__numero} cerrada.")
def ver_estado(self):
"""Imprime el estado actual de la cuenta."""
estado = "🟢 Abierta" if self.__abierta else "🔴 Cerrada"
print(f"\n{'='*45}")
print(f" Cuenta: {self.__numero}")
print(f" Titular: {self.__titular}")
print(f" Saldo: ${self.__saldo:,.2f}")
print(f" Estado: {estado}")
print(f" Últimos movimientos:")
for mov in self.__movimientos[-5:]:
print(f" {mov}")
print(f"{'='*45}")
| Fragmento | Explicación |
|---|---|
raise SaldoInsuficienteError(self.__saldo, monto) | raise lanza la excepción. La ejecución del método se detiene aquí y el error "sube" hasta que alguien lo capture con except. |
except SaldoInsuficienteError as e: | Captura específicamente esa excepción. e es el objeto de la excepción — puedes acceder a e.faltante para obtener datos. |
self.__movimientos[-5:] | Notación de slice: obtiene los últimos 5 elementos de la lista. Muy útil para mostrar el historial reciente. |
demo_banco.py)from cuenta_bancaria import CuentaBancaria
from excepciones import SaldoInsuficienteError, MontoInvalidoError, CuentaCerradaError
if __name__ == "__main__":
print("\n🏦 UTH BANK — Demostración de Excepciones")
print("=" * 50)
# Crear cuentas
cuenta1 = CuentaBancaria("UTH-001", "Ana López", 5000.0)
cuenta2 = CuentaBancaria("UTH-002", "Carlos Mendoza", 1200.0)
# ── Depósito exitoso ──
print("\n1. Depósito exitoso:")
try:
nuevo_saldo = cuenta1.depositar(2000.0)
print(f" ✅ Depósito exitoso. Saldo: ${nuevo_saldo:,.2f}")
except MontoInvalidoError as e:
print(f" ❌ {e}")
# ── Retiro con saldo insuficiente ──
print("\n2. Retiro con saldo insuficiente:")
try:
cuenta2.retirar(5000.0) # intenta retirar más de lo disponible
except SaldoInsuficienteError as e:
print(f" ❌ {e}")
print(f" ℹ️ Faltan: ${e.faltante:,.2f}")
# ── Monto inválido (negativo) ──
print("\n3. Monto inválido:")
try:
cuenta1.depositar(-100) # monto negativo
except MontoInvalidoError as e:
print(f" ❌ {e}")
# ── Transferencia ──
print("\n4. Transferencia entre cuentas:")
cuenta1.transferir(cuenta2, 1500.0)
# ── Estado de las cuentas ──
cuenta1.ver_estado()
cuenta2.ver_estado()
unittestLas pruebas unitarias verifican automáticamente que cada unidad (método) de tu código funciona correctamente. Con unittest puedes ejecutar cientos de pruebas en segundos.
test_ es una prueba.
a y b sean iguales.
test_cuenta.pyPor convención, los archivos de prueba empiezan con test_. Esto permite a unittest descubrirlos automáticamente.
# =====================================================
# test_cuenta.py
# Pruebas unitarias para CuentaBancaria
# =====================================================
import unittest
from cuenta_bancaria import CuentaBancaria
from excepciones import (SaldoInsuficienteError,
MontoInvalidoError,
CuentaCerradaError)
class TestCuentaBancaria(unittest.TestCase):
"""
Clase de pruebas para CuentaBancaria.
Hereda de unittest.TestCase para acceder a los métodos de aserción.
"""
def setUp(self):
"""
Se ejecuta automáticamente ANTES de cada prueba.
Aquí preparamos el estado inicial para cada test.
"""
self.cuenta_ana = CuentaBancaria("UTH-001", "Ana López", 5000.0)
self.cuenta_carlos = CuentaBancaria("UTH-002", "Carlos Mendoza", 1200.0)
# ────── PRUEBAS DE DEPÓSITO ──────
def test_deposito_exitoso(self):
"""Verifica que un depósito válido aumenta el saldo correctamente."""
nuevo_saldo = self.cuenta_ana.depositar(1000.0)
self.assertEqual(nuevo_saldo, 6000.0) # saldo esperado = 5000 + 1000
self.assertEqual(self.cuenta_ana.saldo, 6000.0)
def test_deposito_monto_cero_lanza_error(self):
"""Depositar 0 debe lanzar MontoInvalidoError."""
with self.assertRaises(MontoInvalidoError):
self.cuenta_ana.depositar(0)
def test_deposito_monto_negativo_lanza_error(self):
"""Depositar un monto negativo debe lanzar MontoInvalidoError."""
with self.assertRaises(MontoInvalidoError):
self.cuenta_ana.depositar(-500)
# ────── PRUEBAS DE RETIRO ──────
def test_retiro_exitoso(self):
"""Verifica que un retiro válido reduce el saldo correctamente."""
nuevo_saldo = self.cuenta_ana.retirar(2000.0)
self.assertEqual(nuevo_saldo, 3000.0) # 5000 - 2000 = 3000
def test_retiro_saldo_insuficiente(self):
"""Intentar retirar más del saldo debe lanzar SaldoInsuficienteError."""
with self.assertRaises(SaldoInsuficienteError):
self.cuenta_carlos.retirar(5000.0) # Carlos tiene 1200, intenta 5000
def test_retiro_exacto_del_saldo(self):
"""Retirar exactamente el saldo disponible debe funcionar."""
nuevo_saldo = self.cuenta_carlos.retirar(1200.0)
self.assertEqual(nuevo_saldo, 0.0)
def test_saldo_insuficiente_datos_correctos(self):
"""Verifica que SaldoInsuficienteError contiene los datos correctos."""
try:
self.cuenta_carlos.retirar(2000.0)
self.fail("Debería haber lanzado SaldoInsuficienteError")
except SaldoInsuficienteError as e:
self.assertEqual(e.saldo_actual, 1200.0)
self.assertEqual(e.monto_retirar, 2000.0)
self.assertEqual(e.faltante, 800.0)
# ────── PRUEBAS DE CUENTA CERRADA ──────
def test_operaciones_en_cuenta_cerrada(self):
"""Las operaciones en cuenta cerrada deben lanzar CuentaCerradaError."""
# Primero retiramos todo para poder cerrar
self.cuenta_carlos.retirar(1200.0)
self.cuenta_carlos.cerrar_cuenta()
with self.assertRaises(CuentaCerradaError):
self.cuenta_carlos.depositar(100)
with self.assertRaises(CuentaCerradaError):
self.cuenta_carlos.retirar(50)
def test_cerrar_cuenta_con_saldo_falla(self):
"""Cerrar una cuenta con saldo debe lanzar ValueError."""
with assertRaises(ValueError):
self.cuenta_ana.cerrar_cuenta() # Ana tiene 5000, no puede cerrar
# ────── PRUEBAS DE TRANSFERENCIA ──────
def test_transferencia_exitosa(self):
"""Verifica que una transferencia válida mueve el dinero correctamente."""
self.cuenta_ana.transferir(self.cuenta_carlos, 500.0)
self.assertEqual(self.cuenta_ana.saldo, 4500.0) # 5000 - 500
self.assertEqual(self.cuenta_carlos.saldo, 1700.0) # 1200 + 500
# ────── OTRAS ASERCIONES ÚTILES ──────
def test_cuenta_creada_con_datos_correctos(self):
"""Verifica los datos iniciales de la cuenta."""
self.assertEqual(self.cuenta_ana.numero, "UTH-001")
self.assertEqual(self.cuenta_ana.titular, "Ana López")
self.assertEqual(self.cuenta_ana.saldo, 5000.0)
self.assertTrue(self.cuenta_ana.esta_abierta) # debe ser True
def test_creacion_con_saldo_negativo_falla(self):
"""Crear cuenta con saldo negativo debe lanzar MontoInvalidoError."""
with self.assertRaises(MontoInvalidoError):
CuentaBancaria("UTH-999", "Error", -1000.0)
# Punto de entrada: ejecuta todas las pruebas
if __name__ == "__main__":
unittest.main(verbosity=2) # verbosity=2 muestra nombre de cada prueba
| Método de aserción | ¿Qué verifica? |
|---|---|
assertEqual(a, b) | Que a == b. Si no son iguales, la prueba falla. |
assertNotEqual(a, b) | Que a != b. |
assertTrue(x) | Que x sea verdadero (True). |
assertFalse(x) | Que x sea falso (False). |
assertRaises(Error) | Que el bloque with lance esa excepción. |
assertIsNone(x) | Que x is None. |
assertIsNotNone(x) | Que x is not None. |
fail(mensaje) | Falla la prueba manualmente con un mensaje. |
python -m unittest test_cuenta.py -v
Git registra el historial de cambios de tu código. GitHub es un servidor donde puedes guardar ese historial en internet y compartirlo.
git --version
Tu código pasa por 3 zonas: Working Directory (archivos editados) → Staging Area (con git add) → Repository (con git commit). GitHub es una copia remota del Repository.
git init
Inicializa un repositorio Git en la carpeta actual. Crea una carpeta oculta .git/ que registra todos los cambios. Solo se hace UNA VEZ por proyecto.
git config --global user.name "Tu Nombre"
Configura tu nombre de usuario para los commits. Esto se hace una vez en tu computadora.
git config --global user.email "tu@email.com"
Configura tu correo. Debe coincidir con el correo de tu cuenta GitHub.
git status
Muestra el estado actual: qué archivos se modificaron, cuáles están listos para commit, cuáles son nuevos. Úsalo constantemente para saber en qué estado estás.
git add .
Agrega TODOS los archivos modificados al "área de staging" (zona de preparación). El punto . significa "todos". También puedes agregar un archivo específico: git add cuenta_bancaria.py
git commit -m "feat: crear clase CuentaBancaria con excepciones"
Guarda los cambios del staging como un "punto de control" en el historial. El mensaje entre comillas describe QUÉ se hizo. Sé descriptivo — futuros tú lo agradecerás.
git log --oneline
Muestra el historial de commits en una línea por commit. Cada commit tiene un código único (hash) y tu mensaje.
.gitignore — archivos que NO quieres guardarAlgunos archivos no deben ir al repositorio (archivos temporales, contraseñas, configuraciones locales). El archivo .gitignore le dice a Git qué ignorar. Crea este archivo en la carpeta raíz:
# Archivos de Python compilados (caché)
__pycache__/
*.pyc
*.pyo
# Entorno virtual de Python
venv/
.venv/
env/
# Archivos de VS Code
.vscode/settings.json
# Archivos con contraseñas o configuración local
configuracion_local.py
*.env
poo-uth-2026. NO marques "Initialize with README".# 1. Agrega GitHub como servidor remoto
git remote add origin https://github.com/TU_USUARIO/poo-uth-2026.git
# 2. Renombra la rama principal a 'main'
git branch -M main
# 3. Sube el código a GitHub
git push -u origin main
Después de hacer push, visita github.com/TU_USUARIO/poo-uth-2026. Deberías ver tus archivos listados ahí. ¡Tu código ahora está en la nube!
Cada vez que trabajas en el proyecto, el flujo es:
# Revisar el estado de tus archivos
git status
# Agregar todos los cambios al staging
git add .
# Crear un commit con descripción clara
git commit -m "feat: agregar pruebas para transferencia"
# Subir a GitHub
git push
# --- Otros comandos útiles ---
# Ver historial de commits
git log --oneline
# Deshacer cambios no guardados en un archivo
git restore nombre_archivo.py
# Ver diferencias entre versión actual y último commit
git diff
| Comando | ¿Qué hace? |
|---|---|
git init | Inicializa repositorio (una sola vez) |
git status | Muestra estado de archivos |
git add . | Prepara todos los cambios |
git commit -m "msg" | Guarda punto de control |
git push | Sube a GitHub |
git pull | Descarga cambios de GitHub |
git log --oneline | Historial de commits |
git diff | Ver qué cambió |
git restore archivo.py | Deshacer cambios no guardados |
Ejecuta todo en orden para completar la práctica al 100%:
# 1. Crear carpeta y abrir en VS Code
mkdir Unidad4
cd Unidad4
# 2. Inicializar repositorio Git
git init
git config --global user.name "Tu Nombre"
git config --global user.email "tu@email.com"
# 3. Crea los archivos: excepciones.py, cuenta_bancaria.py,
# demo_banco.py, test_cuenta.py, .gitignore
# (escribir el código de cada archivo)
# 4. Probar la demo
python demo_banco.py
# 5. Ejecutar las pruebas unitarias
python -m unittest test_cuenta.py -v
# 6. Si todas pasan, hacer commit
git add .
git commit -m "feat: sistema bancario con excepciones y pruebas unitarias"
# 7. Subir a GitHub
git remote add origin https://github.com/TU_USUARIO/poo-uth-2026.git
git branch -M main
git push -u origin main
excepciones.py con 3 excepciones personalizadas que heredan de ExceptionCuentaBancaria usa raise para lanzar excepciones en situaciones inválidastransferir() usa try/except para capturar errorestest_cuenta.py con al menos 10 pruebassetUp() para preparar objetos antes de cada testpython -m unittest test_cuenta.py -v muestra "OK" — todas pasangit init ejecutado.gitignore creadogit push)| Unidad | Tema Principal | Conceptos Clave |
|---|---|---|
| U1 📘 | Introducción a la POO | Clase, Objeto, Atributo, Método, Constructor __init__, UML |
| U2 💎 | Los 4 Pilares | Encapsulamiento (__atributo), Herencia (super()), Abstracción (ABC), Polimorfismo |
| U3 ⚙️ | SOLID, Flet y MySQL | Principios SOLID, CRUD (Create/Read/Update/Delete), SQL, Interfaz Gráfica con Flet |
| U4 🛡️ | Excepciones, Tests y Git | try/except/raise, Excepciones personalizadas, unittest, Git/GitHub |