不一定。Python 的系统调用本身不会自动创建 PTY,要看你调用的是什么 API。

可以把它分成三种情况:

1. subprocess.run() / Popen():默认不是 PTY

例如:

import subprocess

subprocess.run(["bash"])

默认情况下,Python 会创建子进程,并给它:

stdin  → pipe
stdout → pipe
stderr → pipe

大致是:

Python
  │
  ├── pipe ──> bash stdin
  │
  ├── pipe <── bash stdout
  │
  └── pipe <── bash stderr

这里没有 PTY。

所以:

python test.py

里面启动:

subprocess.run(["bash"])

这个 bash 通常会发现:

tty

不是一个真正的 /dev/pts/N


2. Python 可以明确创建 PTY

Python 有标准库:

import pty

pty.spawn("/bin/bash")

这时候才会创建 PTY。

结构变成:

Python
  │
  │ forkpty()
  ▼
PTY master
  │
  │
  ▼
/dev/pts/XX
  │
  ▼
bash

更准确地说,Linux 内核会创建:

PTY master
PTY slave

slave 对应:

/dev/pts/12

bash 运行在 slave 上。


3. subprocess + PTY

你也可以自己使用:

import os
import pty

pid, fd = pty.fork()

if pid == 0:
    os.execvp("bash", ["bash"])
else:
    # fd 是 PTY master
    ...

这就是很多 Web Terminal、SSH Terminal、终端管理器的基础。

结构:

                 Python
                   │
              pty.fork()
                   │
          ┌────────┴────────┐
          ▼                 ▼
     PTY master          PTY slave
       fd = 5           /dev/pts/12
          │                 │
          │                 ▼
          │               bash
          │
          ▼
     Python 读写

为什么 PTY 很重要?

这正好和你前面研究的 Zellij / ttyd / Web SSH 联系起来。

如果你:

subprocess.Popen(
    ["bash"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
)

bash 看到的不是终端,而是 pipe。

所以很多终端程序的行为会不一样,例如:

bash
vim
top
htop
ssh
sudo

它们通常需要知道:

“我是不是连接在一个真正的 terminal 上?”

PTY 会提供终端语义,例如:


所以可以这样记

Python subprocess
       │
       ├── 默认 ──> pipe
       │
       └── 使用 pty ──> PTY

而 Zellij 更接近:

Zellij
   │
   ├── 创建 PTY
   │      │
   │      └── bash
   │
   ├── Client A
   │
   ├── Client B
   │
   └── Client C

所以如果你正在用 Rust 实现你前面说的那个极简 Zellij,核心其实就是:

forkpty()
   ↓
bash
   ↓
PTY master
   ↓
你的 Session Manager
   ↓
多个 terminal client

PTY master 是“程序控制 Shell”的入口;多个终端 attach 时,实际上是多个 client 共享/复用这个 PTY 的输入输出。