Módulo 3: Detectar Hallucinations en Código

Herramientas de Detección de Hallucinations

Herramientas de Detección de Hallucinations

Descripción de la cápsula

Tu ojo humano es la primera línea de defensa contra hallucinations — pero no es infalible. En las cápsulas 03 y 04 entrenaste tu ojo para detectar imports falsos, APIs inventadas, parámetros incorrectos, y lógica fabricada. Ahora vas a construir tu red de seguridad automatizada: herramientas que detectan lo que tu ojo no ve.

Esta cápsula te da un toolkit concreto. No teoría sobre qué herramientas existen — sino comandos exactos, configuraciones, y procesos que puedes usar mañana. Al terminar, tendrás un proceso de verificación en 4 capas que atrapa hallucinations en cada nivel.


Las 4 Capas de Verificación

El modelo de defensa en profundidad

Ninguna herramienta detecta todos los tipos de hallucinations. Por eso usas capas: si una falla, la siguiente atrapa el error.

Capa 1: Linters estáticos (ruff, flake8, pylint)
├── Detecta: imports no existentes, variables no definidas, 
│            syntax errors, imports no usados
├── Tipo que atrapa: Tipo 1 (Imports falsos) — parcial
├── Tiempo: < 1 segundo
└── Esfuerzo: 0 (se ejecuta automáticamente)

Capa 2: Type checkers (mypy, pyright)
├── Detecta: tipos incorrectos, atributos no existentes,
│            signatures incompatibles
├── Tipo que atrapa: Tipo 2 (APIs inventadas) — parcial
│                    Tipo 3 (Parámetros incorrectos) — parcial
├── Tiempo: 2-5 segundos
└── Esfuerzo: configuración inicial

Capa 3: Quick tests (pytest, python -c)
├── Detecta: lógica incorrecta, edge cases, 
│            comportamiento inesperado
├── Tipo que atrapa: Tipo 4 (Lógica fabricada) — parcial
│                    Tipo 2 y 3 — en runtime
├── Tiempo: 1-5 minutos (escribir + ejecutar)
└── Esfuerzo: medio (escribir tests)

Capa 4: Documentación oficial
├── Detecta: todo lo que las otras capas no atrapan
├── Tipo que atrapa: Todos los tipos
├── Tiempo: 2-10 minutos
└── Esfuerzo: alto (leer y comparar)

La idea: las capas 1 y 2 son automáticas y rápidas. La capa 3 requiere esfuerzo pero es la más efectiva contra lógica fabricada. La capa 4 es el último recurso para verificaciones que ninguna herramienta puede hacer.


Capa 1: Linters Estáticos

ruff — el linter rápido de Python

ruff es el linter más rápido para Python. Detecta cientos de tipos de errores en milisegundos.

Instalación:

pip install ruff

Uso básico:

# Analizar un archivo
ruff check app.py

# Analizar un directorio completo
ruff check src/

# Mostrar errores con contexto
ruff check app.py --show-source

# Corregir errores automáticamente donde sea posible
ruff check app.py --fix

Lo que detecta relevante para hallucinations:

F821: Undefined name → Variable o función no definida
F401: Imported but unused → Import que se importa pero no se usa
E902: Syntax error → Error de sintaxis
F811: Redefined unused name → Redefinición de variable

Ejemplo:

# archivo: app.py
from fastapi.security import OAuth2TokenValidator  # ← ¿Hallucination?
from pydantic import BaseModel

class User(BaseModel):
    name: str
$ ruff check app.py
app.py:1:1: F401 `fastapi.security.OAuth2TokenValidator` imported but unused

Espera — ruff lo marca como "imported but unused", no como "import no existente." Esto es porque ruff analiza texto, no ejecuta imports. Para verificar que el import existe, necesitas la capa 2 o ejecutar Python directamente.

Limitación importante: ruff detecta imports no usados pero no verifica que el import exista. Un import falso que se usa en el código no será marcado por ruff. Necesitas type checkers (capa 2) para eso.

flake8 con plugins

Si prefieres flake8 o tu proyecto ya lo usa:

pip install flake8 flake8-import-order flake8-bugbear
# Analizar un archivo
flake8 app.py

# Con más detalle
flake8 app.py --show-source --statistics

Configuración recomendada de ruff

Crea un archivo ruff.toml en la raíz de tu proyecto:

# ruff.toml
line-length = 88
target-version = "py311"

[lint]
select = [
    "E",    # pycodestyle errors
    "F",    # pyflakes (imports, undefined names)
    "I",    # isort (import ordering)
    "N",    # pep8 naming
    "UP",   # pyupgrade (deprecated syntax)
    "B",    # bugbear (common bugs)
    "S",    # bandit (security issues)
    "T20",  # print statements
    "SIM",  # simplify
]

[lint.per-file-ignores]
"tests/*" = ["S101"]  # Allow assert in tests

Lo que ruff atrapa y lo que no

Hallucination¿ruff lo detecta?Notas
Import no existente (no usado)✅ F401Marca como unused
Import no existente (usado)❌Necesitas type checker o python -c
Parámetro incorrecto❌No analiza runtime
Lógica incorrecta❌No entiende semántica
Security issues básicos✅ SCon bandit rules habilitado
Variable no definida✅ F821
Código deprecated✅ UP

Capa 2: Type Checkers

mypy — verificación de tipos estática

mypy va más allá de ruff: verifica que los tipos de datos sean consistentes, que los atributos y métodos que llamas existan en los tipos declarados, y que las signatures de funciones sean correctas.

Instalación:

pip install mypy

Uso básico:

# Verificar un archivo
mypy app.py

# Verificar con más estrictez
mypy app.py --strict

# Ignorar imports sin stubs
mypy app.py --ignore-missing-imports

Lo que detecta relevante para hallucinations:

# archivo: app.py
from fastapi.security import OAuth2TokenValidator  # ← Hallucination

from pydantic import BaseModel, Field

class User(BaseModel):
    email: str = Field(..., unique=True)  # ← unique no es parámetro de Field
$ mypy app.py
app.py:1: error: Module "fastapi.security" has no attribute "OAuth2TokenValidator"
app.py:5: error: Unexpected keyword argument "unique" for "Field"

mypy detecta ambas hallucinations: el import falso Y el parámetro incorrecto. Esto lo hace significativamente más útil que ruff para detectar hallucinations.

pyright — alternativa más rápida

Si usas VS Code, pyright (el engine detrás de Pylance) es otra opción excelente:

pip install pyright
# Verificar un archivo
pyright app.py

# Verificar con output detallado
pyright app.py --outputjson

Configuración de mypy para detección de hallucinations

Crea mypy.ini en la raíz del proyecto:

[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
check_untyped_defs = True
warn_unused_ignores = True
show_error_codes = True

# Para librerías sin type stubs
[mypy-uvicorn.*]
ignore_missing_imports = True

Lo que mypy atrapa y lo que no

Hallucination¿mypy lo detecta?Notas
Import no existente✅"has no attribute X"
Parámetro incorrecto (con types)✅"Unexpected keyword argument"
Parámetro incorrecto (con **kwargs)❌**kwargs acepta cualquier nombre
Método no existente✅"has no attribute X"
Valor de parámetro incorrecto❌No verifica valores, solo tipos
Lógica incorrecta❌No entiende semántica
Tipo de retorno incorrecto✅Si los tipos están anotados

Ejemplo completo de mypy atrapando hallucinations

# archivo: auth_service.py
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2TokenValidator  # ← H1
from pydantic import BaseModel, Field
import jwt

app = FastAPI()

class UserCreate(BaseModel):
    email: str = Field(..., unique=True)  # ← H2
    name: str

def create_token(data: dict) -> str:
    return jwt.encode(data, "secret", algorithm="HS256")

def verify_token(token: str) -> dict:
    return jwt.decode(token, "secret", algorithms=["HS256"], verify=True)  # ← H3
$ mypy auth_service.py
auth_service.py:2: error: Module "fastapi.security" has no attribute 
    "OAuth2TokenValidator"  [attr-defined]
auth_service.py:6: error: Unexpected keyword argument "unique" for 
    "Field"  [call-arg]
auth_service.py:14: error: Unexpected keyword argument "verify" for 
    "decode"  [call-arg]
Found 3 errors in 1 file (checked 1 source file)

mypy detectó las 3 hallucinations. Esto es por lo que la capa 2 es tan valiosa: una sola ejecución puede encontrar múltiples hallucinations que tu ojo podría pasar por alto.


Capa 3: Quick Tests

Por qué los tests son esenciales contra hallucinations de lógica

Las capas 1 y 2 no detectan hallucinations de lógica — código que compila, tiene tipos correctos, pero produce resultados incorrectos. Para eso necesitas ejecutar el código y verificar el output.

El proceso de 3 minutos

Para cualquier función sospechosa, escribe un quick test en 3 minutos o menos:

# Método 1: python -c (para verificaciones rápidas de una línea)
python -c "
from app import validate_email
tests = [
    ('user@domain.com', True),
    ('@domain.com', False),
    ('user@.com', False),
    ('', False),
]
for email, expected in tests:
    result = validate_email(email)
    status = '✅' if result == expected else '❌'
    print(f'{status} validate_email(\"{email}\") = {result}, expected {expected}')
"
# Método 2: pytest con un archivo temporal
cat > test_quick.py << 'EOF'
from app import calculate_median
import statistics

def test_median_odd():
    assert calculate_median([1, 2, 3]) == statistics.median([1, 2, 3])

def test_median_even():
    assert calculate_median([1, 2, 3, 4]) == statistics.median([1, 2, 3, 4])

def test_median_single():
    assert calculate_median([5]) == 5.0

def test_median_empty():
    import pytest
    with pytest.raises(ValueError):
        calculate_median([])
EOF

pytest test_quick.py -v

Plantillas de quick tests por tipo de función

Para funciones de validación:

def test_validation_function(validate_func):
    """Template for testing validation functions."""
    valid_inputs = [
        "normal_valid_input",
    ]
    invalid_inputs = [
        "",              # vacío
        None,            # null
        " ",             # solo espacios
        "a" * 10000,     # muy largo
    ]
    
    for inp in valid_inputs:
        assert validate_func(inp) == True, f"Should accept: {inp}"
    
    for inp in invalid_inputs:
        assert validate_func(inp) == False, f"Should reject: {inp}"

Para funciones de cálculo:

import math

def test_calculation_function(calc_func, reference_func):
    """Template for testing calculation functions against a reference."""
    test_cases = [
        [1, 2, 3, 4, 5],       # normal
        [1],                     # un elemento
        [0, 0, 0],              # todos cero
        [-1, -2, -3],           # negativos
        [1.5, 2.7, 3.14],      # decimales
        list(range(1000)),      # grande
    ]
    
    for data in test_cases:
        result = calc_func(data)
        expected = reference_func(data)
        assert math.isclose(result, expected, rel_tol=1e-9), \
            f"For {data[:5]}...: got {result}, expected {expected}"

Para funciones de transformación:

def test_transformation_function(transform_func):
    """Template for testing data transformation functions."""
    assert transform_func("hello") is not None  # no retorna None
    
    original = "test_input"
    result = transform_func(original)
    assert isinstance(result, str)  # tipo correcto
    
    assert transform_func("") == ""  # vacío produce vacío

Verificar imports con python -c

Para la capa más básica de verificación, ejecuta los imports directamente:

# Verificar un import específico
python -c "from fastapi.security import OAuth2TokenValidator" 2>&1
# ImportError: cannot import name 'OAuth2TokenValidator'...

# Verificar múltiples imports de un archivo
python -c "
imports_to_check = [
    ('fastapi.security', 'OAuth2PasswordBearer'),
    ('fastapi.security', 'OAuth2TokenValidator'),
    ('pydantic', 'BaseModel'),
    ('pydantic', 'EmailStr'),
]

for module, name in imports_to_check:
    try:
        exec(f'from {module} import {name}')
        print(f'  ✅ from {module} import {name}')
    except ImportError as e:
        print(f'  ❌ from {module} import {name} — {e}')
"

Verificar signatures con inspect

# Ver la signature de una función
python -c "
import inspect
import jwt

sig = inspect.signature(jwt.decode)
print(f'jwt.decode{sig}')
print()
for name, param in sig.parameters.items():
    print(f'  {name}: {param.kind.name} = {param.default}')
"

Output:

jwt.decode(jwt, key='', algorithms=None, options=None, ...)

  jwt: POSITIONAL_OR_KEYWORD = <class 'inspect._empty'>
  key: POSITIONAL_OR_KEYWORD = 
  algorithms: POSITIONAL_OR_KEYWORD = None
  options: POSITIONAL_OR_KEYWORD = None
  ...

Si el código usa jwt.decode(..., verify=True) y verify no aparece en la signature, es una hallucination confirmada.


Capa 4: Documentación Oficial

Cuándo usar la documentación

Usa la documentación cuando:

  • Las capas 1-3 no dan una respuesta clara
  • El parámetro podría ser aceptado via **kwargs (no aparece en la signature pero podría funcionar)
  • Necesitas verificar valores válidos para un parámetro (no solo que el parámetro exista)
  • El comportamiento depende de la versión de la librería

Cómo verificar eficientemente

Para verificar un parámetro:

1. Abre la documentación oficial de la librería
2. Busca la función/clase específica
3. Verifica:
   a. ¿El parámetro existe?
   b. ¿El tipo es correcto?
   c. ¿El valor es válido?
   d. ¿Está deprecated?

Ejemplo: verificar pd.DataFrame.to_json(orient="dict")

  1. Abre: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html
  2. Busca el parámetro orient
  3. Valores válidos: "split", "records", "index", "columns", "values", "table"
  4. "dict" no está en la lista → hallucination confirmada

Ejemplo: verificar requests.get(url, verify_ssl=True)

  1. Abre: https://requests.readthedocs.io/en/latest/api/#requests.get
  2. Busca parámetros aceptados
  3. El parámetro de verificación SSL es verify, no verify_ssl
  4. Hallucination confirmada

Documentación offline con help()

Si no tienes acceso a internet o prefieres verificar rápido:

# Ver documentación completa
python -c "import requests; help(requests.get)"

# Ver solo la primera parte (signature y descripción)
python -c "
import requests
doc = requests.get.__doc__
print(doc[:500] if doc else 'No documentation')
"

# Ver los parámetros de un método
python -c "
import pandas as pd
help(pd.DataFrame.to_json)
" | head -30

Links de documentación esenciales

Para el stack FastAPI que usarás en el proyecto integrador:

LibreríaDocumentaciónQué verificar
FastAPIhttps://fastapi.tiangolo.com/reference/Clases de security, parámetros de decoradores
Pydantichttps://docs.pydantic.dev/latest/Field(), validators, model_config
SQLAlchemyhttps://docs.sqlalchemy.org/Query API, Column types, relationships
PyJWT / python-josehttps://pyjwt.readthedocs.io/encode/decode signatures
requestshttps://requests.readthedocs.io/Parámetros de get/post/put/delete
pandashttps://pandas.pydata.org/docs/Métodos de DataFrame, parámetros

El Proceso Completo: De Código a Confianza

Workflow de verificación en 4 pasos

Cuando Claude Code genera código, aplica este proceso:

PASO 1: ruff check (< 1 segundo)
├── Ejecuta: ruff check archivo.py
├── Busca: F401 (unused imports), F821 (undefined), E902 (syntax)
├── Si encuentra errores → Corrige antes de continuar
└── Si pasa → Continúa a paso 2

PASO 2: mypy (2-5 segundos)
├── Ejecuta: mypy archivo.py --ignore-missing-imports
├── Busca: attr-defined, call-arg, type errors
├── Si encuentra errores → Investiga cada uno
│   ├── attr-defined → Import falso o método no existente
│   ├── call-arg → Parámetro incorrecto
│   └── type error → Tipo incorrecto
└── Si pasa → Continúa a paso 3

PASO 3: Quick tests (1-5 minutos)
├── Identifica funciones de alto riesgo:
│   ├── Validación
│   ├── Cálculos
│   ├── Seguridad
│   └── Transformación de datos
├── Escribe 3-5 tests rápidos para cada una
├── Ejecuta: pytest test_quick.py -v
├── Si algún test falla → Hallucination de lógica encontrada
└── Si todos pasan → Confianza razonable

PASO 4: Documentación (2-10 minutos, solo si necesario)
├── Para parámetros sospechosos que pasaron mypy (**kwargs)
├── Para valores de parámetros (mypy no verifica valores)
├── Para comportamiento version-specific
└── Verifica contra documentación oficial

Ejemplo completo: verificando código generado por AI

Claude Code genera este código:

from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel, Field
import jwt
import requests

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

class Item(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    price: float = Field(..., gt=0)
    category: str = Field(..., unique=True)

def verify_token(token: str) -> dict:
    return jwt.decode(token, "secret", algorithms=["HS256"], verify=True)

def fetch_external_data(url: str) -> dict:
    response = requests.get(url, verify_ssl=True, timeout=30)
    return response.json()

PASO 1: ruff check

$ ruff check app.py
# (suponiendo que todos los imports se usan) → Sin errores

PASO 2: mypy

$ mypy app.py
app.py:13: error: Unexpected keyword argument "unique" for "Field"  [call-arg]
app.py:16: error: Unexpected keyword argument "verify" for "decode"  [call-arg]

Resultados:

  • ❌ Field(..., unique=True) — unique no es parámetro de Pydantic Field
  • ❌ jwt.decode(..., verify=True) — verify no es parámetro de jwt.decode

PASO 3: Quick test (para verificar verify_ssl)

mypy no detectó verify_ssl porque requests usa **kwargs. Verificación manual:

python -c "
import inspect
import requests
sig = inspect.signature(requests.get)
print(sig)
"
# (url, **kwargs) — no podemos ver los parámetros reales
python -c "
import requests
help(requests.get)
" | grep -i "verify"
# :param verify: ... Either a boolean, in which case...
# El parámetro es "verify", no "verify_ssl"

Resultado:

  • ❌ requests.get(url, verify_ssl=True) — el parámetro es verify, no verify_ssl

PASO 4: Documentación (confirmación)

Resumen: 3 hallucinations detectadas en un archivo de 20 líneas. Tiempo total: ~5 minutos.


Automatización: Integrar en tu Workflow

Pre-commit hooks

Configura verificación automática antes de cada commit:

pip install pre-commit

Crea .pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.9.0
    hooks:
      - id: mypy
        additional_dependencies:
          - fastapi
          - pydantic
          - types-requests
pre-commit install

Ahora, cada vez que hagas git commit, ruff y mypy se ejecutan automáticamente. Si detectan hallucinations, el commit falla y puedes corregir antes de subir código.

Script de verificación rápida

Crea un script que ejecute las 3 primeras capas con un solo comando:

#!/bin/bash
# verify.sh — Verificación rápida de código AI-generated

FILE=${1:-"app.py"}

echo "=== Capa 1: ruff ==="
ruff check "$FILE" --show-source
echo ""

echo "=== Capa 2: mypy ==="  
mypy "$FILE" --ignore-missing-imports --show-error-codes
echo ""

echo "=== Capa 3: Import verification ==="
python -c "
import ast
import sys

with open('$FILE') as f:
    tree = ast.parse(f.read())

for node in ast.walk(tree):
    if isinstance(node, ast.ImportFrom):
        module = node.module or ''
        for alias in node.names:
            name = alias.name
            try:
                exec(f'from {module} import {name}')
                print(f'  ✅ from {module} import {name}')
            except ImportError as e:
                print(f'  ❌ from {module} import {name} — {e}')
    elif isinstance(node, ast.Import):
        for alias in node.names:
            try:
                __import__(alias.name)
                print(f'  ✅ import {alias.name}')
            except ImportError as e:
                print(f'  ❌ import {alias.name} — {e}')
"

echo ""
echo "=== Verificación completa ==="
chmod +x verify.sh
./verify.sh app.py

VS Code / Cursor integración

Si usas Cursor (como en esta guía), configura estas extensiones:

  1. Pylance (pyright built-in) — type checking en tiempo real
  2. Ruff — linting en tiempo real
  3. Python — IntelliSense y autocompletado

Con Pylance habilitado, verás errores de type checking directamente en el editor mientras Claude Code genera código. Si ves líneas subrayadas en rojo después de que Claude Code termina, revísalas — pueden ser hallucinations.


Conexión con Proyecto

Toolkit para el proyecto integrador

En el módulo 8, recibirás un codebase FastAPI con ~15-20 problemas. Tu proceso de verificación debería ser:

1. ruff check src/ → Encuentra imports no usados, syntax issues
2. mypy src/ → Encuentra imports falsos, parámetros incorrectos
3. Quick tests para funciones de seguridad → Encuentra lógica fabricada
4. Documentación para parámetros sospechosos → Confirma hallucinations

Estimación de tiempo:

  • Capa 1 (ruff): 30 segundos
  • Capa 2 (mypy): 2 minutos
  • Capa 3 (quick tests): 15-20 minutos
  • Capa 4 (docs): 5-10 minutos según necesidad

Total: ~30 minutos para la fase de detección de hallucinations del proyecto.

Qué te faltaría sin herramientas

Sin las capas automatizadas, la única herramienta sería tu ojo. Un developer promedio revisando 500-800 líneas de código detectaría:

  • ~80% de imports falsos (los más obvios)
  • ~50% de parámetros incorrectos (los que conoce)
  • ~30% de lógica fabricada (solo en dominios que domina)

Con las 4 capas:

  • ~95% de imports falsos (ruff + mypy + import verification)
  • ~80% de parámetros incorrectos (mypy + docs)
  • ~60% de lógica fabricada (quick tests + dominio knowledge)

Las herramientas no reemplazan tu ojo — lo complementan. Juntos cubren significativamente más que cualquiera por separado.


Troubleshooting

Problema 1: "mypy da demasiados errores en mi código — no sé cuáles son hallucinations"

Causa: mypy en modo strict puede dar cientos de errores en código que no tiene type annotations.

Solución: No uses --strict para la detección de hallucinations. Usa el modo default y enfócate en estos error codes:

[attr-defined] → Import falso o método no existente (ALTA prioridad)
[call-arg]     → Parámetro incorrecto (ALTA prioridad)
[import]       → Módulo no encontrado (ALTA prioridad)
[name-defined] → Variable no definida (MEDIA prioridad)

Ignora errores de tipos genéricos [type-arg], [return-type] — son issues de typing, no hallucinations.

Problema 2: "El import pasa python -c pero mypy dice que no existe"

Causa: El paquete está instalado pero no tiene type stubs.

Solución: Instala type stubs para las librerías principales:

pip install types-requests types-PyYAML types-redis

Para librerías sin stubs oficiales, agrega a mypy.ini:

[mypy-nombre_libreria.*]
ignore_missing_imports = True

Problema 3: "Las herramientas no detectan hallucinations en funciones con **kwargs"

Causa: **kwargs acepta cualquier argumento, ocultando hallucinations.

Solución: Para funciones con **kwargs (como requests.get()):

  1. No confíes en mypy para estas funciones
  2. Usa inspect.signature() para ver los parámetros documentados
  3. Verifica contra la documentación oficial
  4. Haz un quick test para confirmar que el parámetro tiene efecto

Problema 4: "No tengo tiempo para ejecutar 4 capas en cada archivo"

Causa: La verificación completa toma tiempo.

Solución: Prioriza:

  • Siempre: Capa 1 (ruff) — toma <1 segundo
  • Siempre: Capa 2 (mypy) — toma 2-5 segundos
  • Para código de riesgo: Capa 3 (quick tests) — toma 1-5 minutos
  • Solo si hay duda: Capa 4 (documentación) — toma 2-10 minutos

Las capas 1 y 2 deben ser automáticas (pre-commit hooks o extensiones de editor). Las capas 3 y 4 las usas selectivamente para código de alto riesgo.

Problema 5: "¿Estas herramientas funcionan para JavaScript/TypeScript?"

Causa: Esta guía usa Python, pero los principios aplican a cualquier lenguaje.

Solución: Los equivalentes en JS/TS:

  • Capa 1: ESLint (linting)
  • Capa 2: TypeScript compiler (type checking)
  • Capa 3: Jest/Vitest (quick tests)
  • Capa 4: MDN / documentación de npm packages

Los principios de las 4 capas son universales.


Ejercicios

Ejercicio 1: Ejecutar las 4 capas (Fácil)

Toma este código y ejecuta las 4 capas de verificación. Documenta qué encuentra cada capa:

from fastapi import FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel, Field
import jwt

app = FastAPI()

class UserLogin(BaseModel):
    email: str = Field(..., format="email")
    password: str = Field(..., min_length=8)

def create_token(user_id: str) -> str:
    return jwt.encode(
        {"sub": user_id}, 
        "secret", 
        algorithm="HS256"
    )
Ver solución

Capa 1 (ruff):

  • F401: OAuth2PasswordBearer imported but unused
  • No otros errores

Capa 2 (mypy):

  • Field(..., format="email") — format no es un parámetro de Pydantic Field(). Es una hallucination Tipo 3. mypy reporta: Unexpected keyword argument "format" for "Field".

Capa 3 (quick test):

  • No hay funciones de lógica compleja para testear. create_token es una llamada directa a jwt.encode con parámetros simples.

Capa 4 (documentación):

  • Confirma que Field() no acepta format. Para validar email, Pydantic tiene EmailStr: email: EmailStr.
  • jwt.encode con algorithm="HS256" es correcto.

Hallucinations encontradas: 1

  • Field(..., format="email") → format no es parámetro de Field. Lo correcto es usar EmailStr como tipo: email: EmailStr.

Ejercicio 2: Configurar verificación (Medio)

Configura ruff + mypy para un proyecto Python. Crea los archivos de configuración y ejecuta contra un archivo de ejemplo. Documenta:

  1. Los archivos de configuración que creaste
  2. Los comandos que ejecutaste
  3. Los resultados
Ver solución

1. Archivos de configuración:

ruff.toml:

line-length = 88
target-version = "py311"

[lint]
select = ["E", "F", "I", "B", "S", "UP"]

mypy.ini:

[mypy]
python_version = 3.11
check_untyped_defs = True
show_error_codes = True

[mypy-uvicorn.*]
ignore_missing_imports = True

2. Comandos:

pip install ruff mypy
ruff check app.py --show-source
mypy app.py --show-error-codes

3. Resultados típicos:

  • ruff reporta imports no usados, posibles bugs
  • mypy reporta atributos no existentes, parámetros incorrectos

La configuración exacta depende de tu proyecto. Lo importante es tener ambas herramientas configuradas y corriendo.

Ejercicio 3: Script de verificación de imports (Medio)

Escribe un script que reciba un archivo Python y verifique automáticamente todos los imports. El script debe:

  1. Extraer todos los imports del archivo
  2. Intentar ejecutar cada import
  3. Reportar cuáles funcionan y cuáles no
Ver solución
import ast
import sys
import importlib

def verify_imports(filepath: str) -> None:
    """Verifies all imports in a Python file."""
    with open(filepath) as f:
        tree = ast.parse(f.read())
    
    print(f"Verifying imports in {filepath}:\n")
    
    passed = 0
    failed = 0
    
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom):
            module = node.module or ""
            for alias in node.names:
                name = alias.name
                try:
                    exec(f"from {module} import {name}")
                    print(f"  ✅ from {module} import {name}")
                    passed += 1
                except ImportError as e:
                    print(f"  ❌ from {module} import {name} — {e}")
                    failed += 1
        elif isinstance(node, ast.Import):
            for alias in node.names:
                try:
                    importlib.import_module(alias.name)
                    print(f"  ✅ import {alias.name}")
                    passed += 1
                except ImportError as e:
                    print(f"  ❌ import {alias.name} — {e}")
                    failed += 1
    
    print(f"\nResults: {passed} passed, {failed} failed")
    if failed > 0:
        print(f"⚠️  {failed} potential hallucination(s) found!")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python verify_imports.py <file.py>")
        sys.exit(1)
    verify_imports(sys.argv[1])

Uso: python verify_imports.py app.py

Ejercicio 4: Detectar con mypy (Difícil)

Este código tiene 3 hallucinations. Usa mypy para encontrar al menos 2 de las 3:

from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import Session, declarative_base, joinedload
from typing import Optional, List
import jwt

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allowed_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

Base = declarative_base()

class UserDB(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True)
    name = Column(String)

class UserCreate(BaseModel):
    email: str = Field(..., unique=True)
    name: str = Field(..., min_length=2, max_length=100)

class UserResponse(BaseModel):
    id: int
    email: str
    name: str
    
    model_config = {"from_attributes": True}

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)):
    payload = jwt.decode(token, "secret", algorithms=["HS256"], verify=True)
    return payload.get("sub")
Ver solución

Las 3 hallucinations:

  1. allowed_origins=["*"] (línea 14) — El parámetro correcto es allow_origins, no allowed_origins. mypy puede no detectar esto porque add_middleware puede aceptar **kwargs. Este es un caso donde la Capa 4 (documentación) es necesaria.

  2. Field(..., unique=True) (línea 29) — unique no es un parámetro de Pydantic Field. mypy sí detecta esto: Unexpected keyword argument "unique" for "Field" [call-arg].

  3. jwt.decode(..., verify=True) (línea 42) — verify no es un parámetro de jwt.decode. mypy sí detecta esto: Unexpected keyword argument "verify" for "decode" [call-arg].

Resultado: mypy detecta 2 de las 3 hallucinations. La tercera (allowed_origins) requiere verificación manual o documentación porque CORSMiddleware acepta **kwargs.

Lección: mypy es poderoso pero no omnisciente. Las funciones con **kwargs son un punto ciego. Para esas, necesitas la capa 4 (documentación) o quick tests.

Ejercicio 5: Diseñar tu pipeline de verificación (Difícil)

Diseña un pipeline de verificación personalizado para tu stack de trabajo. Debe incluir:

  1. Herramientas de cada capa
  2. Configuración mínima
  3. Comandos para ejecutar
  4. Qué tipo de hallucination detecta cada paso
  5. Cuándo se ejecuta (manual vs automático)
Ver solución (ejemplo con FastAPI + PostgreSQL + pytest)

Pipeline de verificación:

CapaHerramientaComandoDetectaEjecución
1ruffruff check src/Imports no usados, syntaxAutomático (pre-commit)
1bruff formatruff format src/FormateoAutomático (pre-commit)
2mypymypy src/ --show-error-codesImports falsos, parámetros incorrectosAutomático (pre-commit)
3aImport checkpython verify_imports.pyImports que no existenManual (post-generación)
3bpytestpytest tests/ -v --tb=shortLógica incorrectaManual (post-generación)
4Docs checkVerificar contra docs oficialesTodo lo que 1-3 no atrapaManual (cuando hay duda)

Configuración mínima:

  • ruff.toml con rules E, F, I, B, S, UP
  • mypy.ini con check_untyped_defs = True
  • .pre-commit-config.yaml con ruff + mypy hooks
  • Script verify_imports.py en la raíz del proyecto

Estimación de tiempo por ejecución:

  • Capas 1-2 (automático): 0 segundos (se ejecutan en pre-commit)
  • Capa 3a (import check): 30 segundos
  • Capa 3b (tests): 1-5 minutos
  • Capa 4 (docs): 5-10 minutos (solo si necesario)

Resumen

En esta cápsula aprendiste:

  • Las 4 capas de verificación: linters → type checkers → quick tests → documentación
  • Capa 1 (ruff): detecta imports no usados, syntax errors — automática, < 1 segundo
  • Capa 2 (mypy): detecta imports falsos, parámetros incorrectos — automática, 2-5 segundos
  • Capa 3 (tests): detecta lógica fabricada — manual, 1-5 minutos
  • Capa 4 (docs): verifica todo lo que las otras no cubren — manual, 2-10 minutos
  • mypy es la herramienta más valiosa contra hallucinations de tipo 1-3
  • Las funciones con **kwargs son un punto ciego de todas las herramientas automatizadas
  • La automatización (pre-commit hooks, extensiones de editor) convierte la verificación en parte de tu flujo natural

Próxima cápsula: Ejercicio: Detectar Hallucinations — 5 snippets con hallucinations ocultas. Es hora de poner a prueba todo lo que aprendiste.


Recursos Adicionales

  1. ruff — Python Linter - Documentación oficial del linter más rápido de Python
  2. mypy — Type Checker - Documentación oficial del type checker
  3. pyright - Type checker de Microsoft, engine de Pylance
  4. pre-commit - Framework para git hooks automatizados
  5. pytest — Testing Framework - Documentación oficial de pytest
  6. Python inspect module - Inspeccionar signatures y metadata de funciones

Debugging & Code Review with Claude Code — Módulo 3, Cápsula 05 Claude Code Agentic Development Path — Guía #6 de 11