Quick answer: configparser reads and writes Windows-style .ini files so settings live outside your code, and paramiko opens SSH/SFTP connections from Python to run commands and transfer files on remote servers. Use ConfigParser for app settings and credentials paths, and Paramiko when you need to automate a fleet of machines without shelling out to ssh.

Part 8 of our Python series — Module 2. Prerequisites: file handling and functions and scope.

Why settings belong in a file, not in your script

The moment a script has a hostname, a database name, or a threshold, hardcoding it means editing code to deploy. .ini files separate configuration from logic — one file per environment, same code everywhere. ConfigParser is in the standard library, needs no dependency, and every ops person already knows the format.

Reading and writing .ini files

import configparser

config = configparser.ConfigParser()
config["server"] = {"host": "10.0.0.5", "port": "22", "timeout": "30"}
config["paths"] = {"uploads": "/var/www/uploads", "backups": "/srv/backups"}

with open("app.ini", "w", encoding="utf-8") as fh:
    config.write(fh)

That produces a real, hand-editable file:

[server]
host = 10.0.0.5
port = 22
timeout = 30

[paths]
uploads = /var/www/uploads
backups = /srv/backups

Reading it back, with types you control:

config = configparser.ConfigParser()
config.read("app.ini", encoding="utf-8")

host = config["server"]["host"]                    # '10.0.0.5'
port = config.getint("server", "port")             # 22  (int, not str)
timeout = config.getfloat("server", "timeout")     # 30.0
region = config.get("server", "region", fallback="default")  # safe default

getint, getfloat, and getboolean are the reason ConfigParser beats hand-rolled open().readlines() parsing: config values are strings, and every one of them needs converting exactly once.

Updating a single value without rewriting the file by hand is the daily use case:

config["server"]["port"] = "2222"
with open("app.ini", "w", encoding="utf-8") as fh:
    config.write(fh)

Paramiko: SSH from Python

Paramiko is a third-party library, so install it first:

pip install paramiko

Then a connection is five lines, and command execution is another three:

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())  # see caveats below
ssh.connect(hostname="10.0.0.5", username="deploy", key_filename="id_ed25519", timeout=30)

stdin, stdout, stderr = ssh.exec_command("uptime && df -h /")
print(stdout.read().decode())
err = stderr.read().decode()
if err:
    print("stderr:", err)

ssh.close()

Key points: use key_filename rather than passwords; exec_command returns three file-like streams that you .read(); and the exit status lives in stdout.channel.recv_exit_status():

_, stdout, _ = ssh.exec_command("systemctl is-active nginx")
status = stdout.channel.recv_exit_status()   # 0 means active

SFTP transfers use the same connection:

sftp = ssh.open_sftp()
sftp.put("local_report.csv", "/srv/reports/report.csv")
sftp.get("/srv/logs/app.log", "app.log")
sftp.close()

Complete executable example

# deploy_check.py — read config, check every server, write a report
import configparser
import json
import os

INI = "fleet.ini"

# seed a config file so the script is runnable as-is
if not os.path.exists(INI):
    cfg = configparser.ConfigParser()
    cfg["fleet"] = {
        "hosts": "10.0.0.5,10.0.0.6",
        "user": "deploy",
        "command": "uptime",
        "timeout": "15",
    }
    with open(INI, "w", encoding="utf-8") as fh:
        cfg.write(fh)

config = configparser.ConfigParser()
config.read(INI, encoding="utf-8")

hosts = [h.strip() for h in config["fleet"]["hosts"].split(",") if h.strip()]
user = config["fleet"]["user"]
command = config["fleet"]["command"]
timeout = config.getint("fleet", "timeout")

print(f"Checking {len(hosts)} hosts with timeout={timeout}s")
for host in hosts:
    try:
        import paramiko
        ssh = paramiko.SSHClient()
        ssh.load_system_host_keys()
        ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
        ssh.connect(hostname=host, username=user, key_filename="id_ed25519", timeout=timeout)
        _, stdout, _ = ssh.exec_command(command)
        output = stdout.read().decode().strip()
        code = stdout.channel.recv_exit_status()
        ssh.close()
        print(f"{host:>12} exit={code} {output[:60]}")
    except Exception as exc:
        print(f"{host:>12} FAILED {type(exc).__name__}: {exc}")

# persist the effective settings for auditability
with open("fleet_effective.json", "w", encoding="utf-8") as fh:
    json.dump({"user": user, "timeout": timeout, "hosts": hosts}, fh, indent=2)

Line by line: the seeding block makes the example self-contained; the comma-separated host list in the ini keeps config flat and human-editable; getint converts the timeout once; load_system_host_keys() plus RejectPolicy is the safe pairing (see below); the try/except Exception per host means one unreachable machine does not abort the sweep; and dumping the effective settings creates an audit record of what actually ran.

Common mistakes and edge cases

  • AutoAddPolicy() in production — it accepts any host key blindly, which defeats the point of SSH host verification. Load known hosts and use RejectPolicy, or pin the fingerprint you expect.
  • Missing config sections — config["server"] raises KeyError when the section is absent. Call config.has_section("server") first or use get(..., fallback=...).
  • Interpolation surprises — ConfigParser treats % as interpolation syntax. A literal % in a value raises InterpolationSyntaxError; escape it as %% or construct with ConfigParser(interpolation=None).
  • AuthenticationException — the private key is wrong, is in the wrong format (Paramiko wants OpenSSH/PEM, not PuTTY .ppk), or the server rejected the key. Check paramiko.SSHException messages before blaming the network.
  • Hanging connections — without a timeout, a dead host blocks forever. Set the timeout in config and pass it to connect.

Key takeaways and challenge

  • ConfigParser keeps settings out of code and converts types with getint/getfloat/getboolean.
  • Paramiko runs commands and moves files over SSH/SFTP; read all three streams.
  • Verify host keys; never ship AutoAddPolicy to production.

Challenge: extend deploy_check.py to read the host list from a JSON file instead of an ini, then compare the two approaches in a comment — which felt clearer for a flat list, and which for nested settings? Choosing the right config format is half of ops work.

Want one-to-one help getting ramped in Python? Ampersand Academy offers hands-on training.

How do I read an integer from a config file with ConfigParser?

Use config.getint(section, option) instead of indexing the section directly. Also available are getfloat and getboolean, which convert the stored strings for you.

Is paramiko part of the Python standard library?

No. Install it with pip install paramiko. ConfigParser, by contrast, ships with Python, so ini handling needs no dependency at all.

Why does paramiko raise InterpolationSyntaxError on a percent sign?

ConfigParser treats percent as interpolation syntax. Escape a literal percent as two percent characters, or construct ConfigParser with interpolation=None.

Is AutoAddPolicy safe for SSH connections?

Not for production, because it accepts any host key without verification and removes protection against man-in-the-middle attacks. Load known hosts and use RejectPolicy, or pin the expected fingerprint.

How do I get the exit status of a remote command in paramiko?

exec_command returns stdout, stderr and stdin. The exit code comes from stdout.channel.recv_exit_status(), where zero means success.

Last updated on · Written by