Strings, f-strings & Slicing: Parsing IPs, MACs, and Banner Output

About 70 percent of the work in network automation is shoveling text — IPs, MACs, hostnames, banner output, log lines — from one shape into another. Today we cover the small handful of built‑in string operations that handle most of that shoveling. No regex (that’s Day 9), no external libraries — just str methods you’ll use in every script you write from here on.

This is Day 3 of the 21‑post Python for Network Engineers series. You should be able to open a REPL with python and try every snippet below as you read.

Strings are sequences — you can index and slice them

Treat a string like a list of characters with zero‑based indexing:

>>> intf = "GigabitEthernet0/1"
>>> intf[0]
'G'
>>> intf[-1]      # negative indexes count from the right
'1'
>>> intf[-3:]     # last three characters
'0/1'
>>> intf[:7]      # first seven characters
'Gigabit'

The general slice form is s[start:stop:step]. start is inclusive, stop is exclusive (just like range), and any of them can be omitted or negative. s[::-1] reverses a string — handy for the rare time you need to compare a name backwards.

One trap: indexing past the end of a string raises IndexError. Slicing past the end just returns what’s available, no error. So when you don’t know the exact length, prefer slicing.

The string methods you’ll use weekly

Method What it does Network use
.split(sep) Splits into a list on sep Break “10.0.0.1” into octets
.strip() Removes leading/trailing whitespace Clean lines from readlines()
.upper() / .lower() Change case Normalize hostnames before comparison
.replace(a, b) Returns string with all ab Convert MAC formats
.startswith(p) / .endswith(s) Boolean prefix/suffix test Filter “show ip route” lines
.join(iterable) Glue list elements with the string Rebuild dotted notation
in operator Substring membership "GigabitEthernet" in line

Strings in Python are immutable. Every method above returns a new string; the original is never modified. So intf.upper() alone does nothing useful — you have to assign the result: intf = intf.upper().

Splitting an IP into octets

>>> ip = "192.168.10.55"
>>> ip.split(".")
['192', '168', '10', '55']
>>> ip.split(".")[1]
'168'
>>> ".".join(ip.split(".")[:3]) + ".0"
'192.168.10.0'      # quick subnet base for a /24, ugly but works

Notice that split returns strings, not integers — '168', not 168. To do math, wrap with int(). From Day 8 the ipaddress module will handle this properly with subnet awareness, but the manual version is good to know for one‑off awk‑style work.

Normalizing MAC addresses

Cisco shows MACs as aabb.ccdd.eeff. Linux shows them as aa:bb:cc:dd:ee:ff. Some vendors use aa-bb-cc-dd-ee-ff. You’ll convert between these constantly. With pure string ops:

def normalize_mac(mac):
    """Return MAC as colon-separated lowercase: aa:bb:cc:dd:ee:ff"""
    clean = mac.lower().replace(":", "").replace("-", "").replace(".", "")
    if len(clean) != 12:
        raise ValueError(f"Not a 12-hex-digit MAC: {mac!r}")
    return ":".join(clean[i:i+2] for i in range(0, 12, 2))

print(normalize_mac("AABB.CCDD.EEFF"))   # aa:bb:cc:dd:ee:ff
print(normalize_mac("aa-bb-cc-dd-ee-ff")) # aa:bb:cc:dd:ee:ff
print(normalize_mac("AA:BB:CC:DD:EE:FF")) # aa:bb:cc:dd:ee:ff

Three calls to .replace, one length check, one slice loop wrapped in ":".join(). That’s a complete utility you’ll reuse for years.

Cleaning up banner / show output

Output from a router often comes back with trailing whitespace, leading spaces, or stray \r\n from the SSH transport. .strip() is your friend:

raw = "   GigabitEthernet0/1  is up, line protocol is up   \r\n"
print(raw.strip())
# 'GigabitEthernet0/1  is up, line protocol is up'

To filter only interface‑status lines from a multi‑line block, combine .split("\n") with a comprehension and .startswith:

show_int = """GigabitEthernet0/1 is up, line protocol is up
  Hardware is iGbE, address is aabb.cc00.0001
GigabitEthernet0/2 is administratively down, line protocol is down
  Hardware is iGbE, address is aabb.cc00.0002"""

status_lines = [line for line in show_int.split("\n") if line.startswith("Gigabit")]
for line in status_lines:
    print(line)

That’s a full one‑liner for “give me only the headlines from show interfaces.” We’ll formalize the comprehension syntax tomorrow.

f‑strings: more than just {var}

You met f‑strings yesterday. They have a small format mini‑language after a colon that’s worth memorizing — it removes the need for str.format() calls and ad‑hoc padding loops:

name = "Gi0/1"
in_pkts = 1234567
util = 0.7634

print(f"{name:<25} {in_pkts:>12,} {util:.2%}")
# Gi0/1                          1,234,567 76.34%
  • :<25 — left‑align in a 25‑char field (great for column reports)
  • :>12, — right‑align in a 12‑char field, with thousands separators
  • :.2% — render as a percentage with two decimals
  • :08x — render an int as 8‑digit lowercase hex, zero‑padded — useful for MAC vendor OUIs

Spend ten minutes in the REPL trying these. They turn ad‑hoc print calls into reports that fit on a screen.

One bigger example: extract management IPs from a CDP table

cdp = """Device ID    Local Intf   Hold     Capability  Platform     Port ID
core-sw01    Gig0/0       142      R S I       WS-C2960     Gig1/0/24
edge-rtr02   Gig0/1       170      R           ISR4321      Gig0/0/0
ap-floor3    Gig0/2       162      T           AIR-CAP3702  Gig0
"""

for line in cdp.split("\n"):
    line = line.strip()
    if not line or line.startswith("Device"):
        continue                               # skip header and blanks
    parts = line.split()
    device_id, local_intf = parts[0], parts[1]
    print(f"{device_id:<15} reachable on {local_intf}")

Six lines of pure built‑in string code, no imports. Read it once and notice every operation we covered today is in there: .split, .strip, startswith, slicing via positional unpacking, an f‑string with alignment.

Exercises

  1. Given intf = "TenGigabitEthernet1/0/24", write one expression that returns just the slot number "1" as a string.
  2. Given " csr1000v# ", return the bare hostname "csr1000v" with no whitespace and no trailing #.
  3. Convert "aabb.ccdd.eeff" to "AA-BB-CC-DD-EE-FF" (uppercase, hyphen‑separated). Use the normalize_mac idea but adapt the formatting.
  4. You have a list of interface names: ["Gi0/1", "Gi0/2", "Te1/0/1", "Lo0"]. Print only the ones that start with "Gi", one per line.
  5. Stretch: given the multi‑line show ip int brief‑style block below, print only the interfaces whose protocol is up, formatted as <name> -> <ip> in left‑aligned 25‑char columns.
    Interface              IP-Address      OK? Method Status                Protocol
    GigabitEthernet0/0     10.0.0.1        YES NVRAM  up                    up
    GigabitEthernet0/1     unassigned      YES unset  administratively down down
    GigabitEthernet0/2     10.0.1.1        YES NVRAM  up                    up
    Loopback0              192.168.0.1     YES NVRAM  up                    up
    

Answers

Show answer 1
>>> intf = "TenGigabitEthernet1/0/24"
>>> intf.split("Ethernet")[1].split("/")[0]
'1'

Split on the literal word "Ethernet" to get the trailing "1/0/24", then split on "/" and take the first piece. Many ways to skin this — intf[len("TenGigabitEthernet"):].split("/")[0] works too.

Show answer 2
>>> "   csr1000v#   ".strip().rstrip("#")
'csr1000v'

.strip() removes all leading/trailing whitespace; .rstrip("#") then removes any trailing # characters. Order matters — strip whitespace first so the # is at the actual end.

Show answer 3
def to_dash_upper(mac):
    clean = mac.lower().replace(":", "").replace("-", "").replace(".", "")
    return "-".join(clean[i:i+2] for i in range(0, 12, 2)).upper()

print(to_dash_upper("aabb.ccdd.eeff"))  # AA-BB-CC-DD-EE-FF

Same pattern as normalize_mac — strip all separators, slice into pairs, join with the new separator, then uppercase the whole result.

Show answer 4
intfs = ["Gi0/1", "Gi0/2", "Te1/0/1", "Lo0"]
for name in intfs:
    if name.startswith("Gi"):
        print(name)

Or as a one‑liner with a comprehension: print("\n".join(n for n in intfs if n.startswith("Gi"))) — but the explicit loop is fine and reads better day one.

Show answer 5
block = """Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet0/0     10.0.0.1        YES NVRAM  up                    up
GigabitEthernet0/1     unassigned      YES unset  administratively down down
GigabitEthernet0/2     10.0.1.1        YES NVRAM  up                    up
Loopback0              192.168.0.1     YES NVRAM  up                    up
"""
for line in block.split("\n"):
    if not line or line.startswith("Interface"):
        continue
    parts = line.split()
    name, ip, protocol = parts[0], parts[1], parts[-1]
    if protocol == "up":
        print(f"{name:<25} -> {ip:<25}")

Skip blanks and the header, split each line on whitespace, take name (first), ip (second), and protocol (last). Filter on protocol == "up" and format with f‑string alignment. This is the shape of nearly every show‑output parser you’ll write before we get to TextFSM in Week 3.

Coming tomorrow

Day 4: Lists, Tuples, Dicts, Sets — Modeling Devices and Inventories. We’ll build the data structures every real automation script revolves around, including the dictionary form Netmiko expects when you connect to a device. By the end you’ll have an inventory of mock devices in pure Python, ready to loop over.

Leave a Reply