Embercode
✕
LoopsLesson 16 of 16

30 min read

Buffered output in Python

Why this matters

A program often prints a lot of lines. It may print a report with 200,000 rows, a list of every prime below one million, or a grid of characters for a game. The simplest code calls print once for each line, and that works. Each call adds a small piece of text to the output.

A buffer makes this faster. A buffer is a block of memory that holds data for a while before the data is sent. The program adds text to the buffer, which is fast because it stays in memory. When the buffer is full, or when the program asks for it, the whole block is sent in one step. Fewer, larger sends are faster than many small ones.

Python already uses a buffer for normal output. When the output goes to a file or to another program, print does not send each line on its own. This lesson shows how that works, how to build text in pieces before printing it, and when to ask Python to send the buffer early. It then measures the difference, and it covers the mistakes that appear with buffers. Reading input is not covered. The loops that produce the lines were covered in the earlier lessons of this module.

Readers who know Java may expect print to send each line at once. In Java, System.out.println does that. In Python it depends on where the output goes, as the measurements below show.

The idea: many small pieces, one send

Start with the simplest case. The program below prints three lines, one print call for each.

Python · editable

The output is:

Step 1: one call per line
line 1
line 2
line 3

sys.stdout is the standard output of the program. It is the stream that the terminal shows by default. A stream is a flow of data from the program to somewhere else. The function print writes its text to sys.stdout and adds a line break at the end. With three lines, nobody notices. With 200,000 lines, the number of sends becomes a real cost.

When the output goes to a file or another program, Python keeps the text in a buffer inside sys.stdout and sends it in blocks. When the output goes to a terminal, Python sends each finished line as soon as it ends, so the lines appear while the program runs. The program does not need any extra code for either case.

Building the text in one piece

Python strings cannot grow in place. A string is a value that cannot be changed after it is made. The usual way to build a large text is to collect the pieces in a list, and then join them into one string at the end. The method join puts every item of a list into one string, with the string in front of .join placed between the items.

Python · editable

The output is:

row 1
row 2
row 3

The method append adds one piece to the end of the list. The call "".join(parts) joins the pieces with nothing between them. The line breaks are already in the pieces, so the print call uses end="". This stops print from adding a second line break at the end, which would leave an empty line.

The standard library also has io.StringIO, an in-memory file with a write method. It fills the same role as Java's StringBuilder. This lesson uses lists, because they are simpler to read.

Writing in pieces with sys.stdout.write

The function sys.stdout.write writes a string and adds no line break. The program must add "\n" itself. The method flush sends everything waiting in the buffer now, so the output appears even if the program keeps running.

Python · editable

The output is:

buffered 1
buffered 2
buffered 3
done

The line import sys loads the module that holds sys.stdout. The final print writes into the same buffer as the write calls, so the lines keep their order. Mixing print and sys.stdout.write in one program is safe for this reason. The next section also shows a case where mixing is not safe.

The call print(..., flush=True) is a third option. It sends the buffer after that one call, so every line goes out at once. It is slower for large output, and the measurements below show how much.

Worked example 1: the same report, four ways

The next program writes a report of 200,000 numbers, one per line. It has four modes, and the mode is chosen by a word on the command line. The command line is the text typed after the program name. Python puts those words in the list sys.argv. The item at position 0 is the program name, so the first word the user typed is sys.argv[1]. In Java the first word is at position 0 of args, so this is one place where the positions differ.

import sys
import time
 
LINES = 200_000
mode = sys.argv[1]
start = time.perf_counter()
 
if mode == "print":
    for i in range(1, LINES + 1):
        print(i)
elif mode == "print_flush":
    for i in range(1, LINES + 1):
        print(i, flush=True)
elif mode == "join":
    parts = []
    for i in range(1, LINES + 1):
        parts.append(f"{i}\n")
    sys.stdout.write("".join(parts))
elif mode == "write":
    for i in range(1, LINES + 1):
        sys.stdout.write(f"{i}\n")
    sys.stdout.flush()
 
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"{mode} took {elapsed_ms:.0f} ms", file=sys.stderr)

The number 200_000 is written with underscores, which Python ignores. It makes the number easier to read. The function time.perf_counter returns a clock reading in seconds, with a fine resolution. Taking the time before and after the work gives the duration. The timing line goes to sys.stderr, the error stream, through the file= argument. Keeping it separate from sys.stdout means the timing does not mix into the report.

Run each mode with the output sent to a file. The file is written with the > symbol, which means "send the output to this file".

python3 main.py print > out_print.txt
python3 main.py print_flush > out_print_flush.txt
python3 main.py join > out_join.txt
python3 main.py write > out_write.txt

Each run prints one timing line. These results come from one run of each mode on a Linux machine under WSL2, with Python 3.14.4, and the output going to a file. The exact numbers depend on the machine, and they will be different on yours. Three runs of each mode gave these ranges.

print        35 to 39 ms
print_flush  352 to 370 ms
join         28 to 31 ms
write        25 to 30 ms

Each run wrote 200,000 lines to its file, which the command wc -l out_print.txt confirms.

The print_flush mode was about ten times slower than the others. It sends one piece for each line. The default print mode was almost as fast as the buffered modes, because Python does not send each call when the output goes to a file. The join and write modes were the fastest of the four in these runs. The join mode uses the most memory, because it holds the whole report in memory before sending it. The write mode sends the text in blocks as it goes, so it uses a small, steady amount of memory no matter how long the report is.

The lesson for Python is to measure before you change code. A plain print loop is often fast enough, and the buffer is already there.

Worked example 2: a grid of characters

Buffered output is most useful when each line is built from several parts. A game board is a good example. The next program prints a 4 by 4 grid where each cell is a # or a .. It builds each row as a list of characters, joins the list into one string, and writes the row with its line break.

Python · editable

The output is:

#.#.
.#.#
#.#.
.#.#

Each row is built in its own short list, which is replaced by a new, empty list at the start of each row. The row is then written as one string. This split is common. A small list handles one line, and sys.stdout handles the whole output. The % symbol is the remainder operator. The expression (row + col) % 2 == 0 is true when the sum of the row and column numbers is even, and this gives the checkerboard pattern. The same choice can be written in one line as "#" if (row + col) % 2 == 0 else ".", which is the conditional expression.

Worked example 3: when the buffer is not flushed

The next program writes three lines and never calls flush. It then prints a last line with print.

Python · editable

The output is:

this line is never flushed 1
this line is never flushed 2
this line is never flushed 3
program ended

All four lines appear. When a Python program ends normally, or when it stops because of an error, the interpreter sends whatever is waiting in sys.stdout before it exits. So a missing flush does not lose text in these cases.

Text can be lost when the program ends in a way that skips this step. The function os._exit ends a program at once, and it does not send the buffer. The next program uses it.

import os
import sys
 
for i in range(1, 4):
    sys.stdout.write(f"this line is never flushed {i}\n")
print("program ended")
os._exit(0)

Run with the output sent to a file, the file is empty.

python3 main.py > out.txt
wc -c out.txt

The output is:

0 out.txt

When the output goes to a terminal, Python sends each finished line at once, so the lines may still appear. Do not depend on that. Call flush before any exit that skips the normal shutdown, A signal that kills the process, such as SIGKILL, skips the normal shutdown too, and no code runs to flush after that.

Worked example 4: one line of numbers

The last example adds a rule. A line of output must hold the squares of 1 to 10 separated by commas, with no comma after the last number. A loop that adds a comma after every number would leave one at the end. One way to avoid this is to add the comma before every number except the first. Python has a shorter way, which is the method join used with a generator expression.

Python · editable

The output is:

1, 4, 9, 16, 25, 36, 49, 64, 81, 100

The text str(i * i) for i in range(1, 11) is a generator expression. It makes each piece as join asks for it, so your code does not build a list first. Inside, join still collects the pieces into a list, so the memory use is about the same. The function str turns each number into text. The separator ", " is placed between the pieces, and never after the last one.

Choosing an approach

The approaches produce the same text. The choice depends on how large the output is, and whether each line must appear as soon as it is made.

ApproachGood forWatch out for
print in a loopMost programs, when lines do not need to appear at onceWhen the output goes to a file or pipe, lines wait in the buffer until it is sent
print(..., flush=True) in a loopLines that must show up at once, such as progress messages in a long calculationAbout ten times slower for 200,000 lines in the test above
"".join and one writeA report that is complete before printing, and a single line made of partsAll the text sits in memory until the write
sys.stdout.write and flushLarge output written in piecesNothing adds a line break, so the program must add "\n"

A progress message in a long calculation is a case for print(..., flush=True), because the user should see it at once. A report with 200,000 lines is a case for a single write or a loop of sys.stdout.write, because the speed matters and the memory use should stay small.

Common mistakes

The first mistake is building text in memory and then crashing before it is printed. The next program joins its pieces in a list and divides by zero during the loop.

Python · editable

The output is:

Traceback (most recent call last):
  File "main.py", line 4, in <module>
    parts.append(f"{100 // v}\n")
                    ~~~~^^~~
ZeroDivisionError: division by zero

The path in front of main.py depends on where you saved the file, so it is shortened here. No numbers were printed. The line that calls print was never reached, because the crash happened first, so the pieces were never sent. The version below prints each number with print in the loop.

Python · editable

The output is:

10
5
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    print(100 // v)
          ~~~~^^~~
ZeroDivisionError: division by zero

The two numbers appear before the traceback, which is the error message. The print calls wrote into the buffer, and the interpreter sent the buffer before it printed the error. A program that writes a large report should still call flush at the end, and it should call flush before any step that might fail if the text before that step matters.

The second mistake is passing the wrong type to a write. The function print accepts any values and turns them into text. The method write takes only a string, so passing a list fails at once.

Python · editable

The output is:

Traceback (most recent call last):
  File "main.py", line 3, in <module>
    sys.stdout.write(["hello"])
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^
TypeError: write() argument must be str, not list

The fix is to join the list first, with "".join(...). The same kind of mistake appears with join when the items are numbers. The call ", ".join([1, 2]) raises TypeError: sequence item 0: expected str instance, int found, so each number must be turned into text with str first. Python has no step that checks types before the program runs. The error appears only when the line runs, so a path through the code that is rarely used can hide it.

The third mistake is using two writers on the same stream. The function print and sys.stdout.write share one buffer, so their lines keep their order. A second writer built over the same stream has its own buffer, and the two buffers are sent at different times. The next program creates one, and it writes line A to the second writer, prints line B, then writes line C to the second writer.

Python · editable

The output, with the output sent to a file or pipe, is:

buffered line A
buffered line C
direct line B

Line B was printed second in the code, but it came out last. The second writer sent lines A and C when flush was called. The print output waited in sys.stdout until the program ended. A program should use one writer for one report. If two must be used, flush the second writer after each write, and pass flush=True to each print call.

The fourth mistake is building a huge list for output that could be written as it is made. The list keeps all of its text in memory until the program joins and writes it. For a report with millions of lines, the memory use can become large. A loop of sys.stdout.write sends pieces as it goes, so it does not need to hold the whole report, and this is the better choice when the report is very large.

Practice

Exercise 1. Print the squares of the numbers 1 to 5, one per line, using a list of pieces and a single print call.

Show the answer

parts = []
for i in range(1, 6):
    parts.append(f"{i * i}\n")
print("".join(parts), end="")

The output is 1, 4, 9, 16 and 25, each on its own line. The list collects all five lines, and join puts them into one string. The line breaks are already in the text, so end="" is used.

Exercise 2. The program below writes three pieces with sys.stdout.write, but the output is one long line. Find the missing part and add it.

import sys
 
for i in range(1, 4):
    sys.stdout.write("line " + str(i))

The output is line 1line 2line 3, with no line breaks.

Show the answer

The write method adds no line break, so the program must add one. Change the line inside the loop to sys.stdout.write("line " + str(i) + "\n"). Then the output is line 1, line 2 and line 3, each on its own line.

Exercise 3. A program prints one line in each step of a loop. In step 6 it divides by zero. How many lines appear on the screen, and why?

for i in range(6):
    print(10 // (5 - i))

Show the answer

Five lines appear, with the values 2, 2, 3, 5 and 10. Each print call adds its line to the buffer, and the interpreter sends the buffer before it prints the error. The traceback appears after the five lines. If the five values had been joined into one string in a list, and the join had been printed after the loop, none of them would appear, because the crash would happen before the print.

Exercise 4. Print the multiplication table for 1 to 3, one line for each pair, such as 1 x 2 = 2. Use sys.stdout.write and call flush once at the end.

Show the answer

import sys
 
lines = [f"{i} x {j} = {i * j}" for i in range(1, 4) for j in range(1, 4)]
sys.stdout.write("\n".join(lines) + "\n")
sys.stdout.flush()

The output has nine lines, starting with 1 x 1 = 1 and ending with 3 x 3 = 9. The list comprehension makes every pair, and the single join puts the lines together with a line break between them. The final "\n" ends the last line, and the single flush sends everything.

Summary

  • print writes to standard output. When the output goes to a file or pipe, Python sends it in blocks, so a loop of print calls is already fast.
  • "".join(parts) builds one string from a list of pieces, and one write sends it.
  • sys.stdout.write adds no line break, and flush sends the buffer now.
  • print(..., flush=True) sends each line at once, which is slower for large output.
  • A normal end or an error sends the buffer, but os._exit does not.
  • Do not mix two separate writers over the same stream, because the order of lines can change.
Check yourself: Float drift and output in Python (6 questions) →Practice this topic: coding problems for Loops →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Half-open ranges in Python