String += en un loop es O(n²), usa join
Este hábito de un solo carácter hace tu loop cuadrático. += sobre un string dentro de un loop parece inofensivo — un operador, una línea — pero en Python puede convertir una construcción de n elementos en trabajo O(n²). La solución también es una línea.
Por qué += en un loop se vuelve cuadrático
Los strings de Python son inmutables: ninguna operación modifica un string en su lugar. Así que s += part no agrega al final — asigna un string completamente nuevo y copia todo s más part, en cada iteración.
Hazlo n veces y el copiado crece: 1 carácter, luego 2, luego 3 … aproximadamente n²/2 copias de caracteres en total. El propio FAQ de Python lo dice claro: concatenar muchos strings así tiene un costo total de ejecución "cuadrático en la longitud total del string".
Con 100 partes no lo notarás. Con 100,000 líneas de un log o un reporte generado, el loop que debería tomar milisegundos toma minutos — y empeora cuanto más grande es la entrada, justo cuando menos lo puedes permitir.
La solución: junta las partes, une una vez
El idiom recomendado, directo del FAQ de Python: acumula las piezas en una lista y llama str.join() una sola vez al final.
# O(n²): copies the whole string every pass
result = ""
for row in rows:
result += render(row)
# O(n): one allocation, one copy at the end
parts = []
for row in rows:
parts.append(render(row))
result = "".join(parts)
list.append es O(1) amortizado, y "".join(parts) mide la longitud total, asigna memoria una vez y copia cada parte exactamente una vez. Misma salida, tiempo lineal. Si el cuerpo del loop es simple, compáctalo más: result = "".join(render(row) for row in rows).
Construcción incremental: io.StringIO
Cuando construyes texto de forma incremental entre funciones — un generador de código, un acumulador de tokens de un LLM, cualquier cosa con forma de writer — una lista de partes puede resultar incómoda. io.StringIO te da un buffer de texto en memoria con API de archivo, y el FAQ de Python lo lista como el otro idiom razonablemente eficiente:
import io
buf = io.StringIO()
for chunk in stream:
buf.write(chunk)
result = buf.getvalue()
Bonus: cualquier cosa que espere un objeto de archivo (csv.writer, print(file=...), writers de plantillas) puede escribir directo al buffer.
Reglas prácticas
| Situación | Usa |
|---|---|
| Unas pocas piezas conocidas | + simple o un f-string |
| Loop que construye un string a partir de n partes | Lista + "".join(parts) |
| Construcción incremental estilo writer, APIs de archivo | io.StringIO |
Acumular bytes en un loop | bytearray + += (es mutable, así que esto SÍ es eficiente) |
Notas para usuarios avanzados
- CPython a veces oculta el bug. CPython tiene una optimización basada en refcounts que puede hacer
s += partrápido en algunas ejecuciones. PEP 8 te dice que no dependas de ella: la optimización "es frágil incluso en CPython (solo funciona para algunos tipos) y no existe en absoluto en implementaciones que no usan refcounting". Tu código andará bien en tu laptop y será cuadrático en PyPy — escribe la forma conjoin(). - Con bytes la regla se invierte. Para
bytes, el idiom recomendado del FAQ tiene la forma opuesta: extender unbytearraycon+=.bytearrayes mutable, así que agregar en el lugar sí es barato. - Es un olor clásico de código generado por AI. Los asistentes entrenados con snippets de tutoriales emiten loops con
result += ...sin problema. Al revisar código generado, busca+=sobre un string dentro defor/while— es una victoria de revisión de 10 segundos. - Compruébalo con
timeit.python -m timeitcon 10k partes hace visible la diferencia en un solo comando; medir gana a discutir.
Recursos
- Python FAQ — What is the most efficient way to concatenate many strings together?
str.join()— referencia de tipos estándar de Pythonio.StringIO— referencia del módulo io de Python- PEP 8 — Programming Recommendations (no dependas de la optimización de
+=de CPython) - Glosario de Python — immutable
¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.