BasicsLesson 4 of 16

Functions and Code Organization

Write reusable functions with parameters and return values, organize code into modules, and follow Python conventions—essential for maintainable, shareable automation scripts.

🧒 Simple Explanation (ELI5)

A function is like a recipe you can reuse. Instead of repeating the steps every time you want to make coffee, you write down "Make Coffee Recipe" once, and call it whenever you need coffee. Functions take ingredients (parameters), follow the recipe, and produce a result (return value). This saves time and reduces mistakes.

🔧 Why Do We Need Functions?

⚙️ Technical Explanation

A function is a reusable block of code that accepts inputs (parameters), performs operations, and returns an output. Parameters are named inputs; arguments are actual values passed when calling the function. Return value is what the function outputs. Functions can have default parameters and return multiple values (as a tuple).

💡
Docstrings for Documentation

Every function should start with a docstring (triple-quoted string) explaining what it does, its parameters, and return value. Tools like Sphinx use docstrings to auto-generate documentation. DevOps teams share functions as modules—good docstrings are documentation for teammates.

⌨️ Functions and Code Organization

python
# ===== SIMPLE FUNCTION =====
def greet(name):
    """Greet  a person by name."""
    return f"Hello, {name}!"

message = greet("Alice")
print(message)

# ===== FUNCTION WITH DEFAULT PARAMETERS =====
def check_disk_usage(path="/", threshold=90):
    """
    Check disk usage on a path.
    
    Args:
        path (str): Target filesystem path (default: root)
        threshold (int): Alert if usage exceeds this % (default: 90)
    
    Returns:
        bool: True if usage is above threshold, False otherwise
    """
    # Pretend we query the filesystem
    usage_pct = 85
    if usage_pct > threshold:
        print(f"ALERT: {path} is {usage_pct}% full!")
        return True
    else:
        print(f"OK: {path} is {usage_pct}% full")
        return False

check_disk_usage()                          # uses defaults
check_disk_usage("/data", threshold=75)     # override params

# ===== FUNCTION RETURNING MULTIPLE VALUES =====
def parse_connection_string(conn_str):
    """Parse connection string into components."""
    parts = conn_str.split("://")
    protocol = parts[0]
    rest = parts[1].split(":")
    host = rest[0]
    port = int(rest[1])
    return protocol, host, port

protocol, host, port = parse_connection_string("http://localhost:8080")
print(f"Protocol: {protocol}, Host: {host}, Port: {port}")

# ===== VARIABLE SCOPE =====
def modify_list(servers):
    """Lists are mutable—modifications affect the original."""
    servers.append("new-server")

original = ["web01", "web02"]
modify_list(original)
print(original)  # includes "new-server"

# ===== ORGANIZING CODE: MODULE STRUCTURE =====
# File: utils.py
def check_service(service_name):
    """Check if a service is running."""
    # Implementation
    return status

def restart_service(service_name):
    """Restart a service."""
    # Implementation
    pass

# File: main.py (uses utils.py)
from utils import check_service, restart_service

if not check_service("nginx"):
    print("Nginx is down, restarting...")
    restart_service("nginx")

# ===== ANONYMOUS FUNCTIONS (lambda) =====
# Sort servers by port number
servers = [("web01", 8080), ("api01", 9000), ("db01", 5432)]
sorted_servers = sorted(servers, key=lambda x: x[1])
print(sorted_servers)  # [("db01", 5432), ("web01", 8080), ("api01", 9000)]

# ===== LIST OPERATIONS WITH FUNCTIONS =====
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))  # [1, 4, 9, 16, 25]
evens = list(filter(lambda x: x % 2 == 0, numbers))  # [2, 4]

# ===== UNPACKING ARGUMENTS =====
def deploy_servers(server1, server2, server3):
    """Deploy to three servers."""
    print(f"Deploying to {server1}, {server2}, {server3}")

servers = ["web01", "web02", "api01"]
deploy_servers(*servers)   # unpacks list into arguments

# ===== CATCHING FUNCTION ERRORS =====
def safe_int(value):
    """Convert to int, return None if fails."""
    try:
        return int(value)
    except ValueError:
        return None

result = safe_int("123")    # 123
result = safe_int("abc")    # None

💼 Example (Real-world Use Case)

A DevOps team writes a library of functions for VM management: `create_vm(name, size, region)`, `start_vm(id)`, `stop_vm(id)`, `get_vm_status(id)`. Each function is tested individually. The team then uses these functions in deployment scripts without duplicating logic. When a bug is found in `get_vm_status`, fixing it in one place fixes all scripts that use it.

🧪 Hands-on

  1. Write a function that takes a server name and port, and returns the URL (e.g., "http://web01:8080").
  2. Write a function that checks if a number is even (return True/False).
  3. Write a function that takes a list of numbers and returns the average.
  4. Create a module file with 2-3 utility functions, then import and call them from another script.
  5. Write a function that returns multiple values (unpacking on the caller side).
🎮
Try It Yourself

Write a function called `check_services()` that takes a list of service names. It loops through each one, checks if it is "running" (fake check—hardcode statuses), and returns a dictionary with service names as keys and statuses as values. Test with ["nginx", "postgres", "redis"].

🐛 Debugging Scenario

Problem: you call a function with an argument, but inside the function the variable is undefined.

🎯 Interview Questions

Beginner

What is the difference between a parameter and an argument?

A parameter is a variable in the function definition. An argument is the actual value passed when calling the function. Example: `def func(name):` — "name" is a parameter. `func("Alice")` — "Alice" is an argument.

Why write functions instead of just writing code inline?

Functions enable reuse (call once, use many times), maintainability (fix once, all calls are fixed), testing (test functions in isolation), and clarity (well-named functions document intent). Teams share functions as libraries—collective code reduces duplicate work.

What is a docstring and why include one?

A docstring is a triple-quoted string at the start of a function explaining what it does, its parameters, and return value. Docstrings serve as documentation for teammates and enable tools like Sphinx to auto-generate docs. Always include them in shared code.

Scenario-based

You have a function that queries an API. How would you organize it so multiple scripts can reuse it?

Put it in a module (e.g., api_utils.py) with a clear name like `query_api(endpoint, method="GET", payload=None)`. Include a docstring with parameter and return value documentation. Write unit tests for the function. Put the module in a shared location (git repo, Python package) so team members can `from api_utils import query_api` in their scripts.

🌐 Real-world Usage

Ansible modules are Python functions called by playbooks. Kubernetes Python client provides functions for cluster operations. boto3 (AWS SDK) is organized as functions and classes. The entire DevOps ecosystem runs on shared, reusable functions—writing good functions is a core skill.

📝 Summary

Functions are reusable blocks of code with inputs (parameters) and outputs (return values). Functions enable code reuse, maintainability, testing, and team collaboration. Organize functions into modules for sharing across scripts. Include docstrings to document function behavior. Default parameters, multiple returns, and keyword arguments make functions flexible. Functions are how teams avoid duplicating code and build collective libraries of automation logic.