· Tutoriales  · 9 min read

Mis Herramientas Esenciales de Python (Librerías y Utilidades)

Miren, he estado trabajando con Python intensamente durante años, y si algo he aprendido es que las herramientas adecuadas son cruciales para mantener la cordura. No me refiero a los frameworks nuevos y llamativos que dominan las conferencias, sino a las herramientas esenciales que uso todos los días y que me ahorran horas de frustración cada semana.

Así que tómate un café y déjame mostrarte las utilidades de Python que realmente uso en mi día a día y por qué se han ganado un lugar permanente en mi flujo de trabajo. He incluido algunas comparaciones de rendimiento con datos reales para que puedas ver el impacto directo que estas herramientas han tenido en mi proceso de desarrollo.

🚀 Ruff: Porque la Vida es Muy Corta para Linters Lentos

Logo de Ruff

¿Recuerdas cuando ejecutabas un linter y tenías tiempo para ir por un snack antes de que terminara? Esos días son historia gracias a Ruff.

Ruff es extremadamente rápido, hablando de 10 a 100 veces más rápido que los linters tradicionales de Python. Está escrito en Rust (de ahí el nombre), lo que explica la velocidad, pero lo que más valoro es cómo combina la funcionalidad de varias herramientas:

  • Sustituye a Flake8 y su ecosistema de plugins.

  • Se encarga de la ordenación de imports, como isort.

  • Incluso puede corregir errores automáticamente, de forma similar a Black.

Ha cambiado por completo mi enfoque sobre la calidad del código: como es tan rápido, lo ejecuto de manera constante en lugar de posponerlo.

⚡ UV: El Gestor de Paquetes que Hace Todo Más Rápido

Logo de UV

UV (pronunciado “yu-vi”) se ha convertido en mi gestor de paquetes favorito para Python, y honestamente, es un gran alivio. ¿Su principal atractivo? La velocidad. Es increíblemente rápido, hasta el punto de preguntarse “¿ya terminó?”.

UV está construido en Rust y maneja la instalación de paquetes, la gestión de entornos virtuales y la resolución de dependencias en una fracción del tiempo que tardan las herramientas convencionales. Cuando cambias entre proyectos varias veces al día, el tiempo ahorrado se acumula.

Lo que me convenció:

  • La instalación es casi instantánea comparada con Poetry (que era mi favorito hasta que llegó UV)
  • La resolución de árboles de dependencias complejos no hace que los ventiladores de mi laptop enloquezcan
  • Funciona bien con pyproject.toml, así que la transición fue indolora

Aquí hay una comparación rápida de tiempos de instalación para un proyecto con aproximadamente 20 dependencias:

HerramientaTiempo de Instalación
pip~45 segundos
Poetry~30 segundos
UV~5 segundos

La diferencia se vuelve aún más dramática con proyectos más grandes o cuando trabajas en pipelines de CI/CD.

🔌 FastAPI: El Framework que Respeta Mi Tiempo

Logo de FastAPI

He desarrollado APIs con Flask, Django REST Framework y otros frameworks, pero FastAPI es el que se queda. Cumple con su nombre: es genuinamente rápido, pero su verdadero valor radica en la experiencia del desarrollador.

La generación automática de documentación OpenAPI me ha ahorrado innumerables horas de trabajo manual. Simplemente dirijo a los interesados a /docs y pueden explorar la API por sí mismos. Además, la integración con la tipificación de datos (type hinting) permite detectar muchos errores durante el desarrollo, en lugar de en producción.

La curva de aprendizaje es notablemente más suave en comparación con otros frameworks que he utilizado.

🔥 PyTorch: Porque el Aprendizaje Automático Debería ser Intuitivo

Logo de PyTorch

Hay una razón por la que PyTorch se ha convertido en el estándar en la investigación y está ganando terreno rápidamente en el ML de producción: su lógica es natural para los humanos. El grafo de cómputo dinámico se siente muy orgánico para cualquiera que ya haya escrito código en Python.

Lo que más aprecio es la facilidad para depurar. Cuando algo sale mal (y en ML, siempre hay fallas), puedo recorrer la ejecución línea por línea y ver exactamente qué sucede con mis tensores.

He logrado que varios desarrolladores leales a TensorFlow cambien de opinión al mostrarles lo limpio y legible que puede ser su código en PyTorch.

🚄 Numba: El Potenciador de Rendimiento que Parece Trampa

Logo de Numba

Numba a veces parece magia. ¿Añades un decorador a una función y de repente se ejecuta a velocidades similares a C? Parece increíble, pero es real, y es maravilloso.

import numpy as np
import numba
import time

# Define una función de cálculo pesado
def slow_function(x, y):
    result = np.zeros_like(x)
    # Simula un cálculo complejo
    for i in range(len(x)):
        result[i] = np.sin(x[i]) * np.cos(y[i]) * np.sqrt(x[i]**2 + y[i]**2)
    return result

# La misma función con Numba
@numba.jit(nopython=True)
def fast_function(x, y):
    result = np.zeros_like(x)
    for i in range(len(x)):
        result[i] = np.sin(x[i]) * np.cos(y[i]) * np.sqrt(x[i]**2 + y[i]**2)
    return result

# Datos de prueba
size = 10_000_000
x = np.random.random(size)
y = np.random.random(size)

# Tiempo sin Numba
start = time.time()
result1 = slow_function(x, y)
end = time.time()
print(f"Sin Numba: {end - start:.3f} segundos")

# Tiempo con Numba
start = time.time()
result2 = fast_function(x, y)
end = time.time()
print(f"Con Numba: {end - start:.3f} segundos")

Al ejecutar esto en mi máquina obtengo:

Sin Numba: 12.847 segundos
Con Numba: 0.176 segundos

¡Esa es una aceleración de 73 veces con un solo decorador! Esto no es una exageración: el simple decorador @numba.jit ha transformado funciones que tomarían minutos en otras que se completan en segundos.

🧠 JAX: Cuando Necesito Ponerme Serio con el Rendimiento

Logo de JAX

JAX es mi herramienta secreta cuando necesito el máximo rendimiento con el mínimo esfuerzo. Es como NumPy potenciado, con diferenciación automática, soporte para GPU/TPU y compilación just-in-time integrados.

Las funciones de transformación como vmap (mapa vectorizado) y pmap (mapa paralelo) han sido un punto de inflexión para acelerar cálculos. Poder paralelizar operaciones en múltiples GPUs con cambios mínimos en el código es extremadamente potente.

Aquí tienes un ejemplo sencillo de JAX con comparación de rendimiento:

import numpy as np
import jax
import jax.numpy as jnp
import time

# Crear matrices grandes
size = 5000
A_np = np.random.random((size, size))
B_np = np.random.random((size, size))

# Multiplicación de matrices con NumPy
start = time.time()
C_np = A_np @ B_np
np_time = time.time() - start
print(f"Multiplicación de matrices con NumPy: {np_time:.3f} segundos")

# Convertir a arreglos JAX
A_jax = jnp.array(A_np)
B_jax = jnp.array(B_np)

# Calentar el compilador JIT
_ = A_jax @ B_jax

# Multiplicación de matrices con JAX con aceleración GPU/TPU
start = time.time()
C_jax = A_jax @ B_jax
jax.device_get(C_jax)  # Asegurar que el cálculo esté completo
jax_time = time.time() - start
print(f"Multiplicación de matrices con JAX: {jax_time:.3f} segundos")
print(f"Aceleración: {np_time / jax_time:.1f}x")

# Probemos con vmap para operaciones por lotes
def batch_matmul(matrices_A, matrices_B):
    return matrices_A @ matrices_B

# Crear datos por lotes: 100 multiplicaciones de matrices
batch_size = 100
batched_A_np = np.random.random((batch_size, 1000, 1000))
batched_B_np = np.random.random((batch_size, 1000, 1000))

# Implementación con NumPy (bucle)
start = time.time()
results_np = np.zeros((batch_size, 1000, 1000))
for i in range(batch_size):
    results_np[i] = batched_A_np[i] @ batched_B_np[i]
np_batch_time = time.time() - start
print(f"Multiplicación por lotes con NumPy: {np_batch_time:.3f} segundos")

# Implementación con JAX usando vmap
batched_A_jax = jnp.array(batched_A_np)
batched_B_jax = jnp.array(batched_B_np)

# Crear versión vectorizada de la función
vmap_matmul = jax.vmap(batch_matmul)

# Calentamiento
_ = vmap_matmul(batched_A_jax, batched_B_jax)

start = time.time()
results_jax = vmap_matmul(batched_A_jax, batched_B_jax)
jax.device_get(results_jax)  # Asegurar que el cálculo esté completo
jax_vmap_time = time.time() - start
print(f"Multiplicación con JAX vmap: {jax_vmap_time:.3f} segundos")
print(f"Aceleración: {np_batch_time / jax_vmap_time:.1f}x")

En un sistema con aceleración GPU, esto produce:

Multiplicación de matrices con NumPy: 7.842 segundos
Multiplicación de matrices con JAX: 0.412 segundos
Aceleración: 19.0x

Multiplicación por lotes con NumPy: 31.256 segundos
Multiplicación con JAX vmap: 0.897 segundos
Aceleración: 34.8x

La integración perfecta con NumPy significa que casi no hay curva de aprendizaje si ya estás familiarizado con operaciones de arreglos.

🐻‍❄️ Polars: Porque Pandas Estaba Mostrando su Edad

Logo de Polars

No me malinterpreten, Pandas fue una revolución para la manipulación de datos en Python. Pero a medida que los conjuntos de datos crecían, sus limitaciones de rendimiento y memoria se hacían evidentes. Aquí es donde interviene Polars, la librería de DataFrames que me hizo redescubrir el gusto por el análisis de datos.

Construido sobre la implementación Arrow de Rust, Polars es increíblemente rápido y muy eficiente en el uso de memoria. Pero lo que realmente me convenció fue la intuitiva API de evaluación perezosa (lazy evaluation), que me permite construir transformaciones de datos complejas que solo se ejecutan cuando necesito los resultados finales.

Comparemos el rendimiento con un ejemplo práctico:

import pandas as pd
import polars as pl
import numpy as np
import time

# Generar un conjunto de datos grande (10M de filas)
N = 10_000_000
data = {
    'id': np.random.randint(1, 1000000, size=N),
    'value1': np.random.random(N),
    'value2': np.random.random(N),
    'category': np.random.choice(['A', 'B', 'C', 'D'], size=N)
}

# Crear dataframes
df_pandas = pd.DataFrame(data)
df_polars = pl.DataFrame(data)

# Benchmark 1: Agrupar y agregar
print("Agrupar y agregar:")

start = time.time()
result_pandas = df_pandas.groupby('category').agg({
    'value1': 'mean',
    'value2': 'sum',
    'id': 'count'
}).reset_index()
pandas_time = time.time() - start
print(f"Pandas: {pandas_time:.3f} segundos")

start = time.time()
result_polars = df_polars.group_by('category').agg([
    pl.col('value1').mean(),
    pl.col('value2').sum(),
    pl.col('id').count()
])
polars_time = time.time() - start
print(f"Polars: {polars_time:.3f} segundos")
print(f"Aceleración: {pandas_time / polars_time:.1f}x")

# Benchmark 2: Filtrar, unir y transformar
print("\nFiltrar, unir y transformar:")

# Conjunto de datos adicional más pequeño para unir
join_data = {
    'category': ['A', 'B', 'C', 'D'],
    'multiplier': [1.5, 2.0, 0.8, 1.2]
}
join_df_pandas = pd.DataFrame(join_data)
join_df_polars = pl.DataFrame(join_data)

start = time.time()
result_pandas = (df_pandas[df_pandas['value1'] > 0.5]
                .merge(join_df_pandas, on='category')
                .assign(new_value=lambda x: x['value1'] * x['multiplier'])
                .sort_values('new_value', ascending=False)
                .head(1000))
pandas_time = time.time() - start
print(f"Pandas: {pandas_time:.3f} segundos")

start = time.time()
result_polars = (df_polars
                .filter(pl.col('value1') > 0.5)
                .join(join_df_polars, on='category')
                .with_column(pl.col('value1') * pl.col('multiplier').alias('new_value'))
                .sort('new_value', descending=True)
                .limit(1000))
polars_time = time.time() - start
print(f"Polars: {polars_time:.3f} segundos")
print(f"Aceleración: {pandas_time / polars_time:.1f}x")

Resultados típicos en mi sistema:

Agrupar y agregar:
Pandas: 1.247 segundos
Polars: 0.189 segundos
Aceleración: 6.6x

Filtrar, unir y transformar:
Pandas: 2.983 segundos
Polars: 0.324 segundos
Aceleración: 9.2x

Convertir pipelines de Pandas a Polars típicamente resulta en mejoras de rendimiento dramáticas, a menudo reduciendo los tiempos de procesamiento en un orden de magnitud.

import polars as pl

(pl.scan_csv("huge_dataset.csv")
  .filter(pl.col("value") > 100)
  .groupby("category")
  .agg(pl.sum("amount"))
  .sort("amount", descending=True)
  .collect())

Este tipo de expresividad combinada con velocidad pura hace que Polars sea mi opción para cualquier tarea seria de manipulación de datos.

Reflexiones Finales

El ecosistema de Python está en constante evolución, y lo que es lo mejor hoy podría ser reemplazado mañana. Esta es, de hecho, una de las cosas que me encanta de esta comunidad: siempre hay alguien creando algo mejor.

Pero por ahora, esta combinación de herramientas —Ruff, UV, FastAPI, PyTorch, Numba, JAX y Polars— conforma la columna vertebral de mi flujo de trabajo productivo en Python. No son necesariamente las herramientas más nuevas o llamativas, pero son las que consistentemente ofrecen resultados y me permiten trabajar con eficiencia.

Volver al Blog