
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/backupsReading 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 defaultgetint, 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 paramikoThen 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 activeSFTP 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 useRejectPolicy, or pin the fingerprint you expect.- Missing config sections —
config["server"]raisesKeyErrorwhen the section is absent. Callconfig.has_section("server")first or useget(..., fallback=...). - Interpolation surprises — ConfigParser treats
%as interpolation syntax. A literal%in a value raisesInterpolationSyntaxError; escape it as%%or construct withConfigParser(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. Checkparamiko.SSHExceptionmessages before blaming the network.- Hanging connections — without a
timeout, a dead host blocks forever. Set the timeout in config and pass it toconnect.
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
AutoAddPolicyto 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 Dinesh Kumar R
