M1 — Python idiomático
Outcome del módulo: escribir Python que un senior firmaría, y detectar código no-idiomático a simple vista.
Plantilla de concepto (formato del curso)
Cada concepto sigue esta estructura. Copiala para cualquier concepto nuevo:
### <Concepto>
Idea núcleo (1 línea)
── Entender (capa 1) ──
· Texto : explicación mínima
· Visual : diagrama (Mermaid)
· Video : canal/búsqueda recomendada
· Ejemplo : código trabajado (mal → bien)
── Fijar (capa 2) ──
· Nota atómica: front / back (preguntable)
· Feynman : explicalo como a un junior
── Aplicar (capa 3) ──
· Micro-ejercicio
── Límites ──
· Cuándo NO usarlo / qué se rompe
Fuentes de video recomendadas (verificar antes de fijar como oficiales): canales mCoding, ArjanCodes, Real Python (YouTube) — idioms y diseño Python de nivel serio. Marcar [verificar] hasta confirmar el video puntual.
Concepto 1 — Generadores (evaluación perezosa)
Idea núcleo: un generador produce valores de a uno, bajo demanda, sin cargar todo en memoria.
Entender (capa 1)
Texto: una lista calcula y guarda todos sus elementos ya. Un generador (yield o (x for x in ...)) calcula el siguiente solo cuando se lo pedís. Para 10 millones de filas: la lista te revienta la RAM; el generador procesa una a la vez.
Visual:
flowchart LR
subgraph Lista["Lista (eager)"]
A[calcula TODO] --> B[guarda TODO en RAM] --> C[itera]
end
subgraph Gen["Generador (lazy)"]
D[pide 1] --> E[calcula 1] --> F[entrega 1] --> D
end
Video: Python Generators — mCoding
Ejemplo (mal → bien):
def leer_ids_mal(filas):
resultado = []
for f in filas:
resultado.append(f["id"])
return resultado # carga TODO en memoria
def leer_ids_bien(filas):
for f in filas:
yield f["id"] # una a la vez, memoria constante
# uso idéntico, memoria radicalmente distinta
for id_ in leer_ids_bien(millones_de_filas):
procesar(id_)
Fijar (capa 2)
Nota atómica:
- Front: ¿Cuándo usar generador en vez de lista?
- Back: Cuando iterás una sola vez y/o el dataset no entra (o no conviene) en memoria. Generador = memoria constante, lazy. Lista = acceso repetido/aleatorio,
len(), slicing.
Feynman: “Una lista es cocinar todo el buffet antes de que llegue nadie. Un generador es cocinar cada plato cuando el comensal lo pide. Si son 10.000 comensales, el buffet no cabe en la cocina.”
Aplicar (capa 3)
Escribí una función generadora que lea un archivo grande línea por línea y entregue solo las que matchean un patrón, sin cargar el archivo entero.
Límites
- No uses generador si necesitás recorrer los datos varias veces (se agota tras una pasada) o necesitás
len()/índice. - Se consume una vez:
list(gen)dos veces = la segunda da vacío.
Concepto 2 — El GIL, async y multiprocessing (el trío de entrevista)
Idea núcleo: el GIL deja correr un solo hilo de bytecode Python a la vez. Por eso async/threads aceleran I/O-bound pero no CPU-bound; ahí va multiprocessing.
Entender (capa 1)
Texto: el Global Interpreter Lock serializa la ejecución de bytecode. Dos tareas:
- I/O-bound (esperar red/disco/DB): mientras una espera, otra avanza →
async(un hilo cooperativo) o threads ganan. En threads, el GIL se libera durante la espera de I/O. - CPU-bound (cálculo puro): ni
asyncni threads aceleran, por razones distintas —asyncioes un solo hilo cooperativo (no paraleliza aunque el GIL no existiera); los threads sí correrían en paralelo pero el GIL los serializa. Solución real =multiprocessing(varios procesos, cada uno con su intérprete y su GIL).
Visual:
flowchart TD
T{¿espera I/O o calcula CPU?} -->|I/O-bound| IO[async o threads<br/>✓ acelera]
T -->|CPU-bound| CPU[multiprocessing<br/>✓ acelera]
CPU -.->|async/threads NO sirven acá| BAD[✗ asyncio = 1 hilo · threads = GIL los serializa]
Video: Python’s Global Interpreter Lock (GIL): Concurrency, Threading & Multitasking — Real Python
Ejemplo:
# I/O-bound: async ACELERA (mientras una espera la red, otra corre)
async def fetch_all(urls):
async with aiohttp.ClientSession() as s:
return await asyncio.gather(*(s.get(u) for u in urls))
# CPU-bound: async NO ayuda; procesos SÍ
from multiprocessing import Pool
def hash_pesado(x): ...
with Pool() as p:
resultados = p.map(hash_pesado, datos)
Fijar (capa 2)
Nota atómica:
- Front: ¿Por qué
asyncno acelera una tarea CPU-bound? - Back: Porque el GIL solo deja ejecutar un hilo de bytecode a la vez. En I/O el GIL se libera durante la espera (async gana); en CPU no hay espera, el cálculo compite por el GIL → no hay paralelismo real. Solución CPU-bound =
multiprocessing(cada proceso su propio GIL).
Feynman: “El GIL es un solo micrófono en una sala. Si todos esperan una llamada telefónica (I/O), se pasan el micro y avanza el grupo. Si todos tienen que hablar sin parar (CPU), el micro único los hace esperar en fila igual. Solución: darle a cada uno su propia sala (proceso).”
Aplicar (capa 3)
Tomá una función que sume/calcule sobre una lista enorme. Medí (time) con un loop normal, con asyncio, y con multiprocessing.Pool. Comprobá vos mismo cuál acelera y cuál no.
Límites
multiprocessingtiene costo: serializar datos entre procesos (pickle) + arranque. Para tareas chicas, el overhead > la ganancia.- El GIL cambió en versiones recientes (free-threading experimental) — mencionarlo muestra que estás al día [verificar versión].
Concepto 3 — Context managers (with)
Idea núcleo: garantizan setup/teardown (abrir/cerrar, adquirir/liberar) aunque haya excepción.
Entender: with llama __enter__ al entrar y __exit__ al salir, siempre (incluso si explota). Reemplaza el try/finally repetitivo.
# frágil: si algo explota entre open y close, el archivo queda abierto
f = open("x"); data = f.read(); f.close()
# idiomático: se cierra pase lo que pase
with open("x") as f:
data = f.read()
Crear el tuyo con contextlib.contextmanager:
from contextlib import contextmanager
@contextmanager
def timer():
import time; t = time.perf_counter()
try:
yield
finally: # corre aun si el bloque lanza excepción
print(f"{time.perf_counter()-t:.3f}s")
Video: Building A Custom Context Manager In Python: A Closer Look — ArjanCodes
Nota atómica: Front: ¿qué garantiza with que un try/finally manual olvida? Back: el teardown corre siempre, aun con excepción; menos código, imposible olvidar el cierre.
Feynman: “Es un contrato: al entrar te presto algo, al salir lo recupero sí o sí, aunque te caigas.”
Aplicar: escribí un context manager que abra una transacción de DB y haga commit al salir bien / rollback si hay excepción.
Límites: no lo fuerces donde no hay un par adquirir/liberar real.
Concepto 4 — Decorators
Idea núcleo: una función que envuelve a otra para agregarle comportamiento sin tocar su código.
Entender:
from functools import wraps
def log_calls(fn):
@wraps(fn) # preserva nombre/docstring del original
def wrapper(*args, **kwargs):
print(f"llamando {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
@log_calls
def sumar(a, b): return a + b
Visual:
flowchart LR
call[llamada] --> wrapper[wrapper: antes] --> orig[función original] --> after[wrapper: después] --> ret[retorno]
Video: Python Decorators: The Complete Guide — ArjanCodes
Nota atómica: Front: ¿para qué sirve functools.wraps? Back: copia metadata (__name__, __doc__) de la función original al wrapper; sin él, el debug y las tools ven “wrapper”.
Feynman: “Es papel de regalo con lógica: la caja adentro es la misma función, pero el envoltorio hace algo antes/después.”
Aplicar: decorator @retry(3) que reintente una función hasta 3 veces si lanza excepción.
Límites: demasiados decorators apilados = magia difícil de debuggear.
Concepto 5 — Typing + dataclasses/pydantic
Idea núcleo: los type hints documentan y habilitan chequeo estático; dataclasses/pydantic modelan datos sin boilerplate.
Entender:
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
active: bool = True # sin __init__ manual, con __eq__/__repr__ gratis
dataclass= estructura de datos en memoria, sin validación.pydantic= igual + valida y castea en runtime (ideal en bordes: API input, config).- Typing útil:
Optional[X],list[X],Protocol(duck typing tipado), generics. Video: Why Python Needs Pydantic for Real Applications — ArjanCodes Nota atómica: Front: ¿dataclass o pydantic? Back: dataclass para datos internos de confianza; pydantic cuando el dato viene de afuera y hay que validar/castear (API, config, DB rows externas). Feynman: “dataclass es una ficha en blanco donde vos ponés datos correctos. pydantic es una ficha con un guardia en la puerta que rechaza datos mal formados.” Aplicar: modelá un request de API con pydantic que rechace un email inválido. Límites: pydantic valida en runtime (costo); no lo pongas en un hot loop interno donde el dato ya es confiable.
Concepto 6 — Trampas clásicas (bugs que todos pisan una vez)
Idea núcleo: tres comportamientos contra-intuitivos que delatan al que no domina Python.
Entender:
# 1) Mutable default argument: el default se crea UNA vez, se comparte entre llamadas
def add(x, acc=[]): # ❌ acc persiste entre llamadas
acc.append(x); return acc
def add(x, acc=None): # ✅
acc = [] if acc is None else acc # NO uses `acc or []`: descartaría una lista vacía pasada aposta
acc.append(x); return acc
# 2) Late binding en closures
fns = [lambda: i for i in range(3)] # ❌ todas devuelven 2
fns = [lambda i=i: i for i in range(3)]# ✅ captura por valor
# 3) is vs ==
a == b # ¿mismo valor?
a is b # ¿mismo objeto en memoria? (usar solo con None: x is None)
Nota atómica: Front: ¿por qué def f(x=[]) es un bug? Back: el default se evalúa una sola vez al definir la función; la misma lista se reusa en cada llamada → estado compartido inesperado. Usar None centinela.
Feynman: “El default mutable es como un carrito de compras que creés que es nuevo cada vez, pero es el mismo carrito para todos los clientes.”
Aplicar: reproducí los tres bugs, después arreglalos. Escribí el porqué de cada uno.
Límites: is solo para singletons (None, True, False), nunca para comparar valores.
Concepto 7 — pytest en serio
Idea núcleo: tests legibles con fixtures (setup reusable), parametrize (mismos asserts, muchos inputs) y mocking (aislar dependencias).
Entender:
import pytest
@pytest.fixture
def db(): # setup reusable, se inyecta por nombre
conn = crear_db_temporal(); yield conn; conn.close()
@pytest.mark.parametrize("entrada,esperado", [(2,4),(3,9),(4,16)])
def test_cuadrado(entrada, esperado):
assert cuadrado(entrada) == esperado
def test_llama_api(mocker): # aislar la red
m = mocker.patch("modulo.requests.get")
...
Video: Pytest Tutorial – How to Test Python Code — freeCodeCamp.org
Nota atómica: Front: ¿qué resuelve parametrize? Back: correr el mismo test con muchos pares input/output sin duplicar código; cada caso reporta por separado.
Feynman: “fixture = el mise en place que preparás una vez y usás en varios platos. parametrize = probar la misma receta con distintos ingredientes de un tiro.”
Aplicar: testeá una función con parametrize (3 casos) + una fixture + un mock de una llamada externa.
Límites: no mockees lo que estás testeando; mockeá solo los bordes (red, DB, tiempo).
Concepto 8 — Tooling moderno (2026)
Idea núcleo: el stack actual de proyecto Python: uv (entornos/deps rapidísimo), ruff (lint+format en uno), pyproject.toml (config única).
Entender:
uvreemplaza pip/venv/poetry para la mayoría de flujos; muy rápido.ruffreemplaza flake8 + black + isort; un solo tool.pyproject.toml= fuente única de config del proyecto (deps, ruff, pytest).
uv init && uv add fastapi
uv run ruff check --fix .
uv run pytest
Nota atómica: Front: ¿qué reemplaza ruff? Back: flake8 (lint) + black (format) + isort (imports), en una sola herramienta rápida.
Feynman: “Antes tenías 4 cajas de herramientas que discutían entre sí; ahora una sola navaja suiza.”
Aplicar: iniciá un proyecto con uv, agregá ruff y pytest, configurá todo en pyproject.toml.
Límites: [verificar] versión/estado de uv — ecosistema en movimiento; confirmá antes de fijarlo como estándar.
Checklist de dominio M1
Marcás cuando podés explicar (Feynman) + aplicar cada uno:
- Generadores (lazy vs eager)
- GIL / async / multiprocessing
- Context managers
- Decorators
- Typing + dataclasses/pydantic
- Trampas (mutable default, late binding, is/==)
- pytest (fixtures, parametrize, mocking)
- Tooling (uv, ruff, pyproject)
Salida verificable del módulo: una CLI con Typer que use generadores + context manager + decorator, tipada, con tests pytest, formateada con ruff, gestionada con uv. Y poder explicar en voz alta por qué cada elección es idiomática.