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)
Profundizando — qué es un thread realmente (por debajo del GIL)
Un thread no es un concepto de Python — es un mecanismo del sistema operativo: una secuencia de ejecución con su propio stack, compartiendo memoria heap con los demás threads del proceso. El scheduler del SO decide cuándo corre cada thread, de forma preemptiva — le puede quitar el CPU a un thread en cualquier momento (cada pocos milisegundos) sin pedirle permiso. Ese cambio de un thread a otro (context switch) tiene costo real: guardar/restaurar registros del CPU, stack pointer, program counter.
En Python, los threads sí son threads reales del SO — el scheduler los intercala igual que en cualquier lenguaje. Lo que agrega CPython es una capa extra encima: cada thread necesita tomar el GIL antes de ejecutar bytecode. Por eso hay dos niveles de “turno”: el del SO (quién tiene el CPU) y el del GIL (quién puede tocar el intérprete). Aunque el SO reparta 8 cores entre 8 threads, el GIL sigue dejando avanzar bytecode Python de a uno.
El problema real de compartir memoria — race condition:
saldo = 100
def retirar(monto):
global saldo
if saldo >= monto: # thread A lee saldo=100
# <-- el scheduler puede interrumpir justo acá
saldo -= monto # thread B también leyó saldo=100 antes de que A restara
Dos threads pueden leer saldo=100 antes de que ninguno reste — ambos retiran creyendo que hay fondos. El resultado depende del orden exacto en que el scheduler los intercaló: impredecible. Se arregla con un lock (threading.Lock): solo un thread entra a esa sección a la vez, los demás esperan.
sequenceDiagram
participant SO as Scheduler SO
participant A as Thread A
participant B as Thread B
SO->>A: corre (tiene el CPU)
A->>A: lee saldo=100
SO-->>A: context switch (interrumpe)
SO->>B: corre
B->>B: lee saldo=100 (todavía no restó A)
B->>B: saldo -= monto
SO-->>B: context switch
SO->>A: retoma
A->>A: saldo -= monto (sobre un saldo ya viejo)
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).
Nota atómica 2:
- Front: ¿Por qué dos threads que leen y modifican la misma variable pueden dar un resultado incorrecto, aunque el GIL exista?
- Back: El GIL protege el intérprete de corromper memoria a nivel de bytecode, pero no hace atómica una secuencia de varias operaciones Python (leer, comparar, restar son 3 pasos). El scheduler del SO puede interrumpir entre esos pasos e intercalar otro thread — eso es la race condition. Se arregla con un lock explícito, no con el 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). Y el race condition es como dos cajeros de un banco mirando el mismo saldo en la pantalla antes de que ninguno actualice el sistema — ambos ven ‘$100 disponibles’ y ambos aprueban el retiro.”
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. Después reproducí el race condition de retirar() con 2 threads y 1000 iteraciones cada uno — contá cuántas veces el saldo final no cuadra. Arreglalo con threading.Lock().
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].
- Un lock mal puesto (o dos locks en orden distinto en dos threads) produce deadlock — ambos threads esperan para siempre. Orden consistente de locks lo evita.
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
Profundizando — la pirámide de testing (qué capa testea qué, y qué mockear):
Cuatro capas, de más rápida/aislada a más lenta/realista:
- Unit: una función/clase sola, sin red ni DB real. Rápido (ms), corre miles en segundos. Mockeás TODO lo que no es tuyo.
- Integration: varias piezas propias juntas (tu código + una DB real, ej: testcontainers) — verifica que TUS componentes funcionan juntos, sin mockear tu propia lógica.
- Contract: verifica que el contrato con un servicio externo (otro microservicio, una API de terceros) no se rompió — sin correr el sistema completo.
- E2E: el sistema completo, como lo usaría un usuario real. Lento y frágil, pero la única capa que prueba que todo junto funciona.
Regla de mockeo: mockeá lo que no es tuyo (red, DB externa, reloj, terceros) — nunca lo que SÍ es tuyo y estás probando, o el test deja de probar algo real.
TDD (red-green-refactor), con matiz: test que falla (red) → código mínimo que lo pasa (green) → mejorás diseño sin romper el test (refactor). Útil cuando el comportamiento esperado es claro de entrada; no es dogma — en exploración donde el diseño no está claro todavía, tests después de estabilizar la forma tiene más sentido.
Coverage, la trampa: 100% de cobertura solo significa que cada línea se ejecutó una vez, no que se validó el comportamiento correcto bajo casos edge. Un test sin assert real cuenta como cobertura y no prueba nada.
flowchart TD
U[Unit — todo mockeado, ms] --> I[Integration — tus piezas + DB real]
I --> C[Contract — verifica contrato externo]
C --> E[E2E — sistema completo, lento y frágil]
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.
Nota atómica 2: Front: ¿por qué un integration test que mockea tu propia DB no sirve para lo que promete? Back: porque el punto es verificar que TUS piezas funcionan juntas de verdad — si mockeás la DB, volviste a tener (a lo sumo) un unit test disfrazado.
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. La pirámide de testing es como probar un auto: unit es probar cada pieza suelta en el banco, integration es el motor completo armado, e2e es sacarlo a la calle — necesitás las tres, pero no en la misma proporción.” Aplicar: testeá una función con parametrize (3 casos) + una fixture + un mock de una llamada externa. Después escribí 1 integration test real (testcontainers o DB de prueba) para la misma funcionalidad y notá la diferencia de qué prueba cada uno. Límites: no mockees lo que estás testeando; mockeá solo los bordes (red, DB, tiempo). Demasiados tests E2E vuelven el CI lento y frágil — la pirámide tiene esa forma (muchos unit, pocos e2e) por algo.
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.
Concepto 9 — Estructuras de datos: dict vs list (por qué cada una)
Idea núcleo: una lista busca recorriendo (O(n)); un diccionario busca calculando una posición (O(1)). La diferencia de mecanismo, no solo de sintaxis, es lo que separa cuándo usar cada una.
Entender (capa 1)
Texto: una list guarda elementos en secuencia; buscar uno (x in lista) implica recorrerla de a uno hasta encontrarlo — O(n). Un dict usa una tabla hash: al guardar dic["clave"] = valor, Python aplica una función hash a "clave" que da un número, y ese número indica directamente la posición de memoria. Buscar recalcula el mismo hash y va directo — O(1) amortizado, casi sin importar el tamaño.
Visual:
flowchart LR
subgraph Lista["list — O(n)"]
Q1[buscar 'x'] --> R1[recorrer 0,1,2...] --> R2[hasta encontrar o terminar]
end
subgraph Dict["dict — O(1)"]
Q2[buscar 'x'] --> H[hash('x') = posición] --> D[ir directo a esa posición]
end
Video: Time complexities of Python’s built-in data types — mCoding [verificar]
Ejemplo:
lista = [f"item{i}" for i in range(1_000_000)]
dic = {f"item{i}": i for i in range(1_000_000)}
"item999999" in lista # recorre hasta 1 millón de elementos → lento
"item999999" in dic # calcula 1 hash, va directo → rápido, ~constante
Colisiones (por qué no es O(1) “puro”): si dos claves distintas producen el mismo hash, Python resuelve internamente (probing) — por eso se dice O(1) amortizado, no garantizado en el peor caso teórico, pero efectivamente constante en la práctica.
Fijar (capa 2)
Nota atómica:
- Front: ¿cuándo elegir
listen vez dedict/setaunque sea más lento buscar? - Back: cuando necesitás orden por posición + acceso por índice numérico, duplicados (dict no permite claves repetidas), vas a recorrer todo siempre (no buscar puntual), o el dataset es chico y la diferencia no importa.
Feynman: “Una lista es buscar un libro en un estante sin orden: los revisás uno por uno. Un diccionario es una biblioteca con ficha catalográfica: calculás el código y vas directo al estante exacto.”
Aplicar (capa 3)
Medí (time) "x" in lista vs "x" in dic/set sobre 1 millón de elementos, buscando el último. Compará los tiempos. Después implementá un contador de frecuencia de palabras con dict y explicá por qué una list de tuplas sería más lenta para ese caso.
Límites
dict/setgastan más memoria quelist(la tabla hash necesita espacio extra).- Las claves de un
dictdeben ser hashables (inmutables) — no podés usar unalistcomo clave, sí unatuple. - Si necesitás mantener orden de inserción Y buscar rápido,
dicten Python 3.7+ ya mantiene orden de inserción por default — no hace faltaOrderedDictsalvo necesitarmove_to_end.
Concepto 10 — Debugging bajo presión: el formato “Bug Bash” de entrevista
Idea núcleo: varias empresas (Stripe, Retool) usan una ronda donde te dan un repo AJENO con un bug/test roto, 45-60 min. No evalúan si llegás al fix completo — evalúan cómo razonás código que no escribiste vos.
Entender (capa 1)
Texto: el formato reportado por candidatos: repo real (a veces una librería OSS con versión fijada) o codebase propio de la empresa con bugs sembrados, elegís lenguaje/IDE, se espera que preguntes antes de zambullirte. Una hipótesis bien articulada aunque el fix quede a medias pesa más que un fix completo sin explicar el razonamiento — es lo opuesto a un take-home, donde solo importa el resultado final.
La secuencia que separa señal de ruido (aplica a cualquier debugging bajo tiempo, no solo entrevista):
- Reproducir primero — nunca leas código sin poder disparar el fallo de forma confiable. Si no reproducís, estás adivinando.
- Aislar / bisectar — reducí la superficie: comentar capas,
git bisectsi es una regresión, binary search sobre el pipeline hasta encontrar el punto exacto donde el comportamiento cambia. - Narrar la hipótesis en voz alta y probar la más barata primero — no la más probable, la que menos cuesta descartar.
- Preguntar antes de zambullirse: ¿cuál es el comportamiento esperado vs el actual? ¿es regresión de un cambio reciente o siempre estuvo roto? ¿qué ya se descartó? ¿puedo correr/modificar los tests?
Visual:
flowchart TD
START[bug reportado] --> Q[preguntar: esperado vs actual,<br/>regresión o siempre roto, qué se descartó]
Q --> R[reproducir el fallo<br/>de forma confiable]
R --> I[aislar / bisectar<br/>reducir superficie]
I --> H{hipótesis}
H --> T[probar la MÁS BARATA primero]
T -->|confirma| FIX[fix + explicar el porqué]
T -->|descarta| H
Fijar (capa 2)
Nota atómica:
- Front: en un Bug Bash de entrevista, ¿qué pesa más — terminar el fix o narrar bien la hipótesis?
- Back: la hipótesis bien articulada y el proceso de descarte sistemático — candidatos que llegan al fix sin explicar razonamiento puntúan peor que quienes se quedan a medias pero muestran cómo reducen el problema. Se evalúa el proceso, no el resultado.
Feynman: “Es como un médico con un paciente nuevo: no empieza a operar a ciegas. Pregunta síntomas, descarta lo más barato de chequear primero (temperatura antes que una resonancia), y dice en voz alta qué está pensando y por qué — aunque todavía no tenga el diagnóstico final.”
Aplicar (capa 3)
Elegí un repo open source que no conozcas, clonalo, y buscá (o provocá vos mismo) un test que falle. Cronometrate 45 min: reproducí, aislá con git bisect si aplica, narrá en voz alta (grabate) cada hipótesis antes de probarla. Al final, revisá la grabación — ¿cuánto tiempo pasó entre “tengo una idea” y “la probé”, sin decir nada en el medio?
Límites
- Esta técnica es para debugging bajo tiempo/observado (entrevista, incidente en vivo). Para bugs sin presión de tiempo, alcanza con el proceso normal — no hace falta narrar en voz alta si nadie te está evaluando el razonamiento.
- No confundir “preguntar mucho” con “no saber por dónde empezar” — las preguntas del paso 4 tienen que reducir el espacio de búsqueda, no ser genéricas.
Concepto 11 — Algoritmia clásica (DSA): eficiencia mecánica, no solo que funcione
Idea núcleo: “que funcione” y “que sea eficiente” son dos barras distintas — un senior sabe cuándo la diferencia entre O(n) y O(n²) importa de verdad, y conoce las estructuras que resuelven cada patrón sin reinventarlas.
Entender (capa 1)
Texto — las estructuras que resuelven el 80% de los casos de entrevista/producción:
heapq(heap/priority queue): obtiene el mínimo (o máximo con signo invertido) en O(log n), no hace falta ordenar todo. Uso real: “dame los 10 elementos más caros de 1M sin ordenar el millón”, colas de prioridad (procesar la tarea más urgente primero).collections.deque: lista doblemente enlazada — insertar/sacar de ambos extremos en O(1), a diferencia de unalistnormal donde sacar del principio es O(n) (tiene que recorrer todo para reacomodar). Uso real: sliding window, BFS (recorrido por niveles), cola de trabajo.bisect: búsqueda binaria en una lista ordenada — O(log n) en vez de recorrer, y sirve para insertar manteniendo el orden sin re-ordenar todo.- Two pointers / sliding window: patrón para recorrer un array una sola vez con dos índices en vez de anidar loops (que da O(n²)) — típico en “substring más largo sin repetir” o “suma de subarray que cumple condición”.
Big-O de las operaciones que realmente se preguntan:
| Estructura | Acceso | Insertar | Buscar |
|---|---|---|---|
list (final) | O(1) | O(1) amortizado | O(n) |
list (principio) | O(1) | O(n) | O(n) |
dict/set | — | O(1) amortizado | O(1) amortizado |
deque (ambos extremos) | O(1) | O(1) | O(n) |
heapq (heap) | O(1) el mínimo | O(log n) | O(n) (no está ordenado) |
lista ordenada + bisect | O(1) | O(n) (mover elementos) | O(log n) |
Visual — por qué anidar loops es la trampa más común:
flowchart LR
subgraph Malo["O(n²) — loop dentro de loop"]
A1[para cada elemento] --> A2[recorrer TODOS los demás] --> A3[comparar]
end
subgraph Bueno["O(n) — two pointers / set"]
B1[un solo recorrido] --> B2[estructura auxiliar dict/set/deque] --> B3[respuesta directa]
end
Video: Big O Notation — Full Course — freeCodeCamp.org [verificar]
Ejemplo (el mismo problema, O(n²) vs O(n)):
# ❌ O(n²): para cada número, recorre TODOS los demás buscando el par
def dos_suman(nums, objetivo):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == objetivo:
return (i, j)
# ✅ O(n): un solo recorrido, dict guarda "qué falta" para cada número visto
def dos_suman_bien(nums, objetivo):
visto = {} # valor -> índice
for i, n in enumerate(nums):
falta = objetivo - n
if falta in visto: # O(1) — la razón de usar dict
return (visto[falta], i)
visto[n] = i
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué sacar el primer elemento de una
listes O(n) pero de undequees O(1)? - Back: una
listen Python es un array contiguo en memoria — sacar el primero obliga a correr todos los demás elementos una posición hacia atrás. Undequees una estructura pensada para operar en ambos extremos sin reacomodar el resto — por eso es la elección correcta para colas/sliding windows, nolist.pop(0).
Feynman: “Un heap es una fila de emergencias en un hospital: no está ordenada de punta a punta, pero siempre sabés instantáneamente quién es el más urgente. Un deque es una fila de gente donde podés atender o sumar gente por cualquiera de las dos puntas sin reacomodar a todo el mundo — una list normal es una fila donde, si sacás al primero, todos tienen que dar un paso adelante.”
Aplicar (capa 3)
Resolvé “dos números que suman un objetivo” primero con doble loop (O(n²)) y medí el tiempo con 10.000 elementos; después con dict (O(n)) y medí de nuevo. Por separado, implementá “los 3 elementos más grandes de una lista sin ordenarla completa” usando heapq.nlargest.
Límites
- Optimizar prematuramente un código que corre una vez sobre 100 elementos es desperdiciar tiempo — la complejidad importa cuando el input crece o el código corre con frecuencia.
- Conocer la estructura correcta no reemplaza medir: en datasets chicos, una
listsimple puede ser más rápida en la práctica que una estructura “teóricamente mejor” por el overhead de la estructura misma.
Checklist de dominio M1
Marcás cuando podés explicar (Feynman) + aplicar cada uno:
- Generadores (lazy vs eager)
- GIL / async / multiprocessing / threads reales / race conditions
- Context managers
- Decorators
- Typing + dataclasses/pydantic
- Trampas (mutable default, late binding, is/==)
- pytest (fixtures, parametrize, mocking, pirámide de testing, TDD, coverage)
- Tooling (uv, ruff, pyproject)
- Estructuras de datos: dict vs list (hash table vs recorrido)
- Debugging bajo presión: formato Bug Bash (reproducir→aislar→hipótesis→narrar)
- Algoritmia clásica: heapq, deque, bisect, two pointers, Big-O real
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.