mrkeyoor.com_
Thu 06 Aug 02:41 UTC
PyPIInfraupdated 06 Aug 2026

paramiko

Paramiko is a Python implementation of the SSHv2 protocol, giving you both a client and a server in pure Python instead of shelling out to the ssh binary. You get SSHClient for running remote commands, an SFTP client for file transfer, Transport and Channel for raw port forwarding and tunnels, and key classes for RSA, ECDSA and Ed25519. The cryptographic work is delegated to the cryptography package, so it is only pure-Python in the protocol sense. The maintainers say plainly in the README that most people running remote commands or copying files should use Fabric, which wraps Paramiko, and that direct Paramiko use is for people who need low-level primitives or an in-process sshd.

Verdict

Paramiko is still the reference SSH implementation for Python and the only realistic choice if you need the server side or raw channels. For ordinary remote commands and file copies, take the maintainers' own advice and use Fabric instead.

API stability3/5The core SSHClient and SFTP surfaces have barely changed in a decade, but 4.0.0 and 5.0.0 removed DSA, GSSAPI, SHA-1 kex, and SHA-1 RSA signatures, and renamed PKey.from_path's passphrase argument to password, so recent majors break real deployments even when your code compiles.
Docs4/5docs.paramiko.org has full versioned API reference and an honest changelog that flags each backwards-incompatible removal with a warning box, but there are few end-to-end guides and common tasks like proxy jumps or keepalives are folk knowledge in issues and blog posts.
Maintenance3/5Effectively one primary maintainer with a slow but real release cadence: 4.0.0 in August 2025, 5.0.0 in May 2026, last push May 2026. 890 open issues (1,188 including PRs) is a large backlog for a library this widely deployed.
Ecosystem5/5The default SSH layer for Python: Fabric, Ansible's paramiko connection plugin, netmiko, and most network automation tooling sit on top of it, and it is imported by build and deploy systems everywhere.

Use it if

  • You need SSH from inside a Python process where subprocess-ing to the ssh binary is not acceptable: containers without an ssh client, Windows hosts, or sandboxes that block process spawning
  • You need to speak SFTP programmatically with stat, chmod, rename, and streaming file handles rather than driving sftp or scp as a child process
  • You need to write an SSH server in Python: Paramiko is one of the very few libraries that implements the server side of SSHv2 including auth callbacks and channel handling
  • You need low-level control over channels, direct-tcpip tunnels, agent forwarding, or per-connection algorithm negotiation via disabled_algorithms
Skip it if

Setup reality

pip install paramiko is usually fine because cryptography, bcrypt, and pynacl all ship wheels for common platforms, but on musl images or odd architectures you are compiling Rust and C, and that is where CI breaks. Since 4.0.0 invoke is a hard dependency rather than an extra, so the install is heavier than people expect. The real friction is behavioral: SSHClient rejects unknown host keys by default, so first-run scripts fail until you call load_system_host_keys() or set a policy; the widely copy-pasted AutoAddPolicy silently disables host key checking. Upgrading from 3.x to 5.x can break connections to older servers because SHA-1 kex, SHA-1 RSA signatures, DSA keys, and GSSAPI have all been removed.

Patterns

Connect and run a commandrun-remote-command

import paramiko

client = paramiko.SSHClient()
client.load_system_host_keys()  # trust ~/.ssh/known_hosts
client.connect("host.example.com", username="deploy", timeout=10)

stdin, stdout, stderr = client.exec_command("uname -a")
print(stdout.read().decode())
client.close()

Without load_system_host_keys() or an explicit policy, connect() raises SSHException on any host it has never seen. That is the correct default, not a bug.

Get the exit code and stderr of a commandcheck-exit-status

stdin, stdout, stderr = client.exec_command("exit 3; echo nope")

out = stdout.read().decode()
err = stderr.read().decode()
code = stdout.channel.recv_exit_status()  # read streams FIRST

if code != 0:
    raise RuntimeError(f"remote failed ({code}): {err}")

recv_exit_status() blocks until the command finishes. Call it before draining stdout and a command that writes more than the channel window (about 2 MB) deadlocks forever.

Authenticate with a private key filekey-file-auth

from paramiko import PKey, SSHClient

key = PKey.from_path("/home/me/.ssh/id_ed25519", password="passphrase-or-None")

client = SSHClient()
client.load_system_host_keys()
client.connect("host.example.com", username="deploy", pkey=key, look_for_keys=False)

PKey.from_path sniffs the key type for you. In Paramiko 5 the keyword is password; it was called passphrase in 3.x, so this line breaks silently on upgrade if you passed it by name.

Pin host keys instead of blindly accepting themverify-host-keys

import paramiko

client = paramiko.SSHClient()
client.load_host_keys("/etc/myapp/known_hosts")  # writable store
client.set_missing_host_key_policy(paramiko.RejectPolicy())

# AutoAddPolicy() accepts ANY key on first sight and writes it down.
# That is trust-on-first-use with no user prompt: fine for a lab, not for prod.

load_host_keys() marks the file writable so accepted keys are saved; load_system_host_keys() is read-only. Mixing them up means new keys are never persisted.

Transfer files over SFTPsftp-upload-download

sftp = client.open_sftp()
sftp.put("local/app.tar.gz", "/srv/releases/app.tar.gz")
sftp.get("/var/log/app.log", "local/app.log")
sftp.chmod("/srv/releases/app.tar.gz", 0o640)
sftp.close()

put() will not create missing parent directories and overwrites without warning. Wrap sftp.stat(path) in a try/except IOError to test existence; there is no exists().

List a remote directory with metadatasftp-list-directory

import stat

sftp = client.open_sftp()
for entry in sftp.listdir_attr("/srv/releases"):
    kind = "dir" if stat.S_ISDIR(entry.st_mode) else "file"
    print(f"{kind}\t{entry.st_size}\t{entry.filename}")

listdir_attr gives you one round trip for names plus stat data; calling listdir() then stat() per file is an extra request each and gets slow over high-latency links.

Stream output line by line instead of bufferingstream-command-output

stdin, stdout, stderr = client.exec_command("tail -f /var/log/app.log", get_pty=True)

for line in iter(stdout.readline, ""):
    print(line.rstrip())

get_pty=True merges stderr into stdout and makes the remote side line-buffer. Without it many programs block-buffer and you see nothing until the process exits.

Reach a private host through a jump boxjump-host-tunnel

import paramiko

jump = paramiko.SSHClient()
jump.load_system_host_keys()
jump.connect("bastion.example.com", 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)

This is Paramiko's ProxyJump. Keep the jump client alive for as long as the target connection: closing it kills the channel underneath.

Honour ~/.ssh/config entriesread-ssh-config

import os, paramiko

cfg = paramiko.SSHConfig.from_path(os.path.expanduser("~/.ssh/config"))
opts = cfg.lookup("myhost")

client = paramiko.SSHClient()
client.load_system_host_keys()
client.connect(
    opts["hostname"],
    port=int(opts.get("port", 22)),
    username=opts.get("user"),
    key_filename=opts.get("identityfile"),
)

Paramiko parses the config but never applies it. Everything (ProxyJump, IdentityFile, Port) must be wired into connect() by hand, which is one of the main reasons people move to Fabric.

Write a key out in OpenSSH formatconvert-key-format

from paramiko import PKey
from paramiko.pkey import OPENSSH

key = PKey.from_path("/home/me/.ssh/id_rsa", password="old-passphrase")
key.write_private_key_file(
    "/home/me/.ssh/id_rsa_new", password="new-passphrase", file_format=OPENSSH
)

file_format arrived in 5.0.0 and still defaults to legacy PEM; the changelog warns a future major will flip the default, so pass it explicitly now.

Stop long-lived connections from silently dyingkeepalive-and-timeouts

client.connect(
    "host.example.com",
    username="deploy",
    timeout=10,        # TCP connect
    banner_timeout=15, # slow SSH banner
    auth_timeout=20,   # slow PAM/LDAP auth
)
client.get_transport().set_keepalive(30)  # seconds

The three timeouts are separate and all default to None or generous values; without keepalive, NAT and load balancers drop idle sessions and the next read hangs instead of raising.

Disable weak or problematic algorithmsrestrict-algorithms

client.connect(
    "host.example.com",
    username="deploy",
    disabled_algorithms={
        "pubkeys": ["rsa-sha2-512", "rsa-sha2-256"],
    },
)

This dict is the standard workaround for servers that advertise SHA-2 RSA but reject it. Note that Paramiko 5 already removed SHA-1 kex and ssh-rsa signing, so you cannot re-enable those here.

Alternatives

PackageRegistryPick it when
fabricPyPIYou want the high-level command runner and file transfer API; same maintainer, built directly on Paramiko
asyncsshPyPIYour app is asyncio-based and you want SSH client and server without dedicating a thread per connection
scpPyPIYou already have a Paramiko transport and only need scp-style file copying, which is faster than SFTP for bulk pushes