Módulo 00 · Cómo piensa Python¶
Prerrequisitos: ninguno
Tiempo estimado: 180 min
Si ya dominas esto: salta al módulo 01
Este módulo no enseña sintaxis. Enseña dos cosas: qué es un programa y con qué modelos mentales razona un pythonista sobre cualquier código. Eso es lo que separa a quien traduce sintaxis de otro lenguaje de quien piensa en este.
Puedes saltártelo y empezar por el 01. Mucha gente lo hace y aprende a programar igual. Pero cuando en el módulo 09 te encuentres un grafo de estado de LangGraph, la diferencia entre "esto es magia que memorizo" y "esto es el protocolo de iteración otra vez" se decide aquí.
Qué vas a poder hacer al terminar¶
- Explicar qué hace un ordenador cuando ejecuta tu programa
- Distinguir compilación de interpretación y saber qué implica cada una
- Explicar por qué una variable es una etiqueta y no una caja
- Reconocer código idiomático y el acento de otros lenguajes
- Usar los protocolos para integrar tus tipos con el lenguaje
- Elegir entre EAFP y LBYL con criterio
- Diagnosticar errores de nombres con la regla LEGB
1. Qué es un programa, en realidad¶
Un ordenador sin programa es un objeto, igual que un piano sin pianista es una caja de madera. Y lo que hace un ordenador es mucho más simple de lo que parece: solo sabe ejecutar operaciones elementales —sumar, dividir, comparar, mover un dato de un sitio a otro— pero las hace muy rápido y las repite cuantas veces haga falta.
Supón que quieres la velocidad media de un viaje. Sabes la distancia y el tiempo. El ordenador no tiene ni idea de qué es la velocidad, así que hay que decírselo paso a paso:
- toma un número que representa la distancia
- toma un número que representa el tiempo
- divide el primero entre el segundo y guarda el resultado
- muestra ese resultado
Esas cuatro acciones son un programa. Y el punto que conviene retener: programar no es explicarle un concepto al ordenador, es descomponer ese concepto en operaciones que ya sabe hacer.
El lenguaje de la máquina¶
El conjunto completo de operaciones que un procesador reconoce se llama su lista de instrucciones, y es su alfabeto. Es rudimentario: "coge ese número, divídelo por aquel, guarda el resultado".
Todo lenguaje —humano o de máquina— se compone de cuatro cosas:
| Elemento | Qué es | Ejemplo de error |
|---|---|---|
| Alfabeto | los símbolos disponibles | escribir en un alfabeto que el lenguaje no reconoce |
| Léxico | las palabras que existen | pritn(...) — esa palabra no está en el diccionario |
| Sintaxis | cómo se combinan | if x = 3: — el orden no forma una frase válida |
| Semántica | si la frase tiene sentido | edad = "hola" / 2 — es válido de escribir y no significa nada |
Los cuatro tipos de error existen en programación, y cada uno se descubre en un momento distinto. Los tres primeros los caza el intérprete antes o al arrancar. El cuarto lo descubres tú, en producción, cuando el resultado no cuadra. Por eso este repositorio insiste tanto en los tests: son la única red para la cuarta categoría.
Un programa escrito en un lenguaje que un humano puede leer se llama código fuente. Lo que ejecuta la máquina es otra cosa, y algo tiene que traducir.
2. Compilar o interpretar¶
Hay dos formas de pasar del código fuente al lenguaje de la máquina:
Compilar. Se traduce el programa entero, una vez, y sale un archivo ejecutable. Se distribuye ese archivo y se ejecuta directamente.
Interpretar. No hay traducción previa: un programa —el intérprete— lee tu código y lo va ejecutando línea a línea, cada vez que corres el programa.
Ninguna de las dos es mejor; tienen contratos distintos:
| Compilado | Interpretado | |
|---|---|---|
| Velocidad de ejecución | alta: ya está traducido | menor: se traduce sobre la marcha |
| Ver el resultado de un cambio | hay que recompilar | ejecutas y ya |
| Errores de sintaxis | todos, antes de ejecutar | cuando la línea se alcanza |
| Distribuir | un ejecutable por plataforma | el código, y el intérprete en destino |
| Ocultar el código | sí | no |
Python es interpretado, y de ahí salen tres consecuencias que vas a notar todos los días:
- El ciclo es corto. Escribes, ejecutas, ves el resultado. Sin paso de compilación. Es la razón principal de que Python domine el prototipado y la ciencia de datos.
- Un error de sintaxis en la línea 200 no impide que se ejecuten las 199 primeras. El programa arranca y revienta al llegar. En un lenguaje compilado no habrías podido ni ejecutarlo.
- Es más lento. Bastante. Y esto explica la anomalía que más desconcierta a quien llega: el stack de IA está escrito en Python siendo Python lento.
La resolución de esa paradoja: NumPy, PyTorch y compañía no ejecutan Python en el bucle caliente. Ejecutan C, CUDA y Rust. Python es la capa donde un humano describe qué quiere. Es el lenguaje de coordinación, no el de cálculo.
Tenlo presente durante toda la ruta: casi nunca vas a escribir Python rápido. Vas a escribir Python claro que orquesta cosas rápidas.
Hay más de un Python¶
Cuando alguien dice "Python" puede referirse a dos cosas distintas: al lenguaje (las reglas) o a una implementación (un programa concreto que las ejecuta).
- CPython es la implementación de referencia, escrita en C. Es la que instalas por defecto y la que usa este repositorio.
- PyPy ejecuta el mismo lenguaje con compilación al vuelo: mucho más rápido en código Python puro, y peor integrado con extensiones en C.
- Existen otras (Jython, IronPython, MicroPython) para entornos concretos.
Esta distinción no es trivia: cuando en el módulo 06 hablemos del GIL, verás que es una característica de CPython, no del lenguaje. Confundir el lenguaje con su implementación lleva a conclusiones equivocadas sobre qué se puede cambiar y qué no.
3. Un lenguaje que nadie planeó¶
Python no nació de un comité ni de una empresa. Guido van Rossum lo empezó en las navidades de 1989 como proyecto personal, arrastrando la lección de un lenguaje anterior llamado ABC: ABC era elegante y pedagógicamente brillante, pero cerrado — no podías extenderlo ni conectarlo con el sistema operativo, así que nadie lo usó para trabajar de verdad.
Python heredó la legibilidad de ABC y corrigió su error: se diseñó para ser extensible y para hablar con el resto del mundo. Esa decisión —poder envolver bibliotecas escritas en C— es exactamente la que treinta años después lo convirtió en el lenguaje de la IA.
4. El Zen como criterio de decisión¶
Escribe esto en un intérprete:
Salen diecinueve aforismos (PEP 20). Leídos como póster de oficina son perogrulladas. Leídos como criterios de decisión son una herramienta de ingeniería: deciden qué código pasa una revisión y qué API se considera bien diseñada. Los cinco que más consecuencias tienen:
Explicit is better than implicit¶
No prohíbe abstraer; prohíbe la magia que no puedes rastrear.
# Opaco: ¿de dónde sale `settings`?
from app.config import *
# Rastreable: puedes seguir el hilo hasta la definición
from app.config import settings
El criterio no es "poca abstracción", es rastreabilidad. Un decorador visible encima de la función es explícito. Un parche aplicado en otro módulo al importarse, no.
El mismo principio explica decisiones profundas del lenguaje: el self
explícito en los métodos, que no haya conversiones automáticas entre tipos
("1" + 1 es un error, no un "11" sorpresa como en JavaScript), y que haga
falta declarar global para reasignar una variable de fuera.
Simple is better than complex. Complex is better than complicated.¶
Tres niveles que conviene distinguir, porque el uso coloquial los confunde:
| Nivel | Qué es | Veredicto |
|---|---|---|
| Simple | las mínimas partes móviles para el problema real | ideal |
| Complejo | muchas partes, cada una justificada por el dominio | aceptable |
| Complicado | partes que existen por historia, moda o descuido | deuda técnica |
La complejidad esencial (un motor de conciliación bancaria lo es) se gestiona. La accidental (tres capas de indirección porque "así lo hace Netflix") se elimina. Distinguirlas es probablemente el criterio de diseño más importante de toda la ingeniería de software.
Errors should never pass silently. Unless explicitly silenced.¶
# Condenado: esto oculta desde un error de tipo hasta una caída de red
try:
process_payment(order)
except Exception:
pass
# Aprobado: excepción concreta, tratada, y con rastro
try:
process_payment(order)
except PaymentDeclinedError as exc:
logger.info("Pago rechazado para la orden %s: %s", order.id, exc)
order.mark_declined(reason=str(exc))
La regla que se deriva: captura la excepción más específica posible, tan cerca como puedas de donde sabes qué hacer con ella, y nunca sin dejar rastro.
There should be one obvious way to do it¶
Es una declaración de guerra contra el "cada uno a su manera" (y contra el lema opuesto de Perl). Para cada tarea común hay una forma canónica, y apartarse de ella tiene coste social y técnico.
# Acento extranjero: correcto, pero delata que vienes de C o Java
squares = []
i = 0
while i < 10:
if i % 2 == 0:
squares.append(i * i)
i += 1
# Idiomático
squares = [n * n for n in range(10) if n % 2 == 0]
Los dos funcionan. Solo uno pasa una revisión sin comentarios. Y no es estética: el segundo elimina dos fuentes de bugs —el contador manual y la condición de parada— y comunica la intención (transformar filtrando) en vez del mecanismo (iterar mutando).
Namespaces are one honking great idea¶
El último aforismo es una pista sobre la arquitectura interna: en Python casi todo mecanismo de organización es un espacio de nombres —módulos, paquetes, clases, instancias, ámbitos de función— es decir, un mapeo de nombres a objetos. Volveremos a ello en el modelo mental 5.
5. Modelo mental 1: todo es un objeto¶
En Python no hay ciudadanos de segunda. Los enteros son objetos. Las funciones son objetos. Las clases son objetos. Los módulos son objetos.
def greet(name: str) -> str:
return f"Hola, {name}"
print(greet.__name__) # 'greet' — tiene atributos
print(greet.__annotations__) # {'name': <class 'str'>, 'return': <class 'str'>}
handlers = {"greeting": greet} # cabe en una estructura de datos
def twice(func, value): # se pasa como argumento
return func(func(value))
Que las funciones sean objetos de primera clase es lo que hace posibles los decoradores, los callbacks y media programación funcional. Que las clases sean objetos es lo que hace posibles las factorías y los registros de plugins.
Cuando algo en Python te parezca magia, la primera pregunta correcta es: ¿qué
objeto es esto y qué atributos tiene? Casi siempre la magia se disuelve ahí.
Las herramientas para preguntarlo son type(), dir() y vars().
6. Modelo mental 2: nombres, no cajas¶
La metáfora escolar —una variable es una caja que guarda un valor— produce predicciones equivocadas en Python de forma sistemática. La metáfora correcta es la etiqueta: un nombre atado a un objeto, y varios nombres pueden estar atados al mismo objeto.
a = [1, 2, 3]
b = a # b NO es una copia: es otra etiqueta sobre el MISMO objeto
b.append(4)
print(a) # [1, 2, 3, 4] ← solo sorprende si piensas en cajas
Modelo "caja" (incorrecto) Modelo "etiqueta" (correcto)
a ┌─────────┐ b ┌─────────┐ a ──┐
│ [1,2,3] │ │ [1,2,3] │ ├──▶ [1, 2, 3, 4]
└─────────┘ └─────────┘ b ──┘
(dos objetos) (un objeto, dos nombres)
De este modelo se deducen, sin memorizar nada, media docena de preguntas que
llenan foros: por qué copiar una lista exige list(otra) o otra[:], qué
diferencia hay entre == (mismo valor) e is (mismo objeto), y por qué "Python
pasa por referencia" y "Python pasa por valor" son las dos descripciones
incorrectas. El término preciso es paso por asignación.
La trampa más cara que se deriva de no tener este modelo:
# El valor por defecto se crea UNA vez, al definir la función.
def add_item(item, basket=[]): # ← bug
basket.append(item)
return basket
add_item("pan") # ['pan']
add_item("leche") # ['pan', 'leche'] ← el mismo objeto de la llamada anterior
Lo arreglarás tú mismo en el ejercicio carrito.
7. Modelo mental 3: protocolos, no jerarquías¶
Quien viene de Java pregunta: ¿qué interfaz implementa este objeto? Un
pythonista pregunta: ¿qué métodos especiales define? El lenguaje entero
está construido sobre protocolos: contratos de métodos __dunder__ que
cualquier clase puede adoptar sin pedirle permiso a ninguna jerarquía.
| Si tu objeto define… | …habla el protocolo | …y funciona con |
|---|---|---|
__len__ |
tamaño | len(x) |
__bool__ |
verdad | if x: |
__iter__ / __next__ |
iteración | for, comprehensions, sum, max |
__getitem__ |
indexación | x[i], slicing |
__contains__ |
pertenencia | x in y |
__enter__ / __exit__ |
contexto | with |
__call__ |
invocación | x(...) |
__eq__, __lt__ |
comparación | ==, sorted, min |
__add__, __mul__ |
aritmética | +, * |
Los protocolos además encadenan: el valor de verdad de un objeto se resuelve
preguntando primero por __bool__; si no está, por __len__; y si tampoco,
es verdadero. Eso explica por qué una lista vacía es falsa sin que nadie haya
escrito una regla especial para las listas.
La consecuencia estratégica: tus tipos se integran con la sintaxis del lenguaje
y con toda la biblioteca estándar simplemente hablando el protocolo
adecuado. NumPy, Pandas y PyTorch son, vistos así, colecciones enormes de
objetos que hablan los protocolos de aritmética, indexación e iteración — por
eso matriz_a + matriz_b y tensor[mask] se sienten parte del lenguaje.
8. Modelo mental 4: la iteración es la abstracción central¶
Si hubiera que elegir una sola abstracción como corazón de Python, sería el
iterable. El for de Python no es el for de C (un contador con condición de
parada): es una petición de protocolo — dame tus elementos, uno a uno, hasta
que te agotes.
for char in "python": ... # caracteres de un texto
for line in open("data.log"): ... # líneas de un archivo, sin cargarlo entero
for row in db_cursor: ... # filas de una consulta SQL
for chunk in llm_stream: ... # tokens de un LLM en streaming
El mismo patrón sobre cuatro cosas radicalmente distintas. De aquí nacen las
comprehensions, los generadores (procesar gigabytes con memoria constante),
itertools, y por extensión directa el async for y los streams asíncronos que
dominan el backend moderno y los sistemas de agentes.
En Python, pensar un problema es muy a menudo pensar qué fluye y cómo se transforma ese flujo.
9. Modelo mental 5: todo nombre vive en un espacio de nombres¶
Cuando Python encuentra el nombre x, lo busca en una cadena ordenada de
diccionarios: Local, Enclosing (funciones que la envuelven), Global
del módulo y Builtin del lenguaje. Es la regla LEGB.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local → si no existiera: enclosing → global → builtin
inner()
Para atributos vale lo mismo con otra cadena: obj.attr se busca en el
__dict__ de la instancia, luego en el de su clase, luego en las clases base.
No hay excepciones ocultas: toda resolución de nombres es un recorrido
predecible sobre diccionarios que puedes inspeccionar.
El valor de este modelo es diagnóstico: convierte los errores más confusos para
quien empieza —UnboundLocalError, haber llamado list a una variable y romper
la función list, atributos "que desaparecen"— en recorridos mecánicos que se
razonan en segundos con vars(), dir() y __dict__.
Y explica una regla que si no parece arbitraria: para reasignar una variable
de fuera desde dentro de una función hay que declararlo (global o nonlocal).
Sin esa declaración, asignar crea una variable local nueva. Lo vas a usar en el
ejercicio contador.
10. Duck typing, EAFP y batteries included¶
Duck typing¶
Si camina como un pato y grazna como un pato, es un pato. La pregunta relevante
sobre un objeto no es de qué clase es sino qué sabe hacer. Una función que
necesita algo iterable no debe exigir una list:
from collections.abc import Iterable
def total_amount(payments: Iterable[float]) -> float:
return sum(payments)
Funciona igual con una lista en memoria, con un generador que lee un CSV de 10 GB línea a línea o con un cursor que pagina PostgreSQL.
Dónde no confiar en duck typing: en las fronteras del sistema —entrada de usuario, payload de red, datos de terceros— donde un objeto con la forma equivocada debe rechazarse pronto y con un mensaje claro, en vez de propagarse hasta reventar tres capas más adentro. Ahí entra la validación explícita, que verás con Pydantic en el módulo 07.
EAFP frente a LBYL¶
Dos formas de tratar lo incierto: mirar antes de saltar (Look Before You Leap) o actuar y pedir perdón (Easier to Ask Forgiveness than Permission).
# LBYL
if "user_id" in payload:
user_id = payload["user_id"]
else:
user_id = None
# EAFP — idiomático
try:
user_id = payload["user_id"]
except KeyError:
user_id = None
# ...que en este caso concreto se condensa
user_id = payload.get("user_id")
Python prefiere EAFP por dos razones técnicas, no estéticas:
- Elimina condiciones de carrera. Entre comprobar que un archivo existe y
abrirlo puede pasar tiempo en el que otro proceso lo borra.
try: open(...)no tiene esa ventana;if os.path.exists(...)sí. En sistemas concurrentes esto es una clase entera de bugs. - Optimiza el camino feliz. Si la clave casi siempre está, evitas una comprobación en cada llamada.
Cuándo preferir LBYL: cuando el fallo es frecuente y esperado (lanzar excepciones sí cuesta), cuando la comprobación se lee mejor para el caso de negocio, o al validar argumentos al principio de una función pública.
Batteries included¶
Python trae más de doscientos módulos de serie. El orden correcto de búsqueda ante una necesidad nueva es: (1) biblioteca estándar, (2) paquete maduro y mantenido, (3) código propio. Invertirlo produce árboles de dependencias frágiles, superficie de ataque en la cadena de suministro y proyectos que envejecen mal. Cada dependencia es un contrato de mantenimiento que firmas con un desconocido.
Caso real¶
Estás diseñando el SDK de un servicio de pagos. Dos borradores para crear un cargo:
# Borrador A — configuración implícita, estado global
import paysdk
paysdk.init("sk_live_...")
charge = paysdk.charge(1000, "USD")
# Borrador B — dependencias explícitas
from paysdk import Client
client = Client(api_key="sk_live_...", timeout=10.0, max_retries=3)
charge = client.charges.create(amount=1000, currency="USD")
El Zen decide sin ambigüedad, y no por gusto:
- Explícito: en B, las credenciales y la configuración tienen un origen
rastreable. En A,
chargeusa un estado global que se puso en otro sitio. - Namespaces:
client.charges.createorganiza la superficie de la API en espacios navegables; con autocompletado descubres qué existe. - Testabilidad: un
Clientse inyecta y se sustituye en un test. Un módulo con estado global se parchea con dolor.
Esto es lo que quiere decir que la filosofía es operativa: no fue decoración, fue el árbol de decisión.
Ejercicios¶
Los verás en rojo: los ejercicios están sin resolver, y ese es el estado correcto de partida.
ejercicios/base/idiomatico.py— reescribir un bucle con acento extranjero en la forma idiomática.ejercicios/base/carrito.py— arreglar la trampa del argumento mutable por defecto. Aquí compruebas si de verdad tienes el modelo de etiquetas.ejercicios/base/protocolo.py— la cascada del valor de verdad:__bool__,__len__y qué pasa cuando no hay ninguno.ejercicios/reto/baraja.py— hacer que una clase tuya hable los protocolos del lenguaje. Sale más corto de lo que esperas.ejercicios/reto/contador.py— closures ynonlocal: la regla LEGB en acción, y por qué asignar no es lo mismo que leer.
Resumen¶
- Un ordenador solo hace operaciones elementales, muy rápido. Programar es descomponer un concepto en operaciones que ya sabe hacer.
- Todo lenguaje tiene alfabeto, léxico, sintaxis y semántica. Los tres primeros errores los caza el intérprete; el semántico lo descubres tú.
- Python es interpretado: ciclo corto, errores que aparecen al alcanzarlos, y más lento. Por eso es la capa de coordinación, no la de cálculo.
- "Python" es un lenguaje; CPython es una implementación. El GIL es de CPython.
- El Zen no es decoración: es el árbol de decisión de una revisión de código.
- Distingue complejidad esencial (se gestiona) de accidental (se elimina).
- Todo es un objeto. Ante algo que parece magia: ¿qué objeto es y qué atributos tiene?
- Nombres, no cajas. Varias etiquetas pueden apuntar al mismo objeto; de ahí salen los bugs de aliasing y el del argumento mutable por defecto.
- Protocolos, no jerarquías. Tus tipos se integran con el lenguaje hablando
los
__dunder__adecuados, y los protocolos encadenan. - La iteración es el corazón. El mismo
forrecorre un texto, un archivo, un cursor de base de datos y un stream de LLM. - Todo nombre se resuelve en un espacio de nombres, siguiendo LEGB. Es una herramienta de diagnóstico, no un dato de examen.
- EAFP por defecto; LBYL cuando el fallo es frecuente y esperado.
- Biblioteca estándar primero. Cada dependencia es un contrato con un desconocido.
Preguntas de repaso¶
- ¿Cuál de los cuatro tipos de error de un lenguaje no puede detectar el intérprete, y qué haces al respecto?
- Tu programa tiene un error de sintaxis en la línea 200. ¿Qué pasa al ejecutarlo en Python, y qué habría pasado en un lenguaje compilado?
- Si Python es lento, ¿por qué el stack de IA está escrito en Python?
- ¿Qué diferencia hay entre "el lenguaje Python" y "CPython", y por qué importa esa distinción?
a = [1, 2]; b = a; c = a[:]— después deb.append(3), ¿qué valena,byc? Explícalo con el modelo de etiquetas, sin ejecutarlo.- ¿Por qué una lista vacía es falsa sin que exista una regla especial para las listas?
- ¿Por qué
except Exception: passes peor que no capturar nada? - Una función necesita "algo con lo que iterar". ¿Por qué anotarla como
listes una mala decisión, y qué anotarías en su lugar? - Da un caso donde LBYL sea preferible a EAFP y explica por qué.
- ¿Qué tienen en común un archivo abierto, un cursor de base de datos y la respuesta en streaming de un LLM?
Recursos¶
- El Zen de Python (PEP 20) —
doc-oficial·en·principiante. Diecinueve líneas; se lee en un minuto y se entiende en toda una carrera. - PEP 8 — Guía de estilo —
doc-oficial·en·principiante. Ruff la aplica por ti, pero conviene saber de dónde salen las reglas. - Modelo de datos de Python
—
doc-oficial·es·avanzado. La referencia de todos los protocolos. No se lee de corrido: se consulta. - Ejecución de programas
—
doc-oficial·es·avanzado. Cómo se resuelven los nombres, con la precisión de una especificación.
Más enlaces por tema en recursos/enlaces/.
Siguiente¶
Módulo 01 · Fundamentos, donde estos modelos se convierten en sintaxis.