Antes de empezar
Instala las herramientas y verifica cada una. Si un comando de verificación no responde como se indica, arréglalo antes de seguir; nada de lo que viene funciona sin esto.
1. Terminal
En macOS: la app Terminal (o iTerm2). En Windows: instala WSL2 con Ubuntu y trabaja siempre ahí; todo lo que sigue asume una terminal tipo Unix.
2. uv (instala Python y maneja proyectos)
# macOS (con Homebrew) o Linux
brew install uv # macOS
curl -LsSf https://astral.sh/uv/install.sh | sh # Linux / WSL
# instala Python 3.12 (uv lo descarga; no toca el Python del sistema)
uv python install 3.12
Deberías veruv 0.12.5 (o superior)
Por qué uv y no pip. Hace cuatro cosas que antes eran cuatro herramientas: instala Python, crea el entorno virtual, agrega dependencias y fija versiones exactas en uv.lock. Un entorno por proyecto es como un medio de cultivo por cepa: nunca se mezclan.
3. git y GitHub
git --version
git config --global user.name "Alexandra"
git config --global user.email "tu-correo@ejemplo.com"
git config --global init.defaultBranch main
Crea tu cuenta en github.com si no la tienes. Instala la CLI de GitHub e inicia sesión (te guía para crear la llave SSH):
brew install gh # macOS · en Linux: https://cli.github.com
gh auth login # elige GitHub.com → SSH → seguir las instrucciones
gh auth status
Deberías ver✓ Logged in to github.com account alexandra-… (keyring)
4. Docker Desktop
Descarga Docker Desktop, ábrelo y espera a que el ícono diga que está corriendo. Luego:
docker run --rm hello-world
Deberías verHello from Docker!
This message shows that your installation appears to be working correctly.
5. VS Code
Instala VS Code y, dentro, la extensión Python (de Microsoft). Con eso vienen el debugger y el panel de tests. Opcional pero recomendado: la extensión Ruff.
6. Claude Code
Instálalo siguiendo la documentación oficial. En este proyecto lo usas solo para preguntar, no para escribir. Ver Con Claude más abajo.
Checklist antes de seguir
uv --version · git --version · gh auth status · docker run hello-world · VS Code abre una carpeta y reconoce archivos .py. Cinco verdes, empezamos.
Construcción paso a paso
Escribe cada archivo tú misma (no copies y pegues bloques enteros: tipear es parte de aprender). Corre cada comando y compara con lo que "deberías ver". Si difiere, detente ahí y entiende por qué antes de avanzar.
Paso 1 Crear el proyecto y su entorno
Crea la carpeta, inicializa git y fija la versión de Python del proyecto:
mkdir hola-alexandra
cd hola-alexandra
git init
uv python pin 3.12
mkdir -p src/hola tests
uv python pin crea un archivo .python-version: cualquiera que abra este proyecto usará 3.12, aunque tenga otro Python instalado.
Ahora el archivo que describe el proyecto. Escríbelo a mano; cada línea se explica en Entiende el código:
pyproject.toml[project]
name = "hola"
version = "0.1.0"
description = "Proyecto 0: validar la cadena de herramientas"
requires-python = ">=3.12"
dependencies = []
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/hola"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.ruff]
line-length = 100
El paquete necesita un archivo __init__.py (puede estar vacío) para que Python lo reconozca como paquete, y el proyecto necesita ignorar lo que no se versiona:
touch src/hola/__init__.py
.gitignore.venv/
__pycache__/
.pytest_cache/
.ruff_cache/
*.pyc
.env
dist/
Agrega las herramientas de desarrollo. Este comando crea el entorno virtual (.venv/), instala pytest y ruff, e instala tu propio paquete en modo editable:
Deberías verResolved 6 packages in …
Installed 5 packages in …
+ hola==0.1.0 (from file:///…/hola-alexandra)
+ pytest==9.x
+ ruff==0.x
…
Mira lo que apareció:
Deberías ver. .. .git .gitignore .python-version .venv pyproject.toml src tests uv.lock
uv.lock es la lista exacta de versiones que se instalaron. Se versiona en git. .venv/ es el entorno con esos paquetes instalados: no se versiona (está en .gitignore) porque se regenera con uv sync. Fíjate que pyproject.toml ahora tiene una sección [dependency-groups] con dev = ["pytest…", "ruff…"]: la agregó uv add.
Paso 2 Primer commit: la estructura vacía
Antes de escribir una sola línea de código, guarda la estructura. Es una foto del "antes".
git status
git add .
git commit -m "Estructura inicial del proyecto con uv, pytest y ruff"
git log --oneline
Deberías vera1b2c3d (HEAD -> main) Estructura inicial del proyecto con uv, pytest y ruff
Comprueba que .venv/ no entró: git ls-files debe listar solo .gitignore, .python-version, pyproject.toml, src/hola/__init__.py y uv.lock. (La carpeta tests/ está vacía y git no versiona carpetas vacías; entra en el siguiente paso.)
Sobre el mensaje. Dice qué se hizo en modo imperativo y por qué se entiende solo. Cuando el "por qué" no sea obvio, va en el mensaje: en tres meses git log es tu bitácora.
Paso 3 El test, antes que el código
Vas a escribir primero cómo debe comportarse la función, y recién después la función. Un test es un control: una entrada de la que ya conoces la respuesta.
tests/test_core.pyimport pytest
from hola.core import gc_content
def test_gc_content_mitad():
assert gc_content("ATGC") == 0.5
def test_gc_content_todo_gc():
assert gc_content("GGCC") == 1.0
def test_gc_content_ignora_minusculas():
assert gc_content("atgc") == 0.5
def test_gc_content_secuencia_vacia_lanza_error():
with pytest.raises(ValueError):
gc_content("")
Córrelo. Tiene que fallar: la función no existe todavía.
Deberías ver (rojo, y está bien)==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_core.py ______________________
…
tests/test_core.py:3: in <module>
from hola.core import gc_content
E ModuleNotFoundError: No module named 'hola.core'
=========================== short test summary info ============================
ERROR tests/test_core.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
Lee el error de abajo hacia arriba. La última línea con E es la causa: no existe el módulo hola.core. Todo lo demás es contexto. Aprender a leer un traceback así es la habilidad más rentable de este mes.
Los cuatro tests son, en lenguaje de laboratorio: dos controles positivos con resultado conocido (0.5 y 1.0), un caso de robustez (minúsculas) y un control negativo (entrada inválida debe fallar de una forma específica). Con eso, la función queda definida antes de existir.
Paso 4 La función que hace pasar el test
src/hola/core.py"""Funciones sobre secuencias de ADN. Solo cálculo: nada de archivos ni print."""
def gc_content(seq: str) -> float:
"""Fracción de bases G y C en la secuencia, entre 0.0 y 1.0.
Ignora mayúsculas/minúsculas. Lanza ValueError si la secuencia está vacía.
"""
if not seq:
raise ValueError("la secuencia está vacía")
seq = seq.upper()
gc = seq.count("G") + seq.count("C")
return gc / len(seq)
Deberías vertests/test_core.py::test_gc_content_mitad PASSED [ 25%]
tests/test_core.py::test_gc_content_todo_gc PASSED [ 50%]
tests/test_core.py::test_gc_content_ignora_minusculas PASSED [ 75%]
tests/test_core.py::test_gc_content_secuencia_vacia_lanza_error PASSED [100%]
============================== 4 passed in 0.01s ===============================
Ahora el linter y el formateador. Ruff revisa errores comunes y deja el formato uniforme (dos líneas en blanco entre funciones, comillas dobles, etc.). Se corre siempre antes de commitear:
uv run ruff check .
uv run ruff format .
Deberías verAll checks passed!
3 files left unchanged (o "1 file reformatted" si tenías algo distinto: mira el diff con git diff)
Rompe algo a propósito para ver el test fallar por la razón correcta: cambia if not seq: por if False:, corre uv run pytest, mira cómo se ve un test que falla (no un error de import: una aserción), y vuelve a dejarlo bien.
git add .
git commit -m "Agrega gc_content con sus tests"
Paso 5 Empaquetar con Docker
Hasta aquí todo corre en tu laptop, con tu uv y tu Python. Docker congela ese entorno en una imagen que corre igual en cualquier máquina. Primero, qué no entra a la imagen:
.dockerignore.venv/
.git/
__pycache__/
.pytest_cache/
.ruff_cache/
Y la receta:
Dockerfile# Imagen base: Python 3.12 mínimo, sobre Debian
FROM python:3.12-slim
# Copiamos el binario de uv desde su imagen oficial
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Primero las dependencias (cambian poco → la capa se cachea)
COPY pyproject.toml uv.lock ./
# Luego el código y los tests
COPY src ./src
COPY tests ./tests
# Instala exactamente lo que dice uv.lock, nada más
RUN uv sync --frozen
# Lo que corre el contenedor por defecto: los tests
CMD ["uv", "run", "pytest", "-v"]
Construye la imagen (la primera vez descarga la base; tarda un minuto) y luego corre un contenedor a partir de ella:
docker build -t hola .
docker run --rm hola
Deberías ver…
tests/test_core.py::test_gc_content_mitad PASSED [ 25%]
tests/test_core.py::test_gc_content_todo_gc PASSED [ 50%]
tests/test_core.py::test_gc_content_ignora_minusculas PASSED [ 75%]
tests/test_core.py::test_gc_content_secuencia_vacia_lanza_error PASSED [100%]
============================== 4 passed in 0.01s ===============================
Los mismos cuatro tests, pero corriendo dentro de un Linux mínimo con su propio Python, sin usar nada de tu máquina salvo el código. Eso es reproducibilidad.
Deberías verREPOSITORY TAG IMAGE ID CREATED SIZE
hola latest ba5ccbd2560c 1 minute ago ~320MB
Imagen vs contenedor. La imagen es la receta congelada (hola:latest, 320 MB, no cambia). El contenedor es una corrida de esa receta: nace con docker run, ejecuta su comando y muere (--rm lo borra al terminar). Puedes correr diez contenedores de la misma imagen. Si cambias el código, la imagen vieja no se entera: hay que hacer docker build de nuevo.
git add .
git commit -m "Dockerfile que corre los tests en un contenedor"
Paso 6 README y subir a GitHub
El README es el protocolo: alguien que llega sin contexto tiene que poder correr esto. Escríbelo corto y verdadero:
README.md# hola-alexandra
Proyecto 0 del cuaderno: valida la cadena de herramientas con una sola
función (`gc_content`) y su test.
## Correr los tests
uv sync
uv run pytest
## Correr los tests en Docker
docker build -t hola .
docker run --rm hola
## Estructura
src/hola/core.py la función (solo cálculo, sin I/O)
tests/test_core.py sus controles
Dockerfile entorno reproducible
pyproject.toml descripción del proyecto y dependencias
## Decisiones
- `src/` layout: obliga a instalar el paquete para probarlo, igual que
lo haría un usuario. Evita que "funcione en mi máquina" por accidente.
- Ver DECISIONES.md.
DECISIONES.md# Decisiones
## 2026-08-xx · uv en lugar de pip + venv
Una herramienta hace lo que antes eran cuatro. Alternativa: pip + venv +
pip-tools. Cambio si uv deja de mantenerse.
## 2026-08-xx · La imagen Docker corre los tests
En este proyecto no hay "programa" que correr; lo que quiero verificar es
que el entorno se reproduce. Los tests son la prueba de eso.
git add .
git commit -m "README y registro de decisiones"
gh repo create hola-alexandra --private --source . --push
Deberías ver✓ Created repository alexandra-…/hola-alexandra on GitHub
✓ Added remote git@github.com:alexandra-…/hola-alexandra.git
✓ Pushed commits to git@github.com:alexandra-…/hola-alexandra.git
Abre el repo en el navegador (gh repo view --web) y agrega a Rodrigo como colaborador: Settings → Collaborators → Add people. Lo necesita para revisar tu PR en el paso 8.
Paso 7 El debugger, antes que print
Abre la carpeta en VS Code (code .). Cuando te pregunte qué intérprete usar, elige el de .venv (aparece como Python 3.12 ('.venv')). Configura el panel de tests:
.vscode/settings.json{
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"]
}
- Abre
src/hola/core.py y haz click a la izquierda del número de la línea seq = seq.upper(). Aparece un punto rojo: un breakpoint.
- Ve al panel Testing (ícono del matraz en la barra izquierda). Deberías ver los cuatro tests. En
test_gc_content_ignora_minusculas, click derecho → Debug Test.
- La ejecución se detiene en tu breakpoint. En el panel Variables ves
seq = 'atgc'. Pulsa Step Over (F10) una vez: ahora seq = 'ATGC'. Otra vez: aparece gc = 2.
- Pulsa Continue (F5). El test termina en verde.
Por qué esto y no print. Con el debugger ves todas las variables, en cualquier punto, sin ensuciar el código y sin volver a correr nada. Cuando algo falle en el Proyecto 4 dentro de una función que llama a otra que llama a otra, esto es lo que te va a salvar. Agrega .vscode/ al .gitignore o commitéalo; las dos cosas son válidas, decide y anótalo.
Paso 8 Tu primer Pull Request
De aquí en adelante, ningún cambio entra a main directo. Se hace en una rama, se sube, se abre un PR y alguien lo revisa. Aunque el cambio sea de una línea; la práctica es lo que importa. Vas a agregar una segunda función pequeña, en rama:
git switch -c feat/reverse-complement
Test primero (agrégalo al final de tests/test_core.py):
tests/test_core.py (agregar al final)from hola.core import reverse_complement
def test_reverse_complement_basico():
assert reverse_complement("ATGC") == "GCAT"
def test_reverse_complement_ignora_minusculas():
assert reverse_complement("atgc") == "GCAT"
def test_reverse_complement_base_invalida_lanza_error():
with pytest.raises(ValueError):
reverse_complement("ATXG")
Muévelo junto al otro import de arriba (los imports van todos al inicio del archivo; ruff te lo va a marcar si no). Corre uv run pytest: rojo. Ahora la función, en src/hola/core.py:
src/hola/core.py (agregar al final)COMPLEMENTO = {"A": "T", "T": "A", "G": "C", "C": "G"}
def reverse_complement(seq: str) -> str:
"""Complemento reverso de una secuencia de ADN (5'→3')."""
seq = seq.upper()
invalidas = set(seq) - set(COMPLEMENTO)
if invalidas:
raise ValueError(f"bases inválidas: {sorted(invalidas)}")
return "".join(COMPLEMENTO[base] for base in reversed(seq))
uv run pytest -q
uv run ruff check . && uv run ruff format .
docker build -t hola . && docker run --rm hola
Deberías ver7 passed in 0.01s (y lo mismo dentro de Docker)
git add .
git commit -m "Agrega reverse_complement con validación de bases"
git push -u origin feat/reverse-complement
gh pr create --title "Agrega reverse_complement" --body "Segunda función del P0. Valida bases y devuelve el complemento reverso. Incluye 3 tests." --reviewer rotorrest
Avísale a Rodrigo. Él va a dejar comentarios en el PR (en GitHub, pestaña Files changed). Responde cada uno: o cambias el código y haces push a la misma rama (el PR se actualiza solo), o explicas por qué no. Cuando apruebe, mergea desde GitHub y trae el resultado a tu máquina:
git switch main
git pull
git log --oneline
Este ciclo es el trabajo. Rama → commits → push → PR → revisión → merge → pull. Lo vas a hacer en cada proyecto, varias veces. Aquí lo hiciste con siete líneas de código para que la única cosa nueva sea el ciclo.
Entiende el código
No sigas al Proyecto 1 hasta poder explicar cada una de estas piezas con tus palabras.
Estructura
hola-alexandra/
├── .python-version versión de Python del proyecto (la lee uv)
├── pyproject.toml nombre, versión, dependencias, config de pytest y ruff
├── uv.lock versiones exactas instaladas (se versiona)
├── .venv/ entorno virtual (NO se versiona; se regenera con uv sync)
├── src/hola/
│ ├── __init__.py marca la carpeta como paquete
│ └── core.py la lógica: funciones puras
├── tests/test_core.py los controles
├── Dockerfile receta de la imagen
├── .dockerignore lo que no entra a la imagen
├── README.md el protocolo
└── DECISIONES.md el porqué de lo no obvio
pyproject.toml, sección por sección
[project]: identidad del paquete. requires-python es una promesa: este código asume 3.12 o más.
[build-system]: qué herramienta convierte tu carpeta en un paquete instalable (hatchling). Sin esto, uv add no instala tu propio paquete y el import hola.core falla desde tests/.
[tool.hatch.build.targets.wheel]: dónde está el código (src/hola). Es lo que hace que from hola.core import … resuelva.
[tool.pytest.ini_options]: dónde buscar tests. [tool.ruff]: largo de línea. Cada herramienta lee su sección de este mismo archivo: un solo lugar de configuración.
[dependency-groups] dev (lo agregó uv add --dev): herramientas que necesitas para desarrollar, no para que el paquete funcione. Un usuario de hola no necesita pytest.
Por qué src/
Si el código estuviera en la raíz (hola/core.py al lado de tests/), Python lo encontraría "por casualidad" desde la carpeta actual, y los tests pasarían aunque el paquete estuviera mal instalado. Con src/, la única forma de importar hola es tenerlo instalado en el entorno, igual que lo tendría un usuario. Es una restricción que te protege de un "funciona en mi máquina".
Anatomía de un test
tests/test_core.pydef test_gc_content_mitad(): # 1. nombre: empieza con test_, dice qué caso prueba
assert gc_content("ATGC") == 0.5 # 2. arrange + act + assert en una línea: entrada conocida, salida esperada
def test_gc_content_secuencia_vacia_lanza_error():
with pytest.raises(ValueError): # 3. "dentro de este bloque DEBE saltar ValueError; si no salta, el test falla"
gc_content("")
pytest descubre solo las funciones que empiezan con test_ en archivos que empiezan con test_. Un assert que da False = test fallido, y pytest te muestra los dos lados de la comparación. No hay más magia que esa.
La función
seq: str) -> float: anotaciones. No cambian cómo corre, pero documentan el contrato y VS Code las usa para avisarte errores antes de correr.
if not seq: raise ValueError(...): validar en el borde y fallar temprano con un mensaje que dice qué pasó. Sin esto, gc / len(seq) daría ZeroDivisionError: también falla, pero con un mensaje que no explica el problema real.
- La función no lee archivos, no imprime, no pide nada al usuario. Entra un
str, sale un float. Por eso testearla es trivial. Esta separación entre lógica y entrada/salida es el concepto central del Proyecto 1.
El Dockerfile, línea por línea
FROM python:3.12-slim: parto de una imagen que ya tiene Python 3.12 sobre un Debian mínimo. Nunca instalo Python a mano dentro de Docker.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv: copio el ejecutable de uv desde su imagen oficial. Un solo archivo, sin pip.
WORKDIR /app: de aquí en adelante todo pasa en /app dentro de la imagen.
COPY pyproject.toml uv.lock ./ antes que COPY src: Docker cachea cada capa. Si solo cambias código, la capa de dependencias no se reconstruye. Es una optimización que verás en todos los Dockerfiles serios.
RUN uv sync --frozen: instala exactamente lo que dice uv.lock. --frozen falla si el lock no coincide con pyproject.toml: mejor fallar en el build que instalar algo distinto en silencio.
CMD [...]: qué corre el contenedor si no le dices otra cosa. Se puede sobreescribir: docker run --rm hola uv run python -c "from hola.core import gc_content; print(gc_content('GGGA'))".