os.pipe
os.pipe() gives (read_end, write_end). The reader sees b"" only after every copy of the write end is closed. pipe, dup, dup2, get/set_inheritable and get/set_blocking work on Unix and Windows (blocking mode on Windows: pipes only, 3.12+); pipe2 is Unix-only (not macOS); get/set_handle_inheritable are Windows-only.
Common call
r, w = os.pipe()
Returns
(read_fd, write_fd)
Replaces
temporary files for passing bytes between processes
Watch out
Close the write end, or the reader never sees EOF
os.pipe() -> (r, w)
→ tuple[int, int]
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| flags (pipe2) | int | yes | pipe2(flags): O_NONBLOCK and/or O_CLOEXEC, set atomically on both ends. Unix only, not macOS. |
| fd, fd2 (dup2) | int | yes | dup2(fd, fd2, inheritable=True) makes fd2 a copy of fd, closing fd2 first, and returns fd2 (3.7+). |
| inheritable | bool | no (True) | dup2 only: the copy is inheritable unless you pass False. dup() copies are always non-inheritable. |
| blocking | bool | yes | set_blocking(fd, blocking): False sets O_NONBLOCK, so reads that would wait raise BlockingIOError. |
| handle | int | yes | get/set_handle_inheritable take a Windows HANDLE (msvcrt.get_osfhandle(fd)), not an fd. |
Return value
tuple[int, int] — The read end and the write end, both non-inheritable.
Common patterns
Read a child process through a pipe
Pass the write end to the child, close your copy, read until EOF.
import os, subprocess, sys r, w = os.pipe() proc = subprocess.Popen([sys.executable, '-c', 'print(42)'], stdout=w) os.close(w) with os.fdopen(r, 'rb') as f: data = f.read() proc.wait()
Temporarily redirect fd 1 (Unix)
dup saves the real stdout, dup2 swaps a file in and back. This also catches output from C code.
import os, sys sys.stdout.flush() saved = os.dup(1) fd = os.open('out.log', os.O_WRONLY | os.O_CREAT | os.O_TRUNC) try: os.dup2(fd, 1) os.system('echo captured') finally: os.dup2(saved, 1) os.close(saved) os.close(fd)
Non-blocking pipe in one call (Linux)
pipe2 sets the flags atomically, with no window between creation and fcntl.
import os r, w = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
Self-pipe wake-up for select
A non-blocking pipe that a signal handler or thread writes one byte to.
import os, selectors r, w = os.pipe() os.set_blocking(r, False) os.set_blocking(w, False) sel = selectors.DefaultSelector() sel.register(r, selectors.EVENT_READ)
Examples
1. Bytes in one end, out the other
import os
r, w = os.pipe()
try:
os.write(w, b'hello')
data = os.read(r, 100)
finally:
os.close(r)
os.close(w)
data
Returns
b'hello'2. Closing the write end means EOF
import os
r, w = os.pipe()
os.write(w, b'hi')
os.close(w)
try:
result = [os.read(r, 100), os.read(r, 100)]
finally:
os.close(r)
result
Returns
[b'hi', b'']3. Pipes are not inherited until you say so
import os
r, w = os.pipe()
try:
before = os.get_inheritable(w)
os.set_inheritable(w, True)
after = os.get_inheritable(w)
finally:
os.close(r)
os.close(w)
(before, after)
Returns
(False, True)4. dup: a second fd for the same pipe
import os
r, w = os.pipe()
w2 = os.dup(w)
try:
os.write(w2, b'via copy')
result = (w2 != w, os.get_inheritable(w2), os.read(r, 100))
finally:
for fd in (r, w, w2):
os.close(fd)
result
Returns
(True, False, b'via copy')5. dup2 returns fd2 and makes it inheritable
import os
r, w = os.pipe()
r2, w2 = os.pipe()
try:
same = os.dup2(w, w2) == w2
os.write(w2, b'redirected')
result = (same, os.get_inheritable(w2), os.read(r, 100))
finally:
for fd in (r, w, r2, w2):
os.close(fd)
result
Returns
(True, True, b'redirected')6. Blocking by default; switch it off
import os
r, w = os.pipe()
try:
before = os.get_blocking(r)
os.set_blocking(r, False)
result = (before, os.get_blocking(r))
finally:
os.close(r)
os.close(w)
result
Returns
(True, False)7. Empty non-blocking pipe: BlockingIOError (EAGAIN)
import errno, os
r, w = os.pipe()
os.set_blocking(r, False)
try:
os.read(r, 100)
except BlockingIOError as e:
result = e.errno == errno.EAGAIN
finally:
os.close(r)
os.close(w)
result
Returns
TruePitfalls
1. Swapping the two ends
os.pipe() returns (read_end, write_end) in that order. Writing to the read end fails with OSError (EBADF).
write to r
import os w, r = os.pipe() try: os.write(w, b'x') except OSError as e: result = type(e).__name__ finally: os.close(r) os.close(w) result
'OSError'
r, w = os.pipe()
import os r, w = os.pipe() try: result = os.write(w, b'x') finally: os.close(r) os.close(w) result
1
2. dup2 copies are inheritable
Unlike os.dup and os.pipe, os.dup2 makes fd2 inheritable by default, so child processes get it. Pass inheritable=False when you do not want that.
default
import os r, w = os.pipe() r2, w2 = os.pipe() try: os.dup2(w, w2) result = os.get_inheritable(w2) finally: for fd in (r, w, r2, w2): os.close(fd) result
True
inheritable=False
import os r, w = os.pipe() r2, w2 = os.pipe() try: os.dup2(w, w2, inheritable=False) result = os.get_inheritable(w2) finally: for fd in (r, w, r2, w2): os.close(fd) result
False
3. Treating "no data yet" as end of file
A non-blocking read of an empty pipe does not return b"" — it raises BlockingIOError. Only a closed write end gives b"". Catch BlockingIOError and try again later.
expects b""
import os r, w = os.pipe() os.set_blocking(r, False) try: result = os.read(r, 100) == b'' except OSError as e: result = type(e).__name__ finally: os.close(r) os.close(w) result
'BlockingIOError'
catch BlockingIOError
import os r, w = os.pipe() os.set_blocking(r, False) try: data = os.read(r, 100) except BlockingIOError: data = None finally: os.close(r) os.close(w) data is None
True
When to use
Use it
- Passing bytes to or from a child process at the fd level
- Redirecting fd 0/1/2 with dup/dup2 (catches output from C extensions too)
- Wake-up pipes for select/selectors loops
Reach for something else
- Running a command and collecting its output → subprocess.run(..., capture_output=True)
- Sending Python objects between processes → multiprocessing.Pipe / Queue
- Redirecting only Python-level prints → contextlib.redirect_stdout
Notes
CPython impl
All of these live in Modules/posixmodule.c. PEP 446 (3.4) made new fds non-inheritable; dup2 kept inheritable=True as its default
Availability
pipe, dup, dup2, get_inheritable, set_inheritable: Unix and Windows. get_blocking / set_blocking: Unix, and Windows for pipes only since 3.12. pipe2: Unix, not macOS, not iOS (3.3+). get_handle_inheritable / set_handle_inheritable: Windows only
EOF
A read returns b"" only when every copy of the write end is closed — including copies held by child processes. A forgotten copy makes the reader wait forever
Buffer size
A pipe holds a limited amount of data; when it is full, os.write blocks (or raises BlockingIOError in non-blocking mode) until the reader drains it. Read and write from different threads or processes
dup on Windows
docs.python.org: when dup duplicates a standard stream (0, 1, 2) on Windows, the new fd is inheritable
FAQ
os.read blocks until data arrives or every copy of the write end is closed. Close your own copy of w after starting the writer (and after handing it to a child process); otherwise the reader keeps waiting for data you could still write. This page has no live demo because a blocked read would hang; the examples close every fd they open.