Writing robust pipeline tools

sys · Practical · 11 min

Tools used in pipelines like cat big.log | python tool.py | head must not crash or garble data.

A good filter reads stdin incrementally, writes stdout, and plays well with the rest of the pipeline.

Closed pipes. In python gen.py | head -n 3, head exits after three lines. The next write raises BrokenPipeError, and an ugly traceback appears. The recipe from the Python docs: catch it, point stdout at os.devnull so the final flush at shutdown doesn't fail again, and exit with code 1.

Encodings. Text streams use the locale encoding unless UTF-8 mode is active; Python 3.15 will make UTF-8 mode the default. Set PYTHONIOENCODING=utf-8, run with -X utf8, or call sys.stdout.reconfigure(encoding="utf-8", errors="replace") to be explicit.

Binary data. Images or compressed data must go through sys.stdin.buffer and sys.stdout.buffer, never through the text layer.

Many inputs. fileinput.input() iterates over the lines of every file named in sys.argv[1:], or stdin if there are none, mirroring tools like cat and grep.

Interrupts. Ctrl-C raises KeyboardInterrupt; catch it in main and exit with code 130 to avoid a traceback.

Example

import subprocess
import sys

gen = '''
import os, sys
try:
    for i in range(100_000):
        print(f"line {i}")
    sys.stdout.flush()
except BrokenPipeError:
    devnull = os.open(os.devnull, os.O_WRONLY)
    os.dup2(devnull, sys.stdout.fileno())   # silence the shutdown flush
    sys.exit(1)
'''
p1 = subprocess.Popen([sys.executable, "-c", gen], stdout=subprocess.PIPE,
                      stderr=subprocess.PIPE)
first = p1.stdout.readline()
p1.stdout.close()                          # reader goes away, like `head`
_, err = p1.communicate()
print(first.decode().strip())              # line 0
print(p1.returncode, err == b"")           # 1 True

Remember

  • Catch BrokenPipeError, redirect stdout to devnull, and exit non-zero
  • Use sys.stdin.buffer/sys.stdout.buffer for bytes
  • fileinput.input() reads named files or stdin, like classic Unix tools

Watch out for

  • Decoding binary input through sys.stdin; it raises UnicodeDecodeError or corrupts the data.

Check yourself

  1. Why does the BrokenPipeError recipe redirect stdout to os.devnull before exiting?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/signal.html#note-on-sigpipe

Builds on

Unlocks

Nothing builds on this yet.