Network automation as pure Python: the Nornir object model (inventory → filter → task → runner → results), inventory inheritance, connection plugins (netmiko/napalm/scrapli), failure handling, and Nornir vs Ansible.
Nornir is network automation as pure Python — no YAML DSL, no templating language standing between you and the logic. It gives you the boring-but-hard parts (a typed inventory, threaded parallelism, structured results, pluggable connections) and lets you write tasks as ordinary functions. It's the answer to "I've outgrown Ansible's YAML" without dropping to raw scripts.
The model: a Nornir object holds an inventory of hosts; you
filter to a subset, then .run() a task against them; the
runner executes it in parallel and hands back a structured Result per host.
The smallest end-to-end program — initialize from config, define a task, run it across the inventory in parallel, print the structured result:
# hello_nornir.py
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
nr = InitNornir(config_file="config.yaml") # inventory + threaded runner
def hello(task: Task) -> Result:
return Result(host=task.host, result=f"Hello from {task.host.name} ({task.host.hostname})")
result = nr.run(task=hello) # runs on every host, in parallel
print_result(result)
# config.yaml
inventory:
plugin: SimpleInventory
options:
host_file: "hosts.yaml"
runner:
plugin: threaded
options: { num_workers: 10 }
# hosts.yaml
r1: { hostname: 10.0.0.1, platform: ios }
r2: { hostname: 10.0.0.2, platform: ios }
$ python hello_nornir.py
hello************************************************************
* r1 ** changed : False *******************************************
vvvv hello ** INFO
Hello from r1 (10.0.0.1)
^^^^ END hello ^^^^
* r2 ** changed : False *******************************************
vvvv hello ** INFO
Hello from r2 (10.0.0.2)
^^^^ END hello ^^^^
No device connection yet — a task is just a Python function of task. To actually talk to a
Cisco box, swap the body for a netmiko task (set platform: cisco_ios so netmiko picks
the right driver).
show version from CiscoFilter to the Cisco hosts and run a command task — one SSH session per host, all in parallel:
# hosts.yaml
# r1: { hostname: 10.0.0.1, platform: cisco_ios, username: admin, password: secret }
# r2: { hostname: 10.0.0.2, platform: cisco_ios, username: admin, password: secret }
from nornir_netmiko import netmiko_send_command
cisco = nr.filter(platform="cisco_ios")
out = cisco.run(task=netmiko_send_command, command_string="show version")
print_result(out) # full output per host
# or use the text programmatically:
for host, mr in out.items():
first = mr.result.splitlines()[0] # "Cisco IOS Software, ..."
print(host, "->", first)
Build the config from per-host data (your source of truth), then apply it with a config task — the list of lines is the generated config:
# hosts.yaml
# r1:
# hostname: 10.0.0.1
# platform: cisco_ios
# username: admin
# password: secret
# data:
# iface: GigabitEthernet0/1
# ip: 10.0.0.1
# mask: 255.255.255.0
# mtu: 9000
from nornir_netmiko import netmiko_send_config
def configure_interface(task: Task) -> Result:
d = task.host.data
cfg = [
f"interface {d['iface']}",
f" ip address {d['ip']} {d['mask']}",
f" mtu {d['mtu']}",
]
# preview instead of pushing: return Result(host=task.host, result="\n".join(cfg))
r = task.run(task=netmiko_send_config, config_commands=cfg)
return Result(host=task.host, result=r.result)
print_result(nr.filter(platform="cisco_ios").run(task=configure_interface))
# generated + applied on r1:
interface GigabitEthernet0/1
ip address 10.0.0.1 255.255.255.0
mtu 9000
netmiko enters config mode and applies the lines; add a write memory task to persist. For a
Jinja2-templated version and a NAPALM dry-run/diff before commit, see
Nornir — Practice.
| Ansible | Nornir | |
|---|---|---|
| You write | YAML playbooks (+ Jinja) | Python functions |
| Logic | Constrained (when/loop); complex logic is painful | Full Python — loops, classes, libraries, tests |
| Parallelism | Forks (processes) | Threaded runner (num_workers) |
| Debugging | Harder (through the engine) | Normal Python (pdb, pytest, stack traces) |
| Best for | Declarative fleet config, big module library | Complex workflows, scale, dev-heavy teams |
They're not enemies — many shops use Ansible for straightforward pushes and Nornir when the logic gets real. See Ansible — Theory.
| Piece | Role |
|---|---|
| Inventory | Hosts, groups, and defaults with inherited data + connection_options. From SimpleInventory (YAML) or a plugin (NetBox, Ansible inventory). |
| Nornir object | Built by InitNornir() — ties inventory + runner + config together. |
| Task | A Python function def my_task(task, ...); can call subtasks via task.run(). |
| Runner | Executes the task across hosts — the threaded runner with num_workers controls concurrency. |
| Results | AggregatedResult (all hosts) → MultiResult (per host, one entry per subtask) → Result (.result, .failed, .changed, .exception). |
Three files in the classic layout — a host inherits from its groups, which inherit from defaults:
hosts.yaml — per-device hostname, platform, groups, data.groups.yaml — shared creds/vars per role/site/vendor.defaults.yaml — fallbacks for everything.Filtering selects which hosts a task hits — the everyday operation:
nr.filter(platform="ios") or the F object for richer queries
(F(groups__contains="edge"), F(site="lhr") & F(role="spine")).
Nornir itself doesn't talk to devices — connection plugins do. You pick the library per task:
| Plugin | Gives |
|---|---|
nornir_netmiko | Screen-scraping CLI over SSH/telnet (netmiko_send_command/config). |
nornir_napalm | Multi-vendor getters + config diff/commit (napalm_get, napalm_configure). |
nornir_scrapli | Fast, async-capable CLI transport. |
nornir_jinja2 | Render templates in a task. |
nornir_utils / nornir_netbox | print_result; NetBox as the inventory source. |
AggregatedResult keyed by host; inspect .failed /
nr.data.failed_hosts.print_result can filter noise.try/except, add retries, or fan out with your own
logic — no engine to fight.Complex, conditional workflows; anything you want to unit-test; large fleets where threaded control and structured results matter; teams comfortable in Python. For simple, declarative "make these lines present" changes, Ansible is often less code.
filter / F object)num_workers)