Network Automation with Python¶
Where Ansible and Terraform hand you guardrails, Python hands you a blank canvas. This article covers the Python network-automation toolbox — which library to reach for by transport, how to push and read configuration with Netmiko, NAPALM, and ncclient, how to turn CLI text into structured data, how to scale across hundreds of devices, and the habits that separate code that works from code that is safe to run in production.
Why Python, and where it fits¶
Python is a general-purpose language with a deep networking ecosystem, and it quietly underpins the other
tools. Ansible modules are written in Python; the Cisco IOS XE Terraform provider ultimately drives
RESTCONF, which you can call directly from Python with the requests library. Learning the toolbox pays
off twice — once for the scripts you write yourself, and again for understanding how the higher-level
tools work underneath.
Reach for Python when a requirement has custom logic that does not fit a declarative tool — conditional workflows, data transformation, gluing several systems together, or anything the model of "declare the end state" cannot express. The cost is ownership: error handling, testing, retries, and security are all yours to build. A declarative tool gives you those for free; Python gives you total flexibility in exchange for writing them.
The toolbox: choose by transport¶
Pick a library by how you talk to the device (SSH CLI, NETCONF, or REST) and by how custom the logic is.
| Library | Transport | Use it for |
|---|---|---|
netmiko |
SSH (CLI) | Multi-vendor CLI push/read; the most common starting point |
scrapli |
SSH (CLI) | A faster, modern CLI library with native asyncio support |
napalm |
SSH / API | A multivendor abstraction with one common API and config rollback |
ncclient |
NETCONF | YANG-modeled, transactional configuration over SSH (port 830) |
requests |
HTTP | RESTCONF and any REST API |
Jinja2 |
— | Templating device configuration from data |
pyATS / Genie |
— | Cisco's parsing, testing, and validation framework |
The first three cover the overwhelming majority of CLI and multivendor work; the rest slot in as the task demands.
Netmiko: automating the CLI¶
Netmiko's ConnectHandler is a factory that opens an SSH session and returns the right class for your
platform based on device_type (for IOS XE use cisco_xe, or cisco_ios). send_config_set enters
configuration mode, sends a list of commands, and exits.
# netmiko_vlan.py — push VLANs and configure an access port
import os
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException
device = {
"device_type": "cisco_xe",
"host": "192.0.2.11",
"username": "automation",
"password": os.environ["NET_PASSWORD"], # read the secret from the environment
}
vlan_cfg = [
"vlan 10", "name SALES",
"vlan 20", "name VOICE",
"interface GigabitEthernet3",
"switchport mode access",
"switchport access vlan 10",
]
try:
with ConnectHandler(**device) as conn:
output = conn.send_config_set(vlan_cfg)
conn.save_config() # write mem
print(output)
except (NetmikoTimeoutException, NetmikoAuthenticationException) as err:
print(f"{device['host']}: {err}")
The with block guarantees the session closes even if an error is raised, and catching Netmiko's
timeout and authentication exceptions means an unreachable device or a bad login fails gracefully instead
of crashing the whole run.
Turning CLI text into data¶
Raw show output is just text — fragile to parse with string slicing. Passing use_textfsm=True to
send_command runs the output through a TextFSM template
(Netmiko bundles the NTC templates) and returns a list of dictionaries you can program against. This is
the key to inventory and asset management.
# netmiko_facts.py — structured output for asset management
with ConnectHandler(**device) as conn:
facts = conn.send_command("show version", use_textfsm=True)
inv = conn.send_command("show inventory", use_textfsm=True)
# facts is a list of dicts, e.g. [{"version": "17.9.1", "serial": ["FXS..."], ...}]
print(facts[0]["version"])
print(inv[0]["name"])
Prefer structured data like this — or NAPALM's dictionaries below — over parsing strings by hand. It is the difference between a script that survives a firmware upgrade and one that breaks the next time the output format shifts by a space.
NAPALM: one API across vendors¶
NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) gives every supported platform the same method names, so one script runs unchanged against IOS XE, EOS, or Junos. It also exposes a candidate-configuration workflow: stage a change, review the diff, then commit or discard.
# napalm_demo.py — stage, review the diff, then commit
from napalm import get_network_driver
driver = get_network_driver("ios")
dev = driver(hostname="192.0.2.11", username="automation", password=PASSWORD)
dev.open()
print(dev.get_facts()) # vendor-neutral facts dict
dev.load_merge_candidate(filename="vlan10.cfg")
print(dev.compare_config()) # human-readable diff before you commit
dev.commit_config() # or dev.discard_config() to back out
dev.close()
The compare_config() step is what makes NAPALM safe: you see exactly what will change before it lands,
and rollback() can revert a committed change on platforms that support it.
ncclient: transactional configuration over NETCONF¶
When a task needs structured, transactional configuration rather than screen-scraping the CLI,
ncclient is the standard NETCONF client. It opens a NETCONF session,
lets you get_config and edit_config against YANG models, and — on platforms that support it — use a
candidate datastore with an explicit commit, so a batch of changes either fully applies or not at all.
# ncclient_read.py — read interface config as structured XML
from ncclient import manager
with manager.connect(host="192.0.2.11", port=830, username="automation",
password=PASSWORD, hostkey_verify=False,
device_params={"name": "iosxe"}) as m:
filt = "<interfaces xmlns='urn:ietf-params:xml:ns:yang:ietf-interfaces'/>"
reply = m.get_config(source="running", filter=("subtree", filt))
print(reply) # XML you can parse into data
The mental model mirrors RESTCONF — the same YANG models, a different transport — but ncclient adds the
NETCONF extras (candidate config, locking, commit/discard) that a plain REST call cannot give you. Choosing
between requests + RESTCONF and ncclient + NETCONF is the familiar trade-off between quick, RESTful
access and careful, transactional change, now expressed in code.
Concurrency: automating hundreds of devices¶
A loop that logs into devices one at a time is fine for ten switches and painfully slow for five hundred,
because almost all of the time is spent waiting on the network, not on the CPU. Network automation is
I/O-bound, so a thread pool is usually enough:
concurrent.futures.ThreadPoolExecutor runs
many SSH or API sessions in parallel and collects the results, cutting a job from minutes to seconds.
# fan_out.py — read one command from many devices in parallel
from concurrent.futures import ThreadPoolExecutor
def grab(host):
with ConnectHandler(device_type="cisco_xe", host=host,
username="automation", password=PASSWORD) as conn:
return host, conn.send_command("show version", use_textfsm=True)
with ThreadPoolExecutor(max_workers=20) as pool:
for host, output in pool.map(grab, inventory):
save(host, output)
Concurrency multiplies your blast radius: twenty parallel workers pushing configuration means a bad change
hits twenty devices at once. Cap max_workers sensibly, roll changes out in batches, and validate on a
single canary device first — speed is only safe once the change itself has been proven. For genuinely large
fleets or when you want a maintained framework rather than a hand-rolled pool,
Nornir provides pure-Python inventory and parallel execution built for
exactly this.
Write robust code, not just working code¶
The examples above hint at the habits that matter in production. Make them non-negotiable:
- Never hard-code credentials. Passwords baked into a
.pyfile end up in version control and logs. Read them from environment variables or a secrets vault. - Wrap device calls in
try/except. Catch the library's specific exceptions (Netmiko'sNetmikoTimeoutExceptionandNetmikoAuthenticationException, for instance) so one unreachable device fails gracefully instead of aborting the run. - Prefer structured data over string parsing.
use_textfsm=True, NAPALM's dictionaries, and NETCONF's XML all give you data you can rely on across firmware versions. - Validate inputs and diff before you commit. A candidate-config workflow (
compare_config→commit_config) shows you the change before it lands.
Key takeaways¶
- Python is the most flexible option and the foundation of the others — choose it when the logic is too custom for a declarative tool, and accept that error handling, testing, and security become yours to own.
- Pick the library by transport:
netmiko/scraplifor SSH CLI,napalmfor a multivendor common API with rollback,ncclientfor transactional NETCONF, andrequestsfor RESTCONF/REST. - Turn text into data with
use_textfsm=Trueor NAPALM dictionaries — essential for asset management and validation, and far more durable than parsing strings. - Scale with concurrency using
ThreadPoolExecutorbecause the work is I/O-bound, but cap workers, batch changes, and canary first — parallelism multiplies your blast radius. - Write robust code: keep secrets out of source, wrap calls in
try/except, and diff before you commit.
Sources: Netmiko documentation,
NAPALM documentation,
ncclient documentation,
Python concurrent.futures,
NTC Templates (TextFSM).