El módulo timeit de Python permite medir pequeños fragmentos de código de forma más confiable que una resta manual con el reloj del sistema. Ejecuta la operación varias veces, reduce interferencias comunes y facilita comparar alternativas. Es especialmente útil para responder preguntas concretas: ¿una comprensión es más rápida que un bucle?, ¿conviene usar un set para pertenencia?, ¿una función de la biblioteca estándar supera una implementación manual?
Antes de optimizar, identifica qué parte del programa realmente consume tiempo. La guía de cProfile en Python ayuda a localizar cuellos de botella en aplicaciones completas. timeit sirve después para analizar una operación pequeña y aislada.
Por qué no basta con time.time()
Una medición manual puede verse afectada por la resolución del reloj, procesos del sistema, calentamiento de cachés y errores en la preparación del test.
import time
inicio = time.time()
resultado = sum(range(1000))
fin = time.time()
print(fin - inicio)Este ejemplo ejecuta la operación una sola vez. Para fragmentos rápidos, el ruido puede ser mayor que la duración real. timeit repite el código y utiliza un temporizador apropiado para medir intervalos cortos.
Primera medición con timeit()
import timeit
duracion = timeit.timeit("sum(range(1000))", number=100_000)
print(duracion)El resultado representa el tiempo total de todas las ejecuciones. Para obtener el promedio, divide por number:
promedio = duracion / 100_000
print(f"{promedio:.10f} segundos por ejecución")No interpretes diferencias diminutas sin repetir la prueba. El sistema operativo, la CPU y la versión de Python influyen en el resultado.
Usar una función en lugar de una cadena
La forma más clara suele ser pasar un objeto invocable:
import timeit
def calcular():
return sum(range(1000))
resultado = timeit.timeit(calcular, number=100_000)
print(resultado)Así evitas problemas de comillas y mantienes el código bajo prueba dentro del editor, con análisis estático y formato normal.
Comparar dos implementaciones
Supongamos que quieres construir cuadrados mediante un bucle o una comprensión:
def con_bucle():
resultado = []
for numero in range(1000):
resultado.append(numero * numero)
return resultado
def con_comprension():
return [numero * numero for numero in range(1000)]import timeit
veces = 10_000
tiempo_bucle = timeit.timeit(con_bucle, number=veces)
tiempo_comprension = timeit.timeit(con_comprension, number=veces)
print(tiempo_bucle)
print(tiempo_comprension)La guía de comprensiones de listas explica sintaxis, filtros y criterios de legibilidad. Una implementación ligeramente más rápida no es mejor si vuelve el código difícil de mantener.
Usar repeat() para reducir ruido
repeat() ejecuta varios grupos de mediciones:
resultados = timeit.repeat(
stmt=con_comprension,
repeat=7,
number=10_000,
)
print(resultados)
print(f"Mejor resultado: {min(resultados):.6f}")La documentación suele recomendar observar el valor mínimo porque las interrupciones externas pueden hacer una ejecución más lenta, pero rara vez la vuelven artificialmente más rápida. Aun así, informa también la dispersión cuando la estabilidad importe.
Elegir un number adecuado
Si number es demasiado pequeño, el ruido domina. Si es excesivo, la prueba tarda demasiado, calienta la CPU y puede cambiar su frecuencia. El objetivo es obtener una duración suficiente para medir, sin convertir el benchmark en una carga prolongada.
La línea de comandos puede elegir automáticamente un número razonable:
python -m timeit "sum(range(1000))"Utilizar setup con cadenas
Cuando empleas una sentencia como texto, setup prepara nombres fuera de la parte cronometrada:
import timeit
resultado = timeit.timeit(
stmt="objetivo in datos",
setup="datos = list(range(10000)); objetivo = 9999",
number=10_000,
)
print(resultado)No incluyas la creación de los datos dentro de stmt si solo quieres medir la búsqueda. En cambio, inclúyela cuando la construcción forme parte real de la operación que deseas evaluar.
Comparar list y set para pertenencia
datos_lista = list(range(10000))
datos_set = set(datos_lista)
objetivo = 9999
lista_tiempo = timeit.timeit(
lambda: objetivo in datos_lista,
number=100_000,
)
set_tiempo = timeit.timeit(
lambda: objetivo in datos_set,
number=100_000,
)
print(lista_tiempo, set_tiempo)El operador in en Python tiene costes diferentes según la estructura. Sin embargo, construir el set también cuesta tiempo y memoria. Si solo realizas una búsqueda, la conversión puede no compensar.
No incluir print() en el benchmark
La salida de terminal es lenta y variable. Este benchmark mediría principalmente el sistema de entrada y salida:
# Evitar para comparar cálculo puro
timeit.timeit(lambda: print(sum(range(1000))), number=100)Devuelve el resultado o consúmelo de una forma mínima. También evita acceso a red, disco y APIs en microbenchmarks; su latencia es demasiado variable para una comparación local sencilla.
Comprobar que ambas funciones hacen lo mismo
Antes de comparar velocidad, verifica resultados:
assert con_bucle() == con_comprension()Una función que omite validaciones puede parecer más rápida porque realiza menos trabajo. Define entradas, salidas y errores equivalentes.
Medir generadores correctamente
Crear un generador es barato porque su cuerpo todavía no se ejecuta:
def generar():
return (numero * numero for numero in range(1000))Medir solo generar() no evalúa el procesamiento. Debes consumirlo:
tiempo = timeit.timeit(lambda: list(generar()), number=10_000)La guía de yield y generadores explica evaluación perezosa, memoria y pipelines.
Evitar optimizaciones eliminadas o resultados no usados
CPython no elimina agresivamente cálculos como algunos compiladores, pero debes diseñar el test para representar el uso real. Guarda o devuelve el resultado y evita medir código simplificado que nunca aparece en la aplicación.
Comparar varias versiones de Python
Los resultados pueden cambiar entre versiones, sistemas y arquitecturas. Registra al menos:
import platform
import sys
print(sys.version)
print(platform.platform())
print(platform.processor())Un benchmark publicado sin entorno, datos y número de repeticiones es difícil de reproducir.
Calentamiento y cachés
La primera ejecución puede cargar módulos, asignar memoria o llenar cachés. timeit reduce algunos problemas al repetir, pero no sustituye un diseño cuidadoso. Si comparas funciones que usan caché, decide si quieres medir la primera llamada o llamadas posteriores.
El artículo sobre lru_cache en Python muestra cómo la memoización cambia completamente el coste entre una llamada inicial y una repetida.
GC y comportamiento de timeit
timeit desactiva temporalmente la recolección de basura por defecto durante la medición. Esto hace comparables muchos fragmentos, pero puede ocultar un coste real si el código crea muchos objetos cíclicos. Puedes activarla explícitamente en el setup cuando forme parte del escenario:
timeit.timeit(
stmt="crear_objetos()",
setup="gc.enable()",
globals={"crear_objetos": crear_objetos, "gc": __import__("gc")},
number=1000,
)Usar Timer y globals
from timeit import Timer
medidor = Timer(
stmt="funcion(datos)",
globals={"funcion": procesar, "datos": datos},
)
print(medidor.repeat(repeat=5, number=1000))globals evita imports artificiales dentro de cadenas y facilita trabajar con objetos del programa.
Microbenchmark frente a rendimiento real
Una mejora local puede no afectar al tiempo total de la aplicación. Si una función representa el 1% del proceso, duplicar su velocidad apenas cambia el resultado final. Primero perfila el programa y después optimiza el cuello de botella.
La guía sobre por qué un script Python es lento presenta algoritmos, estructuras, I/O, NumPy y concurrencia como causas más importantes que pequeños cambios sintácticos.
Presentar resultados
No digas únicamente “A es más rápido”. Indica datos, código, versión, hardware, repeticiones y unidad por operación. Calcula una proporción:
mejor_a = min(timeit.repeat(funcion_a, repeat=7, number=10000))
mejor_b = min(timeit.repeat(funcion_b, repeat=7, number=10000))
print(f"A/B: {mejor_a / mejor_b:.2f}x")Una diferencia pequeña puede desaparecer con otra entrada. Prueba tamaños representativos y casos normales, mejores y peores.
Errores frecuentes
Los errores más comunes son medir una sola vez, incluir preparación distinta, comparar funciones que producen resultados diferentes, usar entradas diminutas, medir print o red, ignorar calentamiento y afirmar conclusiones universales. También es frecuente optimizar antes de perfilar.
Fuentes oficiales
La documentación oficial de timeit explica timeit(), repeat(), Timer, línea de comandos y recolección de basura. La documentación oficial de los perfiles de Python aclara cuándo usar cProfile para programas completos.
Conclusión
timeit es la herramienta adecuada para microbenchmarks reproducibles dentro de Python. Separa preparación y operación, repite varias veces, consume generadores, verifica resultados y registra el entorno. Usa el benchmark para responder una pregunta concreta después de encontrar un cuello de botella real, no para reemplazar la claridad con optimizaciones prematuras.






