Functions: Refactoring Your Parsers

Five lessons of parsing has left you with working code that lives and dies inside one script. Functions give that logic a name, a contract, and a future — this is the lesson where scripts become tools you keep.

In this lesson you will:
  • Define functions with def, parameters, and return values
  • Know why return beats print — and spot the None it leaves behind
  • Use default parameter values for sane, overridable behavior
  • Return tuples and None to signal multiple results or no result
  • Compose small functions into real workflows — and document them

Naming logic

You’ve typed 2 ** (32 - prefix) - 2 at least a dozen times since Lesson 2. A function lets you type it once, name it, and call the name forever:

def usable_hosts(prefix_len):
    return 2 ** (32 - prefix_len) - 2
  • def opens the definition; the indented block is the body
  • prefix_len is a parameter — a placeholder filled in at call time
  • return hands the result back to whoever called
>>> usable_hosts(24)
254
>>> usable_hosts(26) + usable_hosts(27)     # results compose
92

That second line is the point. Returned values feed math, comparisons, f-strings, other functions — your logic becomes a building block.

return, not print

The single most common beginner detour: writing functions that print their answer instead of returning it.

def show_hosts(prefix_len):          # looks fine in the REPL...
    print(2 ** (32 - prefix_len) - 2)

>>> x = show_hosts(24)
254                                   # printed on the way through
>>> print(x)
None                                  # ...but nothing came BACK

Parameters that earn their keep

Defaults let a function assume the normal case while staying overridable:

def connect(host, port=22):
    return f"{host}:{port}"

>>> connect("den-acc-sw01")              # default applies
'den-acc-sw01:22'
>>> connect("den-acc-sw01", port=830)    # NETCONF day? override it
'den-acc-sw01:830'

Naming arguments at the call site (port=830) costs nothing and makes the line self-documenting — six months from now, connect("sw1", 830) makes you check the definition; connect("sw1", port=830) doesn’t.

Multiple results, and no result

Returning a tuple gives a function two answers; unpacking catches them:

import re

def parse_interface(line):
    m = re.search(r"^(\S+)\s+(\d+\.\d+\.\d+\.\d+)", line)
    if m:
        return m.group(1), m.group(2)    # a tuple
    return None                          # the "not found" contract

name, ip = parse_interface("Gi1/0/1  10.20.30.1  YES manual up  up")

Returning None for “nothing found” is the same contract you’ve consumed from .get() and re.search() since Lesson 4 — now you’re the one honoring it, and callers guard with if result: exactly as you’ve learned.

Composition: the audit, rebuilt

Watch Lesson 3’s audit become three named, testable pieces:

def get_interface_names(config_lines):
    """Return the interface names in a config, in order."""
    names = []
    for line in config_lines:
        if line.startswith("interface "):
            names.append(line.split()[1])
    return names

def missing_descriptions(config_lines):
    """Return interface names whose next line isn't a description."""
    findings = []
    for i, line in enumerate(config_lines):
        if line.startswith("interface "):
            has_next = i + 1 < len(config_lines)
            if not has_next or not config_lines[i + 1].startswith(" description"):
                findings.append(line.split()[1])
    return findings

def format_report(hostname, findings):
    """Render findings as a human-readable block."""
    if not findings:
        return f"{hostname}: clean"
    lines = [f"{hostname}: {len(findings)} findings"]
    for name in findings:
        lines.append(f"  - {name} missing description")
    return "\n".join(lines)
⬡ Composition — each function's return feeds the next
Rendering diagram…
View diagram source — it's just text (Mermaid). Diagrams-as-code is how modern network docs work; the flagship course has a free module on it.
flowchart LR
  A["config_lines"] --> B["missing_descriptions()<br/>lines → names"]
  B -- "findings list" --> C["format_report()<br/>findings → text"]
  C -- "report string" --> D["print / write to file<br/>(the program's edge)"]

Each piece has one job, a docstring stating its contract, and a return value the next piece consumes. The triple-quoted docstring is the same text help() shows and the same format every lab handed you — writing one is how your future self learns to trust your past self.

🖥 From scripts to tools
▶ Try it yourself (Python runs in your browser)
Output appears here. First run downloads the Python runtime (~10 MB), so give it a few seconds.

Exercises (graded)

cd labs/python-foundations/lesson06
pytest -q

Five functions in exercises.py:

  1. subnet_summary(prefix_len) — a formatted one-liner from the host math
  2. normalize_hostname(raw, domain="corp.example.com") — cleanup with a default
  3. parse_interface_line(line) — your Lesson 5 regex, now returning a tuple or None
  4. audit_inventory(inventory, required_key) — hostnames missing a required fact
  5. format_findings(findings) — render (hostname, problem) tuples into a report
✅ Check your understanding

def report(): print("4 findings") — then x = report(). What is x?

1 / 3

Summary

Functions name your logic and define its contract: parameters in, return out — and return, not print, because returned values compose while printed ones evaporate (leaving None behind). Defaults make the common case free and the unusual case explicit, tuples carry multiple results, None signals “not found” under the same guard discipline you already practice, and docstrings write the contract down. Composition — small functions feeding each other — is the actual architecture of production automation, and it’s why every grader in this course tests functions. Next lesson: classes, where the device dict and the functions that operate on it finally move in together.