Python for Network Engineers — Part 2: Strings, Numbers, Files, Lists, and Tuples
In Part 1 we set up a working environment and ran our first script. That script used strings, files, and a list without explaining any of them properly. This post fixes that.
Python’s built-in data types are the vocabulary of everything you’ll write. You’ll use strings to represent hostnames, interface names, and CLI output. You’ll use files to store device inventories and configs. You’ll use lists to hold collections of devices, routes, and interfaces. Once these feel natural, the networking-specific libraries we introduce in later posts are much easier to absorb — they’re just these types dressed up with domain knowledge.
We’ll use the REPL throughout this post for short experiments. For anything that grows into a real script, we’ll write a file.
The Python REPL
The REPL (Read–Eval–Print Loop) is the interactive Python shell. It evaluates expressions immediately and prints the result. It’s the fastest way to test an idea or check your understanding.
Start it:
python3
You’ll see a >>> prompt. Type Python, press Enter, see the result:
>>> 2 + 2
4
>>> "core-sw-01".upper()
'CORE-SW-01'
>>> exit()
Use the REPL to experiment with everything in this post. If you’re unsure what a string method does, try it immediately. If an expression doesn’t behave as expected, the REPL gives you instant feedback with no files to edit and no scripts to run.
Strings
A string is a sequence of characters. In network automation, strings are everywhere — they’re the raw material of CLI output, config files, hostname lookups, log entries, and API responses.
Create a string with single or double quotes:
hostname = "core-sw-01"
interface = 'GigabitEthernet0/0/1'
Python doesn’t distinguish between the two. Single quotes are common for short values; double quotes are useful when your string contains an apostrophe.
Indexing and Slicing
Strings are sequences — you can access individual characters by position, starting at zero:
>>> hostname = "core-sw-01"
>>> hostname[0]
'c'
>>> hostname[-1] # negative index counts from the end
'1'
You can extract a substring with a slice: [start:stop]. The start is inclusive, the stop is exclusive:
>>> hostname = "GigabitEthernet0/0/1"
>>> hostname[0:8]
'Gigabit'
>>> hostname[:8] # start defaults to 0
'Gigabit'
>>> hostname[15:] # stop defaults to end
'0/0/1'
Slicing is useful for extracting fixed-width fields from device output, though you’ll reach for string methods more often in practice.
f-strings
f-strings are the modern way to build strings from variables. Prefix the string with f and put expressions inside {}:
hostname = "core-sw-01"
ip = "192.168.1.1"
vlan = 100
print(f"Device: {hostname}, IP: {ip}, VLAN: {vlan}")
# Device: core-sw-01, IP: 192.168.1.1, VLAN: 100
You can include formatting instructions after a colon inside the braces:
# Pad to a fixed width — useful for tabular output
print(f"{hostname:<20} {ip:<16} {vlan:>5}")
# core-sw-01 192.168.1.1 100
# < is left-aligned, > is right-aligned, the number is the column width
You’ll use this pattern constantly when printing device summaries. Use f-strings exclusively — older approaches (% formatting and .format()) still work but add no value and are harder to read.
Useful String Methods
Python strings come with a large set of built-in methods. These are the ones you’ll reach for in network automation:
.strip() — remove leading and trailing whitespace:
>>> " GigabitEthernet0/0/1 ".strip()
'GigabitEthernet0/0/1'
CLI output often has trailing spaces or newlines. Always strip before processing.
.split() — break a string into a list on a delimiter:
>>> "192.168.1.1".split(".")
['192', '168', '1', '1']
>>> "hostname,192.168.1.1,Cisco".split(",")
['hostname', '192.168.1.1', 'Cisco']
Without an argument, .split() splits on any whitespace and discards empty strings — useful for tokenising CLI output:
>>> "GigabitEthernet0/0/1 up up".split()
['GigabitEthernet0/0/1', 'up', 'up']
.join() — the reverse of split, combine a list into a string:
>>> octets = ['192', '168', '1', '1']
>>> ".".join(octets)
'192.168.1.1'
.startswith() and .endswith() — check the beginning or end:
>>> "GigabitEthernet0/0/1".startswith("Giga")
True
>>> "show ip route".startswith("show")
True
>>> "config.bak".endswith(".bak")
True
Useful for filtering lines of CLI output or categorising interface names.
.upper(), .lower() — case conversion:
>>> "CORE-SW-01".lower()
'core-sw-01'
>>> "core-sw-01".upper()
'CORE-SW-01'
Normalise case before comparing strings — "GigabitEthernet0/0/1" == "gigabitethernet0/0/1" is False without it.
.replace() — substitute substrings:
>>> "GigabitEthernet0/0/1".replace("GigabitEthernet", "Gi")
'Gi0/0/1'
.strip() with a character argument — remove specific characters:
>>> "---interface---".strip("-")
'interface'
.in operator — check membership:
>>> "down" in "GigabitEthernet0/0/1 is down"
True
>>> "192.168" in "192.168.1.1"
True
Multi-line Strings
Triple quotes create strings that span multiple lines — useful for storing config templates or multi-line CLI commands:
config_block = """
interface GigabitEthernet0/0/1
description WAN link to DC
ip address 10.0.0.1 255.255.255.252
no shutdown
"""
Numbers
Python has two main numeric types: int (integers) and float (decimal numbers). For most network automation work you’ll use integers — VLAN IDs, port numbers, prefix lengths, interface indices.
vlan_id = 100
prefix_length = 24
port = 22
Basic arithmetic:
>>> 10 + 3
13
>>> 10 - 3
7
>>> 10 * 3
30
>>> 10 / 3 # always returns a float
3.3333333333333335
>>> 10 // 3 # integer (floor) division
3
>>> 10 % 3 # modulo — remainder after division
1
>>> 2 ** 8 # exponentiation — 2 to the power of 8
256
The distinction between / (float division) and // (integer division) matters when you need a whole number result:
>>> total_hosts = 254
>>> hosts_per_group = 10
>>> groups_needed = total_hosts // hosts_per_group # 25, not 25.4
Type Conversion
CLI output comes back as strings. Before doing arithmetic with a value from device output, convert it:
>>> cpu_str = "87" # from a 'show processes cpu' scrape
>>> cpu_int = int(cpu_str)
>>> cpu_int > 80
True
>>> latency_str = "1.23" # from a ping output
>>> latency_float = float(latency_str)
Convert a number back to a string for display or concatenation:
>>> vlan = 100
>>> "vlan" + str(vlan)
'vlan100'
Booleans and None
Booleans are True and False — capitalised, no quotes. They’re used in conditions (covered in Part 3) and returned by comparison operations:
>>> 100 > 50
True
>>> "up" == "down"
False
>>> 22 in [22, 80, 443]
True
Truthiness: in Python, many values are considered “truthy” or “falsy” without being literally True or False. This matters when you use a value directly in a condition:
| Falsy | Truthy |
|---|---|
False | True |
0 | Any non-zero number |
"" (empty string) | Any non-empty string |
[] (empty list) | Any non-empty list |
None | Most objects |
>>> bool("")
False
>>> bool("down")
True
>>> bool([])
False
>>> bool(["192.168.1.1"])
True
None represents the absence of a value — similar to null in other languages. You’ll see it returned by functions that have nothing to give back, and you’ll use it as a default when you don’t know a value yet:
description = None # interface has no description yet
Files
Reading and writing files is fundamental to network automation. Device inventories, config templates, output logs, and credential files all live on disk.
Reading a File
Always use the with statement when opening files. It ensures the file is closed properly, even if something goes wrong:
with open("devices.txt") as f:
contents = f.read() # read the entire file as one string
print(contents)
Reading the entire file into memory works fine for small files. For larger files — say, a full running config — read line by line instead:
with open("running-config.txt") as f:
for line in f:
line = line.strip() # remove newline and whitespace
print(line)
To get all lines as a list:
with open("devices.txt") as f:
lines = f.readlines() # list of strings, newlines included
Or more cleanly:
with open("devices.txt") as f:
lines = [line.strip() for line in f] # list comprehension — covered in Part 3
Writing a File
with open("output.txt", "w") as f: # "w" = write, creates or overwrites
f.write("core-sw-01,192.168.1.1\n")
f.write("core-sw-02,192.168.1.2\n")
The \n is a newline character — without it, everything ends up on one line. Append to an existing file with "a" instead of "w":
with open("output.txt", "a") as f:
f.write("edge-rtr-01,10.0.0.1\n")
Specifying Encoding
On Windows, text files sometimes use a different encoding. To be explicit and avoid surprises:
with open("devices.txt", encoding="utf-8") as f:
contents = f.read()
Making utf-8 a habit saves debugging time later.
A Note on File Paths
Use forward slashes or raw strings for file paths on Windows to avoid issues with backslash escaping:
# These all work on Windows:
open("C:/configs/core-sw-01.txt")
open(r"C:\configs\core-sw-01.txt")
# This does NOT work — \c is not a valid escape sequence:
# open("C:\configs\core-sw-01.txt")
In practice, use relative paths (just the filename or configs/filename.txt) unless you have a reason to be explicit about location.
Lists
A list is an ordered, mutable collection. Use square brackets:
devices = ["core-sw-01", "core-sw-02", "edge-rtr-01"]
vlans = [10, 20, 30, 100]
Mutable means you can change a list after creating it — add items, remove items, change items. This is different from tuples (covered next).
Indexing and Slicing
Lists use the same indexing and slicing syntax as strings:
>>> devices = ["core-sw-01", "core-sw-02", "edge-rtr-01"]
>>> devices[0]
'core-sw-01'
>>> devices[-1]
'edge-rtr-01'
>>> devices[0:2]
['core-sw-01', 'core-sw-02']
Adding and Removing Items
devices = ["core-sw-01", "core-sw-02"]
devices.append("edge-rtr-01") # add one item to the end
# ["core-sw-01", "core-sw-02", "edge-rtr-01"]
devices.extend(["spine-01", "spine-02"]) # add multiple items
# ["core-sw-01", "core-sw-02", "edge-rtr-01", "spine-01", "spine-02"]
devices.remove("core-sw-02") # remove by value (first match)
# ["core-sw-01", "edge-rtr-01", "spine-01", "spine-02"]
last = devices.pop() # remove and return the last item
# last = "spine-02"
devices.insert(0, "mgmt-sw-01") # insert at a specific position
Checking Membership
>>> "core-sw-01" in devices
True
>>> "missing-device" in devices
False
Sorting
vlans = [100, 10, 50, 20]
vlans.sort() # sorts in place
# [10, 20, 50, 100]
devices = ["edge-rtr-01", "core-sw-01", "spine-01"]
devices.sort() # alphabetical
# ["core-sw-01", "edge-rtr-01", "spine-01"]
sorted() returns a new sorted list without modifying the original:
original = [100, 10, 50, 20]
sorted_vlans = sorted(original)
# original unchanged, sorted_vlans = [10, 20, 50, 100]
Useful List Operations
>>> len(devices) # number of items
4
>>> devices.count("core-sw-01") # how many times a value appears
1
>>> devices.index("spine-01") # position of a value
2
Tuples
A tuple is an ordered, immutable collection. Use parentheses — or just commas:
interface = ("GigabitEthernet0/0/1", "192.168.1.1", "up")
device = "core-sw-01", "192.168.1.1" # parentheses optional
Once created, a tuple cannot be changed. You cannot append, remove, or reassign items. That sounds like a limitation, but it’s useful: when you return structured data from a function and want to guarantee it won’t be accidentally modified, use a tuple.
Unpacking
Tuples (and lists) support unpacking — assigning each element to its own variable in one line:
interface = ("GigabitEthernet0/0/1", "192.168.1.1", "up")
name, ip, status = interface
print(name) # GigabitEthernet0/0/1
print(ip) # 192.168.1.1
print(status) # up
You’ll see this pattern constantly when iterating over a list of pairs — for example, a list of (hostname, ip) tuples from Part 1’s inventory parser.
The underscore _ is conventional for values you want to unpack but don’t need:
name, _, status = interface # we don't need the IP right now
When to Use a Tuple vs a List
Use a list when the collection might change (adding devices, removing routes). Use a tuple when the collection represents a fixed record (a device’s hostname + IP + role, a route’s prefix + next-hop, a connection’s source + destination).
In practice, you’ll use lists far more often. Tuples appear most in function return values and when a pair of values naturally belongs together.
The Walrus Operator
Python 3.8 introduced the walrus operator (:=), which assigns a value and evaluates it in the same expression. It’s useful when reading files in chunks or processing lines where you need to both check and use a value:
# Without walrus — read and check are two operations
with open("devices.txt") as f:
line = f.readline()
while line:
print(line.strip())
line = f.readline()
# With walrus — cleaner
with open("devices.txt") as f:
while line := f.readline():
print(line.strip())
You don’t need to reach for it often, but you’ll see it in modern Python code and it’s worth recognising.
Putting It Together: Interface Categoriser
Here’s a script that uses everything from this post. It reads a file of interface names, categorises them by type, and prints a structured summary.
Create interfaces.txt:
GigabitEthernet0/0/1
GigabitEthernet0/0/2
FastEthernet0/1
Loopback0
Loopback1
Vlan10
Vlan20
Vlan100
TunnelTE1
management0
GigabitEthernet0/0/3
The last two entries are deliberate: management0 is lowercase, and the final entry has extra whitespace. Real device output is messy.
Create categorise.py:
INTERFACE_FILE = "interfaces.txt"
# Map of prefix → category
CATEGORIES = {
"gigabitethernet": "GigabitEthernet",
"fastethernet": "FastEthernet",
"tengigabitethernet": "TenGigabitEthernet",
"fortyge": "FortyGigabitEthernet",
"hundredge": "HundredGigabitEthernet",
"loopback": "Loopback",
"vlan": "VLAN (SVI)",
"tunnel": "Tunnel",
"port-channel": "Port-Channel",
"management": "Management",
}
def categorise_interface(name):
"""Return the category string for an interface name, or 'Unknown'."""
name_lower = name.lower()
for prefix, category in CATEGORIES.items():
if name_lower.startswith(prefix):
return category
return "Unknown"
def load_interfaces(filepath):
"""Read interface names from a file, one per line."""
interfaces = []
with open(filepath, encoding="utf-8") as f:
for line in f:
name = line.strip()
if name: # skip blank lines
interfaces.append(name)
return interfaces
def main():
interfaces = load_interfaces(INTERFACE_FILE)
# Build a dict of category → list of interface names
grouped = {}
for name in interfaces:
category = categorise_interface(name)
if category not in grouped:
grouped[category] = []
grouped[category].append(name)
# Print the summary
print(f"\nFound {len(interfaces)} interface(s) across {len(grouped)} type(s):\n")
for category, members in sorted(grouped.items()):
print(f" {category} ({len(members)})")
for name in members:
print(f" {name}")
print()
if __name__ == "__main__":
main()
Run it:
python3 categorise.py
Expected output:
Found 11 interface(s) across 6 type(s):
FastEthernet (1)
FastEthernet0/1
GigabitEthernet (4)
GigabitEthernet0/0/1
GigabitEthernet0/0/2
GigabitEthernet0/0/3
Loopback (2)
Loopback0
Loopback1
Management (1)
management0
Tunnel (1)
TunnelTE1
VLAN (SVI) (3)
Vlan10
Vlan20
Vlan100
Notice that GigabitEthernet0/0/3 (with the leading whitespace) and management0 (lowercase) are both handled correctly — .strip() on each line and .lower() before prefix matching take care of both.
This script introduces a dict — we’ve used one here but haven’t covered them properly yet. That’s the focus of Part 3, where dictionaries become the primary data structure for representing device state.
What’s Coming in Part 3
Part 3 covers dictionaries, sets, list and dictionary comprehensions, and exception handling. These four topics unlock the ability to work with structured device data — the kind you get back from API calls, parsed CLI output, and YAML config files.
We’ll also revisit the interface categoriser and replace the grouped dictionary we built manually here with cleaner patterns using defaultdict and comprehensions.
No Containerlab topology is needed for this post — all examples run without network devices.