ptylink
Modern process interaction library for Python — expect-style automation with PTY support.
Drop-in replacement for pexpect with full type annotations, native async/await, and zero dependencies.
Why ptylink?
| pexpect | ptylink | |
|---|---|---|
| Type annotations | No | Full (mypy --strict + pyright strict) |
| Async support | Deprecated @asyncio.coroutine |
Native async/await via AsyncSpawn |
| Python 3.12+ fork warning | Yes (#817) | Fixed |
before leaks sendline echo |
Yes (#821) | Fixed |
| Dependencies | ptyprocess |
Zero |
| Performance | Baseline | 3.6–95× faster |
Installation
pip install ptylink
Requires Python 3.10+.
Quick Start
import ptylink
with ptylink.spawn("python3") as child:
child.expect(">>> ")
child.sendline("print(42)")
child.expect("42")
print(child.before) # text before the match
Context Manager
from ptylink import Spawn
with Spawn("ssh user@host") as child:
child.expect("password:")
child.sendline("secret")
child.expect(r"\$ ")
child.sendline("ls")
child.expect(r"\$ ")
print(child.before)
Async Usage
import asyncio
from ptylink import AsyncSpawn
async def main():
async with AsyncSpawn("python3") as child:
await child.expect(">>> ")
await child.sendline("1 + 1")
await child.expect("2")
asyncio.run(main())
Pipe-Based (No PTY)
For environments without PTY support (e.g. Windows):
from ptylink import PopenSpawn
with PopenSpawn("echo hello") as child:
child.expect("hello")
High-Level run()
from ptylink import run
# Simple command
output = run("ls -la")
# With exit status
output, status = run("make test", withexitstatus=True)
# Interactive with events
output = run(
"sudo apt install foo",
events={"password:": "secret\n"},
)
SSH Sessions
from ptylink import SSHSession
with SSHSession("server.example.com", username="admin") as ssh:
ssh.login(password="secret")
output = ssh.run("uname -a")
print(output)
Pattern Matching
import re
from ptylink import Spawn, EOF_TYPE, TIMEOUT_TYPE
with Spawn("some_program") as child:
# String patterns (auto-escaped)
child.expect("login:")
# Regex patterns
child.expect(re.compile(r"[\$#] "))
# Multiple patterns — returns index of match
idx = child.expect_list(["error", "success", EOF_TYPE])
if idx == 0:
print("Error:", child.after)
elif idx == 1:
print("Success!")
elif idx == 2:
print("Process ended")
API Reference
Classes and Functions
Spawn(command, *, timeout=30, encoding='utf-8', env=None, cwd=None)— PTY-based process interactionspawn(command, *, timeout=30, encoding='utf-8')— Factory function; returns aSpawninstanceAsyncSpawn(command, ...)— Async version of SpawnPopenSpawn(command, ...)— Pipe-based (no PTY) process interactionSSHSession(server, *, username=None, port=22, password=None)— SSH session helper
Spawn Methods
| Method | Description |
|---|---|
expect(pattern, *, timeout=-1) |
Wait for pattern in output |
expect_exact(pattern, *, timeout=-1) |
Wait for exact string |
expect_list(patterns, *, timeout=-1) |
Wait for any pattern, return index |
send(s) |
Send string to process |
sendline(s='') |
Send string + newline |
sendcontrol(char) |
Send control character (e.g. 'c' for Ctrl-C) |
sendeof() |
Send EOF (Ctrl-D) |
read(size=-1) |
Read from process output |
readline() |
Read a single line |
isalive() |
Check if process is running |
wait() |
Wait for exit, return exit code |
close(force=True) |
Close process and PTY |
setwinsize(rows, cols) |
Set terminal dimensions |
interact() |
Interactive passthrough mode |
Attributes
before— Text before the last matchafter— Text of the last matchmatch— Match object or string from last expect
Exceptions
PtylinkError— Base exceptionTimeout— Expect timed outEOF— Process closed outputExitStatus— Process exited with non-zero status
Sentinels
EOF_TYPE— Use in pattern lists to match EOF without raisingTIMEOUT_TYPE— Use in pattern lists to match timeout without raising
ANSI Utilities
from ptylink import strip_ansi, has_ansi
clean = strip_ansi("\x1b[31mred text\x1b[0m") # "red text"
has_ansi("\x1b[1mbold\x1b[0m") # True
Migrating from tether
This package was previously named tether. To upgrade:
pip uninstall tether
pip install ptylink
Then update your imports and any exception references:
# Before
import tether
from tether import TetherError
# After
import ptylink
from ptylink import PtylinkError
All other APIs are identical.
pexpect Migration Guide
Zero-Change Migration
# Before
import pexpect
# After — just change the import
import ptylink.compat as pexpect
All pexpect names are available: spawn, run, EOF, TIMEOUT, pxssh.
Manual Migration
| pexpect | ptylink |
|---|---|
pexpect.spawn(cmd) |
ptylink.Spawn(cmd) |
pexpect.run(cmd) |
ptylink.run(cmd) |
pexpect.EOF |
ptylink.EOF_TYPE |
pexpect.TIMEOUT |
ptylink.TIMEOUT_TYPE |
pexpect.pxssh.pxssh() |
ptylink.SSHSession() |
pexpect.spawn(cmd, async_=True) |
ptylink.AsyncSpawn(cmd) |
Breaking Changes
EOFandTIMEOUTare sentinel types, not exception classes. UseEOF_TYPEandTIMEOUT_TYPEin pattern lists.async_=Trueparameter is removed. UseAsyncSpawninstead.beforeattribute no longer contains leakedsendline()echo text.
Fixes from pexpect
- #821 —
beforeattribute no longer contains echoedsendline()input - #817 — No
ResourceWarningfromos.fork()on Python 3.12+ - #677 — No deprecated
@asyncio.coroutineusage; nativeasync defthroughout
Benchmarks
| Operation | ptylink (us/op) | pexpect (us/op) | Speedup |
|---|---|---|---|
| spawn+expect (echo) | 3,002 | 285,821 | 95x |
| spawn+expect (python) | 84,312 | 307,303 | 3.6x |
| run (echo) | 3,352 | 163,946 | 49x |
License
ISC
Metadata
Release files for ptylink 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ptylink-0.1.0.tar.gz | 54.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ptylink-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.8 kB
Release files / ptylink-0.1.0.tar.gz
| Download URL | ptylink-0.1.0.tar.gz |
|---|---|
| Size | 54.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7b09b80998b8b6ddd2423f4dc8bdb2b377deb69f2959e8976b2332ef0ee52d48
|
|
BLAKE2b-256 checksum How to use checksums |
5dd6230522a72e700a1f18f1c4812cc0ca39eb74317c7f00aa17670f8989aacf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 14, 2026.
Transparency logRelease files / ptylink-0.1.0-py3-none-any.whl
| Download URL | ptylink-0.1.0-py3-none-any.whl |
|---|---|
| Size | 25.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
deba02e7bd9619b73255bd1c2beb3c7e2c8830cede117aa68292de82a893f716
|
|
BLAKE2b-256 checksum How to use checksums |
1462cf5fb819a8f766cc3b593c0e5492ef67fb1248df21ece22fea15148df580
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 14, 2026.
Transparency log