Quick answer: Network devices return plain text, not data. Python’s built-in re module lets you describe the shape of the text you want β an interface name followed by “is up” or “is down”, an IP address, a MAC address β and pull exactly those pieces out of a show command’s output as strings, tuples or dictionaries. Named groups, re.MULTILINE and re.finditer() are the three features that turn a fragile one-liner into a reliable parser.
Once you can connect to a device with Netmiko, parsing becomes the real work: a script that prints show interfaces is only slightly more useful than logging in by hand. This guide teaches the regular expression features that matter for network output, walks through parsing three real commands, compares regex with TextFSM and Genie, and lists the mistakes that make parsers break on the next IOS upgrade.
Why parsing is the hard part of network automation
Most routers and switches were designed for humans reading a terminal. Output is formatted into columns, wrapped at 80 characters and sprinkled with headers, and the exact layout differs between vendors, platforms and even software versions. Until every device speaks NETCONF or RESTCONF with structured YANG data, you will need to extract values from text.
You have three tools for the job:
- String methods (
split(),startswith()) β fine for a single, predictable line. - Regular expressions β flexible pattern matching for anything from one value to a whole table. Built into Python, no installation, works on any output.
- Template parsers (TextFSM, TTP, Cisco Genie) β pre-written parsers for common commands that return dictionaries. Faster to use when a template exists; useless when it does not.
Regex is the skill that makes you independent of templates, and it is what template authors use internally anyway.
The regex features network engineers actually need
| Pattern | Meaning | Network example |
|---|---|---|
\S+ |
One or more non-whitespace characters | Interface name: GigabitEthernet0/1 |
\d+ |
One or more digits | VLAN ID, packet counters |
\d{1,3}(?:\.\d{1,3}){3} |
Four dot-separated 1β3 digit groups | IPv4 address |
[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4} |
Cisco-style MAC | 0050.56be.1a2b |
(up|down|administratively down) |
Alternation | Interface status |
(?P<name>...) |
Named capture group | Returns a dictionary instead of a tuple |
^ and $ with re.MULTILINE |
Start and end of each line | Match one row of a table at a time |
\s+ |
Any amount of whitespace | Tolerates column alignment changes |
Always write patterns as raw strings (r"...") so backslashes are passed to the regex engine untouched, and compile patterns you reuse in a loop with re.compile().
Hands-on: parsing show interfaces
The classic example. Each interface block in Cisco IOS begins with a line like GigabitEthernet0/1 is up, line protocol is up. We will connect with Netmiko, then extract name, admin status and line-protocol status using named groups so the result is a list of dictionaries, not anonymous tuples.
import os
import re
from netmiko import ConnectHandler
router = {
"device_type": "cisco_ios",
"host": "192.168.1.1",
"username": os.environ["NET_USER"],
"password": os.environ["NET_PASS"],
}
INTERFACE_RE = re.compile(
r"^(?P<name>\S+) is (?P<status>up|down|administratively down),"
r"\s+line protocol is (?P<protocol>up|down)",
re.MULTILINE,
)
with ConnectHandler(**router) as conn:
output = conn.send_command("show interfaces")
interfaces = [m.groupdict() for m in INTERFACE_RE.finditer(output)]
for intf in interfaces:
flag = "" if intf["status"] == "up" and intf["protocol"] == "up" else " <-- check"
print(f"{intf['name']:24} {intf['status']:22} {intf['protocol']}{flag}")
Three details make this robust. re.MULTILINE lets ^ anchor to the start of every line, so the pattern cannot accidentally match in the middle of a description. \s+ instead of a single space tolerates extra whitespace. And finditer() with groupdict() gives readable keys, so a colleague reading intf["protocol"] does not have to remember what intf[2] meant.
Hands-on: extracting counters and IP addresses
Values buried inside a block need a two-step approach: split the output into per-interface chunks, then search each chunk. re.split() with a lookahead keeps the interface header attached to its block.
import re
# `output` is the text from `show interfaces` in the previous example
blocks = re.split(r"\n(?=\S+ is (?:up|down|administratively down),)", output)
ERRORS_RE = re.compile(r"(?P<errors>\d+) input errors,\s+(?P<crc>\d+) CRC")
IP_RE = re.compile(r"Internet address is (?P<ip>\d{1,3}(?:\.\d{1,3}){3})/(?P<prefix>\d+)")
report = {}
for block in blocks:
name = block.split(" is ", 1)[0].strip()
errors = ERRORS_RE.search(block)
ip = IP_RE.search(block)
report[name] = {
"ip": f"{ip['ip']}/{ip['prefix']}" if ip else None,
"input_errors": int(errors["errors"]) if errors else 0,
"crc": int(errors["crc"]) if errors else 0,
}
for name, data in report.items():
if data["crc"] > 0:
print(f"{name}: {data['crc']} CRC errors on {data['ip']}")
Converting counters with int() at parse time means the rest of your script can compare numbers instead of strings β "9" > "10" is True in Python string comparison, a bug that has bitten many first parsers.
Hands-on: a MAC address table into a lookup dictionary
Tables are the most common shape of output. Match one row per line and build whatever structure your task needs β here, a MAC-to-port map for a “where is this device plugged in?” tool.
import re
mac_table = """
Vlan Mac Address Type Ports
---- ----------- -------- -----
10 0050.56be.1a2b DYNAMIC Gi1/0/5
10 0050.56be.3c4d DYNAMIC Gi1/0/7
20 001a.2b3c.4d5e STATIC Gi1/0/24
"""
ROW_RE = re.compile(
r"^\s*(?P<vlan>\d+)\s+(?P<mac>[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4})"
r"\s+(?P<type>\w+)\s+(?P<port>\S+)\s*$",
re.MULTILINE | re.IGNORECASE,
)
mac_to_port = {m["mac"].lower(): (int(m["vlan"]), m["port"]) for m in ROW_RE.finditer(mac_table)}
print(mac_to_port.get("0050.56be.3c4d")) # (10, 'Gi1/0/7')
Testing against a saved text sample like this β no live device needed β is the fastest way to develop a pattern. Save real output from your lab into a samples/ folder and write small pytest functions against it.
Regex versus TextFSM and Genie
Netmiko’s send_command(cmd, use_textfsm=True) returns structured data for hundreds of common commands using the ntc-templates library, and Cisco’s Genie parsers cover IOS-XE, NX-OS and IOS-XR in depth. Use them when a template exists β there is no point rewriting a parser that thousands of engineers already test. Reach for regex when:
- No template exists for your command or vendor.
- You only need one value and a full template is overkill.
- You are parsing log messages, syslog lines or configuration files rather than
showoutput. - You want to write your own TextFSM or TTP template β both are built from regular expressions.
Combining the two is common: the broader automation workflow in Why Use Python for Network Engineering? uses TextFSM for inventory and regex for the odd vendor-specific line.
Six parsing mistakes that break on the next upgrade
- Matching exact spaces.
"is up, line protocol is up"with single spaces fails when a platform pads differently. Use\s+. - Greedy wildcards.
.*will happily swallow the rest of the line and the value you wanted with it. Prefer\S+,\d+or the non-greedy.*?. - Forgetting
re.MULTILINE. Without it,^matches only the very start of the whole output, so a table pattern returns a single row. - Not handling “no match”.
re.search()returnsNonewhen nothing matches; calling.group()on it raisesAttributeError. Always checkif match:. - Parsing the
--More--prompt. If paging was not disabled, the output contains pager artefacts. Netmiko disables paging automatically; raw Paramiko scripts must sendterminal length 0first β see Using Paramiko and Netmiko for SSH and Telnet Connections. - Testing only on live devices. Save sample output and write unit tests. A parser that works on one IOS 15 switch may fail on IOS-XE 17; samples from each platform catch that before production does.
Frequently asked questions
Should I use re.match(), re.search() or re.findall()?
match() anchors to the start of the string; search() finds the first occurrence anywhere; findall() returns every occurrence as strings or tuples. For tables, finditer() is best because it yields match objects with groupdict().
How do I test a regex quickly?
Paste sample output into regex101.com with the Python flavour selected, or use the Python REPL. Keep the samples that work as test fixtures.
Can regex parse JSON or XML output?
Do not. If a device supports | json (NX-OS, EOS, Junos) or NETCONF, use json.loads() or an XML library. Regex is for text that has no structured alternative.
Is regex slow for large outputs?
Not in practice. Compiled patterns process a show interfaces dump from a 48-port switch in milliseconds. Avoid patterns with nested quantifiers like (\S+\s*)+, which can backtrack catastrophically.
Key takeaways
- Regex turns human-formatted
showoutput into Python data without any extra libraries. - Named groups,
re.MULTILINEandfinditer()are the three features that make parsers readable and reliable. - Use TextFSM or Genie when a template exists; write regex when it does not or when you need one value.
- Tolerate whitespace, avoid greedy wildcards, check for
None, and test against saved samples.
Want to go deeper into Python string handling, data structures and the analysis skills that turn parsed output into dashboards? Our Python & Data Science course covers Python from fundamentals to real projects, with mentor support and placement assistance. For more tutorials, subscribe to our YouTube channel.



