Guía de Python: De 0 a 100

Guía de Python: De 0 a 100

Desde la primera línea de código hasta producción: instalación, ecosistema, frameworks, proyectos reales y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:74 MINUTOS DE LECTURA

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

  1. Descarga el instalador desde python.org/downloads
  2. Marca “Add Python to PATH” en la primera pantalla — esto es crítico
  3. Opcional pero recomendado: elige “Disable path length limit” al final
  4. Verifica: abre cmd o PowerShell y ejecuta python --version
  5. 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, sorted con key
  • EAFP, is vs ==

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 pip
  • itertools, 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

ParadigmaSoporte en PythonEjemplo mínimo
Imperativo/ProceduralNativo, sintaxis básicaif x > 0: print(x)
POOClases, herencia múltiple, mixinsclass Perro(Animal): ...
FuncionalLambdas, map/filter/reduce, itertools, functoolslist(map(str.upper, items))
DeclarativoComprensiones, 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; prefiere isinstance()
  • ❌ 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

TipoDescripciónEjemplo
intPrecisión arbitraria (no hay desbordamiento)x = 2**1000
floatIEEE 754 doble precisión (64 bits)pi = 3.14159
complexNúmeros complejosz = 3 + 4j
boolSubclase de int (True=1, False=0)activo = True
NoneTypeValor nuloresultado = 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
  • int tiene overhead variable: un entero pequeño cabe en 28 bytes, uno grande crece
  • float siempre ocupa 24 bytes (doble precisión)
  • bool ocupa 28 bytes (es subclase de int)
  • Small integer caching: enteros -5 a 256 son singletons

✅ Buenas prácticas

  • ✅ Usa isinstance(x, int) en vez de type(x) == int
  • ⚠️ No compares floats con == — usa math.isclose()
  • ❌ No asumas que is funciona para comparar números fuera del rango -5 a 256

🏗️ Metodología

  • En scripts de datos, prefiere Decimal del módulo decimal para dinero o precisión exacta
  • Para números grandes, int nativo es suficiente (no hay overflow)
  • En computación científica, numpy ofrece 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

TipoMutabilidadSintaxis
listMutable[1, 2, 3]
tupleInmutable(1, 2, 3)
dictMutable{"a": 1}
setMutable{1, 2, 3}
frozensetInmutablefrozenset([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úsqueda
  • dict/set: O(1) promedio en búsqueda, inserción, borrado
  • tuple: más ligera que list (inmutable, menor overhead)
  • array.array: ~4 bytes por entero frente a ~28 bytes de list[int]

✅ Buenas prácticas

  • ✅ Prefiere list.append() sobre list += [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 inmutables
  • dict: búsqueda rápida por clave, mapeos
  • set: 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).

🔗 Python docs — mutable

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 tuple para datos que no deben cambiar (coordenadas, settings)
  • ⚠️ Nunca uses list o dict como 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 frozenset en 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.

🔗 Wikipedia — Unicode

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 str tiene 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²) — prefiere join()

✅ 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.StringIO como buffer
  • Para búsquedas complejas, usa re.compile() una vez
  • Para datos binarios, usa bytes o bytearray, 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.

🔗 Python docs — data model

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 is para comparar con None (x is None), no ==
  • ⚠️ No uses is para comparar números o strings — usa ==
  • ❌ No asumas que del libera memoria inmediatamente

🏗️ Metodología

  • Para millones de objetos, usa __slots__ o array.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() y float() son O(n) para strings largos (parsean carácter por carácter)
  • list() y tuple() 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/elif es O(1) por condición (evaluación lazy)
  • match/case es similar en rendimiento a if/elif
  • Cadenas muy largas de elif (>10) deberían ser un dict de funciones

✅ Buenas prácticas

  • ✅ Usa match/case para pattern matching estructural (Python 3.10+)
  • ⚠️ No encadenes más de 5 elif — refactoriza a un dict de dispatch
  • ❌ No uses if x: para comprobar None — usa if x is not None:

🏗️ Metodología

  • Condicionales simples: if/elif/else es 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: dict como 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.

🔗 Python docs — for

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

  • for en 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 for sobre while cuando recorres colecciones
  • ⚠️ No modifiques la lista que estás iterando — itera sobre una copia
  • ❌ No uses while True sin 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 numpy vectorizado

🔗 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 for equivalentes
  • 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 try sin 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 Exception gené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.

🔗 Python docs — Modules

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

  • import carga el módulo en sys.modules — las siguientes importaciones son instantáneas
  • from 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__.py para exportar la API pública del paquete
  • Para proyectos grandes, estructura en subpaquetes

🔗 Para saber más


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.

🔗 Python docs — File I/O

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 encoding en 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.

🔗 Python docs — pathlib

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_path fixture de pytest
  • Caché: TemporaryDirectory para 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.

🔗 Python docs — io

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

  • StringIO y BytesIO operan 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 StringIO para datos binarios — usa BytesIO
  • ❌ 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.DictReader carga una fila a la vez — eficiente para archivos grandes
  • json.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 — usa json.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.

🔗 Python docs — struct

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.pack es 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

  • struct para formatos binarios simples
  • numpy para arrays grandes (más rápido y flexible)
  • pickle para 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 extra
  • bisect.bisect_left(): O(log n) tiempo, O(1) memoria extra
  • in en set/dict: O(1) promedio, O(n) peor caso

✅ Buenas prácticas

  • ✅ Si buscas frecuentemente, convierte la lista a set
  • ⚠️ bisect requiere datos ordenados
  • ❌ No abuses de in en listas grandes para búsquedas repetitivas

🏗️ Metodología

  • Búsqueda única: list.index() o in es 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().

🔗 Wikipedia — Timsort

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 key en vez de cmp (no existe cmp en Python 3)
  • ⚠️ Si ordenas objetos, implementa __lt__ o usa attrgetter
  • ❌ No redefinas sorted — usa key para 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.

🔗 Python docs — collections

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 que dict, sin overhead por valores ausentes
  • Counter: O(n) para construir, O(n log n) para most_common
  • heapq: O(log n) push/pop, O(1) para ver el mínimo

✅ Buenas prácticas

  • ✅ Usa deque para colas (FIFO) y pilas (LIFO)
  • ⚠️ No uses list.pop(0) — es O(n); usa deque.popleft()
  • ❌ No confundas set con Counter — Counter guarda frecuencias

🏗️ Metodología

  • deque: colas, historial, buffers deslizantes
  • defaultdict: agrupar elementos, acumular valores
  • Counter: análisis de frecuencias, estadísticas
  • heapq: prioridades, top-N elementos
  • ChainMap: 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

  • itertools funciones son lazy — no crean listas intermedias
  • chain evita concatenar listas (O(1) vs O(n) memoria)
  • cache (3.9+) es memoización sin límite
  • groupby requiere datos ordenados por la clave

✅ Buenas prácticas

  • ✅ Usa itertools para evitar bucles anidados y listas intermedias
  • ⚠️ groupby solo agrupa elementos consecutivos — ordena primero
  • ❌ No abuses de reduce si una comprehension es más legible

🏗️ Metodología

  • chain: concatenar múltiples iterables sin copiar
  • groupby: procesar datos agrupados (logs, eventos)
  • cache: funciones costosas con llamadas repetidas
  • singledispatch: 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.

🔗 Wikipedia — Big O

Sintaxis básica

Operaciónlistdictsetdequeheapq
Acceso por índiceO(1)——O(1)—
BúsquedaO(n)O(1) avgO(1) avgO(n)O(n)
Inserción al finalO(1) amortizado——O(1)O(log n)
Inserción al inicioO(n)——O(1)—
EliminaciónO(n)O(1) avgO(1) avgO(n)O(log n)
Mínimo/MáximoO(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 redimensiona
  • dict/set tienen 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.

🔗 Python docs — Decorators

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
  • @wraps es importante pero añade mínima sobrecarga
  • Cadenas de decoradores (>5) pueden afectar legibilidad y rendimiento

✅ Buenas prácticas

  • ✅ Siempre usa @functools.wraps en 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.

🔗 Real Python — GIL

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.

🔗 Wikipedia — Duck typing

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.

🔗 Python glossary — EAFP

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.

🔗 Python docs — Expressions

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.

🔗 Python docs — MRO

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 de ClaseBase.__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.

🔗 PEP 544 — Protocols

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.

🔗 Python docs — dataclasses

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=True usa __setattr__ más lento
  • __slots__ no es compatible directamente con dataclasses

✅ Buenas prácticas

  • ✅ Usa default_factory para valores mutables
  • ⚠️ No uses list/dict como 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ónEn Python
StrategyFunciones como estrategias
ObserverCallbacks en lista
SingletonMódulos son singletons
Factory__init_subclass__
AdapterDuck 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).

🔗 Wikipedia — Polimorfismo

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.

🔗 Pillow docs

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_fourcc con el codec adecuado
  • ⚠️ Libera recursos con cap.release() y cv2.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.

🔗 pydub docs

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 pydub para edición simple y librosa para 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.

🔗 SQLAlchemy docs

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.

🔗 FastAPI WebSockets docs

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).

🔗 Python docs — asyncio

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

EscenarioModeloEjemplo
I/O-bound (HTTP, BD)asyncioMicroservicios, scrapers
CPU-boundmultiprocessingML, procesamiento imágenes
Ambosasyncio + ProcessPoolExecutorAPI que calcula y consulta BD
StreamingasyncioWebSockets, 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

FrameworkPropósitoAsyncCLI
pytestUnitario e integraciónSí (pytest-asyncio)python -m pytest
unittestStdlib para testingSí (3.8+)python -m unittest
hypothesisProperty-based testingNopytest --hypothesis
coverage.pyCobertura de códigoSí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.

🔗 Python docs — asyncio

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")

🔗 Python docs — ctypes

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 launch desde 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

RecursoTipoEnlace
Python Crash Course (Eric Matthes)LibroNoStarch
Fluent Python (Luciano Ramalho)LibroO’Reilly
Automate the Boring StuffCurso gratisautomatetheboringstuff.com
CS50’s PythonCurso Harvardcs50.harvard.edu/python
FastAPI official tutorialDocsfastapi.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

HerramientaPara qué
uv / ryeGestión de proyectos y empaquetado
ruffLinter + formatter ultra-rápido
mypy --strictType checking
pre-commitHooks automáticos
copierTemplates de proyectos
noxAutomatizació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


⚠️ 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.

ErrorCausaSolución
ModuleNotFoundError: No module named 'x'El paquete no está instalado o estás en otro entornopip install x o uv add x; verifica que lo instalas dentro del mismo venv
pip: command not foundNo tienes pip en el PATH o usas el Python del sistemapython -m pip install x o python3 -m pip install x
NameError: name 'x' is not definedUsas una variable o función antes de definirlaRevisa el orden del código y que no haya un typo en el nombre
IndentationError: unexpected indentBloques con espacios y tabulaciones mezcladosElige 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 10Pasas 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 atributoComprueba que la llamada devuelve un objeto real y maneja el caso None
uv: Unable to find interpreterEl proyecto pide una versión de Python que no tienesInstala la versión indicada (.python-version/pyproject.toml) con uv python install <versión>

🔗 Guías relacionadas


🎯 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.
COMPARTIR:
COMENTARIOS:

📋 Contenido