Это связано с тем, что для конкатенации строк должен быть выделен все больший и больший кусок памяти:
x = 'a' # String of size 1 allocated
x += 'b' # String of size 2 allocated, x copied, and 'b' added. Old x discarded
x += 'b' # String of size 3 allocated, x copied, and 'c' added. Old x discarded
x += 'b' # String of size 4 allocated, x copied, and 'd' added. Old x discarded
x += 'b' # String of size 5 allocated, x copied, and 'e' added. Old x discarded
Итак, что происходит, вы выполняете большие выделения и копии, но затем поворачиваетесь и выбрасываете их. Очень расточительно.
x = ['a', 'b', ..., 'z'] # 26 small allocations
x = ''.join(x) # A single, large allocation