2 - Python
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
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:
| |
- Line 2:
- Pip is the package installer Python 3
- Line 3:
- Shows the path: /usr/bin/python3
Variables
Variables in Python do not need declaration, just assign.
| |
- Lines 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.
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
hostname = "R1"
interface = "GigabitEthernet0/1"
ip_address = "10.0.0.1"
print("Device: " + hostname)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.
{{ 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.
| |
- Line 18:
- Checks whether the key “platform” exists in the dictionary using the
inoperator. - Line 19:
- Prints the value of “platform” using an f-string if the condition evaluates to
True. - Line 21:
- Prints all dictionary keyssing
.keys(). - Line 22:
- Prints all dictionary values using
.values(). - Line 23:
- Prints all key-value pairs using
.items().
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.
| |
- Line 25:
- Retrieves R1’s management IP.
- Line 26:
- Retrieves the IP address of R1’s
GigabitEthernet0/0. - Lines 28-29:
- This uses chained
.get()calls with default empty dictionaries{}. Each level safely returns{}if the key does not exist. Returns “up” if everything exsists, or returns “none” if any level is missing.
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
| |
- Lines 1-2:
- These variables drive the conditional checks that follow.
- Lines 4-9:
- If the interface is “up”, it prints a success message. If the interface is “down”, it prints a troubleshooting message. If the state is anything else, it prints an “Unknown state” message.
- Lines 11-12:
- This validates that the VLAN falls within the standard IEEE VLAN range.
- Lines 14-16:
- This is a typical validation step in automation workflows.
- Lines 18-19:
- Uses a one-line conditional expression. If the interface is “up”, “status” becomes “active”. Otherwise, it becomes “inactive”.
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 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 | |
- Line 3-18:
- A structured YAML document.
- Line 20:
- Converts the YAML string into a Python dictionary.
- Line 22:
- Prints “ios”
- Line 23:
- Prints “[10,20,30]”
- Line 25:
- Opens an external file, parses it into a Python dictionary, and stores it in
vars_data. - Lines 28-29:
- Opens
output.yml, writes thedatadictionary to YAML format.default_flow_style=Falseensures block-style YAML.
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
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
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.
-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-packages--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.
| |
- Lines 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.- Lines 16-22:
send_config_set()enters config mode, sends each command in the list, and exits config mode automatically.
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 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.
Part 3 sets up Git for version control.