Python Automation Capsule: convierte scripts sueltos en herramientas profesionales, portables y auditables
Aprende a convertir scripts Python sueltos en herramientas portables con uv, Typer, Pydantic, Ruff y pytest. Un método profesional para automatizar sin romper entornos ni perder control.
Por Equipo Starbyte
El problema no es saber Python; el problema es que tus scripts no sobreviven fuera de tu computadora
Casi todos los que automatizan con Python han vivido esta escena.
Un script funciona perfecto en tu laptop.
Lo copias a otra máquina.
Falla por una librería.
Lo ejecutas en otro usuario.
Falla por la ruta del archivo.
Lo mandas a un compañero.
No tiene la misma versión de Python.
Lo programas en una tarea automática.
No encuentra dependencias.
Lo retomas tres meses después.
Ya no recuerdas qué instalaste.
El problema no era el código.
El problema era que el script no estaba encapsulado.
En automatización profesional, un script útil no debe depender de la memoria del autor. Debe traer consigo:
- versión de Python;
- dependencias;
- validación de entrada;
- comando claro;
- configuración;
- logs;
- pruebas;
- formato;
- bloqueo de versiones;
- instrucciones de ejecución.
A ese patrón lo llamaremos Python Automation Capsule.
No es un framework nuevo. Es un método de trabajo.
La idea es convertir un script suelto en una herramienta pequeña, portable y auditable.
Por qué este tema es actual
El ecosistema Python cambió mucho.
uv, de Astral, se está posicionando como un administrador moderno de Python, proyectos, entornos y dependencias escrito en Rust. Su documentación oficial lo describe como un gestor extremadamente rápido de paquetes y proyectos Python. Además, permite ejecutar scripts con dependencias inline, bloquear dependencias de scripts y solicitar versiones específicas de Python.
Ruff, también de Astral, reúne linting y formateo en una sola herramienta rápida. Su documentación oficial lo describe como un linter y formatter de Python escrito en Rust, y su formatter está pensado como reemplazo compatible con Black en la mayoría de casos.
Typer facilita crear interfaces de línea de comandos basadas en type hints. Su documentación oficial destaca que permite convertir funciones Python en comandos CLI intuitivos.
Pydantic permite validar datos y configuraciones con modelos tipados. pytest sigue siendo una base muy sólida para pruebas automatizadas en Python.
La tendencia no es “usar más librerías”.
La tendencia profesional es esta:
Automatizar menos como script improvisado y más como herramienta reproducible.
La idea propia: el método CÁPSULA
Una Python Automation Capsule debe tener siete capas.
C — Comando claro
A — Ambiente reproducible
P — Parámetros validados
S — Salidas controladas
U — Unit tests mínimos
L — Lint y formato
A — Auditoría de ejecución
Si falta una capa, el script todavía puede funcionar.
Pero será más frágil, más difícil de compartir y más peligroso de automatizar.
Vamos a construir una cápsula desde cero.
El caso será práctico: una herramienta que lee un CSV de entrada, valida columnas, normaliza datos y genera un archivo de salida con registro de errores.
Qué vas a construir
Crearás una herramienta llamada:
data-cleaner
Que podrá ejecutarse así:
uv run data-cleaner samples/clientes.csv --output output/clientes_limpios.csv
La herramienta hará esto:
1. leer archivo CSV;
2. validar que existan columnas obligatorias;
3. normalizar nombres;
4. validar correos;
5. registrar filas con errores;
6. generar archivo limpio;
7. dejar salidas auditables;
8. pasar pruebas;
9. ejecutarse igual en otra máquina.
No es un ejemplo decorativo. Es la base de cualquier automatización real: limpiar padrones, consolidar reportes, procesar exportaciones, validar bases de Excel, preparar datos para Power BI o ejecutar flujos desde n8n.
Estructura final del proyecto
La cápsula quedará así:
data-cleaner/
├── pyproject.toml
├── uv.lock
├── README.md
├── src/
│ └── data_cleaner/
│ ├── __init__.py
│ ├── cli.py
│ ├── models.py
│ └── processor.py
├── tests/
│ └── test_processor.py
├── samples/
│ └── clientes.csv
└── output/
Esta estructura evita el típico archivo eterno:
script_limpieza_final_final_ahora_si.py
Paso 1: crear el proyecto con uv
Instala uv según la documentación oficial de Astral.
En macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
En Windows PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Verifica:
uv --version
Crea el proyecto:
uv init data-cleaner --package
cd data-cleaner
Agrega dependencias:
uv add typer pydantic pandas
Agrega dependencias de desarrollo:
uv add --dev pytest ruff
Agrega soporte para validación de emails:
uv add "pydantic[email]"
Esto crea o actualiza:
pyproject.toml
uv.lock
.venv/
El archivo uv.lock es importante: permite reproducibilidad.
Paso 2: crear el comando CLI
Edita:
mkdir -p src/data_cleaner
touch src/data_cleaner/__init__.py
nano src/data_cleaner/cli.py
Código:
from pathlib import Path
import typer
from data_cleaner.processor import clean_file
app = typer.Typer(help="Limpia y valida archivos CSV de clientes.")
@app.command()
def run(
input_file: Path = typer.Argument(..., help="Archivo CSV de entrada."),
output: Path = typer.Option(
Path("output/clientes_limpios.csv"),
"--output",
"-o",
help="Archivo CSV de salida.",
),
) -> None:
"""
Limpia un archivo CSV y genera una salida validada.
"""
result = clean_file(input_file=input_file, output_file=output)
typer.echo(f"Filas procesadas: {result.total_rows}")
typer.echo(f"Filas válidas: {result.valid_rows}")
typer.echo(f"Filas con error: {result.error_rows}")
typer.echo(f"Salida: {output}")
if __name__ == "__main__":
app()
Typer convierte esa función en un comando usable.
Paso 3: registrar el comando en pyproject.toml
Abre:
nano pyproject.toml
Agrega o ajusta:
[project.scripts]
data-cleaner = "data_cleaner.cli:app"
Ahora podrás ejecutar:
uv run data-cleaner --help
Este es el primer salto profesional: ya no ejecutas un archivo suelto, sino un comando.
Paso 4: crear modelos de validación con Pydantic
Crea:
nano src/data_cleaner/models.py
Código:
from pydantic import BaseModel, EmailStr, Field
class Cliente(BaseModel):
nombre: str = Field(min_length=2)
correo: EmailStr
ciudad: str = Field(min_length=2)
class ProcessResult(BaseModel):
total_rows: int
valid_rows: int
error_rows: int
Pydantic ayuda a evitar datos basura.
Sin validación, cualquier automatización termina llenando Excel o bases con errores silenciosos.
Paso 5: crear el procesador
Crea:
nano src/data_cleaner/processor.py
Código:
from pathlib import Path
import pandas as pd
from pydantic import ValidationError
from data_cleaner.models import Cliente, ProcessResult
REQUIRED_COLUMNS = {"nombre", "correo", "ciudad"}
def normalize_text(value: object) -> str:
return str(value).strip()
def clean_file(input_file: Path, output_file: Path) -> ProcessResult:
if not input_file.exists():
raise FileNotFoundError(f"No existe el archivo: {input_file}")
df = pd.read_csv(input_file)
missing_columns = REQUIRED_COLUMNS - set(df.columns)
if missing_columns:
missing = ", ".join(sorted(missing_columns))
raise ValueError(f"Faltan columnas obligatorias: {missing}")
valid_rows: list[dict[str, str]] = []
error_rows: list[dict[str, str]] = []
for index, row in df.iterrows():
raw_data = {
"nombre": normalize_text(row["nombre"]).title(),
"correo": normalize_text(row["correo"]).lower(),
"ciudad": normalize_text(row["ciudad"]).title(),
}
try:
cliente = Cliente(**raw_data)
valid_rows.append(cliente.model_dump())
except ValidationError as exc:
error_rows.append(
{
"fila": str(index + 2),
"error": exc.errors()[0]["msg"],
**raw_data,
}
)
output_file.parent.mkdir(parents=True, exist_ok=True)
pd.DataFrame(valid_rows).to_csv(output_file, index=False)
if error_rows:
errors_file = output_file.with_name(output_file.stem + "_errores.csv")
pd.DataFrame(error_rows).to_csv(errors_file, index=False)
return ProcessResult(
total_rows=len(df),
valid_rows=len(valid_rows),
error_rows=len(error_rows),
)
Aquí hay tres decisiones profesionales:
- no se procesa si faltan columnas;
- se separan filas válidas y erróneas;
- se genera archivo de errores en vez de ocultarlos.
Paso 6: crear datos de prueba
Crea:
mkdir -p samples output
nano samples/clientes.csv
Contenido:
nombre,correo,ciudad
ana torres,ana@example.com,piura
juan perez,correo_malo,lima
lu,lu@example.com,cusco
maria ruiz,maria@example.com,trujillo
Ejecuta:
uv run data-cleaner samples/clientes.csv --output output/clientes_limpios.csv
Deberías ver algo parecido a:
Filas procesadas: 4
Filas válidas: 3
Filas con error: 1
Salida: output/clientes_limpios.csv
Y tendrás:
output/clientes_limpios.csv
output/clientes_limpios_errores.csv
Paso 7: agregar pruebas con pytest
Crea:
mkdir -p tests
nano tests/test_processor.py
Código:
from pathlib import Path
import pandas as pd
import pytest
from data_cleaner.processor import clean_file
def test_clean_file_generates_valid_rows(tmp_path: Path) -> None:
input_file = tmp_path / "clientes.csv"
output_file = tmp_path / "salida.csv"
input_file.write_text(
"nombre,correo,ciudad\n"
"ana torres,ana@example.com,piura\n"
"juan perez,correo_malo,lima\n",
encoding="utf-8",
)
result = clean_file(input_file=input_file, output_file=output_file)
assert result.total_rows == 2
assert result.valid_rows == 1
assert result.error_rows == 1
assert output_file.exists()
df = pd.read_csv(output_file)
assert df.iloc[0]["nombre"] == "Ana Torres"
def test_clean_file_fails_when_required_column_is_missing(tmp_path: Path) -> None:
input_file = tmp_path / "clientes.csv"
output_file = tmp_path / "salida.csv"
input_file.write_text(
"nombre,correo\n"
"ana torres,ana@example.com\n",
encoding="utf-8",
)
with pytest.raises(ValueError, match="Faltan columnas obligatorias"):
clean_file(input_file=input_file, output_file=output_file)
Ejecuta:
uv run pytest
Esto cambia la naturaleza del script: ahora puedes modificarlo sin miedo.
Paso 8: agregar Ruff para formato y calidad
Ejecuta:
uv run ruff check .
uv run ruff format .
Ruff permite encontrar errores y formatear el código con una sola herramienta.
Agrega configuración en pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
Esto activa reglas útiles:
E/F: errores básicos;
I: orden de imports;
B: posibles bugs;
UP: modernización de Python;
SIM: simplificación.
No actives 900 reglas el primer día. Empieza con reglas que aporten.
Paso 9: crear un README útil
Crea:
nano README.md
Contenido:
# data-cleaner
Herramienta CLI para limpiar y validar archivos CSV de clientes.
## Requisitos
- uv
- Python 3.12+
## Instalación
```bash
uv sync
```
## Uso
```bash
uv run data-cleaner samples/clientes.csv --output output/clientes_limpios.csv
```
## Pruebas
```bash
uv run pytest
```
## Formato y lint
```bash
uv run ruff check .
uv run ruff format .
```
## Columnas obligatorias
- nombre
- correo
- ciudad
## Salidas
- CSV limpio
- CSV de errores si existen filas inválidas
Un README así evita preguntas repetidas.
Paso 10: bloquear dependencias
Con uv, el archivo uv.lock debe quedar versionado.
Ejecuta:
uv lock
Luego:
git add pyproject.toml uv.lock src tests README.md samples
git commit -m "crear capsula de automatizacion Python"
Si otra persona clona el proyecto, puede ejecutar:
uv sync
uv run pytest
uv run data-cleaner samples/clientes.csv
Ese es el objetivo: reproducibilidad.
Paso 11: crear una variante ultraligera con script PEP 723
Hay casos donde no quieres un proyecto completo. Solo necesitas un script portable.
uv permite ejecutar scripts con dependencias inline.
Crea:
nano limpiar_clientes.py
Contenido:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pandas",
# "pydantic[email]",
# "typer",
# ]
# ///
from pathlib import Path
import pandas as pd
import typer
from pydantic import BaseModel, EmailStr, Field, ValidationError
app = typer.Typer()
class Cliente(BaseModel):
nombre: str = Field(min_length=2)
correo: EmailStr
ciudad: str = Field(min_length=2)
@app.command()
def run(input_file: Path, output_file: Path = Path("clientes_limpios.csv")) -> None:
df = pd.read_csv(input_file)
valid_rows = []
for _, row in df.iterrows():
try:
cliente = Cliente(
nombre=str(row["nombre"]).strip().title(),
correo=str(row["correo"]).strip().lower(),
ciudad=str(row["ciudad"]).strip().title(),
)
valid_rows.append(cliente.model_dump())
except ValidationError:
continue
pd.DataFrame(valid_rows).to_csv(output_file, index=False)
typer.echo(f"Archivo generado: {output_file}")
if __name__ == "__main__":
app()
Ejecuta:
uv run limpiar_clientes.py samples/clientes.csv --output-file output.csv
Bloquea dependencias del script:
uv lock --script limpiar_clientes.py
Esto crea:
limpiar_clientes.py.lock
Este patrón es muy útil para automatizaciones pequeñas: un solo archivo, dependencias declaradas y ejecución reproducible.
Cuándo usar proyecto y cuándo script inline
| Caso | Mejor opción |
|---|---|
| automatización pequeña | script PEP 723 con uv |
| herramienta que usarán varios | proyecto con pyproject |
| proceso con pruebas | proyecto |
| tarea de una sola vez | script inline |
| integración con CI/CD | proyecto |
| entrega a otra área | proyecto |
| prototipo rápido | script inline |
No todo necesita una arquitectura grande. Pero todo debería ser reproducible.
Mi método: la prueba de los 3 reinicios
Antes de considerar profesional una automatización, debe pasar tres reinicios.
Reinicio 1: otra terminal
Cierra terminal, abre otra y ejecuta:
uv run data-cleaner samples/clientes.csv
Reinicio 2: otra carpeta
Clona o copia el proyecto en otra ruta y ejecuta:
uv sync
uv run pytest
Reinicio 3: otra persona
Entrega el README a alguien y observa si puede ejecutarlo sin preguntarte.
Si falla, el problema no es la persona. Es la cápsula.
Automatización en GitHub Actions
Crea:
mkdir -p .github/workflows
nano .github/workflows/ci.yml
Contenido:
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Lint
run: uv run ruff check .
- name: Format check
run: uv run ruff format --check .
- name: Test
run: uv run pytest
Este flujo bloquea errores antes de que lleguen a producción.
Trucos poco obvios que elevan el nivel
1. No aceptes rutas implícitas
Mal:
pd.read_csv("clientes.csv")
Bien:
def clean_file(input_file: Path, output_file: Path) -> ProcessResult:
Las rutas deben entrar como parámetros.
2. No ocultes errores en prints
Mal:
print("algo falló")
Bien:
raise ValueError("Faltan columnas obligatorias: correo")
3. No mezcles lectura, lógica y CLI
Separa:
cli.py → entrada del usuario
processor.py → lógica
models.py → validación
tests/ → pruebas
4. No escribas directo sobre el original
Nunca hagas:
clientes.csv → clientes.csv
Siempre genera salida nueva.
5. No pierdas las filas malas
Un archivo de errores vale oro. Sirve para corregir datos y auditar.
6. No dependas de Excel como validador
Valida antes de abrir en Excel.
7. No esperes a tener 100 pruebas
Dos pruebas buenas ya cambian el nivel del proyecto.
Checklist de una Python Automation Capsule
| Capa | Revisión | Estado |
|---|---|---|
| Comando | se ejecuta con un comando claro | ☐ |
| Ambiente | usa uv y lockfile | ☐ |
| Parámetros | rutas y opciones explícitas | ☐ |
| Validación | Pydantic o reglas claras | ☐ |
| Salida | genera archivo nuevo | ☐ |
| Errores | conserva filas problemáticas | ☐ |
| Pruebas | tiene pytest mínimo | ☐ |
| Formato | usa Ruff | ☐ |
| README | explica uso y comandos | ☐ |
| Reproducibilidad | otra persona puede ejecutarlo | ☐ |
Casos donde este método simplifica muchísimo
Scrapers
Cada scraper debería tener:
comando;
configuración;
reintentos;
salida;
log;
tests de parsing;
lockfile.
Procesamiento de Excel
Ideal para validar columnas, limpiar datos y generar salidas controladas.
Reportes automáticos
La cápsula evita que un reporte dependa de una ruta local o una versión de librería.
APIs internas
Typer puede servir para tareas administrativas, migraciones o mantenimiento.
n8n y tareas programadas
En vez de ejecutar scripts sueltos, n8n puede llamar comandos reproducibles.
Sector público
Muy útil para procesos donde necesitas trazabilidad y repetición: padrones, CUI, reportes, consolidación de datos, validación de Excel y generación de salidas.
Plantilla rápida para nuevos proyectos
Cada vez que empieces una automatización, usa esto:
uv init nombre-herramienta --package
cd nombre-herramienta
uv add typer pydantic pandas
uv add --dev pytest ruff
mkdir -p src/nombre_herramienta tests samples output
Luego crea:
cli.py
processor.py
models.py
tests/test_processor.py
README.md
Y no escribas lógica dentro del CLI.
Prompt experto para convertir un script suelto en cápsula
Actúa como arquitecto senior de automatización Python.
Voy a darte un script Python suelto. Quiero que lo conviertas en una Python Automation Capsule.
Reglas:
- Separar CLI, lógica y modelos.
- Usar uv para dependencias.
- Crear pyproject.toml.
- Crear comando con Typer.
- Validar entradas con Pydantic cuando aplique.
- Agregar pruebas con pytest.
- Agregar Ruff para lint y formato.
- Crear README con comandos.
- No sobrescribir archivos originales.
- Generar archivo de errores si hay datos inválidos.
Entrega:
1. estructura de carpetas;
2. pyproject.toml;
3. cli.py;
4. processor.py;
5. models.py;
6. tests;
7. README;
8. comandos de ejecución;
9. checklist de validación.
Plan de 7 días para profesionalizar tus scripts
Día 1: inventario
Elige tres scripts que uses de verdad.
Día 2: encapsula uno
Pásalo a proyecto con uv y Typer.
Día 3: valida entradas
Agrega Pydantic o reglas explícitas.
Día 4: separa errores
Genera archivo de errores o log.
Día 5: agrega pruebas
Cubre al menos caso exitoso y caso fallido.
Día 6: agrega Ruff
Formato y lint.
Día 7: entrega a otra persona
Si puede ejecutarlo con README, ya tienes una herramienta.
Idea clave
El salto profesional en Python no ocurre cuando escribes scripts más largos, sino cuando tus automatizaciones dejan de depender de tu memoria. Una Python Automation Capsule convierte un archivo suelto en una herramienta reproducible: comando claro, ambiente controlado, entradas validadas, salidas auditables, pruebas y formato. Eso simplifica trabajo real porque cualquier persona, servidor o flujo de automatización puede ejecutarla sin adivinar cómo fue construida.