Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read Last reviewed Jul 2026

Python Error Guide: 'BrokenPipeError: [Errno 32] Broken pipe' — Fix Pipeline Writes

Quick answer

Fix Python 'BrokenPipeError: [Errno 32] Broken pipe' errors: handle readers that exit early (head, less), flush stdout safely, restore default SIGPIPE behavior, and stop noisy tracebacks in pipelines.

  • #python
  • #automation
  • #troubleshooting
  • #errors
Free toolkit

Stuck on this Bash & Python Automation error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

Your script streams output to stdout, and everything is fine — until the program reading it downstream closes early. Then Python floods the terminal with a traceback ending in:

Traceback (most recent call last):
  File "/opt/jobs/emit.py", line 14, in <module>
    print(line)
BrokenPipeError: [Errno 32] Broken pipe

You’ll also see a second, confusing message printed during interpreter shutdown, even after you thought you handled the error:

Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe

BrokenPipeError (errno EPIPE, 32) is raised when your process writes to a pipe or socket whose reading end has already been closed. In command-line automation this is routine: python emit.py | head -5 means head reads five lines and exits, and your script’s next write has nowhere to go. On UNIX the kernel normally sends SIGPIPE to end the writer silently, but Python ignores SIGPIPE by default and turns the failed write into this exception instead — which is why a totally normal | head produces a scary traceback.

Symptoms

  • A traceback pointing at a print(), sys.stdout.write(), or .flush() call — always a write, never a read.
  • It appears only when output is piped to a consumer that exits early: | head, | less (then q), | grep -q, or a browser/HTTP client that disconnects.
  • A trailing “Exception ignored in … BrokenPipeError” printed at exit, separate from any you caught — this comes from Python flushing buffered stdout during shutdown.
  • The script “works” when run alone or piped to a file, but errors when piped to a paginator or head.
  • In a web/worker context: BrokenPipeError when the client closes the connection mid-response.
  • Exit code is non-zero (typically 120 from the shutdown-flush path, or 1 from an uncaught raise).

Common Root Causes

  • A downstream reader exits before you finish writinghead, sed q, grep -m1, less closed with q; the classic and usually benign case.
  • Python’s default SIGPIPE handling — the interpreter sets SIGPIPE to SIG_IGN at startup, converting the signal into a BrokenPipeError instead of a clean silent exit.
  • Buffered stdout flushed at shutdown — even if you catch the error in your loop, Python tries to flush the remaining buffer when the process exits, re-raising into the “Exception ignored” message.
  • A client disconnecting in a network/streaming server while you’re still sending the response body.
  • A subprocess you launched closed its stdin — you keep writing to proc.stdin after the child exited.
  • Large buffered writes — the failure surfaces only when the buffer flushes, so the traceback line may be far from the “logical” last write.

Diagnostic Workflow

Reproduce it deterministically by piping into a reader that quits early:

python3 -c 'import sys
for i in range(100000): print(i)' | head -3
0
1
2
Traceback (most recent call last):
  ...
BrokenPipeError: [Errno 32] Broken pipe
Exception ignored in: <_io.TextIOWrapper name='<stdout>' ...>
BrokenPipeError: [Errno 32] Broken pipe

Confirm Python is ignoring SIGPIPE (the reason you get an exception instead of a silent exit):

python3 -c 'import signal; print(signal.getsignal(signal.SIGPIPE))'
# Handlers.SIG_IGN   <-- default; this is what turns SIGPIPE into BrokenPipeError

Distinguish “reader closed early” (benign) from “write to a dead subprocess” (a real bug) by checking where the write goes — inspect the failing line’s target:

python3 emit.py 2>&1 | head -40      # see the traceback's file/line
grep -nE 'print\(|stdout\.write|\.flush\(|\.stdin' emit.py

If it’s a subprocess pipe, verify the child’s state at write time:

# add near the failing write
import os
print("child returncode:", proc.poll(), file=os.sys.stderr)

Example Root Cause Analysis

An operator runs a log-emitting tool through a pager and gets a traceback the moment they quit:

$ python3 tail_events.py | less
# operator presses q after skimming, then:
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe

The core loop was:

for event in stream_events():
    print(format_event(event))   # keeps printing after less exits

Nothing is actually wrong with the program — less closed the read end of the pipe when the operator pressed q, and the next print() (plus the shutdown flush) hit EPIPE. Because Python ignores SIGPIPE, that became a BrokenPipeError and the noisy “Exception ignored” message.

The correct fix for a plain command-line filter is to restore the default SIGPIPE behavior so the process dies silently like cat or grep would — set it once at startup:

import signal
# Behave like a normal UNIX filter: die quietly when the reader goes away.
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

If you can’t reset SIGPIPE (e.g. on Windows, where it doesn’t exist, or in a context that needs the exception), catch it and suppress the shutdown-flush noise by redirecting stdout to devnull before exit:

import sys, os
try:
    for event in stream_events():
        print(format_event(event))
except BrokenPipeError:
    # Prevent Python from re-raising on the final stdout flush at interpreter exit.
    devnull = os.open(os.devnull, os.O_WRONLY)
    os.dup2(devnull, sys.stdout.fileno())
    sys.exit(0)

Either approach makes python3 tail_events.py | less behave exactly like a native tool: quitting the pager ends the writer cleanly, no traceback.

Prevention Best Practices

  • For CLI filters, reset SIGPIPE to default at startup (signal.signal(signal.SIGPIPE, signal.SIG_DFL)) so your tool dies silently when a reader like head/less exits — the standard, least-surprising behavior. Guard it with a check for platforms without SIGPIPE.
  • Or catch BrokenPipeError explicitly and suppress the exit-time flush by dup2-ing stdout to os.devnull, so the “Exception ignored” line never prints.
  • Don’t treat a | head/| less broken pipe as a failure in monitoring — it’s expected when consumers read partially; only alert on writes to subprocesses/sockets that should still be alive.
  • When writing to a subprocess, check proc.poll() before writing and handle the child exiting; use proc.communicate() for one-shot exchanges instead of manual stdin.write loops.
  • Flush deliberately and keep the actual write points few, so a broken pipe surfaces at a predictable location you can wrap.
  • In servers, catch BrokenPipeError/ConnectionResetError around response writes and treat a disconnected client as a normal, logged-at-debug event, not an error.

Quick Command Reference

# Make a CLI tool die silently on a closed reader (UNIX)
import signal
if hasattr(signal, "SIGPIPE"):
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)

# Cross-platform: catch it and silence the shutdown-flush re-raise
import sys, os
try:
    main()
except BrokenPipeError:
    os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
    sys.exit(0)

# Guard writes to a subprocess pipe
if proc.poll() is not None:
    raise RuntimeError("child exited; not writing to a dead pipe")
# Reproduce deterministically
python3 -c 'import sys
[print(i) for i in range(100000)]' | head -3

# Check Python's SIGPIPE disposition
python3 -c 'import signal; print(signal.getsignal(signal.SIGPIPE))'

Conclusion

BrokenPipeError: [Errno 32] Broken pipe is your script writing into a pipe whose reader already left — and in most command-line cases (| head, | less then q) it’s completely benign. The traceback only appears because Python ignores SIGPIPE by default and converts it into an exception, then re-raises once more while flushing stdout at exit. For ordinary CLI filters, restore the default with signal.signal(signal.SIGPIPE, signal.SIG_DFL) so your tool ends quietly like grep or cat; where that isn’t possible, catch BrokenPipeError and redirect stdout to devnull before exiting to kill the “Exception ignored” noise. Reserve real concern for broken pipes to subprocesses and network clients that were supposed to still be listening.

Free download · 368-page PDF

Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.