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:

FalsyTruthy
FalseTrue
0Any non-zero number
"" (empty string)Any non-empty string
[] (empty list)Any non-empty list
NoneMost 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.