Why `PYTHONUNBUFFERED` Exists: Seeing stdout When Python Crashes

pythonperformancePYTHONUNBUFFEREDbuffering

896 Words

2026-03-09 00:00 +0000


Seeing stdout when a program crashes

Python’s I/O layer normally buffers writes to stdout and stderr.
This means some output may remain in Python’s own buffers instead of being handed to the OS immediately. (Buffering also exists at other layers—see the note at the end of this post.)

PYTHONUNBUFFERED=1 (or python -u) disables this buffering.

The two are equivalent: the Python docs define PYTHONUNBUFFERED as “equivalent to specifying the -u option”. Since Python 3.7, -u makes stdout and stderr fully unbuffered, including the text layer.

The difference becomes visible when a program terminates abnormally, such as during a segmentation fault.

The examples below trigger a crash with ctypes.string_at(0), which reads memory at address 0 and reliably kills the process with a segmentation fault. All outputs were reproduced on Python 3.14 on Linux.


Exception vs crash

An uncaught Python exception is not a crash.

When Python exits due to an exception, the interpreter performs normal shutdown steps, including flushing stdout and stderr.
Because of this, buffered output is usually written successfully.

A segmentation fault (or other fatal crash) terminates the process immediately.
In that situation, Python never gets the opportunity to flush its buffers.


Exception and binary stdout

You do not need -u when a Python exception occurs.

❯ python -c "import sys; sys.stdout.buffer.write(b'hello'); raise Exception"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import sys; sys.stdout.buffer.write(b'hello'); raise Exception
                                                   ^^^^^^^^^^^^^^^
Exception
hello%

The hello output appears because Python flushes stdout during interpreter shutdown after the exception.


Segfault and print

Example:

❯ python -c "import ctypes; print('hello'); ctypes.string_at(0)"
hello
[1]  segmentation fault (core dumped)

Why does this appear even without -u?

When stdout is connected to a terminal (TTY), it is typically line buffered. A newline written by print() often triggers an automatic flush.

However, this behavior depends on the environment. If stdout is redirected or piped, the stream becomes block buffered, and the same output is lost during the crash:

❯ python -c "import ctypes; print('hello'); ctypes.string_at(0)" | cat
[1]  segmentation fault (core dumped)

Segfault and logging

❯ python -c "import ctypes, logging; logging.warning('hello'); ctypes.string_at(0)"
WARNING:root:hello
[1]  segmentation fault (core dumped)

This works without -u because of how the default logging configuration behaves: with no handlers configured, logging uses a StreamHandler that writes to stderr, and StreamHandler.emit() flushes the stream after each log record.

Therefore the output reaches the OS before the crash occurs. Custom handlers or streams with different flushing behavior may not provide the same guarantee.


Segfault and binary stdout

Binary writes using sys.stdout.buffer.write() demonstrate the buffering behavior clearly.

Without -u

❯ python -c "import sys, ctypes; sys.stdout.buffer.write(b'hello'); ctypes.string_at(0)"
[1]  segmentation fault (core dumped)

No output appears.

The write remains in Python’s buffered stream and is lost when the process crashes.


With -u

❯ python -u -c "import sys, ctypes; sys.stdout.buffer.write(b'hello'); ctypes.string_at(0)"
hello[1]  segmentation fault (core dumped)

Now the output is visible.

Because buffering is disabled, the bytes are written directly to the operating system before the crash happens.


Performance

Unbuffered output can be slower because every write may result in a system call.

The benchmark below writes a 64-byte payload one million times. Stdout is redirected to /dev/null so the writes do not flood the terminal, and the measurement is printed to stderr:

❯ python -c "
import sys, timeit
payload = b'x' * 64
seconds = timeit.timeit(lambda: sys.stdout.buffer.write(payload), number=1_000_000)
print(f'{seconds:.2f}s for 1M writes', file=sys.stderr)" > /dev/null
0.09s for 1M writes
❯ python -u -c "
import sys, timeit
payload = b'x' * 64
seconds = timeit.timeit(lambda: sys.stdout.buffer.write(payload), number=1_000_000)
print(f'{seconds:.2f}s for 1M writes', file=sys.stderr)" > /dev/null
0.48s for 1M writes

Roughly a 5× slowdown on this machine (Python 3.14, Linux). It occurs because buffering normally allows multiple writes to be combined into fewer system calls.

Actual performance differences depend on:

  • operating system
  • terminal vs pipe
  • write sizes
  • workload

When PYTHONUNBUFFERED=1 is useful

The main use case is programs that may terminate abruptly, such as:

  • segmentation faults
  • os._exit
  • fatal signals
  • native extension crashes

In these situations, buffered stdout/stderr data may be lost.

Disabling buffering ensures that each write reaches the OS immediately.


When it provides little benefit

For many applications, it offers no advantage:

  • programs that exit normally
  • programs that explicitly flush output
  • code using logging handlers that flush automatically

In these cases, enabling unbuffered mode may only reduce I/O performance.


Conclusion

PYTHONUNBUFFERED=1 is mainly useful when a Python process may terminate without running its normal shutdown sequence.

In such cases it ensures that stdout and stderr output is not lost in Python’s internal buffers.

For typical applications that exit normally, buffering already works correctly and disabling it may slightly reduce performance.



A note about “unbuffered” output

Buffering exists at several layers in a typical program:

  • Python’s I/O layer (io.BufferedWriter, TextIOWrapper)
  • the C stdio layer (FILE buffers)
  • the operating system (pipe, terminal, or socket buffers)

python -u (or PYTHONUNBUFFERED=1) disables Python’s buffered I/O and configures the standard streams so writes are flushed to the operating system immediately.

However, this does not eliminate buffering entirely.

After a write() system call succeeds, the bytes are stored in the kernel’s buffers (for example, pipe or terminal buffers). The receiving process or terminal may still read them later.

So in practice, “unbuffered” means:

Python sends the output to the operating system immediately, but the operating system may still buffer it before another process reads it.