contextlib: redirect_stdout() Redirects Every Thread, Not Just Yours (tested)

contextlib.redirect_stdout(buf) reads like it scopes the redirect to the code inside its with block -- the way a local variable or a thread-local setting would. We measured what happens to print() calls made by a different thread while one thread is inside the block.

1. A background thread's print() lands in the main thread's buffer

import contextlib, io, threading, time

def other_thread_work():
    time.sleep(0.05)
    print("from other thread")

t = threading.Thread(target=other_thread_work)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
    t.start()
    time.sleep(0.1)
    print("from main thread")
    t.join()

print("captured in buf:", repr(buf.getvalue()))
captured in buf: 'from other thread\nfrom main thread\n'

Measured: "from other thread" was printed by a thread that never entered the with contextlib.redirect_stdout(buf): block, has no reference to buf, and has no idea the redirect exists. It still ends up captured. redirect_stdout() works by doing sys.stdout = new_target and restoring the old value on exit -- sys.stdout is one global, process-wide object, not something scoped per-thread or per-with-block.

2. That output never reaches the real terminal at all

import contextlib, io, threading, time

def other_thread_work():
    time.sleep(0.05)
    print("you will not see this on the real terminal")

t = threading.Thread(target=other_thread_work)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
    t.start()
    time.sleep(0.1)
    t.join()
# by now the redirect has already ended

Measured: running this prints nothing to the actual terminal -- the other thread's message is sitting in buf instead, silently, with no error, no warning, and nothing to indicate a thread's output went somewhere other than where that thread's own code implies it should go. In a real program this looks like a log line or print statement that simply never happened.

3. The redirect's own exit can race with the other thread, too

import contextlib, io, threading, time

def slow_other_thread():
    time.sleep(0.2)
    print("printed after the with block already exited")

t = threading.Thread(target=slow_other_thread)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
    t.start()
    time.sleep(0.05)  # with block exits long before the other thread prints
print("buf has:", repr(buf.getvalue()))
t.join()
buf has: ''

Measured: if the other thread is slow enough that it prints after redirect_stdout has already restored the original sys.stdout, its output goes to the real terminal instead -- correctly, this time, but only by accident of timing. Whether a concurrent thread's print() gets silently swallowed or shows up normally depends entirely on which side of the with block's exit it happens to land on.

Don't rely on redirect_stdout across threads

import contextlib, io

# only safe if nothing else in the process writes to stdout
# while this block is active
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
    do_single_threaded_work()

redirect_stdout() is fine for single-threaded code, or for capturing a call you know won't spawn concurrent output elsewhere. For multithreaded programs, redirect at the source instead -- pass an explicit stream/logger into the code you want to capture, rather than relying on the shared, global sys.stdout.

Frequently asked questions

Does contextlib.redirect_stdout() only affect the thread that entered the with block?

No -- we measured it reassigning the global sys.stdout for the entire process, so print() calls from any other thread running concurrently get captured into the same buffer too.

Where does a concurrent thread's print() output go while redirect_stdout is active?

Into the buffer passed to redirect_stdout(), not the real terminal -- we measured a background thread's message appearing only in the redirected buffer, with no error or warning that it had been captured.

Is this behavior consistent, or does it depend on timing?

It depends on timing -- we measured output from a thread that prints before the with block exits getting captured, while an identical call from a thread that prints after the block has already exited goes to the real stdout normally.

How do you safely capture output in a multithreaded program?

Avoid contextlib.redirect_stdout() for that case -- redirect at the source by passing an explicit stream or logger into the code you want to capture, since sys.stdout is one shared global, not scoped per thread or per with block.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in contextlib module: started a background thread that calls print() while the main thread is inside a contextlib.redirect_stdout() block, and checked where the background thread's output actually went. PlainHeaders is our extension; Python's contextlib module is built in.