Python os.forkpty Function
Last modified April 11, 2025
This comprehensive guide explores Python's os.forkpty function,
which creates a child process with a pseudo-terminal. We'll cover terminal
emulation, process communication, and practical examples.
Basic Definitions
The os.forkpty function combines fork() and pty creation. It
creates a child process connected to a new pseudo-terminal (pty).
Returns a tuple (pid, fd) where pid is 0 in child, child's PID in parent. fd is the file descriptor of the master end of the pty. The child gets the slave end as stdin/stdout/stderr.
Basic Forkpty Example
This simple example demonstrates the basic usage of os.forkpty. The parent process communicates with the child through the pseudo-terminal.
import os
import sys
pid, fd = os.forkpty()
if pid == 0: # Child process
print("Child process running in pty")
sys.stdout.flush()
data = sys.stdin.readline()
print(f"Child received: {data.strip()}")
sys.exit(0)
else: # Parent process
print(f"Parent process with child PID: {pid}")
os.write(fd, b"Hello from parent\n")
output = os.read(fd, 1024)
print(f"Parent received: {output.decode().strip()}")
os.waitpid(pid, 0)
The child process runs in a pseudo-terminal and can be controlled by the parent. The parent writes to and reads from the child through the pty.
This demonstrates the basic communication pattern between parent and child processes using a pseudo-terminal.
Running Shell in Pty
This example shows how to run a shell in a pseudo-terminal. The parent can send commands and read output as if interacting with a real terminal.
import os
import select
import sys
pid, fd = os.forkpty()
if pid == 0: # Child process
os.execvp("bash", ["bash"])
else: # Parent process
while True:
r, w, e = select.select([fd, sys.stdin], [], [])
if fd in r:
output = os.read(fd, 1024)
if not output:
break
sys.stdout.write(output.decode())
sys.stdout.flush()
if sys.stdin in r:
cmd = sys.stdin.readline()
os.write(fd, cmd.encode())
The child process runs bash in the pseudo-terminal. The parent uses select to handle both user input and pty output in a non-blocking manner.
This creates an interactive shell session where commands can be sent and output received through the pty.
Terminal Size Control
This example demonstrates how to control the terminal size of the pty. The parent can modify the window size which affects programs in the child.
import os
import fcntl
import termios
import struct
def set_winsize(fd, rows, cols):
winsize = struct.pack("HHHH", rows, cols, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
pid, fd = os.forkpty()
if pid == 0: # Child process
os.execvp("bash", ["bash"])
else: # Parent process
# Set initial terminal size
set_winsize(fd, 40, 120)
# After some time, resize the terminal
import time
time.sleep(2)
set_winsize(fd, 20, 60)
os.waitpid(pid, 0)
The set_winsize function uses ioctl with TIOCSWINSZ to change the terminal dimensions. This affects how programs like text editors display content.
The example shows an initial size of 40x120, then changes to 20x60 after 2 seconds. Programs in the child will adapt to the new size.
Raw Mode Terminal
This example puts the pty in raw mode, disabling line buffering and special character handling. This is useful for interactive applications.
import os
import tty
import sys
import select
pid, fd = os.forkpty()
if pid == 0: # Child process
os.execvp("bash", ["bash"])
else: # Parent process
# Put terminal in raw mode
old_settings = tty.tcgetattr(fd)
tty.setraw(fd)
try:
while True:
r, w, e = select.select([fd, sys.stdin], [], [])
if fd in r:
output = os.read(fd, 1024)
if not output:
break
sys.stdout.write(output.decode())
sys.stdout.flush()
if sys.stdin in r:
cmd = sys.stdin.read(1)
os.write(fd, cmd.encode())
finally:
# Restore original terminal settings
tty.tcsetattr(fd, tty.TCSADRAIN, old_settings)
The parent puts the pty in raw mode using tty.setraw(). This disables echo, line buffering, and special character processing (like Ctrl+C).
The example ensures terminal settings are restored when done. This is important to avoid leaving the terminal in an unusable state.
Pty with Environment Control
This example shows how to control the environment of the child process running in the pty. We set custom environment variables before exec.
import os
import sys
pid, fd = os.forkpty()
if pid == 0: # Child process
# Modify environment
os.environ["CUSTOM_VAR"] = "special_value"
os.environ["TERM"] = "xterm-256color"
# Run command that shows environment
os.execvp("bash", ["bash", "-c", "echo $CUSTOM_VAR; echo $TERM; sleep 2"])
else: # Parent process
output = os.read(fd, 1024)
print("Child output:")
print(output.decode())
os.waitpid(pid, 0)
The child process sets custom environment variables before executing bash. These variables are only visible to the child and its descendants.
The example demonstrates setting both a custom variable and standard terminal-related variables like TERM.
Signal Handling in Pty
This example demonstrates signal handling between parent and child processes communicating through a pty. The parent can send signals to the child.
import os
import signal
import time
import sys
pid, fd = os.forkpty()
if pid == 0: # Child process
# Set up signal handler
def handler(signum, frame):
print(f"\nChild received signal {signum}")
sys.exit(0)
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)
print("Child waiting for signals...")
while True:
time.sleep(1)
else: # Parent process
time.sleep(1) # Give child time to start
print(f"Parent sending SIGINT to child {pid}")
os.kill(pid, signal.SIGINT)
status = os.waitpid(pid, 0)
print(f"Child exit status: {status[1]}")
The child process sets up signal handlers for SIGINT and SIGTERM. The parent sends SIGINT to demonstrate signal delivery through the pty.
This shows how signals can be used to control a child process running in a pseudo-terminal.
Pty with Timeout
This example implements a timeout when reading from the pty, demonstrating how to handle cases where the child might hang or take too long to respond.
import os
import select
import sys
import time
pid, fd = os.forkpty()
if pid == 0: # Child process
print("Child will respond after 3 seconds")
time.sleep(3)
print("Child done")
sys.exit(0)
else: # Parent process
timeout = 2 # seconds
start = time.time()
while True:
remaining = timeout - (time.time() - start)
if remaining <= 0:
print("\nTimeout reached, killing child")
os.kill(pid, signal.SIGTERM)
break
r, w, e = select.select([fd], [], [], remaining)
if fd in r:
output = os.read(fd, 1024)
if not output:
break
print(output.decode(), end="")
else:
print("\nNo output within timeout period")
os.kill(pid, signal.SIGTERM)
break
os.waitpid(pid, 0)
The parent waits for output from the child with a 2-second timeout. The child intentionally sleeps for 3 seconds to trigger the timeout.
This demonstrates how to implement timeouts when interacting with processes through a pty, which is important for robust applications.
Security Considerations
- Privilege separation: Child processes inherit parent privileges
- Terminal injection: Be cautious with untrusted input to ptys
- Signal handling: Properly handle signals in both processes
- Resource cleanup: Ensure ptys are properly closed
- Platform differences: Behavior may vary between Unix systems
Best Practices
- Error handling: Check return values and handle errors
- Resource management: Use context managers for file descriptors
- Signal safety: Be aware of signal handling in both processes
- Terminal settings: Restore original settings when done
- Testing: Test with different terminal types and sizes
Source References
Author
List all Python tutorials.