ZetCode

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.

basic_forkpty.py
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.

shell_in_pty.py
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.

terminal_size.py
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.

raw_mode.py
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.

pty_environment.py
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.

pty_signals.py
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.

pty_timeout.py
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

Best Practices

Source References

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.