paramiko review
Paramiko 5.0.0 implements SSHv2 clients, servers, channels, and SFTP in Python. `SSHClient` authenticates and runs remote processes, `SFTPClient` reads and changes remote files, and the lower-level `Transport` API opens forwarding channels or backs an SSH server written in Python. Paramiko's own README recommends Fabric for ordinary remote commands and transfers, leaving direct Paramiko use for code that needs protocol-level control. Version 5 can write OpenSSH private-key files and load encrypted keys through `PKey.from_type_string`; it also removes GSSAPI, SHA-1 key exchange, and SHA-1 RSA signatures. Our import worked, but took 0.99 seconds and the install occupied 22 MB.
Paramiko 5.0.0 installed in 0.3 seconds but brought seven packages and 22 MB, while its import took 0.99 seconds in our sandbox; pay that cost for direct SSH channels, SFTP internals, or server code. Routine deployment commands belong one layer up in Fabric, and legacy SHA-1 or GSSAPI estates should not upgrade blindly.
We installed it
| Install | ✓ · 0.3s | 7 packages on disk · 22 MB |
| Import | ✓ | import paramiko in 0.99s · pure Python · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does paramiko install cleanly?
Yes. In a fresh container with an empty cache, pip install paramiko finished in 0.3s, leaving 7 packages and 22 MB on disk. pip-audit reported no known vulnerabilities.
What does paramiko need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import paramiko succeeded in 0.99s.
paramiko or fabric: which should you use?
fabric: Use it for routine remote commands, sudo, deployment tasks, connection settings, and file transfers built on Paramiko. Paramiko 5.0.0 installed in 0.3 seconds but brought seven packages and 22 MB, while its import took 0.99 seconds in our sandbox; pay that cost for direct SSH channels, SFTP internals, or server code.
When should you not use paramiko?
The work is mainly remote commands, uploads, and sudo. Paramiko's README recommends Fabric for those common client workflows.
Use it if
- A Python service must create SSH or SFTP sessions without shelling out to the system OpenSSH client.
- You need forwarding channels, algorithm controls, custom transports, or protocol details that a task runner intentionally hides.
- The application is an SSH server whose authentication and channel behavior are implemented in Python.
- Remote-file logic needs SFTP metadata, permissions, streaming, rename operations, or an application-defined atomic upload sequence.
- The work is mainly remote commands, uploads, and sudo. Paramiko's README recommends Fabric for those common client workflows.
- The service already uses asyncio or must hold many simultaneous SSH sessions. Paramiko blocks; AsyncSSH integrates with the event loop.
- Servers authenticate with Kerberos or GSSAPI. Paramiko 5 removed its GSSAPI implementation.
- Old devices only offer SHA-1 key exchange or `ssh-rsa` signatures. Those algorithms are absent from version 5, and group exchange now expects larger moduli.
- The main requirement is maximum bulk-copy throughput. Native `rsync` or `scp` may be a better data path than Python SFTP channels.
- Static typing must be complete without third-party stubs. Our 5.0.0 package did not contain `py.typed`.
Setup reality
We installed Paramiko 5.0.0 in a fresh unprivileged Python 3.12 Bookworm sandbox. pip completed in 0.3 seconds and left seven packages using 22 MB. import paramiko succeeded in 0.99 seconds. The top-level package is pure Python, declares four direct dependencies, requires Python 3.9+, and has no py.typed marker. pip-audit found zero known vulnerabilities in that environment. Our package metadata did not identify a license.
The first production connection should fail on an unknown host key. Load a controlled known-hosts file and retain RejectPolicy until an enrollment process has verified the fingerprint. AutoAddPolicy silently trusts the first key it sees. Services should also set allow_agent and look_for_keys deliberately so a developer's agent or home-directory key cannot change which credential gets used.
Each exec_command call returns three streams on one SSH channel. Read stdout and stderr before calling recv_exit_status() when the command can fill the channel window; waiting first can deadlock. Stream long output incrementally. A pseudo-terminal changes buffering and terminal behavior, so ask for one only for software that truly expects a TTY. TCP connection, banner, authentication, and channel opening all have separate timeout settings.
Inventory servers before upgrading to version 5.0.0. GSSAPI, SHA-1 exchanges, and SHA-1 RSA signing have been removed, and group-exchange parameters are stricter. PKey.from_path() now accepts password= for an encrypted key instead of the earlier passphrase= name. Key writing can specify file_format=OPENSSH; choose the format explicitly because the changelog warns that a later major may stop defaulting to PEM.
Patterns
Run a command after checking the host key execute-verified-command
import paramiko
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.RejectPolicy())
client.connect("host.example", username="deploy", timeout=10)
try:
stdin, stdout, stderr = client.exec_command("uname -a")
output = stdout.read().decode()
error = stderr.read().decode()
status = stdout.channel.recv_exit_status()
finally:
client.close()Known-host loading makes `RejectPolicy` useful. Drain both output streams before waiting for status when either stream may fill the channel window.
Open an encrypted private key in version 5 load-encrypted-key
from paramiko import PKey, SSHClient
key = PKey.from_path("/run/secrets/deploy_key", password=key_password)
client = SSHClient()
client.load_system_host_keys()
client.connect(
"host.example", username="deploy", pkey=key,
allow_agent=False, look_for_keys=False,
)Paramiko 5 uses `password=` for `PKey.from_path()`. Disabling agent and key discovery prevents ambient developer credentials from changing authentication.
Verify against an application-owned host store use-private-known-hosts
import paramiko
client = paramiko.SSHClient()
client.load_host_keys("/etc/example/known_hosts")
client.set_missing_host_key_policy(paramiko.RejectPolicy())
client.connect("host.example", username="deploy")Populate the file through a separate fingerprint-verification step. `AutoAddPolicy` accepts an unknown first key without that check.
Upload to a temporary name before publishing upload-atomic-sftp
sftp = client.open_sftp()
try:
temporary = "/srv/releases/release.tar.gz.part"
final = "/srv/releases/release.tar.gz"
sftp.put("release.tar.gz", temporary, confirm=True)
sftp.chmod(temporary, 0o640)
sftp.rename(temporary, final)
finally:
sftp.close()`put()` does not create parent directories. Renaming only after a confirmed transfer keeps readers from opening a partial final file.
Fetch names and metadata together list-sftp-attributes
import stat
sftp = client.open_sftp()
try:
for entry in sftp.listdir_attr("/srv/releases"):
kind = "directory" if stat.S_ISDIR(entry.st_mode) else "file"
print(kind, entry.st_size, entry.filename)
finally:
sftp.close()`listdir_attr()` returns stat-like fields with each name. Calling `stat()` afterward for every entry adds one network round trip per item.
Consume output from a long-running process stream-remote-output
stdin, stdout, stderr = client.exec_command("journalctl -fu example")
try:
for line in iter(stdout.readline, ""):
print(line.rstrip())
finally:
stdout.channel.close()Some remote programs buffer when no terminal is attached. `get_pty=True` can change that, but it also changes signal and stream behavior.
Connect to a target through a direct TCP channel tunnel-through-bastion
jump = paramiko.SSHClient()
jump.load_system_host_keys()
jump.connect("bastion.example", username="deploy")
channel = jump.get_transport().open_channel(
"direct-tcpip", dest_addr=("10.0.1.20", 22),
src_addr=("127.0.0.1", 0),
)
target = paramiko.SSHClient()
target.load_system_host_keys()
target.connect("10.0.1.20", username="deploy", sock=channel)The target runs over the bastion transport. Close the target and its channel before shutting down the jump connection.
Resolve selected values from SSH config parse-openssh-alias
from pathlib import Path
import paramiko
config = paramiko.SSHConfig.from_path(Path.home() / ".ssh" / "config")
options = config.lookup("production")
client = paramiko.SSHClient()
client.load_system_host_keys()
client.connect(
options["hostname"], port=int(options.get("port", 22)),
username=options.get("user"), key_filename=options.get("identityfile"),
)Parsing the file does not apply every OpenSSH option. Proxy commands and jump-host behavior still need explicit wiring.
Bound each stage of connection setup set-phase-timeouts
client.connect(
"host.example", username="deploy",
timeout=10, banner_timeout=15, auth_timeout=20, channel_timeout=30,
)
client.get_transport().set_keepalive(30)TCP setup, banner exchange, authentication, and channel creation can stall separately. A keepalive helps discover idle sessions removed by intervening network devices.
Select OpenSSH private-key output explicitly write-openssh-key
from paramiko import PKey
from paramiko.pkey import OPENSSH
key = PKey.from_path("/secure/id_ed25519", password=old_password)
key.write_private_key_file(
"/secure/id_ed25519.new", password=new_password, file_format=OPENSSH
)Version 5 adds the format argument. The changelog says a later major may change the current PEM default, so explicit output is safer for stored keys.
Open SFTP on a live SSH transport reuse-transport-for-sftp
transport = client.get_transport()
if transport is None or not transport.is_active():
raise RuntimeError("SSH transport is not active")
sftp = paramiko.SFTPClient.from_transport(transport)
try:
with sftp.open("/var/log/example.log", "r") as remote:
first_line = remote.readline()
finally:
sftp.close()Every SFTP client consumes another channel. Close it when finished if the process plans to keep the underlying SSH transport alive.
Remove an algorithm a server advertises incorrectly disable-broken-algorithm
client.connect(
"host.example", username="deploy",
disabled_algorithms={"pubkeys": ["rsa-sha2-512"]},
)`disabled_algorithms` subtracts from the algorithms Paramiko 5 supports. It cannot bring back SHA-1 exchanges or signatures removed by this major version.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fabric | PyPI | Use it for routine remote commands, sudo, deployment tasks, connection settings, and file transfers built on Paramiko. |
| asyncssh | PyPI | Use it for asyncio clients, servers, forwarding, and high connection concurrency. |
| scp | PyPI | Use it when an existing Paramiko transport must transfer files with the SCP protocol. |
| ssh2-python | PyPI | Use it when libssh2 bindings fit the platform and lower native-library overhead or throughput is the priority. |
More infra guides
boto3 · opentelemetry-api · psutil · distro · @opentelemetry/api · google-cloud-storage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

