Unit 11 — Standard Library Part II — Logging, Formatting, and Counting
logging — Replace print() in Real Scripts
print() is fine for learning. Production scripts use logging — it adds timestamps, severity levels, and lets you control what gets written to the screen versus a file without changing the code.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logging.debug("Connecting to sw-core-01") # hidden at INFO level
logging.info("Processing 5 devices")
logging.warning("sw-access-02 is unreachable")
logging.error("Failed to write report.txt")
Output:
2025-04-15 09:30:00 INFO Processing 5 devices
2025-04-15 09:30:00 WARNING sw-access-02 is unreachable
2025-04-15 09:30:00 ERROR Failed to write report.txt
Level order (lowest to highest): DEBUG → INFO → WARNING → ERROR → CRITICAL. Setting level=logging.INFO suppresses DEBUG and shows everything above it.
Write to a file instead of the screen:
logging.basicConfig(
filename="automation.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
pprint — Readable Output for Nested Data
When you print a large dictionary or list of dicts, print() runs it together. pprint formats it with indentation so it is readable:
from pprint import pprint
inventory = [
{"name": "sw-core-01", "ip": "10.1.1.1", "site": "HQ", "vlan": 10},
{"name": "rtr-edge-01", "ip": "10.1.1.254", "site": "HQ", "vlan": 1},
]
pprint(inventory)
Output:
[{'ip': '10.1.1.1', 'name': 'sw-core-01', 'site': 'HQ', 'vlan': 10},
{'ip': '10.1.1.254', 'name': 'rtr-edge-01', 'site': 'HQ', 'vlan': 1}]
pprint(data, width=40) forces narrower wrapping. pprint(data, depth=1) hides nested levels.
string.Template — Safe Variable Substitution
string.Template fills placeholders using $variable syntax. Unlike f-strings, it works with data you do not know at write time — useful for config templates:
from string import Template
cfg_template = Template("""
interface $interface
description $description
switchport access vlan $vlan
""")
output = cfg_template.substitute(
interface="GigabitEthernet0/1",
description="Uplink to core",
vlan=10,
)
print(output)
substitute() raises KeyError if a placeholder is missing. safe_substitute() leaves missing placeholders as-is instead of crashing.
collections.Counter — Count Device Types in One Line
Counter takes any iterable and counts how many times each value appears:
from collections import Counter
device_types = ["switch", "router", "switch", "firewall", "switch", "router"]
counts = Counter(device_types)
print(counts)
# Counter({'switch': 3, 'router': 2, 'firewall': 1})
print(counts.most_common(2))
# [('switch', 3), ('router', 2)]
Use it on a list of values extracted from your inventory:
types = [d["type"] for d in inventory]
counts = Counter(types)
Summary
loggingreplacesprint()in production scripts — adds timestamps, levels, and log file support- Level order: DEBUG < INFO < WARNING < ERROR < CRITICAL
pprintformats nested data structures for readable outputstring.Templatefills$placeholdervalues —safe_substitute()is forgiving on missing keysCountercounts occurrences in an iterable —.most_common(n)returns the top n