os.openpty
A pseudo-terminal is a pair of fds: what a program writes to the slave side appears on the master side, and the program believes it talks to a real terminal (so it prints colours and line-buffers). isatty and device_encoding work on Unix and Windows; everything else here is Unix only.
Common call
sys.stdout.isatty()
Returns
True in an interactive terminal, False when piped or captured
Replaces
checking environment variables to guess whether output is interactive
Watch out
Most terminal work is easier with the pty module (Unix only)
os.openpty() / os.isatty(fd, /) / os.ttyname(fd, /) / os.posix_openpt(oflag, /)
→ tuple[int, int]
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| fd | int | yes | An open file descriptor (isatty, ttyname, device_encoding, tcgetpgrp, tcsetpgrp, login_tty, grantpt, unlockpt, ptsname). |
| pg | int | yes | tcsetpgrp only: the process group to make the foreground group of the terminal. |
| oflag | int | yes | posix_openpt only: open flags such as os.O_RDWR | os.O_NOCTTY (O_CLOEXEC is added automatically where available). |
Return value
tuple[int, int] — openpty: (master_fd, slave_fd). isatty: bool. ttyname / ctermid / ptsname: str. device_encoding: str or None. posix_openpt: an fd. tcgetpgrp: int.
Common patterns
Colours only for terminals
Plain text when output goes to a file or pipe.
import sys use_color = sys.stdout.isatty() red = '\033[31m' if use_color else ''
Run a child on a pseudo-terminal and read its output (Unix)
The child sees a terminal on stdout, so it keeps colours and line buffering.
import os, subprocess master, slave = os.openpty() proc = subprocess.Popen(['ls', '--color=auto'], stdout=slave, stderr=slave, close_fds=True) os.close(slave) output = os.read(master, 65536) proc.wait() os.close(master)
The POSIX way to open a pty (3.13+)
posix_openpt + grantpt + unlockpt + ptsname, as in C.
import os master = os.posix_openpt(os.O_RDWR | os.O_NOCTTY) os.grantpt(master) os.unlockpt(master) slave_name = os.ptsname(master) slave = os.open(slave_name, os.O_RDWR | os.O_NOCTTY)
Higher level: the pty module (Unix)
pty.spawn runs a program connected to a new pseudo-terminal.
import pty pty.spawn(['bash'])
Examples
1. A pipe is not a terminal
import os
r, w = os.pipe()
try:
result = (os.isatty(r), os.isatty(w))
finally:
os.close(r)
os.close(w)
result
Returns
(False, False)2. Neither is a regular file
import os
with open('f.txt', 'w') as f:
f.write('x')
fd = os.open('f.txt', os.O_RDONLY)
try:
result = (os.isatty(fd), os.device_encoding(fd))
finally:
os.close(fd)
result
Returns
(False, None)3. A closed or unknown fd is just False
import os
os.isatty(9999)
Returns
False4. Captured output: no terminal
import sys
sys.stdout.isatty()
Returns
False5. A child with piped stdout sees no terminal
import sys, subprocess
code = 'import sys; print(sys.stdout.isatty())'
subprocess.run([sys.executable, '-c', code], capture_output=True, text=True).stdout.strip()
Returns
'False'6. The pty functions are Unix only
import os
all(hasattr(os, n) == (os.name == 'posix') for n in ('openpty', 'ttyname', 'ctermid', 'tcgetpgrp', 'login_tty'))
Returns
TruePitfalls
1. Calling os.isatty(sys.stdout.fileno()) on a replaced stdout
Test runners, notebooks and contextlib.redirect_stdout replace sys.stdout with objects that have no file descriptor. Ask the stream itself.
fileno()
import contextlib, io, os, sys with contextlib.redirect_stdout(io.StringIO()): try: result = os.isatty(sys.stdout.fileno()) except io.UnsupportedOperation as e: result = type(e).__name__ result
'UnsupportedOperation'
stream.isatty()
import contextlib, io, sys with contextlib.redirect_stdout(io.StringIO()): result = sys.stdout.isatty() result
False
2. Using device_encoding without a fallback
device_encoding returns None when the fd is not a terminal (a file, a pipe), so string methods on the result crash.
assume a str
import os r, w = os.pipe() try: enc = os.device_encoding(w) finally: os.close(r) os.close(w) enc.lower()
AttributeError: 'NoneType' object has no attribute 'lower'
or a default
import os r, w = os.pipe() try: enc = os.device_encoding(w) or 'utf-8' finally: os.close(r) os.close(w) enc.lower()
'utf-8'
When to use
Use it
- Deciding between coloured/interactive and plain output (isatty)
- Driving interactive programs that refuse to work with pipes (openpty, pty module)
- Job control in shells: tcgetpgrp / tcsetpgrp
Reach for something else
- Spawning and expecting interactive programs → pty.spawn or the third-party pexpect
- Windows → os has no pseudo-terminals there; use subprocess pipes
- Terminal size → os.get_terminal_size / shutil.get_terminal_size
Notes
CPython impl
Thin wrappers of the C functions of the same names; openpty and posix_openpt return non-inheritable fds (openpty since 3.4). grantpt, unlockpt and ptsname do not close fd on failure; ptsname uses ptsname_r() where available
Availability
isatty, device_encoding: Unix and Windows. ttyname: Unix. openpty, login_tty, ctermid, tcgetpgrp, tcsetpgrp, posix_openpt, grantpt, unlockpt, ptsname: Unix, not WASI. login_tty is 3.11+; posix_openpt, grantpt, unlockpt, ptsname are 3.13+
device_encoding
Returns None unless fd is a terminal; on Unix in UTF-8 Mode it returns "UTF-8" (since 3.10)
Errors
ttyname and tcgetpgrp on a pipe raise OSError with errno 25 (ENOTTY, "Inappropriate ioctl for device") on Linux; isatty never raises, it returns False
login_tty
Makes the caller a session leader with fd as its controlling terminal and as stdin/stdout/stderr, then closes fd
FAQ
Pseudo-terminals are Unix only (openpty, ttyname and the others: Availability: Unix), and our examples run with captured output, where no terminal exists. Pipes and files give the same answers everywhere: isatty is False and device_encoding is None.