2 - Python
This is me documenting my journey of learning Ansible that is focused on network engineering. It’s not a “how-to guide” per-say, more of a diary. Each part will build upon the last. A lot of information on here is so I can come back to and reference later. I also learn best when teaching someone, and this is kind of me teaching.
Python 3
The goal of this part is to get comfortable enough with Python that it never slows me down when working with Ansible
Here’s where Python shows up:
- Variables and data structures in playbooks are Python dictionaries and lists, just written in YAML syntax
- Jinja2 filters (used constantly in templates) are Python expressions
- Custom filters and plugins I might write for Ansible are Python scripts
ansible-lint,yamllint, and most Ansible tooling is Python- Netmiko and NAPALM: the libraries that underpin many network automation workflows are Python
- When Ansible throws an error, the traceback is Python. If I can’t read a Python traceback, I can’t debug effectively
Note
Ansible modules are Python scripts that run on managed hosts (or locally for network devices). When I call ios_config in a playbook, Ansible is executing a Python script behind the scenes. Understanding Python helps me understand what those modules are actually doing.
Ubuntu 22.04
Ubuntu 22.04 ships with Python 3 already installed. I confirm what I have:
python3 --version
pip3 --version
which python3pip3 --version- Pip is the package installer Python 3which python3- Shows the path: /usr/bin/python3
Variables
Variables in Python do not need declaration, just assign.
| |
Line 1-4: Python variables have no type declaration. The type is inferred from the value. Strings use single or double quotes interchangeably. In Ansible, variable names follow the same rules. Letters, numbers, and underscores only, starting with a letter.
Note
In Ansible playbooks written in YAML, every variable is essentially a Python variable under the hood. When I write vlan_id: 10 in a vars: block, Ansible stores it as a Python integer. When I write hostname: "R1", it’s a Python string. Understanding Python types helps me avoid subtle bugs like a VLAN ID being treated as a string when Ansible expects an integer.
Strings and F-Strings
Old style.
hostname = "R1"
interface = "GigabitEthernet0/1"
ip_address = "10.0.0.1"
print("Device: " + hostname)Modern style.
hostname = "R1"
interface = "GigabitEthernet0/1"
ip_address = "10.0.0.1"
print(f"Device: {hostname}, Interface: {interface}, IP: {ip_address}")F-string can include expressions.
vlan = 100
print(f"VLAN {vlan} is {'even' if vlan % 2 == 0 else 'odd'}")F-strings (formatted string literals) start with f before the quote. Anything inside {} is evaluated as Python.
Tip
Jinja2 templating in Ansible uses {{ variable_name }} syntax. Which is conceptually identical to Python f-strings. Once I understand f-strings, Jinja2 templating feels familiar rather than foreign.
Lists
Lists are ordered collections. In Ansible, it can be lists of VLANs, lists of interfaces, lists of hosts.
| |
- Line 5: Python lists are zero-indexed, the first item is at index
[0], not[1]. This catches people off guard early. - Line 6: Negative indexing counts from the end.
[-1]is always the last item.
Dictionaries
Dictionaries are key-value pairs. This is the most important Python data structure for Ansible: host_vars, group_vars, facts, and registered task output are all dictionaries.
| |
Dictionary vs YAML Syntax
In Python I write {"hostname": "R1", "platform": "ios"}. In YAML (Ansible) I write the same data as:
hostname: R1
platform: iosThey represent identical data structures. When Ansible reads my YAML file, it converts it into Python dictionaries internally. Knowing this makes the relationship between YAML playbooks and Python crystal clear.
Nested Dictionaries
Real device data is nested (dictionaries inside dictionaries). Ansible facts look exactly like this.
| |
This builds a nested dictionary to represent:
- Multiple network devices
- Per-device attributes
- Per-interface attributes
- Safe vs unsafe nested access
- The structure mirrors how real network inventory or API JSON data is modeled.
Loops
| |
- Line 7:
enumerate()gives me both the index and the value in a loop (useful when I need to know the position of an item). - Line 15: List comprehensions are a compact way to build lists.
range(0, 24)generates numbers 0 through 23. This single line creates a list of 24 interface names.
Conditionals
| |
This shows:
- Basic
if / elif / elseconditional logic. - Range validation using comparison operators.
- Membership checks using
in. - A ternary expression for concise conditional assignment.
It models simple network validation logic for interface state and VLAN IDs.
Functions
| |
- Line 1:
defdefines a function.returnsends a value back to whoever called the function. Functions in Python are how I avoid writing the same logic in multiple places.
JSON and YAML
These two formats are everywhere in network automation. REST APIs return JSON. Ansible playbooks and inventory are YAML. I need to be able to read both formats in Python and convert between them.
JSON in Python
JSON (JavaScript Object Notation) looks almost identical to Python dictionaries. Python’s built-in json module handles it.
| |
- Line 1:
import jsonloads Python’s built-in JSON module (no installation needed). - Line 15:
json.loads()(“load string”) parses a JSON string into a Python dictionary. Thesmatters,json.load()(withouts) reads from a file. - Line 25:
json.dumps()(“dump string”) converts a Python dict back to a JSON string.indent=4pretty-prints it with 4-space indentation.
Reading and Writing JSON Files
| |
- Line 9:
with open(...) as f:is the correct way to open files in Python. Thewithblock automatically closes the file when done, even if an error occurs."w"means write mode,"r"means read mode. - Line 10:
json.dump()(nos) writes to a file object. Contrast withjson.dumps()which writes to a string.
YAML in Python
Python doesn’t include a YAML parser in its standard library, so I install PyYAML:
pip3 install pyyaml | |
This shows:
- Parse YAML from a string.
- Convert YAML into a Python dictionary.
- Access nested YAML data.
- Load YAML from a file.
- Write structured data back to a YAML file.
Caution
Never use yaml.load() without a Loader argument in any script, and never use yaml.load(data, Loader=yaml.FullLoader) on YAML files from untrusted sources. The safe habit is yaml.safe_load() always, everywhere. This same principle applies when I see Ansible warning about unsafe YAML loading.
JSON vs YAML
| Situation | Format |
|---|---|
| REST API response (Netbox, AWX, PAN-OS API) | JSON |
| Ansible playbooks and inventory | YAML |
| Ansible facts gathered from devices | Python dict (originally JSON) |
Ansible output with -v flag | JSON-like Python dict |
| Jinja2 template variable files | YAML |
| Network device RESTCONF responses | JSON or XML |
How Ansible Uses Python
Understanding this demystifies a lot of Ansible’s behavior and error messages.
The Execution Flow
When I run ansible-playbook site.yml, here’s what actually happens:
- Ansible reads my YAML playbook and converts it into Python data structures (dictionaries and lists)
- For each task, Ansible finds the corresponding Python module file (e.g.,
ios_config.py) - For network devices using
network_cliconnection, Ansible runs the module locally on the control node (my Ubuntu VM), not on the device itself - The module uses Paramiko or SSH to connect to the device, send commands, and receive output
- The module returns a Python dictionary with keys like
changed,failed,stdout,diff - Ansible evaluates the return dictionary to determine if the task passed or failed, and whether anything changed
Note
This is a key difference between network automation and server automation. When Ansible manages a Linux server, it copies the Python module to the server and runs it there. When Ansible manages a network device (a router or switch), the device doesn’t run Python (Ansible runs the module locally on the control node and communicates with the device over SSH or NETCONF). This is why network modules require connection: network_cli or connection: netconf instead of the default connection: ssh.
Python Tracebacks (Reading Error Messages)
When something goes wrong, Ansible often surfaces a Python traceback. Here’s how to read one:
TASK [ios_config] *************************************
fatal: [R1]: FAILED! => {
"msg": "Traceback (most recent call last):\n
File \"/usr/lib/python3/dist-packages/ansible/...\", line 142, in run\n
connection.get('show version')\n
File \"...\", line 87, in get\n
raise AnsibleConnectionFailure('timed out')\n
ansible.errors.AnsibleConnectionFailure: timed out"
}Reading this from bottom to top is the trick. The last line is the actual error: AnsibleConnectionFailure: timed out. The lines above it are the call stack showing how the code got there. I almost always only need the last line to understand what went wrong.
Tip
When I get a cryptic Ansible error, I add -vvv to my ansible-playbook command. This verbose mode prints the full Python traceback and the exact SSH commands being sent to the device. It’s the fastest way to go from “something failed” to “I know exactly why.”
Python Libraries
These three libraries come up constantly in network automation. I don’t need to master them now but understanding what they do and how to use them in a basic script makes me a more capable engineer.
Installing Libraries
All three go into my virtual environment. For now, I install them into the system Python just to experiment:
pip3 install netmiko napalm requests --break-system-packagesWarning
The --break-system-packages flag is needed on Ubuntu 22.04 because pip is restricted from modifying system packages by default. I use this flag only for quick experiments on a VM I control.
Talking to REST APIs
requests is the standard Python HTTP library. I use it to interact with REST APIs (Netbox, AWX, Palo Alto, etc.).
| |
- Line 4:
requests.get()sends an HTTP GET request and returns a response object. - Line 8:
.json()automatically parses the JSON response body into a Python dictionary (equivalent to callingjson.loads(response.text)). - Line 21:
verify=Falsedisables SSL certificate verification.
Netmiko
Netmiko is a Python library built specifically for SSH connections to network devices. It handles all the quirks of different vendors’ SSH implementations.
| |
- Line 3-9: The device dictionary tells Netmiko exactly what kind of device I’m connecting to. The
device_typefield is critical, it tells Netmiko which SSH behavior patterns to expect. Common values:cisco_ios,cisco_nxos,juniper_junos,paloalto_panos. - Line 11:
**deviceunpacks the dictionary as keyword arguments (equivalent to writingConnectHandler(device_type="cisco_ios", host="192.168.1.1", ...)). - Line 13:
send_command()sends a single show command and returns the output as a string. - Line 16-22:
send_config_set()enters config mode, sends each command in the list, and exits config mode automatically.
Info
Ansible’s network_cli connection plugin is built on top of Netmiko. When I configure an Ansible playbook with connection: network_cli, Ansible is using Netmiko under the hood to connect to devices. Understanding Netmiko directly helps me troubleshoot connectivity issues in Ansible, because the same parameters apply: device_type maps to ansible_network_os, username maps to ansible_user, and so on.
Napalm
NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) sits above Netmiko. Instead of sending raw CLI commands, NAPALM provides vendor-neutral Python methods that work the same way across Cisco, Juniper, and Arista.
| |
- Line 3:
get_network_driver("ios")returns the Cisco IOS driver class. The same code works with"junos","eos"(Arista), or"nxos". - Line 14:
get_facts()returns a standardized dictionary with the same keys regardless of vendor. This is NAPALM’s core value: I write the code once and it works everywhere.
Reading Error Messages
Errors are inevitable. Here’s how I read them without panicking.
Traceback (most recent call last):
File "inventory.py", line 53, in <module>
print(f" {device['hostname']:<6} | {device['ip']}")
KeyError: 'ip'I always read from bottom to top:
- Last line - the actual error type and message:
KeyError: 'ip' - Second to last - the exact line of code that caused it:
device['ip'] - Third to last - the file and line number:
inventory.py, line 53
In this case, I tried to access a key 'ip' that doesn’t exist in my dictionary. The key is actually named 'mgmt_ip'. Fixed.
Common Errors
| Error | What It Means | Typical Fix |
|---|---|---|
KeyError: 'hostname' | Dictionary key doesn’t exist | Use .get() or check the key name |
IndentationError | Wrong whitespace | Fix indentation — Python requires consistency |
SyntaxError | Invalid Python syntax | Check for missing colons, brackets, quotes |
ModuleNotFoundError | Library not installed | pip3 install <library> |
TypeError | Wrong data type | Check what type the function expects |
FileNotFoundError | File path is wrong | Check the path with ls |
Part 3 sets up Git for version control.