Skip to content
💻 🧠 Code 1001 > 📚 Learning Materials > 🐍 Python > 🧲 How to Catch print() in Python: Capturing stdout Without External Libraries

🧲 How to Catch print() in Python: Capturing stdout Without External Libraries


🧩 Understanding Context Managers in Python

What is a Context Manager?

A context manager manages resources safely and cleanly.
It defines setup and cleanup behavior around a block of code.

Example: file handling

with open("file.txt", "w") as f:
    f.write("Hello, world!")

✅ Automatic cleanup: the file is closed even if an exception occurs.


Why Context Managers Matter

  • Automatic Cleanup – resources are released automatically
  • Exception Safety – cleanup runs even if errors occur
  • Readable Code – encapsulates setup/teardown
  • Reusable – works for files, locks, network connections, or stdout

Using @contextmanager from contextlib

Python’s @contextmanager allows writing context managers using a generator:

from contextlib import contextmanager

@contextmanager
def my_context():
    print("Entering context")
    resource = "some resource"
    try:
        yield resource
    finally:
        print("Exiting context")

with my_context() as res:
    print("Inside context:", res)

Output:

Entering context
Inside context: some resource
Exiting context

💡 Explanation:

  • Code before yield → runs at entry
  • yield value → assigned to as res
  • Code after yield → runs at exit, even on exceptions

This mechanism is perfect for temporarily changing global state, like capturing stdout.


⚡ TL;DR: Context Managers for stdout/stderr

Task / ScenarioRecommended Context Manager
Capture only print outputcapture_stdout
Capture only errors/exceptionscapture_stderr
Capture both stdout and stderrcapture_output
Capture and display simultaneouslycapture_stdout_tee / capture_output_tee

📦 Capturing stdout

from contextlib import contextmanager
import sys, io

@contextmanager
def capture_stdout():
    old_stdout = sys.stdout
    sys.stdout = buffer = io.StringIO()
    try:
        yield buffer
    finally:
        sys.stdout = old_stdout

Usage:

with capture_stdout() as out:
    print("This output was captured")

print("Captured:", out.getvalue())

🔧 Capturing stderr or Both Streams

Capture stderr

@contextmanager
def capture_stderr():
    old_stderr = sys.stderr
    sys.stderr = buffer = io.StringIO()
    try:
        yield buffer
    finally:
        sys.stderr = old_stderr

Capture stdout + stderr

@contextmanager
def capture_output():
    old_stdout = sys.stdout
    old_stderr = sys.stderr
    sys.stdout = sys.stderr = buffer = io.StringIO()
    try:
        yield buffer
    finally:
        sys.stdout = old_stdout
        sys.stderr = old_stderr

🔀 Tee Mode: Capture AND Display

class Tee(io.StringIO):
    def __init__(self, original):
        super().__init__()
        self.original = original

    def write(self, text):
        self.original.write(text)
        super().write(text)

    def flush(self):
        self.original.flush()

Context managers:

@contextmanager
def capture_stdout_tee():
    old_stdout = sys.stdout
    sys.stdout = tee = Tee(old_stdout)
    try:
        yield tee
    finally:
        sys.stdout = old_stdout

@contextmanager
def capture_output_tee():
    old_stdout = sys.stdout
    old_stderr = sys.stderr
    sys.stdout = sys.stderr = tee = Tee(old_stdout)
    try:
        yield tee
    finally:
        sys.stdout = old_stdout
        sys.stderr = old_stderr

Usage:

with capture_stdout_tee() as out:
    print("Visible AND captured")

print("Tee captured:", out.getvalue())

📦 Full File: stdout_capture.py

"""
Utility for capturing stdout and stderr with context managers.
Supports:
- stdout capture
- stderr capture
- combined capture
- tee mode
"""

import sys, io
from contextlib import contextmanager

@contextmanager
def capture_stdout():
    old_stdout = sys.stdout
    sys.stdout = buffer = io.StringIO()
    try: yield buffer
    finally: sys.stdout = old_stdout

@contextmanager
def capture_stderr():
    old_stderr = sys.stderr
    sys.stderr = buffer = io.StringIO()
    try: yield buffer
    finally: sys.stderr = old_stderr

@contextmanager
def capture_output():
    old_stdout = sys.stdout
    old_stderr = sys.stderr
    sys.stdout = sys.stderr = buffer = io.StringIO()
    try: yield buffer
    finally:
        sys.stdout = old_stdout
        sys.stderr = old_stderr

class Tee(io.StringIO):
    def __init__(self, original):
        super().__init__()
        self.original = original
    def write(self, text):
        self.original.write(text)
        super().write(text)
    def flush(self):
        self.original.flush()

@contextmanager
def capture_stdout_tee():
    old_stdout = sys.stdout
    sys.stdout = tee = Tee(old_stdout)
    try: yield tee
    finally: sys.stdout = old_stdout

@contextmanager
def capture_output_tee():
    old_stdout = sys.stdout
    old_stderr = sys.stderr
    sys.stdout = sys.stderr = tee = Tee(old_stdout)
    try: yield tee
    finally:
        sys.stdout = old_stdout
        sys.stderr = old_stderr

✅ Usage Examples

from stdout_capture import capture_stdout, capture_output, capture_stdout_tee

def test_func():
    print("Hello from function")

# Capture stdout
with capture_stdout() as out:
    test_func()
print("Captured stdout:", out.getvalue())

# Capture stdout + stderr
try:
    with capture_output() as out:
        print("Something")
        raise Exception("Oops!")
except Exception:
    pass
print("Captured output:", out.getvalue())

# Capture and display
with capture_stdout_tee() as out:
    print("Visible AND captured")
print("Tee captured:", out.getvalue())

⚠️ Practical Tips & Pitfalls

1️⃣ Handling Large Output

  • io.StringIO may consume too much memory for huge outputs.
  • Use a temporary file:
import sys, tempfile

with tempfile.TemporaryFile(mode='w+') as tmp:
    old_stdout = sys.stdout
    sys.stdout = tmp
    try:
        print("A lot of data...")
        tmp.seek(0)
        captured = tmp.read()
    finally:
        sys.stdout = old_stdout

2️⃣ Subprocess Output

  • capture_stdout() does not capture subprocess output.
  • Use subprocess.PIPE:
import subprocess
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)

3️⃣ Thread-Safety

  • sys.stdout and sys.stderr are global. Avoid concurrent captures in multiple threads.

4️⃣ When to Prefer Tee

  • Debugging functions while logging
  • Interactive CLI tools
  • Integration tests with terminal feedback

5️⃣ Reset Streams

  • Always restore sys.stdout/sys.stderr in finally.

6️⃣ Avoid Over-Nesting

  • Nesting multiple captures can mix outputs. Use a single context per scenario.

7️⃣ Testing Best Practices

  • Unit tests → capture_stdout() or capture_output()
  • Manual debugging → Tee mode
  • Assert buffer contents, not side effects

Leave a Reply

Your email address will not be published. Required fields are marked *