Programación Orientada a Objetos

Ing. Bernardo Prado · UTH 2026-2 · DSM3

🛡️ Unidad 4 🐍 Python 3 🧪 unittest 🌿 Git

🛡️ Práctica 4 — Excepciones, Pruebas Unitarias y Git/GitHub

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.

⏱ 120 minutos aprox. 🎯 Nivel: Avanzado 📁 Proyecto: sistema_robusto/ 🛠 unittest + Git

📖 Sección 0 — ¿Por qué es importante esta unidad?

💡 El código real falla — eso es normal Todo programa real encontrará errores: el usuario pone datos incorrectos, el servidor no responde, el archivo no existe. Un buen programador no elimina los errores — aprende a manejarlos con gracia. Las excepciones son tu herramienta para eso.
🧠 ¿Por qué probar código? Imagina que cambias una línea de código y sin querer "rompes" algo que antes funcionaba. Las pruebas unitarias detectan ese problema automáticamente en segundos. Las empresas de software exigen pruebas para todo código que va a producción.
🌿 ¿Por qué usar Git? Git guarda el historial de todos los cambios en tu código. Si algo se rompe, puedes volver atrás. También permite trabajar en equipo sin que los cambios de uno pisen los del otro. GitHub lo lleva a internet para compartirlo.

🏢 Sección 1 — El Escenario

🏦 Sistema de Cuentas Bancarias — UTH Bank

El banco UTH necesita un sistema de cuentas bancarias robusto. Debe manejar errores correctamente cuando:

  • Se intente retirar más dinero del disponible (SaldoInsuficienteError).
  • Se proporcione un monto negativo (MontoInvalidoError).
  • Se intente operar una cuenta cerrada (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.

⚡ Parte 1 — Manejo de Excepciones en Python

¿Qué es una excepción?

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
ValueErrorTipo correcto pero valor inválidoint("hola")
TypeErrorTipo de dato incorrecto"2" + 2
ZeroDivisionErrorDivisión entre cero10 / 0
FileNotFoundErrorEl archivo no existeopen("x.txt")
IndexErrorÍndice fuera de rango[1,2][5]
KeyErrorClave no existe en diccionario{"a":1}["b"]
AttributeErrorEl objeto no tiene ese atributo/métodoNone.upper()

Estructura básica: try / except / else / finally

Ejemplo — Estructura completa de manejo de excepciones
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, primeroEl código que puede fallar
except:Solo si hubo excepciónManejo del error
else:Solo si NO hubo excepciónCódigo que depende del éxito
finally:Siempre, al finalLimpieza (cerrar BD, archivos)
1
Crear excepciones personalizadas (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
# =====================================================
# 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)
ConceptoExplicació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.
2
Clase CuentaBancaria con raise

La 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 con manejo de errores
# =====================================================
# 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}")
FragmentoExplicació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.
3
Programa de demostración (demo_banco.py)
demo_banco.py — Demostración de excepciones
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()
🖥️ Salida esperada: 🏦 UTH BANK — Demostración de Excepciones ================================================== 1. Depósito exitoso: ✅ Depósito exitoso. Saldo: $7,000.00 2. Retiro con saldo insuficiente: ❌ Saldo insuficiente: tienes $1,200.00, intentas retirar $5,000.00. Faltan $3,800.00. ℹ️ Faltan: $3,800.00 3. Monto inválido: ❌ Monto inválido: $-100.00. Debe ser mayor a 0. 4. Transferencia entre cuentas: ✅ Transferencia de $1,500.00 realizada a cuenta UTH-002.

🧪 Parte 2 — Pruebas Unitarias con unittest

Las 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.

💡 Conceptos clave de unittest
TestCase: Clase base de la que hereda tu clase de pruebas.
test_xxx: Cada método que empieza con test_ es una prueba.
assertEqual(a, b): Verifica que a y b sean iguales.
assertRaises(Error): Verifica que se lance esa excepción.
setUp(): Se ejecuta antes de cada prueba (para preparar datos).
4
Crear el archivo de pruebas test_cuenta.py

Por convención, los archivos de prueba empiezan con test_. Esto permite a unittest descubrirlos automáticamente.

test_cuenta.py — Pruebas unitarias completas
# =====================================================
# 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.
5
Ejecutar las pruebas
Terminal
python -m unittest test_cuenta.py -v
✅ Salida esperada cuando TODO pasa: test_cerrar_cuenta_con_saldo_falla ... ok test_conta_creada_con_datos_correctos ... ok test_creacion_con_saldo_negativo_falla ... ok test_deposito_exitoso ... ok test_deposito_monto_cero_lanza_error ... ok test_deposito_monto_negativo_lanza_error ... ok test_operaciones_en_cuenta_cerrada ... ok test_retiro_exacto_del_saldo ... ok test_retiro_exitoso ... ok test_retiro_saldo_insuficiente ... ok test_saldo_insuficiente_datos_correctos ... ok test_transferencia_exitosa ... ok ---------------------------------------------------------------------- Ran 12 tests in 0.003s OK
❌ Ejemplo de prueba fallida (si hubiera un bug): FAIL: test_deposito_exitoso ---------------------------------------------------------------------- AssertionError: 5500.0 != 6000.0 Expected 6000.0 but got 5500.0.
✅ "OK" al final significa que todas las pruebas pasaron — ¡tu código es correcto!

🌿 Parte 3 — Control de Versiones con Git y GitHub

Git registra el historial de cambios de tu código. GitHub es un servidor donde puedes guardar ese historial en internet y compartirlo.

⚠️ Prerequisito: Instalar Git Ve a git-scm.com y descarga Git para Windows. Instala con los valores predeterminados. Para verificar: abre una terminal y escribe git --version

Flujo básico de Git

🖼️ Diagrama: Flujo de Git

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.

Comandos Git paso a paso

1
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.

2
git config --global user.name "Tu Nombre"

Configura tu nombre de usuario para los commits. Esto se hace una vez en tu computadora.

3
git config --global user.email "tu@email.com"

Configura tu correo. Debe coincidir con el correo de tu cuenta GitHub.

4
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.

5
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

6
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.

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

6
Crear .gitignore — archivos que NO quieres guardar

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

.gitignore
# 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
7
Subir a GitHub
  1. Crea una cuenta en github.com si no tienes.
  2. Crea un nuevo repositorio vacío en GitHub (botón "New repository"). Nómbralo poo-uth-2026. NO marques "Initialize with README".
  3. En tu terminal, ejecuta los comandos que GitHub te muestra. Son similares a estos:
Terminal — Conectar con GitHub
# 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
🖼️ Captura: Repositorio en GitHub

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!

8
Flujo de trabajo diario con Git

Cada vez que trabajas en el proyecto, el flujo es:

Flujo diario de Git
# 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 initInicializa repositorio (una sola vez)
git statusMuestra estado de archivos
git add .Prepara todos los cambios
git commit -m "msg"Guarda punto de control
git pushSube a GitHub
git pullDescarga cambios de GitHub
git log --onelineHistorial de commits
git diffVer qué cambió
git restore archivo.pyDeshacer cambios no guardados

🎯 Sección Final — Práctica Integradora Completa

Ejecuta todo en orden para completar la práctica al 100%:

9
Secuencia completa de comandos
Terminal — Secuencia completa
# 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

✅ Lista de Verificación Final — Unidad 4

🏆 Resumen Final — Las 4 Unidades del Curso POO

UnidadTema PrincipalConceptos 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
🎓 ¡Felicidades por completar las 4 prácticas del curso de POO! Has aprendido a diseñar software orientado a objetos como un profesional: con clases bien diseñadas, encapsulamiento, herencia, abstracción, polimorfismo, manejo de errores robusto, pruebas automatizadas y control de versiones. Estas habilidades son la base del desarrollo de software profesional.
🏠 Volver al Inicio del Curso