What is a pseudoterminal (PTY)?
A pseudoterminal (PTY) is a pair of virtual devices that gives a program a terminal and lets a second program type input and read output. It is also written pty or pseudo-terminal. Terminal emulators and interactive Secure Shell (SSH) sessions run shells in PTYs, and tests use them to run programs that need a terminal.
The Linux manual page describes a PTY as "a pair of virtual character devices that provide a bidirectional communication channel." The program holds one end, the terminal end, which is a TTY, a terminal device that a program can detect. A second program, e.g. a terminal emulator, holds the other end, the controller end, where it writes keystrokes and reads output. The Linux manual calls the ends the slave and the master, and POSIX calls them the subsidiary and the manager.
A test of a command-line application needs a PTY when the program does one of these things, which fail or change on a pipe:
- Checks for a terminal. Color and progress bars often appear only when
isatty()returns 1. - Reads the window size. Layout code asks the terminal how many columns it has.
- Changes terminal modes. A password prompt turns echo off, and a key reader switches to raw mode.
How does a PTY work?
A test takes the same steps as a terminal emulator:
- The controller opens a PTY, e.g. with
posix_openpt(), and the kernel creates the terminal end, which Linux places under/dev/pts. - The controller sets the window size with the
TIOCSWINSZrequest. - The controller starts the program with the terminal end as its stdin, stdout, and stderr, usually in a new session with the PTY as its controlling terminal.
- The controller writes keystrokes to its end, and the kernel's terminal driver passes them to the program as typed input.
- The program writes output to the terminal end, and the controller reads it from its own end.
- The program exits. Once no process holds the terminal end open, reading the controller end fails with
EIOon Linux and returns no bytes on macOS.
The terminal driver, including the layer called the line discipline, is the part a pipe lacks. It applies the settings that stty -a prints:
- Echo. The driver copies typed input to the output, so a test reads its own keystrokes unless echo is off.
- Line editing. In the default canonical mode, the program receives input a line at a time. Raw mode passes each key as it is typed.
- Control characters. The driver turns Ctrl-C into SIGINT for the foreground process group, so a test can interrupt a program with one byte.
- Output processing. The driver sends each newline as
\r\n, a carriage return and a newline. - Window size. The driver stores the rows and columns and sends SIGWINCH when they change.
Libraries open the PTY and start the program for a test. Python has the standard pty module and the third-party pexpect package, and Go has github.com/creack/pty. Node.js has node-pty, which uses ConPTY on Windows.
The script command runs a shell in a PTY and records the session to a file. Its Linux manual page says the -c option runs one command instead, to capture output that differs when stdout is not a TTY.
What is an example of a test that needs a PTY?
Here is an illustrative example. Acme Co. sells furniture online, and its command-line tool, acme, lists orders. A developer asks a coding agent to "Fit the orders table to the terminal width." The first attempt takes three steps:
- The agent reads the width with Python's
shutil.get_terminal_size()and pads each address tocolumns - 42, the space the other columns leave. - The agent's test runs
acme orders listthrough a pipe. With no terminal, the function returns its fallback of 80 columns, and the test passes. - The developer runs the command in a pane 40 columns wide. The address width becomes
-2, and Python's string formatting raises aValueError.
The developer writes a test that runs the command in a PTY of 24 rows by 40 columns. It drops COLUMNS, which shutil.get_terminal_size() reads before it asks the terminal, and sets NO_COLOR so escape codes do not count toward the width:
import os
import pexpect
def test_orders_list_fits_40_columns():
env = {k: v for k, v in os.environ.items() if k != "COLUMNS"}
env["NO_COLOR"] = "1"
child = pexpect.spawn("acme orders list", env=env, dimensions=(24, 40))
child.expect(pexpect.EOF)
child.close()
output = child.before.decode()
assert child.exitstatus == 0, output
for line in output.splitlines():
assert len(line) <= 40
The test fails on the agent's code with the same error, shown here shortened:
> assert child.exitstatus == 0, output
E AssertionError: Traceback (most recent call last):
E ...
E ValueError: Sign not allowed in string format specifier
E assert 1 == 0
The agent changes the code to print one field per line when fewer than 10 columns remain for the address, and the test passes. Because the test opens its own PTY, it also runs in a continuous integration and delivery (CI/CD) job on Linux or macOS, which usually has no terminal. This example is simplified. A real command-line tool would need tests at more widths, e.g. one where the table fits exactly.
What changes when a coding agent writes the code?
A coding agent's shell tool usually runs commands with pipes, so its runs skip the terminal code. In the example, those runs got 80 columns and never hit the crash.
Some agent harnesses run commands in a PTY instead, e.g. Gemini CLI, whose tools.shell.enableInteractiveShell setting is on by default. Those runs reach terminal code that waits for a person, e.g. the pager that git log opens on a terminal. Gemini CLI's tools.shell.pager setting defaults to cat, which prints the output and exits.
A practical adjustment is to give the agent a command that opens a PTY of a set width, e.g. script -qec "stty cols 40; acme orders list" /dev/null on Linux. Reading the width in one function, a form of design for testability, also lets most tests pass a width and skip the PTY. Keep one PTY test so the suite still covers the terminal path.
What are the limits of PTY testing?
A PTY test runs the terminal path, with these limits:
- Bytes, not a screen. A test reads raw output with escape codes, redraws, and
\r\nline endings. Normalize the codes and line endings before a snapshot test compares the output. Checking the visible screen needs a terminal emulator library, as in testing a terminal UI. - One stream. When stdout and stderr both go to the terminal end, a test reads them mixed, as in the example's traceback.
- No answers to terminal queries. Some programs ask the terminal a question, e.g. the cursor position, and wait for the reply. With no emulator on the controller end, no reply comes, and the test can time out.
- Timing. The pty manual page warns that data the program writes may not be available at the controller end immediately. A test that reads once or sleeps for a fixed time can become a flaky test, so wait for expected output with a deadline.
- Platform differences. Windows has no Unix PTY devices, so pexpect's
spawnclass does not run there. Windows instead has a pseudoconsole, also called ConPTY, which gives the program a console. The pseudoconsole passes the program's input and output to a host program over two channels, usually pipes.
How is a PTY different from a TTY?
A TTY is any terminal device a program can detect, and a PTY is one kind of TTY. On Linux, the other kinds include the virtual consoles, e.g. /dev/tty1, and serial ports. isatty() returns 1 for each of them, so that check cannot tell a PTY from a console.
The difference is what sits behind the device. A console or a serial line connects to a screen or to hardware. A PTY connects to another program, which can script every keystroke and read every byte.
The tty command prints the terminal on stdin, e.g. /dev/pts/0 on Linux inside a terminal emulator or an SSH session. By default, ssh requests a PTY for a login shell started from a terminal, and for a remote command only with -t.
FAQs
Which libraries create a pseudoterminal in a test?
A test creates a pseudoterminal with a library for its language. Python has the standard pty module and the third-party pexpect package, Node.js has node-pty, and Go has creack/pty. With each one, the test holds the controller end while the program runs on the terminal end.
How do coding agents run commands that need a terminal?
Coding agents usually run commands through a shell tool that uses pipes, so the command gets no terminal. Some harnesses can start a command in a pseudoterminal instead. An agent can also wrap the command in the script command, which runs it in a pseudoterminal.
What does the script command do?
The script command runs a shell in a pseudoterminal and records the session to a file. On Linux, its -c option runs one command instead of a shell. A shell script can then capture output that differs on a terminal.
Does Windows have pseudoterminals?
Windows has its own form of pseudoterminal, which its documentation calls the pseudoconsole or ConPTY. The program runs attached to the pseudoconsole, which passes input and output to a host program over two channels, usually pipes. The node-pty library uses this interface on Windows.