Guía de Python: De 0 a 100
¿Qué es Python?
Python es un lenguaje de programación interpretado, de tipado dinámico y multiparadigma, creado por Guido van Rossum y lanzado por primera vez en 1991. Su filosofía se resume en el Zen de Python (PEP 20): legibilidad, simplicidad y explícito sobre implícito.
¿Dónde se usa?
- Web: Django, FastAPI, Flask — potencian Instagram, Spotify, Netflix
- Ciencia de datos / ML: NumPy, Pandas, TensorFlow, PyTorch — estándar en la industria
- DevOps / infraestructura: Ansible, Terraform providers, AWS CDK, contenedores
- Automatización / scripting: tareas de sistema, ETL, scraping (Scrapy, BeautifulSoup)
- Embebido / IoT: MicroPython, CircuitPython en microcontroladores
- Ciberseguridad: herramientas como Impacket, sqlmap, Volatility están escritas en Python
¿Quién lo usa? Google (YouTube, backend), Netflix (orquestación de CDN), NASA (análisis de datos), Spotify (recomendación), Reddit (backend original).
Fuente: Python.org — About, Real Python — What Is Python?
Prerrequisitos
Antes de empezar, necesitas:
- Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
- El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)
Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.
¿Cómo empezar? Instalación en todos los entornos
Windows
- Descarga el instalador desde python.org/downloads
- Marca “Add Python to PATH” en la primera pantalla — esto es crítico
- Opcional pero recomendado: elige “Disable path length limit” al final
- Verifica: abre
cmdo PowerShell y ejecutapython --version - Alternativa vía winget:
winget install Python.Python.3.13
macOS
- Homebrew (recomendado):
brew install python@3.13 - Xcode Command Line Tools:
xcode-select --install(necesario para compilar algunos paquetes) - Verifica:
python3 --version
Linux (Debian/Ubuntu)
sudo apt update
sudo apt install python3 python3-pip python3-venv build-essential
Para Fedora/RHEL: sudo dnf install python3 python3-pip
pyenv (Linux/macOS/WSL) — versión múltiple
curl https://pyenv.run | bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
exec $SHELL
pyenv install 3.13 3.12 3.11
pyenv global 3.13
Docker
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Usa python:3.13-slim como base (menos de 130 MB). Evita python:latest para builds reproducibles.
Alias útiles para el día a día
Añade a tu ~/.bashrc o ~/.zshrc:
alias py='python3'
alias pyenv-activate='source .venv/bin/activate'
alias pir='pip install -r requirements.txt'
alias pyfind='find . -name "*.py" -not -path "./.venv/*"'
alias pyt='python3 -m pytest -v'
alias pydev='python3 -m pip install --upgrade pip setuptools wheel'
Fuente: pyenv GitHub, Docker Official Images — Python
Escala de aprendizaje: de 0 a 100
Nivel 0-15: Fundamentos absolutos
Qué aprender:
- Sintaxis básica, variables, tipos (
int,str,float,bool) - Condicionales, bucles, listas, diccionarios
- Funciones, file system básico
- Mutabilidad, small integer caching
Proyecto: Adivina el número — número aleatorio entre 1-100, pistas mayor/menor, contador de intentos, manejo de errores con try/except.
Testing: pytest con aserciones simples, pytest.raises para entrada inválida.
Nivel 15-30: Estructuras de datos y funciones avanzadas
Qué aprender:
- Tuplas, sets, comprehensions,
*args/**kwargs - Lambdas,
map/filter/reduce, f-strings avanzadas pathlib,csv,json,sortedconkey- EAFP,
isvs==
Proyecto: Gestor de tareas CLI — añadir, listar, completar, eliminar. Persistencia en JSON. Usa argparse o click.
Testing: @pytest.mark.parametrize, tmp_path para archivos.
Nivel 30-45: POO, módulos y errores
Qué aprender:
- Clases, herencia,
super(), MRO, ABC,@property - Excepciones personalizadas,
match/case dataclasses,collections,heapq- Decoradores básicos
Proyecto: Sistema de inventario con clases Producto, Inventario. Persistencia con JSON. Decorador @loguear_operacion.
Testing: Fixtures, mocks con monkeypatch, pytest.raises para excepciones personalizadas.
Nivel 45-60: Testing, logging y empaquetado
Qué aprender:
- pytest avanzado (fixtures, parametrización,
tmp_path) coverage.py,logging, type hints avanzados (Protocol,Generic)pyproject.toml, empaquetado pipitertools,functools, Pillow básico
Proyecto: Calculadora empaquetada — paquete pip instalable. Tests con parametrize, logging, CI con GitHub Actions (pytest, mypy --strict, ruff).
Nivel 60-75: Web APIs y async
Qué aprender:
- FastAPI, Pydantic, SQLAlchemy 2.0
- asyncio, httpx.AsyncClient, WebSockets
- moviepy/pydub para multimedia básico
Proyecto: API de recetas — FastAPI + SQLAlchemy + PostgreSQL. Tests con pytest-asyncio y httpx.AsyncClient.
Nivel 75-90: Producción
Qué aprender:
- Docker, docker-compose, CI/CD, Redis, Celery
- OpenCV, WebSockets en producción
pydantic-settings,watchfiles
Proyecto: Dashboard de monitorización — FastAPI + Redis + WebSocket. Docker Compose (api + redis + worker). Tests de integración.
Nivel 90-100: Arquitectura
Qué aprender:
- Arquitectura hexagonal, gRPC, GraphQL, Kafka
- OpenTelemetry, profiling con
cProfile/py-spy - free-threaded Python 3.13, Cython
Proyecto: Sistema de notificaciones distribuidas — microservicios con FastAPI + gRPC + Kafka. OpenTelemetry para tracing.
Primeros pasos y configuración del entorno
Si prefieres ver directamente el “Hola Mundo” en Python, consulta El Atlas del Hola Mundo.
El REPL
Python incluye un intérprete interactivo. Ejecuta python3 en terminal y escribe directamente:
>>> print("Hola mundo")
>>> 2 + 2
>>> [x**2 for x in range(10)]
Para salir: exit() o Ctrl+D.
Scripts y estructura de proyecto
def main():
print("Hola mundo")
if __name__ == "__main__":
main()
Ejecuta: python3 hola.py
Entornos virtuales (aislamiento de dependencias)
python3 -m venv .venv
source .venv/bin/activate
# Poetry (moderno, declarativo)
curl -sSL https://install.python-poetry.org | python3
poetry new mi-proyecto
poetry add fastapi
# uv (ultrarrápido, en Rust)
pip install uv
uv venv
uv pip install requests
Editores recomendados
Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.
Configuración inicial del proyecto
python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install ruff mypy pytest
Archivo pyproject.toml básico:
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.backends._legacy:_Backend"
[tool.ruff]
line-length = 100
target-version = "py313"
[tool.mypy]
python_version = "3.13"
strict = true
Fuente: Python Packaging Guide, Poetry docs
Paradigmas de programación
Python es un lenguaje multiparadigma: soporta programación imperativa/procedural, orientada a objetos y funcional, permitiéndote elegir el enfoque según el problema.
Imperativo / Procedural
El paradigma por defecto. El código se ejecuta en secuencia, con condicionales y bucles:
def sumar_pares(numeros):
total = 0
for n in numeros:
if n % 2 == 0:
total += n
return total
print(sumar_pares([1, 2, 3, 4, 5, 6])) # 12
Programación Orientada a Objetos (POO)
Organiza el código en clases y objetos. Python soporta herencia múltiple, polimorfismo, encapsulación:
class Contador:
def __init__(self):
self._valor = 0
def incrementar(self):
self._valor += 1
@property
def valor(self):
return self._valor
c = Contador()
c.incrementar()
c.incrementar()
print(c.valor) # 2
Programación Funcional
Funciones puras (sin efectos secundarios), lambdas, map/filter/reduce:
from functools import reduce
numeros = [1, 2, 3, 4, 5]
doble = list(map(lambda x: x * 2, numeros))
pares = list(filter(lambda x: x % 2 == 0, numeros))
suma = reduce(lambda a, b: a + b, numeros)
doble2 = [x * 2 for x in numeros]
Declarativo (compresiones)
Las comprensiones describen qué quieres, no cómo:
pares = [n for n in range(20) if n % 2 == 0]
pares2 = []
for n in range(20):
if n % 2 == 0:
pares2.append(n)
Tabla comparativa
| Paradigma | Soporte en Python | Ejemplo mínimo |
|---|---|---|
| Imperativo/Procedural | Nativo, sintaxis básica | if x > 0: print(x) |
| POO | Clases, herencia múltiple, mixins | class Perro(Animal): ... |
| Funcional | Lambdas, map/filter/reduce, itertools, functools | list(map(str.upper, items)) |
| Declarativo | Comprensiones, SQL con SQLAlchemy | [x*2 for x in datos if x > 0] |
Fuente: Python docs — Functional Programming, itertools docs
Tipos de datos y variables
Sistema de tipos
¿Qué es?
Python tiene tipado dinámico fuerte: el tipo se infiere en tiempo de ejecución y no se realizan conversiones implícitas entre tipos incompatibles. Es como una caja sin etiqueta: sabes lo que contiene solo cuando la abres.
Duck typing: “si camina como pato y habla como pato, entonces es un pato”. Python mira el comportamiento, no el tipo nominal.
Type hints (PEP 484) añaden tipado gradual opcional para herramientas estáticas.
🔗 Wikipedia — Tipado dinámico, PEP 484
Sintaxis básica
x = 42 # int
x = "hola" # ahora str — dinámico
# "42" + 1 # TypeError: tipado fuerte
def saludo(nombre: str) -> str:
return f"Hola {nombre}"
from typing import Optional, Union, List, Dict, Any
def procesar(items: List[Union[int, str]]) -> Dict[str, Any]:
return {"count": len(items), "data": items}
🧪 Cómo probarlo
import pytest
def test_tipado_dinamico():
x = 42
assert isinstance(x, int)
x = "hola"
assert isinstance(x, str)
def test_tipado_fuerte():
with pytest.raises(TypeError):
"42" + 1
def test_type_hints():
assert procesar([1, "a"]) == {"count": 2, "data": [1, "a"]}
💡 Memoria y rendimiento
- Los type hints no tienen impacto en memoria ni rendimiento: se descartan en runtime
isinstance()es O(1) gracias al caché de tipos de CPython- El duck typing añade overhead de búsqueda de atributos en cada llamada
✅ Buenas prácticas
- ✅ Usa type hints siempre en código nuevo — facilitan el mantenimiento y detección de errores
- ⚠️ No uses
type()para comparar tipos; prefiereisinstance() - ❌ No confíes en type hints para validación en runtime — no son vinculantes
🏗️ Metodología
- Usa type hints desde el primer día en proyectos nuevos
- En scripts pequeños (< 100 líneas), los hints son opcionales
- En bibliotecas y APIs, los hints son obligatorios para buena experiencia de desarrollador
🔗 Para saber más
Primitivos
¿Qué es?
Los tipos primitivos son los bloques básicos de datos en Python. A diferencia de C o Java, en Python todo es un objeto, incluso los enteros. Esto significa que tienen métodos, ocupan memoria en heap y tienen overhead.
🔗 Wikipedia — Tipo de dato elemental
Sintaxis básica
| Tipo | Descripción | Ejemplo |
|---|---|---|
int | Precisión arbitraria (no hay desbordamiento) | x = 2**1000 |
float | IEEE 754 doble precisión (64 bits) | pi = 3.14159 |
complex | Números complejos | z = 3 + 4j |
bool | Subclase de int (True=1, False=0) | activo = True |
NoneType | Valor nulo | resultado = None |
print(2**1000)
print(0.1 + 0.2) # 0.30000000000000004 (IEEE 754)
z = 3 + 4j
print(z.real, z.imag, abs(z)) # 3.0 4.0 5.0
print(True + True) # 2
🧪 Cómo probarlo
import pytest
import math
def test_int_precision_arbitraria():
assert 2**1000 > 10**300
def test_float_precision():
assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)
def test_bool_es_int():
assert True + True == 2
assert issubclass(bool, int)
@pytest.mark.parametrize("valor, tipo", [
(42, int), (3.14, float), (True, bool), (None, type(None))
])
def test_tipos_primitivos(valor, tipo):
assert isinstance(valor, tipo)
💡 Memoria y rendimiento
import sys
print(sys.getsizeof(1)) # 28 bytes
print(sys.getsizeof(2**1000)) # ~160 bytes (crece con el número)
print(sys.getsizeof(3.14)) # 24 bytes
inttiene overhead variable: un entero pequeño cabe en 28 bytes, uno grande crecefloatsiempre ocupa 24 bytes (doble precisión)boolocupa 28 bytes (es subclase deint)- Small integer caching: enteros -5 a 256 son singletons
✅ Buenas prácticas
- ✅ Usa
isinstance(x, int)en vez detype(x) == int - ⚠️ No compares floats con
==— usamath.isclose() - ❌ No asumas que
isfunciona para comparar números fuera del rango -5 a 256
🏗️ Metodología
- En scripts de datos, prefiere
Decimaldel módulodecimalpara dinero o precisión exacta - Para números grandes,
intnativo es suficiente (no hay overflow) - En computación científica,
numpyofrece tipos más compactos (np.int32,np.float32)
🔗 Para saber más
Compuestos
¿Qué es?
Los tipos compuestos (o contenedores) agrupan múltiples valores. Python ofrece listas, tuplas, diccionarios, conjuntos y más. Son como diferentes tipos de mochilas: algunas mantienen el orden, otras son más rápidas para buscar, otras no permiten duplicados.
🔗 Python docs — Data Structures
Sintaxis básica
| Tipo | Mutabilidad | Sintaxis |
|---|---|---|
list | Mutable | [1, 2, 3] |
tuple | Inmutable | (1, 2, 3) |
dict | Mutable | {"a": 1} |
set | Mutable | {1, 2, 3} |
frozenset | Inmutable | frozenset([1, 2]) |
from array import array
datos = bytearray(b"hola")
datos[0] = 77 # b'mola'
arr = array('i', [1, 2, 3, 4])
🧪 Cómo probarlo
import pytest
def test_lista_mutable():
a = [1, 2, 3]
a.append(4)
assert a == [1, 2, 3, 4]
def test_tupla_inmutable():
t = (1, 2, 3)
with pytest.raises(TypeError):
t[0] = 99
def test_set_elimina_duplicados():
assert {1, 2, 2, 3} == {1, 2, 3}
def test_dict_acceso():
d = {"a": 1, "b": 2}
assert d["a"] == 1
with pytest.raises(KeyError):
_ = d["z"]
💡 Memoria y rendimiento
import sys
print(sys.getsizeof([])) # 56 bytes + elementos
print(sys.getsizeof([1, 2, 3])) # 88 bytes
print(sys.getsizeof({})) # 72 bytes
print(sys.getsizeof(set())) # 216 bytes
list: O(1) acceso por índice, O(n) búsquedadict/set: O(1) promedio en búsqueda, inserción, borradotuple: más ligera que list (inmutable, menor overhead)array.array: ~4 bytes por entero frente a ~28 bytes delist[int]
✅ Buenas prácticas
- ✅ Prefiere
list.append()sobrelist += [item](evita crear nueva lista) - ⚠️ No uses listas como valores por defecto en funciones — usa
None - ❌ No modifiques una lista mientras la iteras — crea una copia
🏗️ Metodología
list: colección ordenada que cambia de tamaño (pila, cola)tuple: coordenadas, claves de diccionario, datos inmutablesdict: búsqueda rápida por clave, mapeosset: membresía, operaciones de conjunto (unión, intersección)
🔗 Para saber más
Mutabilidad
¿Qué es?
La mutabilidad determina si un objeto puede cambiar después de creado. Es como la diferencia entre escribir en una pizarra (mutable — puedes borrar y reescribir) y escribir en piedra (inmutable — necesitas una piedra nueva para cambiar el mensaje).
Sintaxis básica
a = [1, 2, 3] # mutable
b = a
a.append(4)
print(b) # [1, 2, 3, 4] — b también cambió
s = "hola" # inmutable
s += " mundo" # nueva cadena, la original no se modifica
🧪 Cómo probarlo
import pytest
def test_mutable_compartido():
a = [1, 2, 3]
b = a
a.append(4)
assert b == [1, 2, 3, 4]
assert a is b
def test_inmutable_nuevo_objeto():
s = "hola"
original_id = id(s)
s += " mundo"
assert id(s) != original_id
def test_mutable_default_arg():
def fn(items=[]):
items.append(1)
return items
assert fn() == [1]
assert fn() == [1, 1]
💡 Memoria y rendimiento
- Mutables: se pasan por referencia (sin copia) — eficiente pero peligroso
- Inmutables: cualquier “cambio” crea un nuevo objeto (más memoria, predecible)
- Default args mutables: se evalúan una vez en definición, no en llamada — causa bugs sutiles
✅ Buenas prácticas
- ✅ Usa
tuplepara datos que no deben cambiar (coordenadas, settings) - ⚠️ Nunca uses
listodictcomo valor por defecto de un parámetro - ❌ No asumas que modificar un mutable dentro de una función no afecta al exterior
🏗️ Metodología
- Datos inmutables = más seguros en concurrencia (no hay race conditions)
- En APIs públicas, prefiere devolver tuplas o
frozenseten vez de listas - En algoritmos que requieren muchos cambios, usa mutables por rendimiento
🔗 Para saber más
Strings en detalle
¿Qué es?
Las cadenas en Python 3 son secuencias inmutables de caracteres Unicode. Cada carácter es un código Unicode de hasta 4 bytes, lo que permite representar cualquier idioma y símbolo del mundo.
Sintaxis básica
texto = "Español ñ"
bytes_utf8 = texto.encode("utf-8")
bytes_latin = texto.encode("latin-1")
texto_recuperado = bytes_utf8.decode("utf-8")
print("\u00F1")
print("\N{LATIN SMALL LETTER N WITH TILDE}")
nombre = "Ana"
edad = 30
print(f"{nombre} tiene {edad} años")
print(f"{nombre:>10}")
print(f"{edad:03d}")
print(f"{1000_000:,}")
ruta = r"C:\Users\nombre\archivo.txt" # raw string
🧪 Cómo probarlo
import pytest
def test_codificacion_utf8():
texto = "Español ñ"
encoded = texto.encode("utf-8")
assert isinstance(encoded, bytes)
assert encoded.decode("utf-8") == texto
def test_f_string():
nombre = "Ana"
assert f"Hola {nombre}" == "Hola Ana"
@pytest.mark.parametrize("entrada, esperado", [
("hola", "HOLA"), ("MUNDO", "mundo"), ("Python", "PYTHON")
])
def test_upper_lower(entrada, esperado):
resultado = entrada.upper() if esperado.isupper() else entrada.lower()
assert resultado == esperado
💡 Memoria y rendimiento
- Cada objeto
strtiene overhead (~49 bytes + 1-4 bytes por carácter) - String interning: cadenas cortas con forma de identificador se cachean automáticamente
- La concatenación con
+en bucles es O(n²) — prefierejoin()
✅ Buenas prácticas
- ✅ Prefiere f-strings sobre
%o.format()(más legibles, más rápidas) - ⚠️ No anides f-strings en Python < 3.12 — causan SyntaxError
- ❌ No concatenes strings en bucles — usa
''.join(lista)
🏗️ Metodología
- Para manipulación intensiva de texto, considera
io.StringIOcomo buffer - Para búsquedas complejas, usa
re.compile()una vez - Para datos binarios, usa
bytesobytearray, no strings
🔗 Para saber más
Memoria y objetos
¿Qué es?
Python no tiene variables al estilo C. Tiene nombres que referencian objetos en memoria. Es como una guía telefónica: los nombres (variables) apuntan a números (objetos). Varios nombres pueden apuntar al mismo número.
Sintaxis básica
import sys
x = 42
print(id(x))
a = [1, 2]
b = [1, 2]
print(a == b) # True (mismo valor)
print(a is b) # False (distinto objeto)
print(sys.getsizeof(1)) # 28 bytes
print(sys.getsizeof("a")) # 50 bytes
print(sys.getsizeof([])) # 56 bytes
🧪 Cómo probarlo
import pytest
import sys
def test_identidad_vs_igualdad():
a = [1, 2]
b = [1, 2]
assert a == b
assert a is not b
def test_small_integer_caching():
assert 256 is 256
def test_getsizeof_crece_con_elementos():
size_vacia = sys.getsizeof([])
size_con = sys.getsizeof([1, 2, 3])
assert size_con > size_vacia
💡 Memoria y rendimiento
- Reference counting: cuando el contador llega a 0, el objeto se libera inmediatamente
- GC cíclico: detecta ciclos de referencias
__slots__elimina el__dict__(~50% menos memoria)sys.getsizeof()mide el objeto, no sus contenidos anidados
✅ Buenas prácticas
- ✅ Usa
ispara comparar conNone(x is None), no== - ⚠️ No uses
ispara comparar números o strings — usa== - ❌ No asumas que
dellibera memoria inmediatamente
🏗️ Metodología
- Para millones de objetos, usa
__slots__oarray.array/numpy.ndarray - Para datos temporales grandes, el GC cíclico libera automáticamente
- En scripts de corta duración,
gc.disable()puede mejorar rendimiento
🔗 Para saber más
Type casting
¿Qué es?
Convertir un valor de un tipo a otro. Python hace conversiones implícitas mínimas (tipado fuerte) y requiere conversiones explícitas (casting) para la mayoría de los casos.
🔗 Python docs — built-in functions
Sintaxis básica
int("42") # 42
float("3.14") # 3.14
str(42) # "42"
list("hola") # ['h', 'o', 'l', 'a']
tuple([1, 2, 3]) # (1, 2, 3)
set([1, 2, 2, 3]) # {1, 2, 3}
def safe_int(valor, default=0):
try:
return int(valor)
except (ValueError, TypeError):
return default
print(safe_int("abc")) # 0
🧪 Cómo probarlo
import pytest
def test_casting_explicito():
assert int("42") == 42
assert str(42) == "42"
assert list("abc") == ["a", "b", "c"]
def test_casting_seguro():
assert safe_int("abc") == 0
assert safe_int("42") == 42
@pytest.mark.parametrize("entrada", ["abc", None, 3.14])
def test_int_raise(entrada):
with pytest.raises((ValueError, TypeError)):
int(entrada)
💡 Memoria y rendimiento
int()yfloat()son O(n) para strings largos (parsean carácter por carácter)list()ytuple()copian el iterable completo — O(n) en tiempo y memoria- Las conversiones crean nuevos objetos; el original no se modifica
✅ Buenas prácticas
- ✅ Siempre maneja excepciones al convertir entradas de usuario
- ⚠️ No confíes en conversiones implícitas — sé explícito con
int(),str() - ❌ No uses
eval()para convertir strings a tipos — es inseguro
🏗️ Metodología
- Para parsing de datos masivos, prefiere validación temprana con
try/except - En APIs, usa Pydantic para validación y casting automático
- En ETL, maneja errores de casting con logging
🔗 Para saber más
Control de flujo y modularidad
Condicionales
¿Qué es?
Las estructuras condicionales permiten ejecutar diferentes bloques de código según condiciones booleanas. Python ofrece if/elif/else, ternarios y match/case (3.10+). Es como un semáforo: el camino que tomas depende del color que veas.
🔗 Wikipedia — Estructuras de control
Sintaxis básica
edad = 18
if edad < 12:
categoria = "niño"
elif edad < 18:
categoria = "adolescente"
elif edad < 65:
categoria = "adulto"
else:
categoria = "senior"
categoria = "mayor" if edad >= 18 else "menor"
def procesar_comando(comando: str) -> str:
match comando.lower().split():
case ["salir"]:
return "Saliendo..."
case ["suma", *numeros]:
total = sum(int(n) for n in numeros)
return f"Total: {total}"
case ["info", ("usuario" | "user") as entidad, id_]:
return f"Buscando {entidad} con id {id_}"
case _:
return "Comando no reconocido"
🧪 Cómo probarlo
import pytest
@pytest.mark.parametrize("edad, esperado", [
(5, "niño"), (12, "adolescente"), (18, "adulto"), (70, "senior")
])
def test_categorias(edad, esperado):
if edad < 12:
cat = "niño"
elif edad < 18:
cat = "adolescente"
elif edad < 65:
cat = "adulto"
else:
cat = "senior"
assert cat == esperado
def test_match_case():
assert procesar_comando("salir") == "Saliendo..."
assert procesar_comando("suma 1 2 3") == "Total: 6"
assert procesar_comando("info usuario 42") == "Buscando usuario con id 42"
💡 Memoria y rendimiento
if/elifes O(1) por condición (evaluación lazy)match/casees similar en rendimiento aif/elif- Cadenas muy largas de
elif(>10) deberían ser undictde funciones
✅ Buenas prácticas
- ✅ Usa
match/casepara pattern matching estructural (Python 3.10+) - ⚠️ No encadenes más de 5
elif— refactoriza a un dict de dispatch - ❌ No uses
if x:para comprobarNone— usaif x is not None:
🏗️ Metodología
- Condicionales simples:
if/elif/elsees suficiente - Múltiples variantes de un algoritmo: patrón Strategy con funciones
- Múltiples condiciones sobre el mismo valor:
match/case - Búsquedas en tablas:
dictcomo dispatch table
🔗 Para saber más
Bucles
¿Qué es?
Los bucles ejecutan un bloque de código repetidamente. Python ofrece for (sobre iterables) y while (condicional), más for/else que ejecuta código si no hubo break.
Sintaxis básica
for i, valor in enumerate(["a", "b", "c"], start=1):
print(f"{i}: {valor}")
contador = 0
while contador < 5:
contador += 1
for n in range(100):
if n % 2 == 0:
continue
if n > 10:
break
for n in range(2, 10):
for divisor in range(2, int(n**0.5) + 1):
if n % divisor == 0:
break
else:
print(f"{n} es primo")
🧪 Cómo probarlo
import pytest
def test_for_sobre_lista():
resultado = []
for i, v in enumerate(["a", "b"], 1):
resultado.append(f"{i}:{v}")
assert resultado == ["1:a", "2:b"]
def test_while_condicion():
contador = 0
while contador < 3:
contador += 1
assert contador == 3
def test_for_else_sin_break():
resultado = []
for n in range(2, 5):
pass
else:
resultado.append("completado")
assert resultado == ["completado"]
💡 Memoria y rendimiento
foren Python es más lento que en C porque itera a través del protocolo de iteración- Los bucles anidados profundos son candidatos a refactorizar con
itertools.product
✅ Buenas prácticas
- ✅ Prefiere
forsobrewhilecuando recorres colecciones - ⚠️ No modifiques la lista que estás iterando — itera sobre una copia
- ❌ No uses
while Truesin una condición de salida clara
🏗️ Metodología
- Para transformar colecciones, prefiere comprehensions o
map/filter - Para operaciones costosas dentro del bucle, extrae a una función
- En procesamiento de datos grande, considera
numpyvectorizado
🔗 Para saber más
Comprehensions
¿Qué es?
Las comprehensions son una forma concisa de crear secuencias aplicando una expresión a cada elemento de un iterable, opcionalmente filtrando. Es azúcar sintáctico sobre bucles for, más legible y ligeramente más rápido.
🔗 Python docs — list comprehensions
Sintaxis básica
cuadrados = [x**2 for x in range(10) if x % 2 == 0]
cuadrados_dict = {x: x**2 for x in range(5)}
pares_unicos = {x % 3 for x in range(10)}
suma_cuadrados = sum(x**2 for x in range(1_000_000))
🧪 Cómo probarlo
import pytest
def test_list_comprehension():
resultado = [x**2 for x in range(5) if x % 2 == 0]
assert resultado == [0, 4, 16]
def test_dict_comprehension():
resultado = {x: x**2 for x in range(3)}
assert resultado == {0: 0, 1: 1, 2: 2}
def test_generator_expression():
total = sum(x for x in range(10) if x % 2 == 0)
assert total == 20
💡 Memoria y rendimiento
- Las comprehensions son ~10-30% más rápidas que bucles
forequivalentes - Las expresiones generadoras (
(...)) son lazy — no ocupan memoria hasta que se iteran
✅ Buenas prácticas
- ✅ Úsalas para transformaciones simples y filtros
- ⚠️ No anides comprehensions más de 2 niveles — usa bucles
- ❌ No abuses de ellas para efectos secundarios
🏗️ Metodología
- Para transformaciones simples: comprehension
- Para transformaciones complejas: pipeline con generadores
- Para datos masivos: expresión generadora (lazy)
🔗 Para saber más
Excepciones
¿Qué es?
Las excepciones son el mecanismo de Python para manejar errores en tiempo de ejecución. Siguen el estilo EAFP (Easier to Ask Forgiveness than Permission): intenta la operación y maneja el error si ocurre.
🔗 Python docs — Errors and Exceptions
Sintaxis básica
try:
archivo = open("datos.txt")
datos = archivo.read()
except FileNotFoundError:
print("Archivo no encontrado")
except PermissionError:
print("Sin permisos")
else:
print(f"Leídos {len(datos)} caracteres")
finally:
archivo.close()
def dividir(a, b):
if b == 0:
raise ValueError("No se puede dividir por cero")
return a / b
class SaldoInsuficienteError(Exception):
def __init__(self, saldo: float, intento: float):
self.saldo = saldo
self.intento = intento
super().__init__(f"Saldo insuficiente: ${saldo}, se requieren ${intento}")
try:
int("no_numero")
except ValueError as e:
raise RuntimeError("Error al procesar entrada") from e
🧪 Cómo probarlo
import pytest
def test_excepcion_esperada():
with pytest.raises(ValueError, match="No se puede dividir por cero"):
dividir(10, 0)
def test_excepcion_personalizada():
with pytest.raises(SaldoInsuficienteError) as exc:
raise SaldoInsuficienteError(50, 100)
assert exc.value.saldo == 50
def test_try_except_else():
resultado = []
try:
resultado.append("try")
except:
pass
else:
resultado.append("else")
finally:
resultado.append("finally")
assert resultado == ["try", "else", "finally"]
💡 Memoria y rendimiento
- Lanzar excepciones es relativamente caro (construcción del traceback)
- El bloque
trysin excepción tiene overhead casi nulo - No uses excepciones para control de flujo normal
✅ Buenas prácticas
- ✅ Sé específico con los tipos de excepción que capturas
- ⚠️ No captures
Exceptiongenérica sin una razón muy concreta - ❌ No uses
except: pass— como mínimo, registra el error
🏗️ Metodología
- EAFP es el estilo Pythonic: intenta y maneja errores después
- Para APIs públicas, documenta las excepciones que puede lanzar cada función
- En programas grandes, crea una jerarquía de excepciones personalizadas
🔗 Para saber más
Módulos y paquetes
¿Qué es?
Un módulo es un archivo .py que puede contener funciones, clases y variables. Un paquete es un directorio con __init__.py que agrupa módulos relacionados. Es la unidad de organización del código en Python.
Sintaxis básica
mi_paquete/
├── __init__.py
├── __main__.py
├── modulo_a.py
└── subpaquete/
├── __init__.py
└── modulo_b.py
from mi_paquete.subpaquete import modulo_b
from mi_paquete import modulo_a
from . import modulo_a
from .subpaquete import modulo_b
import sys
print(sys.path)
🧪 Cómo probarlo
import pytest
import sys
from pathlib import Path
def test_sys_path_es_lista():
assert isinstance(sys.path, list)
assert "" in sys.path or Path.cwd() in sys.path
def test_import_relativo(tmp_path):
pkg = tmp_path / "mipkg"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "modulo.py").write_text("VALOR = 42")
sys.path.insert(0, str(tmp_path))
from mipkg.modulo import VALOR
assert VALOR == 42
sys.path.pop(0)
💡 Memoria y rendimiento
importcarga el módulo ensys.modules— las siguientes importaciones son instantáneasfrom modulo import *puede cargar símbolos no deseados (usa__all__)
✅ Buenas prácticas
- ✅ Usa imports absolutos siempre que sea posible
- ⚠️ No uses
from modulo import *fuera de__init__.py - ❌ No tengas módulos con el mismo nombre que la stdlib (ej:
json.py)
🏗️ Metodología
- Divide en módulos por responsabilidad (un módulo = una preocupación)
- Usa
__init__.pypara exportar la API pública del paquete - Para proyectos grandes, estructura en subpaquetes
🔗 Para saber más
- Python docs — Packages
- Real Python — Python Modules and Packages
- PEP 420 — Implicit Namespace Packages
Sistema de archivos (File System I/O)
Lectura/escritura clásica
¿Qué es?
El sistema de archivos permite leer y escribir datos en el disco. Python ofrece la función open() con múltiples modos para texto y binario. Es como un archivador: abres el cajón (archivo), lees o escribes documentos, y lo cierras cuando terminas.
Sintaxis básica
with open("ejemplo.txt", "w", encoding="utf-8") as f:
f.write("Hola mundo\n")
f.writelines(["línea 2\n", "línea 3\n"])
with open("ejemplo.txt", "r", encoding="utf-8") as f:
contenido = f.read()
lineas = f.readlines()
with open("imagen.jpg", "rb") as f:
datos = f.read(1024)
with open("datos.bin", "wb+") as f:
f.write(b"\x00\x01\x02")
f.seek(0)
print(f.read())
🧪 Cómo probarlo
import pytest
from pathlib import Path
def test_escritura_lectura(tmp_path):
archivo = tmp_path / "test.txt"
with open(archivo, "w", encoding="utf-8") as f:
f.write("contenido")
with open(archivo, "r", encoding="utf-8") as f:
assert f.read() == "contenido"
def test_binary_mode(tmp_path):
archivo = tmp_path / "data.bin"
with open(archivo, "wb") as f:
f.write(b"\x00\xff")
with open(archivo, "rb") as f:
assert f.read() == b"\x00\xff"
def test_archivo_no_existe():
with pytest.raises(FileNotFoundError):
open("/no/existe.txt")
💡 Memoria y rendimiento
read()carga todo el archivo en memoria — peligroso para archivos grandes- Usa
for linea in f:para streaming (carga una línea a la vez) - El buffering por defecto (4096-8192 bytes) es óptimo para la mayoría de casos
✅ Buenas prácticas
- ✅ Siempre usa
with open()— cierra el archivo automáticamente - ⚠️ No leas archivos completos si son grandes — usa iteración línea por línea
- ❌ No especifiques
encodingen modo binario
🏗️ Metodología
- Archivos pequeños (< 100 MB):
read()/write()sin problemas - Archivos grandes: streaming línea por línea o en fragmentos
- Procesamiento por lotes:
iter(f.readline, '')para archivos enormes
🔗 Para saber más
pathlib
¿Qué es?
pathlib es la forma moderna y orientada a objetos de manejar rutas de archivos. Reemplaza a os.path con una API más limpia y multiplataforma. En lugar de strings, usas objetos Path.
Sintaxis básica
from pathlib import Path
ruta = Path("/usr/local/bin/python3")
print(ruta.name, ruta.stem, ruta.parent)
p = Path("datos")
p.mkdir(parents=True, exist_ok=True)
archivo = Path("datos/notas.txt")
archivo.write_text("contenido", encoding="utf-8")
texto = archivo.read_text(encoding="utf-8")
for py in Path("src").rglob("*.py"):
print(py)
from pathlib import PurePath
pp = PurePath("/usr/local/bin")
print(pp.joinpath("python3"))
🧪 Cómo probarlo
import pytest
from pathlib import Path
def test_path_propiedades():
ruta = Path("/usr/local/bin/python3")
assert ruta.name == "python3"
assert ruta.stem == "python3"
assert ruta.parent == Path("/usr/local/bin")
def test_mkdir_y_escritura(tmp_path):
d = tmp_path / "subdir"
d.mkdir()
f = d / "test.txt"
f.write_text("hola")
assert f.read_text() == "hola"
def test_glob(tmp_path):
(tmp_path / "a.py").write_text("")
(tmp_path / "b.txt").write_text("")
archivos_py = list(tmp_path.glob("*.py"))
assert len(archivos_py) == 1
assert archivos_py[0].name == "a.py"
### Tempfile
#### ¿Qué es?
El módulo `tempfile` crea archivos y directorios temporales que se limpian automáticamente. Útil para tests, caché en disco, y procesamiento de datos intermedios.
🔗 [Python docs — tempfile](https://docs.python.org/3/library/tempfile.html)
#### Sintaxis básica
```python
import tempfile
from pathlib import Path
with tempfile.TemporaryFile(mode="w+") as f:
f.write("datos temporales")
f.seek(0)
print(f.read())
with tempfile.NamedTemporaryFile(suffix=".csv", delete=True) as f:
print(f.name)
with tempfile.TemporaryDirectory() as tmpdir:
p = Path(tmpdir) / "archivo.txt"
p.write_text("temporal")
print(p.read_text())
🧪 Cómo probarlo
import pytest
import tempfile
from pathlib import Path
def test_temporary_file():
with tempfile.TemporaryFile(mode="w+") as f:
f.write("test data")
f.seek(0)
assert f.read() == "test data"
def test_temporary_directory():
with tempfile.TemporaryDirectory() as tmpdir:
p = Path(tmpdir) / "test.txt"
p.write_text("contenido")
assert p.exists()
assert not Path(tmpdir).exists()
💡 Memoria y rendimiento
- Los archivos temporales se crean en
/tmp(Linux) o%TEMP%(Windows) - El overhead de crear/eliminar archivos temporales es mínimo
✅ Buenas prácticas
- ✅ Úsalos en tests para evitar limpieza manual
- ⚠️ No asumas una ubicación específica para los archivos temporales
- ❌ No crees archivos temporales en el directorio de trabajo
🏗️ Metodología
- Tests:
tmp_pathfixture de pytest - Caché:
TemporaryDirectorypara conjuntos de archivos - Archivos grandes temporales:
NamedTemporaryFile(delete=False)
🔗 Para saber más
Streams en memoria
¿Qué es?
io.StringIO y io.BytesIO son buffers en memoria que se comportan como archivos. Permiten usar la misma API de archivos sin tocar el disco. Ideales para tests y mockear I/O.
Sintaxis básica
import io
buffer = io.StringIO()
buffer.write("línea 1\nlínea 2\n")
buffer.seek(0)
print(buffer.read())
print(buffer.getvalue())
buf = io.BytesIO()
buf.write(b"\x00\x01\x02")
buf.seek(0)
print(buf.read())
def procesar_archivo(f: io.TextIOBase) -> int:
return len(f.read())
🧪 Cómo probarlo
import pytest
import io
def test_stringio_como_archivo():
f = io.StringIO("contenido de prueba")
assert f.read() == "contenido de prueba"
def test_bytesio_binario():
buf = io.BytesIO()
buf.write(b"\x01\x02\x03")
buf.seek(0)
assert buf.read() == b"\x01\x02\x03"
def test_mock_archivo_con_stringio():
def contar_lineas(f):
return len(f.readlines())
f = io.StringIO("a\nb\nc\n")
assert contar_lineas(f) == 3
💡 Memoria y rendimiento
StringIOyBytesIOoperan en RAM — rápidos pero limitados por memoria disponible- Para buffers muy grandes (>100 MB), considera archivos temporales
getvalue()devuelve el contenido completo — O(n) en tiempo
✅ Buenas prácticas
- ✅ Úsalos para mockear archivos en tests
- ⚠️ No uses
StringIOpara datos binarios — usaBytesIO - ❌ No olvides
seek(0)después de escribir antes de leer
🏗️ Metodología
- Tests: mockea archivos reales con StringIO/BytesIO
- Procesamiento: úsalos como buffers intermedios
- Logging:
logging.StreamHandler(io.StringIO())para capturar logs
🔗 Para saber más
CSV y JSON
¿Qué es?
CSV (Comma-Separated Values) y JSON (JavaScript Object Notation) son los formatos de datos estructurados más comunes. Python incluye módulos para leerlos y escribirlos en la stdlib.
🔗 Wikipedia — CSV, Wikipedia — JSON
Sintaxis básica
import csv, json
with open("datos.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["nombre", "edad"])
writer.writerows([["Ana", 30], ["Luis", 25]])
with open("datos.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for fila in reader:
print(fila["nombre"], fila["edad"])
datos = {"usuarios": [{"nombre": "Ana", "edad": 30}]}
with open("datos.json", "w", encoding="utf-8") as f:
json.dump(datos, f, indent=2, ensure_ascii=False)
🧪 Cómo probarlo
import pytest, csv, json, io
def test_csv_escritura_lectura():
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerow(["nombre", "edad"])
writer.writerow(["Ana", "30"])
buffer.seek(0)
reader = csv.DictReader(buffer)
rows = list(reader)
assert rows[0]["nombre"] == "Ana"
assert rows[0]["edad"] == "30"
def test_json_serializacion():
data = {"clave": "valor", "numero": 42}
serializado = json.dumps(data)
deserializado = json.loads(serializado)
assert deserializado == data
💡 Memoria y rendimiento
csv.DictReadercarga una fila a la vez — eficiente para archivos grandesjson.load()carga todo el archivo en memoria — O(n) en RAM- Para datasets enormes, considera formatos binarios como Parquet
✅ Buenas prácticas
- ✅ Usa
newline=""al escribir CSV para evitar filas en blanco extra - ⚠️ No asumas que CSV tiene un encoding específico — especifícalo siempre
- ❌ No uses
eval()para deserializar JSON — usajson.loads()
🏗️ Metodología
- CSV: datos tabulares simples, intercambio con hojas de cálculo
- JSON: APIs, configuración, intercambio entre servicios
- Para datos complejos: JSON con esquemas (Pydantic) para validación
🔗 Para saber más
Archivos binarios con struct
¿Qué es?
El módulo struct convierte datos Python a bytes y viceversa usando formatos compactos. Es el estándar para leer/escribir archivos binarios, protocolos de red y datos de C.
Sintaxis básica
import struct
datos_binarios = struct.pack("If", 42, 3.14)
numero, flotante = struct.unpack("If", datos_binarios)
struct.pack_into("H", bytearray(b"\x00\x00\x00\x00"), 2, 1024)
🧪 Cómo probarlo
import pytest, struct
def test_pack_unpack():
packed = struct.pack("If", 42, 3.14)
n, f = struct.unpack("If", packed)
assert n == 42
assert abs(f - 3.14) < 0.001
def test_pack_into():
buf = bytearray(8)
struct.pack_into("I", buf, 0, 255)
struct.pack_into("I", buf, 4, 65535)
assert struct.unpack_from("I", buf, 0)[0] == 255
assert struct.unpack_from("I", buf, 4)[0] == 65535
💡 Memoria y rendimiento
struct.packes extremadamente rápido (escrito en C)- Los formatos compactos de struct son más eficientes que JSON o pickle
- Sin alineación:
struct.pack("II", 1, 2)ocupa 8 bytes exactos
✅ Buenas prácticas
- ✅ Usa estructuras de tamaño fijo para archivos binarios
- ⚠️ Respeta el alignment nativo
- ❌ No asumas el mismo endianness en todas las plataformas
🏗️ Metodología
structpara formatos binarios simplesnumpypara arrays grandes (más rápido y flexible)picklepara serializar objetos Python arbitrarios
🔗 Para saber más
Algoritmos y estructuras de datos
Búsqueda
¿Qué es?
La búsqueda localiza un elemento dentro de una colección. Python soporta búsqueda lineal (list.index, in) que es O(n), y búsqueda binaria (bisect) que es O(log n) en datos ordenados.
🔗 Wikipedia — Búsqueda binaria
Sintaxis básica
import bisect
datos = [1, 3, 5, 7, 9, 11]
print(datos.index(7)) # 3 (lineal)
pos = bisect.bisect_left(datos, 7) # 3 (binaria)
print(7 in [1, 3, 5, 7]) # O(n)
print(7 in {1, 3, 5, 7}) # O(1)
🧪 Cómo probarlo
import pytest, bisect
def test_busqueda_lineal():
datos = [1, 3, 5, 7]
assert datos.index(5) == 2
with pytest.raises(ValueError):
datos.index(99)
def test_busqueda_binaria():
datos = [1, 3, 5, 7, 9]
pos = bisect.bisect_left(datos, 5)
assert datos[pos] == 5
def test_in_operator():
assert 3 in [1, 2, 3]
assert 3 in {1, 2, 3}
assert "k" in {"k": 1}
💡 Memoria y rendimiento
list.index(): O(n) tiempo, O(1) memoria extrabisect.bisect_left(): O(log n) tiempo, O(1) memoria extrainen set/dict: O(1) promedio, O(n) peor caso
✅ Buenas prácticas
- ✅ Si buscas frecuentemente, convierte la lista a
set - ⚠️
bisectrequiere datos ordenados - ❌ No abuses de
inen listas grandes para búsquedas repetitivas
🏗️ Metodología
- Búsqueda única:
list.index()oines suficiente - Búsquedas múltiples: convierte a
set/dict(O(1)) - Búsqueda en datos ordenados:
bisect(O(log n))
🔗 Para saber más
Ordenación
¿Qué es?
Python incluye Timsort, un algoritmo híbrido estable (O(n log n)) que combina Merge Sort y Insertion Sort. Es el algoritmo de ordenación por defecto en sorted() y list.sort().
Sintaxis básica
numeros = [3, 1, 4, 1, 5]
ordenados = sorted(numeros)
inverso = sorted(numeros, reverse=True)
numeros.sort()
palabras = ["python", "java", "c", "rust"]
sorted(palabras, key=len)
from operator import attrgetter
class Usuario:
def __init__(self, nombre, edad):
self.nombre, self.edad = nombre, edad
usuarios = [Usuario("Ana", 30), Usuario("Luis", 25)]
sorted(usuarios, key=attrgetter("edad"))
#### 🧪 Cómo probarlo
```python
import pytest
def test_sorted():
assert sorted([3, 1, 2]) == [1, 2, 3]
def test_sorted_reverse():
assert sorted([1, 2, 3], reverse=True) == [3, 2, 1]
def test_sort_inplace():
nums = [3, 1, 2]
nums.sort()
assert nums == [1, 2, 3]
def test_sort_key():
palabras = ["aa", "b", "ccc"]
assert sorted(palabras, key=len) == ["b", "aa", "ccc"]
def test_estabilidad():
datos = [(1, "a"), (2, "b"), (1, "c")]
ordenados = sorted(datos, key=lambda x: x[0])
assert ordenados[0][1] == "a"
💡 Memoria y rendimiento
- Timsort: O(n log n) peor caso, O(n) mejor caso (datos ya ordenados)
sorted()devuelve una nueva lista (O(n) memoria extra)list.sort()ordena in-place (O(1) memoria extra)- Estable: elementos iguales mantienen su orden relativo
✅ Buenas prácticas
- ✅ Usa
keyen vez decmp(no existecmpen Python 3) - ⚠️ Si ordenas objetos, implementa
__lt__o usaattrgetter - ❌ No redefinas
sorted— usakeypara personalizar
🏗️ Metodología
- Lista pequeña (< 1000 elementos): cualquier método funciona
- Lista grande pero ya casi ordenada: Timsort rinde mejor que QuickSort
- Ordenación estable: necesaria cuando hay criterios múltiples
🔗 Para saber más
Estructuras de datos de la stdlib
¿Qué es?
Además de listas, dicts y sets, la stdlib incluye estructuras especializadas: deque (cola doble), defaultdict (dict con valor por defecto), Counter (contador), heapq (cola de prioridad) y más.
Sintaxis básica
from collections import deque, defaultdict, Counter, ChainMap
from heapq import heappush, heappop, nlargest
d = deque([1, 2, 3])
d.append(4)
d.appendleft(0)
d.pop()
d.popleft()
dd = defaultdict(list)
dd["frutas"].append("manzana")
frecuencia = Counter("python es genial")
print(frecuencia.most_common(3))
heap = []
heappush(heap, 3)
heappush(heap, 1)
heappush(heap, 2)
print(heappop(heap))
🧪 Cómo probarlo
import pytest
from collections import deque, defaultdict, Counter
from heapq import heappush, heappop
def test_deque():
d = deque([1, 2])
d.appendleft(0)
d.append(3)
assert list(d) == [0, 1, 2, 3]
def test_defaultdict():
dd = defaultdict(int)
dd["a"] += 1
assert dd["a"] == 1
assert dd["b"] == 0
def test_counter():
c = Counter("abbccc")
assert c.most_common(1) == [("c", 3)]
def test_heapq():
h = []
heappush(h, 5)
heappush(h, 1)
heappush(h, 3)
assert heappop(h) == 1
💡 Memoria y rendimiento
deque: O(1) en ambos extremos (vs O(n) en list para el inicio)defaultdict: mismo rendimiento quedict, sin overhead por valores ausentesCounter: O(n) para construir, O(n log n) paramost_commonheapq: O(log n) push/pop, O(1) para ver el mínimo
✅ Buenas prácticas
- ✅ Usa
dequepara colas (FIFO) y pilas (LIFO) - ⚠️ No uses
list.pop(0)— es O(n); usadeque.popleft() - ❌ No confundas
setconCounter—Counterguarda frecuencias
🏗️ Metodología
deque: colas, historial, buffers deslizantesdefaultdict: agrupar elementos, acumular valoresCounter: análisis de frecuencias, estadísticasheapq: prioridades, top-N elementosChainMap: configuración por capas
🔗 Para saber más
itertools y functools
¿Qué es?
itertools proporciona iteradores eficientes para composición de datos (producto cartesiano, permutaciones, agrupación). functools ofrece herramientas para funciones de orden superior (parciales, memoización, sobrecarga).
🔗 Python docs — itertools, functools
Sintaxis básica
from itertools import chain, cycle, permutations, combinations, groupby, product, accumulate
from functools import reduce, partial, cache, singledispatch
list(chain([1, 2], [3, 4]))
list(zip(range(5), cycle("AB")))
list(permutations("ABC", 2))
list(combinations("ABC", 2))
list(accumulate([1, 2, 3, 4]))
@cache
def fibonacci(n):
return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)
@singledispatch
def procesar(x):
raise NotImplementedError
@procesar.register(str)
def _(x): return f"Texto: {x}"
@procesar.register(int)
def _(x): return f"Número: {x}"
🧪 Cómo probarlo
import pytest
from itertools import chain, accumulate
from functools import partial, cache
def test_chain():
assert list(chain([1, 2], [3, 4])) == [1, 2, 3, 4]
def test_accumulate():
assert list(accumulate([1, 2, 3, 4])) == [1, 3, 6, 10]
def test_partial():
def potencia(base, exp): return base ** exp
cuadrado = partial(potencia, exp=2)
assert cuadrado(5) == 25
def test_cache():
llamadas = 0
@cache
def suma(a, b):
nonlocal llamadas
llamadas += 1
return a + b
assert suma(1, 2) == 3
assert suma(1, 2) == 3
assert llamadas == 1
💡 Memoria y rendimiento
itertoolsfunciones son lazy — no crean listas intermediaschainevita concatenar listas (O(1) vs O(n) memoria)cache(3.9+) es memoización sin límitegroupbyrequiere datos ordenados por la clave
✅ Buenas prácticas
- ✅ Usa
itertoolspara evitar bucles anidados y listas intermedias - ⚠️
groupbysolo agrupa elementos consecutivos — ordena primero - ❌ No abuses de
reducesi una comprehension es más legible
🏗️ Metodología
chain: concatenar múltiples iterables sin copiargroupby: procesar datos agrupados (logs, eventos)cache: funciones costosas con llamadas repetidassingledispatch: sobrecarga de funciones por tipo sin herencia
🔗 Para saber más
Complejidad Big O
¿Qué es?
Big O describe cómo crece el tiempo de ejecución (o memoria) de un algoritmo según el tamaño de la entrada. Es el lenguaje universal para hablar de rendimiento de algoritmos.
Sintaxis básica
| Operación | list | dict | set | deque | heapq |
|---|---|---|---|---|---|
| Acceso por índice | O(1) | — | — | O(1) | — |
| Búsqueda | O(n) | O(1) avg | O(1) avg | O(n) | O(n) |
| Inserción al final | O(1) amortizado | — | — | O(1) | O(log n) |
| Inserción al inicio | O(n) | — | — | O(1) | — |
| Eliminación | O(n) | O(1) avg | O(1) avg | O(n) | O(log n) |
| Mínimo/Máximo | O(n) | O(n) | O(n) | O(n) | O(1) min |
🧪 Cómo probarlo
import pytest
import time
def test_big_o_lineal():
n = 100_000
lista = list(range(n))
inicio = time.perf_counter()
99999 in lista
t_lista = time.perf_counter() - inicio
conjunto = set(lista)
inicio = time.perf_counter()
99999 in conjunto
t_set = time.perf_counter() - inicio
assert t_set < t_lista
💡 Memoria y rendimiento
list.append()es O(1) amortizado — ocasionalmente O(n) cuando redimensionadict/settienen factor de carga ~2/3 — ocupan más memoria que listas- No optimices prematuramente — primero perfila con datos reales
✅ Buenas prácticas
- ✅ Elige la estructura según la operación dominante
- ⚠️ No optimices prematuramente
- ❌ No asumas que O(1) en teoría significa rápido en la práctica
🏗️ Metodología
- Búsqueda intensiva:
set/dict(O(1)) - Cola FIFO:
deque(O(1)) - Prioridad:
heapq(O(log n)) - Ordenación única:
sorted()(Timsort O(n log n))
🔗 Para saber más
Conceptos clave explicados a fondo
Decoradores
¿Qué es?
Un decorador es una función que envuelve a otra para extender su comportamiento sin modificar su código fuente. Es el patrón Decorator integrado en la sintaxis del lenguaje con @. Como ponerle una funda a tu móvil: el móvil sigue siendo el mismo, pero ahora tiene protección extra.
Sintaxis básica
from functools import wraps
import time
def medir_tiempo(func):
@wraps(func)
def wrapper(*args, **kwargs):
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
fin = time.perf_counter()
print(f"{func.__name__} tardó {fin - inicio:.4f}s")
return resultado
return wrapper
@medir_tiempo
def calcular():
return sum(range(10_000_000))
def repetir(n: int):
def decorador(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorador
@repetir(3)
def saludar():
print("Hola!")
🧪 Cómo probarlo
import pytest
from functools import wraps
def test_decorador_simple():
llamado = []
def decorador(func):
@wraps(func)
def wrapper(*args, **kwargs):
llamado.append(1)
return func(*args, **kwargs)
return wrapper
@decorador
def suma(a, b):
return a + b
assert suma(2, 3) == 5
assert len(llamado) == 1
def test_decorador_con_argumentos():
def repetir(n):
def decorador(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorador
resultados = []
@repetir(3)
def registrar():
resultados.append("ok")
registrar()
assert len(resultados) == 3
💡 Memoria y rendimiento
- Cada nivel de decoración añade overhead de llamada a función
@wrapses importante pero añade mínima sobrecarga- Cadenas de decoradores (>5) pueden afectar legibilidad y rendimiento
✅ Buenas prácticas
- ✅ Siempre usa
@functools.wrapsen decoradores - ⚠️ No anides más de 3-4 decoradores — la legibilidad se resiente
- ❌ No uses decoradores para efectos secundarios en tiempo de importación
🏗️ Metodología
- Decoradores para concerns transversales: logging, timing, caché, autorización
- En APIs web: FastAPI usa decoradores (
@app.get()) - En proyectos grandes: considera decoradores de clase
🔗 Para saber más
El GIL (Global Interpreter Lock)
¿Qué es?
El GIL es un mutex que protege el intérprete de CPython: solo un thread ejecuta bytecode Python a la vez. Esto significa que threading no sirve para paralelismo CPU-bound, pero sí para I/O-bound. Es como un turno en una tienda: aunque haya varios dependientes, solo un cliente puede ser atendido a la vez.
Sintaxis básica
import threading, time, multiprocessing
def contar(n):
while n > 0:
n -= 1
t1 = threading.Thread(target=contar, args=(10_000_000,))
t2 = threading.Thread(target=contar, args=(10_000_000,))
inicio = time.time()
t1.start(); t2.start()
t1.join(); t2.join()
print(time.time() - inicio)
p1 = multiprocessing.Process(target=contar, args=(10_000_000,))
p2 = multiprocessing.Process(target=contar, args=(10_000_000,))
🧪 Cómo probarlo
import pytest, time, threading
def test_gil_io_bound():
import urllib.request
urls = ["https://httpbin.org/delay/1"] * 3
def descargar(url):
urllib.request.urlopen(url, timeout=5)
threads = [threading.Thread(target=descargar, args=(u,)) for u in urls]
inicio = time.time()
for t in threads: t.start()
for t in threads: t.join()
duracion = time.time() - inicio
assert duracion < 3
💡 Memoria y rendimiento
- Threading CPU-bound: peor que secuencial (contienda por el GIL)
- Threading I/O-bound: hasta 10x más rápido que secuencial
- multiprocessing: paralelismo real, pero mayor overhead de memoria
- asyncio: cooperativo, sin GIL (un solo hilo, sin contienda de lock)
✅ Buenas prácticas
- ✅ I/O-bound → asyncio o threading
- ⚠️ CPU-bound → multiprocessing o librerías C (numpy) que liberan el GIL
- ❌ No mezcles asyncio con threading sin entender las implicaciones
🏗️ Metodología
- I/O (HTTP, BD, archivos): asyncio (más eficiente que threads)
- CPU puro: multiprocessing
- CPU + I/O: asyncio con ProcessPoolExecutor
- Librerías C (numpy, pandas): liberan el GIL automáticamente
🔗 Para saber más
Duck typing
¿Qué es?
“Si camina como pato y habla como pato, entonces es un pato”. Python no mira el tipo nominal del objeto, sino su comportamiento: si tiene el método que necesitas, funciona.
Sintaxis básica
class Pato:
def hablar(self): return "Cuac"
class Persona:
def hablar(self): return "Hola"
def hacer_hablar(cosa):
print(cosa.hablar())
hacer_hablar(Pato()) # Cuac
hacer_hablar(Persona()) # Hola
from typing import Protocol
class Hablador(Protocol):
def hablar(self) -> str: ...
🧪 Cómo probarlo
import pytest
def test_duck_typing():
class Perro:
def sonido(self): return "Guau"
class Gato:
def sonido(self): return "Miau"
def hacer_sonido(animal):
return animal.sonido()
assert hacer_sonido(Perro()) == "Guau"
assert hacer_sonido(Gato()) == "Miau"
class Coche:
def sonido(self): return "Brum"
assert hacer_sonido(Coche()) == "Brum"
💡 Memoria y rendimiento
- Duck typing tiene overhead: cada llamada a método busca el atributo en el diccionario del objeto
- Protocol es solo para type checkers — no tiene impacto en runtime
✅ Buenas prácticas
- ✅ Usa Protocol para documentar interfaces esperadas
- ⚠️ No confíes en duck typing para APIs públicas sin documentar
- ❌ No abuses de
hasattr()para comprobar capacidades — prefiere EAFP
🏗️ Metodología
- Duck typing para código interno y tests
- Protocol para interfaces públicas y APIs
- ABC para jerarquías de clases con implementación compartida
🔗 Para saber más
EAFP vs LBYL
¿Qué es?
Python prefiere EAFP (Easier to Ask Forgiveness than Permission): intenta la operación y captura la excepción si falla. El enfoque alternativo es LBYL (Look Before You Leap): verifica antes de hacer la operación.
Sintaxis básica
# LBYL
if os.path.exists(archivo):
with open(archivo) as f:
...
# EAFP — Pythonic
try:
with open(archivo) as f:
...
except FileNotFoundError:
print("Archivo no encontrado")
🧪 Cómo probarlo
import pytest
def test_eafp_mas_rapido():
datos_validos = {"a": 1}
import time
def lbyl(d):
if "a" in d:
return d["a"]
return None
def eafp(d):
try:
return d["a"]
except KeyError:
return None
inicio = time.perf_counter()
for _ in range(100_000):
lbyl(datos_validos)
t_lbyl = time.perf_counter() - inicio
inicio = time.perf_counter()
for _ in range(100_000):
eafp(datos_validos)
t_eafp = time.perf_counter() - inicio
assert t_eafp < t_lbyl * 2
💡 Memoria y rendimiento
- EAFP es más rápido cuando el caso exitoso es común
- LBYL es más rápido cuando el fallo es común
- Construir una excepción es caro (~50μs)
✅ Buenas prácticas
- ✅ Usa EAFP para operaciones que esperas que funcionen la mayoría del tiempo
- ⚠️ No uses EAFP si la condición previa es barata y el error es caro
- ❌ No uses EAFP para control de flujo normal
🏗️ Metodología
- APIs de archivos y red: EAFP (los errores son esperados)
- Validación de datos externos: LBYL primero, luego EAFP
- Operaciones atómicas: EAFP evita race conditions
🔗 Para saber más
Peculiaridades del lenguaje
¿Qué es?
Python tiene peculiaridades que lo hacen único: chaining de comparaciones, unpacking con *, for...else, _ como throwaway, y type hints no vinculantes.
Sintaxis básica
# Chaining
if 1 < x < 10:
...
# Unpacking
primero, *resto, ultimo = [1, 2, 3, 4, 5]
# for...else
for n in range(2, 10):
for divisor in range(2, int(n**0.5) + 1):
if n % divisor == 0:
break
else:
print(f"{n} es primo")
# Type hints no vinculantes
def suma(a: int, b: int) -> int:
return a + b
suma("hola", "mundo") # funciona
🧪 Cómo probarlo
import pytest
def test_chaining():
x = 5
assert 1 < x < 10
def test_unpacking():
primero, *resto, ultimo = [1, 2, 3, 4]
assert primero == 1
assert resto == [2, 3]
assert ultimo == 4
def test_type_hints_no_vinculantes():
def suma(a: int, b: int) -> int:
return a + b
assert suma("hola", " mundo") == "hola mundo"
💡 Memoria y rendimiento
- Chaining de comparaciones no tiene overhead adicional
- Unpacking es O(n) pero implementado en C — muy rápido
- Type hints se descartan en runtime — no tienen impacto
✅ Buenas prácticas
- ✅ Usa chaining para rangos (
0 <= x < 100) - ⚠️ No abuses de unpacking con muchos elementos
- ❌ No confíes en type hints para validación en runtime
🏗️ Metodología
- Chaining: mejora legibilidad en rangos y comparaciones múltiples
- Unpacking: intercambio de variables, división de secuencias
for...else: búsquedas con flag de encontrado
🔗 Para saber más
POO avanzada: herencia, protocolos y patrones
Herencia y MRO
¿Qué es?
Python soporta herencia múltiple. El MRO (Method Resolution Order) determina el orden en que se buscan los métodos. Python usa el algoritmo C3 linearization para resolver conflictos.
Sintaxis básica
class A:
def metodo(self): return "A"
class B(A):
def metodo(self): return "B"
class C(A):
def metodo(self): return "C"
class D(B, C):
pass
d = D()
print(d.metodo()) # "B"
print(D.__mro__) # (D, B, C, A, object)
🧪 Cómo probarlo
import pytest
def test_mro():
class A:
def metodo(self): return "A"
class B(A):
def metodo(self): return "B"
class C(A):
def metodo(self): return "C"
class D(B, C):
pass
d = D()
assert d.metodo() == "B"
assert D.__mro__[1] == B
def test_super_sigue_mro():
class A:
def nombre(self): return "A"
class B(A):
def nombre(self): return "B" + super().nombre()
class C(A):
def nombre(self): return "C" + super().nombre()
class D(B, C):
def nombre(self): return "D" + super().nombre()
assert D().nombre() == "DBCA"
💡 Memoria y rendimiento
- El MRO se calcula una vez en la definición de la clase
super()es ligeramente más lento que llamar al método directamente- El MRO se almacena en
__mro__(tupla) — O(1) acceso
✅ Buenas prácticas
- ✅ Usa
super().__init__()en lugar deClaseBase.__init__(self) - ⚠️ La herencia múltiple es compleja — prefiere composición o mixins
- ❌ No crees diamantes profundos (>3 niveles) sin entender el MRO
🏗️ Metodología
- Herencia simple para “es un” (Animal → Perro)
- Mixins para “tiene la capacidad de” (Exportable, Serializable)
- Composición para “tiene un” (Coche → Motor, Ruedas)
- Protocols para “puede ser usado como”
🔗 Para saber más
ABC y Protocolos
¿Qué es?
ABC (Abstract Base Class) define interfaces mediante herencia explícita. Protocol (PEP 544) define interfaces por estructura: si un objeto tiene los métodos, implementa el protocolo.
Sintaxis básica
from abc import ABC, abstractmethod
from typing import Protocol
class Animal(ABC):
@abstractmethod
def hacer_sonido(self) -> str: ...
class Perro(Animal):
def hacer_sonido(self) -> str: return "Guau"
class Volador(Protocol):
def volar(self) -> None: ...
class Pajaro:
def volar(self) -> None:
print("Volando")
def lanzar(objeto: Volador):
objeto.volar()
lanzar(Pajaro()) # OK
🧪 Cómo probarlo
import pytest
from abc import ABC, abstractmethod
from typing import Protocol
def test_abc_no_instanciable():
class Base(ABC):
@abstractmethod
def metodo(self): ...
with pytest.raises(TypeError):
Base()
def test_protocol_estructural():
class Imprimible(Protocol):
def imprimir(self) -> str: ...
class Doc:
def imprimir(self) -> str: return "doc"
def procesar(x: Imprimible):
return x.imprimir()
assert procesar(Doc()) == "doc"
💡 Memoria y rendimiento
- ABC y Protocol tienen zero overhead en runtime
isinstance()con ABC registrado es más caro que sin él
✅ Buenas prácticas
- ✅ Usa ABC cuando hay implementación compartida (template method)
- ⚠️ Usa Protocol cuando solo importa la interfaz
- ❌ No mezcles ABC y Protocol en la misma jerarquía sin necesidad
🏗️ Metodología
- Frameworks: ABC para hooks que el usuario debe implementar
- Librerías: Protocol para tipos genéricos sin acoplar
- Tests: Protocol para mockear dependencias fácilmente
🔗 Para saber más
Dataclasses
¿Qué es?
@dataclass genera automáticamente __init__, __repr__, __eq__ (y opcionalmente __hash__, __lt__). Elimina el boilerplate de clases que solo almacenan datos.
Sintaxis básica
from dataclasses import dataclass, field
from typing import List
@dataclass
class Usuario:
nombre: str
email: str
edad: int = 0
tags: List[str] = field(default_factory=list)
activo: bool = field(default=True, repr=False)
def __post_init__(self):
if self.edad < 0:
raise ValueError("La edad no puede ser negativa")
🧪 Cómo probarlo
import pytest
from dataclasses import dataclass
def test_dataclass_basico():
@dataclass
class Punto:
x: int
y: int
p = Punto(1, 2)
assert p.x == 1
assert p.y == 2
assert repr(p) == "Punto(x=1, y=2)"
assert Punto(1, 2) == Punto(1, 2)
def test_dataclass_frozen():
@dataclass(frozen=True)
class Inmutable:
valor: int
i = Inmutable(42)
with pytest.raises(AttributeError):
i.valor = 99
def test_default_factory():
@dataclass
class Caja:
items: list = None
c = Caja()
assert c.items is None
💡 Memoria y rendimiento
- Las dataclasses son clases Python normales — mismo overhead
frozen=Trueusa__setattr__más lento__slots__no es compatible directamente con dataclasses
✅ Buenas prácticas
- ✅ Usa
default_factorypara valores mutables - ⚠️ No uses
list/dictcomo valor por defecto - ❌ No abuses de dataclasses para objetos con mucha lógica de negocio
🏗️ Metodología
- DTOs, configuraciones, resultados: dataclasses son perfectas
- Objetos de dominio con comportamiento: clases normales
- Validación compleja:
__post_init__o Pydantic
🔗 Para saber más
Métodos mágicos
¿Qué es?
Los métodos mágicos (dunder methods) son métodos con doble guion bajo que permiten que tus objetos se comporten como tipos nativos de Python.
🔗 Python docs — Special method names
Sintaxis básica
class ListaPersonalizada:
def __init__(self, datos=None):
self._datos = datos or []
def __getitem__(self, idx): # objeto[0]
return self._datos[idx]
def __setitem__(self, idx, val): # objeto[0] = "nuevo"
self._datos[idx] = val
def __len__(self): # len(objeto)
return len(self._datos)
def __iter__(self): # for x in objeto:
return iter(self._datos)
def __contains__(self, item): # item in objeto
return item in self._datos
def __call__(self, *args): # objeto(args)
print(f"Llamado con {args}")
def __enter__(self): # with objeto as x:
return self
def __exit__(self, *args): pass
def __str__(self): # str(objeto)
return str(self._datos)
def __repr__(self): # repr(objeto)
return f"ListaPersonalizada({self._datos!r})"
🧪 Cómo probarlo
import pytest
def test_metodos_magicos():
class Contenedor:
def __init__(self, items):
self._items = items
def __getitem__(self, idx):
return self._items[idx]
def __len__(self):
return len(self._items)
def __contains__(self, item):
return item in self._items
def __str__(self):
return f"Contenedor({self._items})"
c = Contenedor([1, 2, 3])
assert c[0] == 1
assert len(c) == 3
assert 2 in c
assert str(c) == "Contenedor([1, 2, 3])"
💡 Memoria y rendimiento
- Los métodos mágicos se buscan en la clase (no en la instancia)
__call__añade overhead de llamada a función- Cada método mágico tiene propósitos específicos
✅ Buenas prácticas
- ✅ Implementa
__repr__siempre (útil para debugging) - ⚠️ Si implementas
__eq__, considera implementar__hash__ - ❌ No implementes
__del__a menos que sea estrictamente necesario
🏗️ Metodología
__str__vs__repr__:__repr__para debugging,__str__para usuario__enter__/__exit__: para context managers personalizados__getitem__/__len__: para objetos que parecen colecciones
🔗 Para saber más
Patrones de diseño en Python
¿Qué es?
Los patrones de diseño son soluciones reutilizables a problemas comunes. Python simplifica muchos patrones gracias a funciones como ciudadanos de primera clase, duck typing y módulos como singletons.
🔗 Wikipedia — Patrón de diseño
Sintaxis básica
| Patrón | En Python |
|---|---|
| Strategy | Funciones como estrategias |
| Observer | Callbacks en lista |
| Singleton | Módulos son singletons |
| Factory | __init_subclass__ |
| Adapter | Duck typing |
| Decorator | @decorador integrado |
class Plugin:
_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._registry[cls.__name__.lower()] = cls
class PluginPDF(Plugin):
def ejecutar(self): return "PDF"
def crear_plugin(tipo: str) -> Plugin:
return Plugin._registry[tipo]()
🧪 Cómo probarlo
import pytest
def test_strategy_con_funciones():
def ordenar_asc(datos): return sorted(datos)
def ordenar_desc(datos): return sorted(datos, reverse=True)
class Analizador:
def __init__(self, estrategia):
self.estrategia = estrategia
def analizar(self, datos):
return self.estrategia(datos)
a = Analizador(ordenar_desc)
assert a.analizar([3, 1, 2]) == [3, 2, 1]
def test_observer():
llamado = []
class Observable:
def __init__(self):
self._obs = []
def conectar(self, cb):
self._obs.append(cb)
def notificar(self, dato):
for cb in self._obs:
cb(dato)
obs = Observable()
obs.conectar(lambda d: llamado.append(d))
obs.notificar("test")
assert llamado == ["test"]
💡 Memoria y rendimiento
- Strategy con funciones: mínimo overhead
- Observer con callbacks: O(n) para notificar a n observadores
- Factory con
__init_subclass__: registro automático O(1) por subclase - Singleton con módulos: sin overhead
✅ Buenas prácticas
- ✅ Prefiere el patrón más simple que funcione (YAGNI)
- ⚠️ Singleton está considerado antipatrón — usa un módulo en vez de clase
- ❌ No implementes todos los patrones de GoF — muchos son innecesarios en Python
🏗️ Metodología
- Strategy: cuando tienes múltiples variantes de un algoritmo
- Observer: eventos, señales, sistemas de plugins
- Factory: cuando la creación de objetos es compleja o dinámica
🔗 Para saber más
Ejemplo completo de POO
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Protocol
from enum import Enum, auto
class FormatoVideo(Enum):
MP4 = auto()
WEBM = auto()
AVI = auto()
MKV = auto()
@dataclass
class Metadatos:
titulo: str
duracion_segundos: int
fecha_creacion: datetime = field(default_factory=datetime.now)
tags: List[str] = field(default_factory=list)
def duracion_formateada(self) -> str:
minutos, segundos = divmod(self.duracion_segundos, 60)
return f"{minutos}:{segundos:02d}"
def __str__(self) -> str:
return f"{self.titulo} ({self.duracion_formateada()})"
class Contenido(ABC):
def __init__(self, metadatos: Metadatos):
self._metadatos = metadatos
self._reproducciones = 0
@property
def metadatos(self) -> Metadatos:
return self._metadatos
@property
def reproducciones(self) -> int:
return self._reproducciones
def reproducir(self) -> str:
self._reproducciones += 1
return f"Reproduciendo {self._metadatos.titulo}..."
@abstractmethod
def obtener_info(self) -> str: ...
def __str__(self) -> str:
return f"[{type(self).__name__}] {self._metadatos}"
class Audio(Contenido):
def __init__(self, metadatos: Metadatos, bitrate_kbps: int):
super().__init__(metadatos)
self.bitrate_kbps = bitrate_kbps
def obtener_info(self) -> str:
return f"Audio: {self.metadatos.titulo} | {self.bitrate_kbps}kbps"
class Video(Contenido):
def __init__(self, metadatos: Metadatos, resolucion: str, formato: FormatoVideo):
super().__init__(metadatos)
self.resolucion = resolucion
self.formato = formato
def obtener_info(self) -> str:
return f"Video: {self.metadatos.titulo} | {self.resolucion}"
class ListaReproduccion:
def __init__(self, nombre: str):
self.nombre = nombre
self._items: List[Contenido] = []
def agregar(self, item: Contenido):
self._items.append(item)
def info_completa(self) -> List[str]:
return [item.obtener_info() for item in self._items]
def __len__(self):
return len(self._items)
def __getitem__(self, idx):
return self._items[idx]
🧪 Cómo probarlo
import pytest
def test_sistema_multimedia():
cancion = Audio(Metadatos("Bohemian Rhapsody", 354), bitrate_kbps=320)
assert cancion.reproducir() == "Reproduciendo Bohemian Rhapsody..."
assert cancion.reproducciones == 1
assert "Bohemian" in cancion.obtener_info()
pelicula = Video(Metadatos("Inception", 8880), "4K", FormatoVideo.MKV)
assert "Inception" in pelicula.obtener_info()
def test_polimorfismo():
items = [
Audio(Metadatos("b", 100), 128),
Video(Metadatos("c", 200), "1080p", FormatoVideo.MP4),
]
for item in items:
assert isinstance(item.obtener_info(), str)
def test_lista_reproduccion():
a1 = Audio(Metadatos("a1", 100), 128)
a2 = Audio(Metadatos("a2", 200), 256)
lista = ListaReproduccion("Fav")
lista.agregar(a1)
lista.agregar(a2)
assert len(lista) == 2
assert len(lista.info_completa()) == 2
Polimorfismo en detalle
¿Qué es?
Polimorfismo significa “muchas formas”: la misma interfaz se comporta distinto según el tipo concreto. Python ofrece tres formas: duck typing (runtime), herencia (clásico) y protocols (estructural).
Sintaxis básica
# Duck typing
class Perro:
def sonido(self): return "Guau"
class Gato:
def sonido(self): return "Miau"
def hacer_sonido(cualquier_cosa):
return cualquier_cosa.sonido()
# Herencia (ABC)
class Forma(ABC):
@abstractmethod
def area(self) -> float: ...
class Circulo(Forma):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
# Protocol
class Imprimible(Protocol):
def imprimir(self) -> str: ...
🧪 Cómo probarlo
import pytest
from abc import ABC, abstractmethod
from typing import Protocol
def test_duck_typing():
class Perro:
def sonido(self): return "Guau"
class Gato:
def sonido(self): return "Miau"
def hacer_sonido(animal):
return animal.sonido()
assert hacer_sonido(Perro()) == "Guau"
assert hacer_sonido(Gato()) == "Miau"
def test_polimorfismo_herencia():
class Forma(ABC):
@abstractmethod
def area(self): ...
class Circulo(Forma):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
class Rectangulo(Forma):
def __init__(self, a, b): self.a, self.b = a, b
def area(self): return self.a * self.b
assert abs(Circulo(1).area() - 3.14) < 0.01
assert Rectangulo(2, 3).area() == 6
def test_polimorfismo_protocol():
class Imprimible(Protocol):
def imprimir(self) -> str: ...
class Doc:
def imprimir(self): return "doc"
def enviar(x: Imprimible):
return x.imprimir()
assert enviar(Doc()) == "doc"
💡 Memoria y rendimiento
- Duck typing: resuelve en runtime — ligero overhead
- Herencia: resuelve usando MRO — O(1) lookup
- Protocol: resuelto en tiempo de análisis (mypy) — zero runtime overhead
✅ Buenas prácticas
- ✅ Duck typing para código interno y rápido
- ⚠️ Protocol para APIs públicas con type hints
- ❌ No combines los tres en la misma función sin necesidad
🏗️ Metodología
- Duck typing: scripts, prototipos, testing
- ABC: frameworks, jerarquías con implementación compartida
- Protocol: bibliotecas, APIs, contratos desacoplados
🔗 Para saber más
Interacción con contenido multimedia
Imágenes (Pillow)
¿Qué es?
Pillow (PIL Fork) es la librería estándar de Python para procesamiento de imágenes. Soporta decenas de formatos y operaciones: redimensionar, filtrar, dibujar, convertir.
Sintaxis básica
from PIL import Image, ImageEnhance, ImageFilter, ImageDraw
img = Image.open("foto.jpg")
print(img.size, img.mode, img.format)
img.thumbnail((300, 300))
img.save("foto.webp", "WEBP", quality=85)
enhancer = ImageEnhance.Contrast(img)
img_contraste = enhancer.enhance(1.5)
draw = ImageDraw.Draw(img)
draw.text((10, 10), "Hola", fill="white")
🧪 Cómo probarlo
import pytest
from PIL import Image, ImageDraw
def test_crear_imagen():
img = Image.new("RGB", (100, 50), "red")
assert img.size == (100, 50)
assert img.mode == "RGB"
def test_redimensionar(tmp_path):
img = Image.new("RGB", (200, 100), "blue")
img.thumbnail((50, 50))
assert img.size[0] <= 50
assert img.size[1] <= 50
#### 💡 Memoria y rendimiento
- Cada píxel RGB ocupa 3 bytes; una imagen 4K ocupa ~25 MB en RAM
- Pillow está escrito en C — las operaciones de píxel son rápidas
- `thumbnail()` mantiene proporción y es más eficiente que `resize()`
#### ✅ Buenas prácticas
- ✅ Usa `thumbnail()` en lugar de `resize()` para miniaturas
- ⚠️ Las imágenes grandes (>4000px) pueden ocupar mucha RAM
- ❌ No guardes en JPEG sin especificar `quality`
#### 🏗️ Metodología
- Procesamiento por lotes: itera con `Path.glob()` y procesa cada imagen
- Pipelines: carga → filtra → redimensiona → guarda
- Web: convierte a WebP/AVIF para mejor compresión
#### 🔗 Para saber más
- [Pillow docs — Handbook](https://pillow.readthedocs.io/en/stable/handbook/)
- [Real Python — Pillow](https://realpython.com/image-processing-with-the-python-pillow-library/)
### Video (OpenCV, moviepy)
#### ¿Qué es?
OpenCV procesa video frame a frame con control total. Moviepy ofrece edición de alto nivel. ffmpeg-python es un wrapper de ffmpeg.
🔗 [OpenCV docs](https://docs.opencv.org/)
#### Sintaxis básica
```python
import cv2
cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
gris = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if cv2.waitKey(1) & 0xFF == ord('q'): break
cap.release()
from moviepy.editor import VideoFileClip, concatenate_videoclips
clip1 = VideoFileClip("video1.mp4").subclip(0, 10)
clip2 = VideoFileClip("video2.mp4").subclip(5, 15)
final = concatenate_videoclips([clip1, clip2])
final.write_videofile("final.mp4")
🧪 Cómo probarlo
import pytest
import cv2
import numpy as np
def test_crear_video_temporal(tmp_path):
ruta = str(tmp_path / "test.avi")
fourcc = cv2.VideoWriter_fourcc(*"XVID")
out = cv2.VideoWriter(ruta, fourcc, 30, (100, 100))
for _ in range(10):
frame = np.zeros((100, 100, 3), dtype=np.uint8)
out.write(frame)
out.release()
cap = cv2.VideoCapture(ruta)
assert cap.isOpened()
frames = 0
while cap.read()[0]:
frames += 1
cap.release()
assert frames == 10
💡 Memoria y rendimiento
- OpenCV procesa en CPU — 30 fps en 1080p es factible
- Moviepy carga clips en RAM — videos largos pueden ocupar GBs
- ffmpeg-python delega en ffmpeg (C) — muy rápido
✅ Buenas prácticas
- ✅ Usa
cv2.VideoWriter_fourcccon el codec adecuado - ⚠️ Libera recursos con
cap.release()ycv2.destroyAllWindows() - ❌ No proceses videos largos enteros en RAM con moviepy
🏗️ Metodología
- Análisis frame a frame: OpenCV
- Edición simple: moviepy
- Pipeline completo: ffmpeg-python
🔗 Para saber más
Audio (pydub, librosa)
¿Qué es?
pydub es una librería simple para manipulación de audio (cortar, unir, cambiar velocidad). librosa está orientada al análisis musical y extracción de características (BPM, espectrogramas, MFCC). soundfile lee/escribe archivos WAV/FLAC.
Sintaxis básica
from pydub import AudioSegment
audio = AudioSegment.from_file("cancion.mp3")
cortado = audio[:10_000] # primeros 10s
mas_lento = audio.speed_change(playback_speed=0.8)
mezcla = audio.overlay(AudioSegment.silent(1000), position=5000)
cortado.export("intro.mp3", format="mp3")
# librosa
import librosa
import librosa.display
y, sr = librosa.load("cancion.mp3")
tempo, beats = librosa.beat.beat_track(y=y, sr=sr)
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
🧪 Cómo probarlo
import pytest
from pydub import AudioSegment
def test_silencio():
silencio = AudioSegment.silent(500)
assert len(silencio) == 500
assert silencio.max == 0
def test_cortar_audio(tmp_path):
audio = AudioSegment.silent(1000) + AudioSegment.silent(500)
cortado = audio[:800]
assert len(cortado) == 800
💡 Memoria y rendimiento
- pydub carga archivos enteros en RAM — archivos largos (>1h) pueden ocupar >100 MB
- librosa también carga en RAM pero ofrece submuestreo (
res_type) - WAV sin comprimir ocupa ~10 MB/minuto (44100 Hz, 16-bit)
✅ Buenas prácticas
- ✅ Usa
pydubpara edición simple ylibrosapara análisis - ⚠️ Archivos largos: procesa por fragmentos con
pydub - ❌ No cargues audios de >2h enteros en RAM
🏗️ Metodología
- Edición: pydub (cortar, unir, exportar)
- Análisis: librosa (BPM, espectrograma, transcripción)
- Pipeline: carga → preprocesa → analiza → exporta
🔗 Para saber más
Bases de datos
SQL (relacionales)
¿Qué es?
Python se conecta a bases de datos relacionales mediante drivers nativos (psycopg3, asyncpg) o a través de ORMs como SQLAlchemy 2.0.
Sintaxis básica
from sqlalchemy import create_engine, select, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Usuario(Base):
__tablename__ = "usuarios"
id: Mapped[int] = mapped_column(primary_key=True)
nombre: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(255), unique=True)
engine = create_engine("sqlite:///db.sqlite3")
Base.metadata.create_all(engine)
with Session(engine) as session:
usuario = Usuario(nombre="Ana", email="ana@email.com")
session.add(usuario)
session.commit()
🧪 Cómo probarlo
import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column
def test_sqlite_en_memoria():
class Base(DeclarativeBase):
pass
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
nombre: Mapped[str] = mapped_column(String(50))
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Item(nombre="test"))
session.commit()
resultado = session.execute(select(Item)).scalars().all()
assert len(resultado) == 1
assert resultado[0].nombre == "test"
💡 Memoria y rendimiento
- SQLite en memoria: muy rápido para tests (~1000 ops/s)
- SQLAlchemy añade ~5-10% overhead sobre SQL plano
- Async drivers (asyncpg) pueden manejar >10000 conexiones simultáneas
✅ Buenas prácticas
- ✅ Usa SQLite para prototipos y tests (:memory:)
- ⚠️ Siempre cierra sesiones — usa
with Session() - ❌ No compartas sesiones entre hilos
🏗️ Metodología
- Prototipos: SQLite
- Producción: PostgreSQL con asyncpg
- Migraciones: Alembic
- ORM vs raw SQL: ORM para el 80%, raw SQL para el 20% crítico
🔗 Para saber más
WebSockets y MQTT
¿Qué es?
WebSockets permiten comunicación bidireccional en tiempo real entre servidor y cliente. MQTT es un protocolo pub/sub ligero para IoT. Ambos son esenciales para aplicaciones en tiempo real.
Sintaxis básica
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Set
app = FastAPI()
conexiones: Set[WebSocket] = set()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
conexiones.add(websocket)
try:
while True:
data = await websocket.receive_text()
for conn in conexiones:
await conn.send_text(f"Echo: {data}")
except WebSocketDisconnect:
conexiones.discard(websocket)
# MQTT con aiomqtt
import asyncio
import aiomqtt
async def mqtt_main():
async with aiomqtt.Client("broker.hivemq.com") as client:
await client.subscribe("casa/#")
async for message in client.messages:
print(f"{message.topic}: {message.payload.decode()}")
🧪 Cómo probarlo
import pytest
import asyncio
import websockets
@pytest.mark.asyncio
async def test_websocket():
async def echo_server(ws):
async for msg in ws:
await ws.send(msg)
return
async with websockets.serve(echo_server, "localhost", 8765):
async with websockets.connect("ws://localhost:8765") as ws:
await ws.send("hola")
respuesta = await ws.recv()
assert respuesta == "hola"
💡 Memoria y rendimiento
- WebSockets: overhead bajo (~2-10 bytes por frame)
- MQTT: extremadamente ligero (2 bytes header mínimo)
- asyncio es ideal para manejar miles de conexiones simultáneas
✅ Buenas prácticas
- ✅ Usa WebSockets para comunicación bidireccional navegador-servidor
- ⚠️ Usa MQTT para IoT y dispositivos con recursos limitados
- ❌ No uses WebSockets para comunicación request-response simple
🏗️ Metodología
- Chats, dashboards en tiempo real: WebSockets
- Sensores, dispositivos IoT: MQTT
- Fallback para navegadores antiguos: Socket.IO
🔗 Para saber más
Concurrencia y paralelismo
¿Qué es?
Python tiene tres modelos de concurrencia: threading (I/O-bound, limitado por GIL), multiprocessing (CPU-bound, sortea el GIL) y asyncio (I/O-bound cooperativo, un solo hilo).
Sintaxis básica
import asyncio
import httpx
async def descargar(url: str):
async with httpx.AsyncClient() as client:
resp = await client.get(url)
return resp.status_code
async def main():
urls = ["https://httpbin.org/delay/1"] * 5
resultados = await asyncio.gather(*[descargar(u) for u in urls])
print(resultados)
asyncio.run(main())
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
with ThreadPoolExecutor(max_workers=5) as pool:
resultados = list(pool.map(descargar, urls))
def calcular_primo(n):
for i in range(2, int(n**0.5) + 1):
if n % i == 0: return False
return True
with ProcessPoolExecutor() as pool:
resultados = list(pool.map(calcular_primo, range(10_000_000, 10_001_000)))
🧪 Cómo probarlo
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async():
async def suma_async(a, b):
await asyncio.sleep(0.01)
return a + b
resultado = await suma_async(2, 3)
assert resultado == 5
resultados = await asyncio.gather(suma_async(1, 1), suma_async(2, 2))
assert resultados == [2, 4]
💡 Memoria y rendimiento
| Escenario | Modelo | Ejemplo |
|---|---|---|
| I/O-bound (HTTP, BD) | asyncio | Microservicios, scrapers |
| CPU-bound | multiprocessing | ML, procesamiento imágenes |
| Ambos | asyncio + ProcessPoolExecutor | API que calcula y consulta BD |
| Streaming | asyncio | WebSockets, dashboards |
✅ Buenas prácticas
- ✅ asyncio para la mayoría de I/O-bound
- ⚠️ No uses threading para CPU-bound (el GIL lo empeora)
- ❌ No mezcles asyncio y threading sin sincronización explícita
🏗️ Metodología
- I/O-bound: asyncio (más eficiente que threads)
- CPU-bound: multiprocessing o librerías C (numpy)
- Archivos: aiofiles para async
- Cálculos pesados: ProcessPoolExecutor con asyncio
🔗 Para saber más
Testing y calidad
Frameworks y herramientas
| Framework | Propósito | Async | CLI |
|---|---|---|---|
| pytest | Unitario e integración | Sí (pytest-asyncio) | python -m pytest |
| unittest | Stdlib para testing | Sí (3.8+) | python -m unittest |
| hypothesis | Property-based testing | No | pytest --hypothesis |
| coverage.py | Cobertura de código | Sí | coverage run -m pytest |
Cómo testear cada concepto
Variables y tipos → aserciones simples
def test_tipos():
assert isinstance(42, int)
assert isinstance("hola", str)
assert isinstance([1, 2], list)
Control de flujo → boundary testing con parametrización
@pytest.mark.parametrize("input, expected", [
(0, "cero"), (1, "positivo"), (-1, "negativo")
])
def test_signo(input, expected):
if input > 0: result = "positivo"
elif input < 0: result = "negativo"
else: result = "cero"
assert result == expected
POO → mocks con monkeypatch
def test_con_mock(monkeypatch):
def mock_obtener_info(self):
return "mock info"
monkeypatch.setattr(Audio, "obtener_info", mock_obtener_info)
a = Audio(Metadatos("test", 100), 128)
assert a.obtener_info() == "mock info"
File I/O → tmp_path fixture
def test_archivo(tmp_path):
f = tmp_path / "test.txt"
f.write_text("contenido")
assert f.read_text() == "contenido"
Async → pytest-asyncio
@pytest.mark.asyncio
async def test_async():
async def fn(): return 42
assert await fn() == 42
Parametrización avanzada
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, 200, 300)
])
def test_suma(a, b, expected):
assert a + b == expected
Property-based testing
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_siempre_ordenado(lista):
resultado = sorted(lista)
for i in range(len(resultado) - 1):
assert resultado[i] <= resultado[i + 1]
Cobertura y CI
coverage run -m pytest
coverage report -m
coverage html # abre htmlcov/index.html
Mini‑scripts de verificación
# Mini-script: verifica tipos y mutabilidad
# python3 -c "
# a = [1, 2, 3]
# b = a
# a.append(4)
# assert b == [1, 2, 3, 4], 'mutabilidad'
# assert isinstance('hola', str)
# print('Todo OK')
# "
Conceptos avanzados para Python
asyncio profundo
¿Qué es?
asyncio permite concurrencia cooperativa con un solo hilo. Usa un event loop que multiplexa tareas mediante async/await. Ideal para I/O-bound.
Sintaxis básica
import asyncio
async def tarea(n):
await asyncio.sleep(1)
return f"Tarea {n} completada"
async def main():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(tarea(1))
t2 = tg.create_task(tarea(2))
print(t1.result(), t2.result())
asyncio.run(main())
🧪 Cómo probarlo
import pytest
import asyncio
@pytest.mark.asyncio
async def test_task_group():
async def fn(x):
await asyncio.sleep(0.01)
return x * 2
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fn(2))
t2 = tg.create_task(fn(3))
assert t1.result() == 4
assert t2.result() == 6
C extensions
Python puede extender su funcionalidad con módulos escritos en C usando la API de CPython, Cython, ctypes o CFFI.
from ctypes import CDLL
libc = CDLL("libc.so.6")
libc.printf(b"Hola desde C\n")
Profile-guided optimization (PGO)
PGO permite optimizar Python compilando con profiling de uso real.
./configure --enable-optimizations
make -j$(nproc)
🔗 Python Developer’s Guide — PGO
Performance tips
from array import array
arr = array('i', range(1_000_000)) # ~4 MB
lista = list(range(1_000_000)) # ~28 MB
Proyecto final integrador: Procesador de logs
Este proyecto combina file I/O, algoritmos, POO, testing y red. Es ejecutable de principio a fin.
from pathlib import Path
from collections import Counter
from dataclasses import dataclass
from typing import List
import json
import re
import urllib.request
@dataclass
class LogEntry:
ip: str
timestamp: str
metodo: str
ruta: str
status: int
tamano: int
class LogParser:
PATRON = re.compile(
r'(?P<ip>[\d.]+).*\[(?P<ts>[^\]]+)\].*'
r'"(?P<metodo>\w+)\s+(?P<ruta>\S+).*".*'
r'(?P<status>\d+)\s+(?P<size>\d+)'
)
def __init__(self, archivo: Path):
self.archivo = archivo
def parsear(self) -> List[LogEntry]:
entradas = []
with open(self.archivo, "r", encoding="utf-8") as f:
for linea in f:
match = self.PATRON.search(linea)
if match:
entradas.append(LogEntry(
ip=match.group("ip"),
timestamp=match.group("ts"),
metodo=match.group("metodo"),
ruta=match.group("ruta"),
status=int(match.group("status")),
tamano=int(match.group("tamano")),
))
return entradas
class LogAnalyzer:
def __init__(self, entradas: List[LogEntry]):
self.entradas = entradas
def top_ips(self, n: int = 5) -> List[str]:
return [ip for ip, _ in Counter(e.ip for e in self.entradas).most_common(n)]
def errores_por_ruta(self) -> dict:
return dict(Counter(
e.ruta for e in self.entradas if e.status >= 400
).most_common(10))
def resumen(self) -> dict:
return {
"total": len(self.entradas),
"ips_unicas": len(set(e.ip for e in self.entradas)),
"top_ips": self.top_ips(),
"errores": self.errores_por_ruta(),
}
def main():
ruta_log = Path("muestra.log")
if not ruta_log.exists():
print("Crea un archivo muestra.log con formato Apache/Nginx")
return
parser = LogParser(ruta_log)
entradas = parser.parsear()
analyzer = LogAnalyzer(entradas)
print(json.dumps(analyzer.resumen(), indent=2))
if __name__ == "__main__":
main()
Tests
import pytest
from pathlib import Path
def test_log_parser(tmp_path):
log = '192.168.1.1 - - [10/Oct/2023:13:55:36] "GET /index.html HTTP/1.1" 200 2326'
f = tmp_path / "test.log"
f.write_text(log)
parser = LogParser(f)
entradas = parser.parsear()
assert len(entradas) == 1
assert entradas[0].ip == "192.168.1.1"
assert entradas[0].status == 200
def test_log_analyzer():
entradas = [
LogEntry("1.1.1.1", "ts", "GET", "/", 200, 100),
LogEntry("1.1.1.1", "ts", "GET", "/", 404, 50),
LogEntry("2.2.2.2", "ts", "POST", "/api", 200, 200),
]
analyzer = LogAnalyzer(entradas)
resumen = analyzer.resumen()
assert resumen["total"] == 3
assert resumen["ips_unicas"] == 2
assert resumen["top_ips"][0] == "1.1.1.1"
assert "/" in resumen["errores"]
def test_log_processor_integracion(tmp_path):
log_data = """192.168.1.1 - - [10/Oct/2023:13:55:36] "GET / HTTP/1.1" 200 100
192.168.1.2 - - [10/Oct/2023:13:55:37] "POST /api HTTP/1.1" 404 50"""
f = tmp_path / "muestra.log"
f.write_text(log_data)
parser = LogParser(f)
entradas = parser.parsear()
assert len(entradas) == 2
analyzer = LogAnalyzer(entradas)
assert analyzer.resumen()["total"] == 2
---
## Despliegue a producción
### Build para producción
Para distribuir como paquete o binario:
```bash
pip install build && python -m build
# Binario portable con PyInstaller:
pip install pyinstaller
pyinstaller --onefile --name logproc src/main.py
Docker multi-stage
FROM python:3.13-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY . .
CMD ["python", "src/main.py"]
Hosting
- Fly.io —
fly launchdesde la raíz del proyecto - Railway — conecta tu repo y despliega automáticamente
- PythonAnywhere — hosting gratuito para apps Python
CI/CD (GitHub Actions)
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -r requirements.txt
- run: pytest
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions@1.5
with:
args: "deploy"
🎯 Recursos, tips y referencias
Libros y cursos
| Recurso | Tipo | Enlace |
|---|---|---|
| Python Crash Course (Eric Matthes) | Libro | NoStarch |
| Fluent Python (Luciano Ramalho) | Libro | O’Reilly |
| Automate the Boring Stuff | Curso gratis | automatetheboringstuff.com |
| CS50’s Python | Curso Harvard | cs50.harvard.edu/python |
| FastAPI official tutorial | Docs | fastapi.tiangolo.com/tutorial |
Canales y comunidades
- Reddit: r/learnpython, r/Python
- Discord: Python Discord, FastAPI Discord
- YouTube: ArjanCodes (código limpio), Tech With Tim (proyectos), Corey Schafer (fundamentos)
- Blogs: Real Python, Talk Python, PyCoder’s Weekly
Herramientas recomendadas
| Herramienta | Para qué |
|---|---|
uv / rye | Gestión de proyectos y empaquetado |
ruff | Linter + formatter ultra-rápido |
mypy --strict | Type checking |
pre-commit | Hooks automáticos |
copier | Templates de proyectos |
nox | Automatización de tests multi-versión |
💡 Tips y trucos
Diarios
# Diario de aprendizaje — escribe cada día qué aprendiste
import json
from pathlib import Path
from datetime import date
diario = Path("diario.json")
entradas = json.loads(diario.read_text()) if diario.exists() else []
hoy = {"fecha": str(date.today()), "aprendizaje": input("¿Qué aprendiste hoy? ")}
entradas.append(hoy)
diario.write_text(json.dumps(entradas, indent=2))
Debugging
# breakpoint() te da un REPL interactivo en el punto exacto
def bug_function(x):
breakpoint()
return 100 / x # ¿por qué falla?
Perfilado rápido
python -m cProfile -s cumulative mi_script.py | head -20
python -m timeit "sum(range(1000))"
📚 Referencias clave
- Python.org docs — La fuente de verdad
- Python Enhancement Proposals — PEPs
- Awesome Python — Lista curada de librerías
- pytest docs — Testing en Python
- Real Python — Tutoriales de alta calidad
- FastAPI docs — Web APIs modernas
- SQLAlchemy docs — ORM y SQL toolkit
- The Hitchhiker’s Guide to Python — Guía de estilo y proyecto
⚠️ Errores comunes
Estos son los errores que más se repiten al escribir y ejecutar Python. Casi todos tienen que ver con imports, tipos o la indentación.
| Error | Causa | Solución |
|---|---|---|
ModuleNotFoundError: No module named 'x' | El paquete no está instalado o estás en otro entorno | pip install x o uv add x; verifica que lo instalas dentro del mismo venv |
pip: command not found | No tienes pip en el PATH o usas el Python del sistema | python -m pip install x o python3 -m pip install x |
NameError: name 'x' is not defined | Usas una variable o función antes de definirla | Revisa el orden del código y que no haya un typo en el nombre |
IndentationError: unexpected indent | Bloques con espacios y tabulaciones mezclados | Elige 4 espacios o una tabulación de forma consistente en todo el archivo |
TypeError: unsupported operand type(s) for +: 'int' and 'str' | Sumas tipos incompatibles (p. ej. int + str) | Convierte explícitamente con int() o str() según lo que quieras |
ValueError: invalid literal for int() with base 10 | Pasas texto no numérico a int() o float() | Valida la entrada antes de convertir o controla la excepción con try/except |
AttributeError: 'NoneType' object has no attribute 'x' | Una función devolvió None y accedes a un atributo | Comprueba que la llamada devuelve un objeto real y maneja el caso None |
uv: Unable to find interpreter | El proyecto pide una versión de Python que no tienes | Instala la versión indicada (.python-version/pyproject.toml) con uv python install <versión> |
🔗 Guías relacionadas
- Para explorar el tipado estático en Python desde la óptica de TypeScript: Guía de TypeScript: De 0 a 100.
- Para empaquetar tu aplicación en contenedores: Guía de Docker: De 0 a 100.
- Generación de XML en Python: Generar XML con Python.
- Procesamiento de documentos y PDFs: PDF Ninja.
🎯 Retos y próximos pasos
Nivel 1 (0-15)
- Calculadora CLI — suma, resta, multiplica, divide. Maneja errores de entrada y división por cero.
- Adivina el número — número aleatorio, pistas mayor/menor, contador de intentos.
Nivel 2 (15-30)
- Gestor de tareas — CLI con añadir, listar, completar, eliminar. Persistencia en JSON.
- Analizador de texto — cuenta palabras, líneas, letras; busca palabras más frecuentes.
Nivel 3 (30-45)
- Sistema de inventario POO — clases Producto, Inventario, persistencia JSON, logging con decoradores.
- API de clima — FastAPI + httpx para consultar OpenWeatherMap.
Nivel 4 (45-60)
- Web scraper de noticias — httpx + BeautifulSoup + análisis de sentimiento básico.
- Pipeline de datos — extrae → transforma → carga (ETL) usando pandas.
Nivel 5 (60-75)
- Dashboard en tiempo real — FastAPI + WebSocket + Redis pub/sub para métricas en vivo.
- Procesador de imágenes async — carga imágenes, aplica filtros, guarda resultados con aiofiles.
Nivel 6 (75-90)
- Sistema de colas — Celery + Redis para procesamiento asíncrono de tareas pesadas.
- Microservicio dockerizado — FastAPI + PostgreSQL + Redis con docker-compose y tests E2E.
Nivel 7 (90-100)
- Plataforma de streaming — WebRTC + Python + OpenCV para transmisión de video en vivo.
- Sistema distribuido — gRPC + Kafka + OpenTelemetry para trazabilidad completa.
