Data Structures

Collections are everywhere in infrastructure automation. Master them for API responses, config parsing, and data transformation.

Lists

Creating Lists

# Empty list
hosts = []

# With values
hosts = ["ise-01", "ise-02", "ise-03"]

# From range
vlans = list(range(10, 20))  # [10, 11, ..., 19]

# From string
chars = list("hello")  # ['h', 'e', 'l', 'l', 'o']

Accessing Elements

hosts = ["ise-01", "ise-02", "ise-03", "ise-04"]

# By index (0-based)
hosts[0]    # "ise-01"
hosts[1]    # "ise-02"
hosts[-1]   # "ise-04" (last)
hosts[-2]   # "ise-03" (second to last)

# Slicing [start:end:step]
hosts[1:3]    # ["ise-02", "ise-03"]
hosts[:2]     # ["ise-01", "ise-02"] (first 2)
hosts[2:]     # ["ise-03", "ise-04"] (from index 2)
hosts[::2]    # ["ise-01", "ise-03"] (every other)
hosts[::-1]   # Reversed

Modifying Lists

hosts = ["ise-01", "ise-02"]

# Append (add to end)
hosts.append("ise-03")  # ["ise-01", "ise-02", "ise-03"]

# Insert at position
hosts.insert(0, "ise-pan")  # ["ise-pan", "ise-01", ...]

# Extend (add multiple)
hosts.extend(["ise-04", "ise-05"])

# Remove by value
hosts.remove("ise-02")

# Remove by index
del hosts[0]
popped = hosts.pop()     # Remove and return last
popped = hosts.pop(0)    # Remove and return first

# Clear all
hosts.clear()

# Sort
hosts = ["ise-03", "ise-01", "ise-02"]
hosts.sort()              # In-place: ["ise-01", "ise-02", "ise-03"]
hosts.sort(reverse=True)  # Descending

# Sorted (returns new list)
sorted_hosts = sorted(hosts)

List Operations

hosts = ["ise-01", "ise-02", "ise-03"]

# Length
len(hosts)  # 3

# Check membership
"ise-01" in hosts      # True
"ise-99" not in hosts  # True

# Count occurrences
hosts.count("ise-01")  # 1

# Find index
hosts.index("ise-02")  # 1

# Copy (shallow)
hosts_copy = hosts.copy()
hosts_copy = hosts[:]
hosts_copy = list(hosts)

# Concatenate
all_hosts = hosts + ["wlc-01", "wlc-02"]

Dictionaries

Creating Dicts

# Empty dict
config = {}

# With values
config = {
    "hostname": "ise-01",
    "ip": "10.50.1.20",
    "port": 443
}

# From tuples
config = dict([("hostname", "ise-01"), ("ip", "10.50.1.20")])

# From keys with default value
hosts = dict.fromkeys(["ise-01", "ise-02"], "active")
# {"ise-01": "active", "ise-02": "active"}

Accessing Values

config = {"hostname": "ise-01", "ip": "10.50.1.20", "port": 443}

# By key
config["hostname"]  # "ise-01"

# With default (no error if missing)
config.get("hostname")         # "ise-01"
config.get("timeout")          # None
config.get("timeout", 30)      # 30 (default)

# Keys, values, items
config.keys()    # dict_keys(['hostname', 'ip', 'port'])
config.values()  # dict_values(['ise-01', '10.50.1.20', 443])
config.items()   # dict_items([('hostname', 'ise-01'), ...])

Modifying Dicts

config = {"hostname": "ise-01"}

# Add/update single
config["ip"] = "10.50.1.20"

# Update multiple
config.update({"port": 443, "timeout": 30})

# Set default (only if key doesn't exist)
config.setdefault("protocol", "https")  # Adds it
config.setdefault("hostname", "new")    # Does nothing

# Remove
del config["timeout"]
port = config.pop("port")           # Remove and return
port = config.pop("port", None)     # With default

# Remove last inserted (Python 3.7+)
key, value = config.popitem()

Nested Dicts

# Common in API responses
deployment = {
    "nodes": {
        "ise-01": {
            "ip": "10.50.1.20",
            "roles": ["PAN", "MNT"],
            "status": "running"
        },
        "ise-02": {
            "ip": "10.50.1.21",
            "roles": ["PSN"],
            "status": "running"
        }
    },
    "version": "3.2"
}

# Access nested
deployment["nodes"]["ise-01"]["ip"]  # "10.50.1.20"

# Safe nested access
def get_nested(d, *keys, default=None):
    for key in keys:
        if isinstance(d, dict):
            d = d.get(key, default)
        else:
            return default
    return d

get_nested(deployment, "nodes", "ise-01", "ip")  # "10.50.1.20"
get_nested(deployment, "nodes", "ise-99", "ip")  # None

Sets

Creating Sets

# Empty set (NOT {} - that's a dict)
vlans = set()

# With values
vlans = {10, 20, 30}

# From list (deduplicates)
vlans = set([10, 20, 20, 30])  # {10, 20, 30}

Set Operations

allowed = {10, 20, 30}
active = {20, 30, 40}

# Union (all from both)
allowed | active           # {10, 20, 30, 40}
allowed.union(active)

# Intersection (in both)
allowed & active           # {20, 30}
allowed.intersection(active)

# Difference (in first, not in second)
allowed - active           # {10}
allowed.difference(active)

# Symmetric difference (in one or other, not both)
allowed ^ active           # {10, 40}

# Membership
20 in allowed    # True
50 in allowed    # False

# Subset/superset
{10, 20} <= allowed    # True (subset)
{10, 20} < allowed     # True (proper subset)
allowed >= {10, 20}    # True (superset)

Practical: Find Rogue VLANs

# VLANs configured on switch
configured_vlans = {10, 20, 30, 40, 50}

# VLANs allowed by policy
allowed_vlans = {10, 20, 30}

# Find unauthorized VLANs
rogue_vlans = configured_vlans - allowed_vlans
print(f"Rogue VLANs: {rogue_vlans}")  # {40, 50}

# Find missing VLANs
missing_vlans = allowed_vlans - configured_vlans
print(f"Missing VLANs: {missing_vlans}")  # set()

Tuples

Immutable Sequences

# Create tuple
point = (10, 20)
host_info = ("ise-01", "10.50.1.20", 443)

# Single element (needs comma)
single = (42,)

# Access (like lists)
host_info[0]   # "ise-01"
host_info[-1]  # 443

# Unpack
hostname, ip, port = host_info

# Partial unpack
hostname, *rest = host_info  # hostname="ise-01", rest=["10.50.1.20", 443]
first, *middle, last = [1, 2, 3, 4, 5]  # first=1, middle=[2,3,4], last=5

# Named tuples (better)
from collections import namedtuple

Host = namedtuple("Host", ["hostname", "ip", "port"])
ise = Host("ise-01", "10.50.1.20", 443)

ise.hostname  # "ise-01"
ise.ip        # "10.50.1.20"
ise[0]        # "ise-01" (still indexable)

Comprehensions

List Comprehensions

# Basic: [expression for item in iterable]
hosts = ["ise-01", "ise-02", "ise-03"]
upper_hosts = [h.upper() for h in hosts]

# With condition: [expression for item in iterable if condition]
active_hosts = [h for h in hosts if is_active(h)]

# Multiple conditions
valid = [h for h in hosts if h.startswith("ise") and is_active(h)]

# Nested loops
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [num for row in matrix for num in row]  # [1, 2, 3, 4, 5, 6]

# With if/else (note: different position)
status = ["active" if is_active(h) else "inactive" for h in hosts]

Dict Comprehensions

# Basic: {key_expr: value_expr for item in iterable}
hosts = ["ise-01", "ise-02", "ise-03"]
host_status = {h: "active" for h in hosts}

# From two lists
hostnames = ["ise-01", "ise-02"]
ips = ["10.50.1.20", "10.50.1.21"]
host_map = {h: ip for h, ip in zip(hostnames, ips)}

# Filter dict
config = {"hostname": "ise-01", "ip": "10.50.1.20", "secret": "password"}
safe_config = {k: v for k, v in config.items() if k != "secret"}

# Transform values
config = {"port": "443", "timeout": "30"}
int_config = {k: int(v) for k, v in config.items()}

# Swap keys and values
inverted = {v: k for k, v in config.items()}

Set Comprehensions

# Extract unique domains from hostnames
hosts = ["ise-01.inside.domusdigitalis.dev", "wlc-01.inside.domusdigitalis.dev"]
domains = {h.split(".", 1)[1] for h in hosts}  # {"inside.domusdigitalis.dev"}

# Filter to unique VLANs
ports = [{"vlan": 10}, {"vlan": 20}, {"vlan": 10}]
unique_vlans = {p["vlan"] for p in ports}  # {10, 20}

Generator Expressions

# Like list comprehension but lazy (memory efficient)
# Use () instead of []

# Sum of large range (doesn't create list in memory)
total = sum(i for i in range(1_000_000))

# Any/all with condition
hosts = ["ise-01", "ise-02", "wlc-01"]
any_ise = any(h.startswith("ise") for h in hosts)  # True
all_ise = all(h.startswith("ise") for h in hosts)  # False

# First match
first_ise = next((h for h in hosts if h.startswith("ise")), None)

Practical Patterns

Parse API Response

# ISE deployment response
response = {
    "response": [
        {"hostname": "ise-01", "status": "running", "roles": ["PAN"]},
        {"hostname": "ise-02", "status": "running", "roles": ["PSN"]},
        {"hostname": "ise-03", "status": "stopped", "roles": ["PSN"]}
    ]
}

# Extract running PSNs
running_psns = [
    node["hostname"]
    for node in response["response"]
    if node["status"] == "running" and "PSN" in node["roles"]
]
# ["ise-02"]

# Create status map
status_map = {
    node["hostname"]: node["status"]
    for node in response["response"]
}
# {"ise-01": "running", "ise-02": "running", "ise-03": "stopped"}

Group By

from collections import defaultdict

endpoints = [
    {"mac": "00:11:22:33:44:55", "vlan": 10},
    {"mac": "00:11:22:33:44:56", "vlan": 20},
    {"mac": "00:11:22:33:44:57", "vlan": 10},
]

# Group endpoints by VLAN
by_vlan = defaultdict(list)
for ep in endpoints:
    by_vlan[ep["vlan"]].append(ep["mac"])

# {10: ["00:11:22:33:44:55", "00:11:22:33:44:57"], 20: ["00:11:22:33:44:56"]}

Next Module

Functions - Definitions, arguments, closures, and decorators.