Data Structures and Logic (Python)¶
Version: 0.2 Year: 2026
Copyright Notice¶
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.
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¶
- Python Tutorial: Data Structures - official guide to lists, dicts, sets, tuples, and their methods
- Real Python: List Comprehensions - when to use them and when not to
- Python Collections Module - Counter, defaultdict, OrderedDict, and other specialized containers
- Real Python: Dictionaries - comprehensive guide to dictionary patterns and best practices
Previous: Introduction to Python | Next: Working with Files and APIs | Back to Index