Skip to content

Data Structures and Logic (Python)

Version: 0.2 Year: 2026


Copyright (c) 2025-2026 Ryan Thomas Robson / Robworks Software LLC. Licensed under CC BY-NC-ND 4.0. You may share this material for non-commercial purposes with attribution, but you may not distribute modified versions.


Modern sysadmin tasks involve more than parsing single lines of text. You need to store collections of data, transform them, and make decisions based on their contents. Python's built-in data structures and clean control flow are designed for exactly these scenarios - from parsing log files to building configuration management tools.

Core Data Structures

Python has four primary collection types, each with unique properties and use cases.

graph TD
    Collections[Python Collections] --> Indexed[Indexed / Ordered]
    Collections --> Unordered[Unordered]
    Indexed --> MutableIndexed[Mutable]
    Indexed --> ImmutableIndexed[Immutable]
    Unordered --> Set[Sets - Unique Items]
    Unordered --> Dict[Dictionaries - Key-Value]
    MutableIndexed --> List[Lists]
    ImmutableIndexed --> Tuple[Tuples]

Lists

A list is an ordered, mutable sequence. It's the most versatile collection and the one you'll reach for most often.

# A list of server names
servers = ["web01", "web02", "db01", "db02"]

# Adding elements
servers.append("cache01")           # Add to end
servers.insert(0, "lb01")           # Insert at position

# Accessing by index (0-indexed)
primary_db = servers[2]             # "web02" (shifted after insert)
last = servers[-1]                  # "cache01" (negative index = from end)

# Slicing [start:stop:step] - stop is exclusive
web_servers = servers[1:3]          # ["web01", "web02"]
every_other = servers[::2]          # Every second element
reversed_list = servers[::-1]       # Reversed copy

# Removing elements
servers.remove("db02")              # Remove by value (first occurrence)
popped = servers.pop()              # Remove and return last element

Iterating Over Lists

# Basic iteration
for server in servers:
    print(f"Checking {server}...")

# With index using enumerate()
for i, server in enumerate(servers):
    print(f"[{i}] {server}")

# Iterate over two lists in parallel
names = ["web01", "web02", "db01"]
ips = ["10.0.0.1", "10.0.0.2", "10.0.1.1"]

for name, ip in zip(names, ips):
    print(f"{name} -> {ip}")

List Comprehensions

A list comprehension creates a new list by applying an expression to each item in an iterable, optionally filtering items. It's a concise alternative to a for loop with append.

# Standard for loop
log_files = []
for f in os.listdir("/var/log"):
    if f.endswith(".log"):
        log_files.append(f)

# Equivalent list comprehension
log_files = [f for f in os.listdir("/var/log") if f.endswith(".log")]

# Transform values: convert strings to uppercase
hostnames = [s.upper() for s in servers]

# Nested comprehension: flatten a list of lists
groups = [["web01", "web02"], ["db01"], ["cache01", "cache02"]]
all_servers = [s for group in groups for s in group]

When to use list comprehensions

If the comprehension fits on one line or is easy to read at a glance, use it. If you need multiple conditions, nested transformations, or side effects (like printing), use a regular for loop. Readability beats cleverness.


Dictionaries

A dictionary maps keys to values. Keys must be unique and immutable (strings, numbers, tuples). Dictionaries are Python's answer to hash maps, associative arrays, and lookup tables.

# Server configuration
config = {
    "hostname": "app01",
    "ip_address": "10.0.0.5",
    "role": "application",
    "uptime_days": 142
}

# Access values
print(config["ip_address"])              # "10.0.0.5"

# Safe access with .get() (returns None instead of raising KeyError)
backup = config.get("backup_enabled")    # None
backup = config.get("backup_enabled", False)  # False (custom default)

# Add or update
config["environment"] = "production"
config["uptime_days"] = 143

# Remove a key
del config["uptime_days"]
removed = config.pop("environment")      # Remove and return value

Iterating Over Dictionaries

# Keys only (default)
for key in config:
    print(key)

# Keys and values together
for key, value in config.items():
    print(f"{key}: {value}")

# Values only
for value in config.values():
    print(value)

Dictionary Comprehensions

# Build a lookup table from two lists
names = ["web01", "web02", "db01"]
ips = ["10.0.0.1", "10.0.0.2", "10.0.1.1"]

host_map = {name: ip for name, ip in zip(names, ips)}
# {"web01": "10.0.0.1", "web02": "10.0.0.2", "db01": "10.0.1.1"}

# Filter while building
prod_hosts = {name: ip for name, ip in host_map.items() if name.startswith("web")}

Useful Patterns

from collections import Counter, defaultdict

# Count occurrences
status_codes = [200, 200, 404, 500, 200, 404, 200]
counts = Counter(status_codes)
# Counter({200: 4, 404: 2, 500: 1})
print(counts.most_common(2))   # [(200, 4), (404, 2)]

# Group items by key
logs = [
    {"level": "ERROR", "msg": "Disk full"},
    {"level": "INFO", "msg": "Service started"},
    {"level": "ERROR", "msg": "Connection timeout"},
]

by_level = defaultdict(list)
for entry in logs:
    by_level[entry["level"]].append(entry["msg"])
# {"ERROR": ["Disk full", "Connection timeout"], "INFO": ["Service started"]}

Tuples

A tuple is like a list but immutable - once created, you cannot add, remove, or change its elements. Use tuples for data that should not be accidentally modified.

# Coordinates, version numbers, database rows
coordinates = (40.7128, -74.0060)
version = (3, 12, 1)

# Tuple unpacking (assign multiple variables in one line)
lat, lon = coordinates
major, minor, patch = version

# Functions often return tuples
import shutil
total, used, free = shutil.disk_usage("/")

# Swap variables without a temp variable
a, b = 1, 2
a, b = b, a   # a=2, b=1

Sets

A set is an unordered collection of unique elements. Sets are optimized for membership testing and mathematical set operations.

# Deduplicate a list of IPs from a log file
all_ips = ["10.0.0.1", "10.0.0.2", "10.0.0.1", "10.0.0.3", "10.0.0.2"]
unique_ips = set(all_ips)   # {"10.0.0.1", "10.0.0.2", "10.0.0.3"}

# Set operations
allowed = {"10.0.0.1", "10.0.0.2", "10.0.0.3"}
active = {"10.0.0.2", "10.0.0.4", "10.0.0.5"}

authorized = allowed & active          # Intersection: {"10.0.0.2"}
all_known = allowed | active           # Union: all 5 IPs
unauthorized = active - allowed        # Difference: {"10.0.0.4", "10.0.0.5"}

# Fast membership testing (O(1) vs O(n) for lists)
if "10.0.0.4" in allowed:
    print("IP is allowed")

Control Flow

Conditionals

Python uses if, elif, and else with the boolean operators and, or, and not.

load_average = 4.5
disk_full = True

if load_average > 5.0 and disk_full:
    print("CRITICAL: High load and low disk space!")
elif load_average > 5.0:
    print("WARNING: High CPU load.")
elif disk_full:
    print("WARNING: Disk is full.")
else:
    print("System health is OK.")

Truthiness

Python evaluates these values as False (everything else is True):

Value Type
False bool
None NoneType
0, 0.0 numeric
"", [], {}, set(), () empty sequences/collections

This means you can write concise checks:

errors = []
if errors:
    print(f"Found {len(errors)} errors")
else:
    print("No errors")

# None handling
result = config.get("timeout")
timeout = result if result is not None else 30
# Or more concisely (but be careful - this also replaces 0):
timeout = config.get("timeout") or 30

Mutable default arguments

Never use a mutable object (list, dict) as a function's default argument. Python creates the default once and reuses it across all calls, so modifications accumulate unexpectedly.

# WRONG - the same list is shared across all calls
def add_server(name, servers=[]):
    servers.append(name)
    return servers

# RIGHT - use None and create a new list each time
def add_server(name, servers=None):
    if servers is None:
        servers = []
    servers.append(name)
    return servers

For Loops

The for loop iterates over any iterable - lists, dicts, strings, files, ranges.

# Loop with range (0 to 4)
for i in range(5):
    print(f"Attempt {i + 1}")

# Loop over a string
for char in "hello":
    print(char)

# Loop over a file line by line
with open("/etc/hosts") as f:
    for line in f:
        if not line.startswith("#"):
            print(line.strip())

While Loops

Use while when you don't know in advance how many iterations you need.

import time

retries = 0
max_retries = 5

while retries < max_retries:
    if check_service("web01"):
        print("Service is up!")
        break
    retries += 1
    print(f"Attempt {retries}/{max_retries} failed, retrying in 5s...")
    time.sleep(5)
else:
    # The else clause runs if the loop completes without break
    print("Service did not recover after all retries.")

Exception Handling Patterns

Beyond basic try/except (covered in the introduction), you'll use these patterns frequently:

# Catch multiple exception types
try:
    data = json.loads(raw_input)
    value = data["key"]
except (json.JSONDecodeError, KeyError) as e:
    print(f"Invalid data: {e}")

# finally runs no matter what (cleanup)
try:
    conn = connect_to_db()
    result = conn.execute(query)
finally:
    conn.close()

# Raise your own exceptions
def deploy(version):
    if not version.startswith("v"):
        raise ValueError(f"Version must start with 'v', got: {version}")


Interactive Quizzes



Further Reading


Previous: Introduction to Python | Next: Working with Files and APIs | Back to Index

Comments