Python Functions: Defining, Calling, and Making Reusable Code That Lasts
A function is a named block of code that performs a specific task. Once defined, it can be called as many times as needed, from anywhere in the program. Functions are the primary tool for avoiding repetition, naming complex logic, and dividing a large program into manageable pieces.
Defining a Function
The def keyword starts a function definition. The name follows immediately, then parentheses that may contain parameters, then a colon. The function body is indented.
def greet(): print("Hello, world!")
greet() # Hello, world!greet() # Hello, world!The function is defined once but can be called any number of times. Nothing happens until you call it.
Parameters and Arguments
A parameter is the name in the function definition. An argument is the actual value you pass when calling the function. The terms are often used interchangeably, but the distinction matters when reading error messages.
def send_email(recipient, subject, body): # recipient, subject, body are parameters print(f"To: {recipient}") print(f"Subject: {subject}") print(f"---\n{body}")
# "alice@example.com", "Hello", "Hi Alice!" are argumentssend_email("alice@example.com", "Hello", "Hi Alice!")Functions can have any number of parameters. Python matches them positionally by default: the first argument goes to the first parameter, the second to the second, and so on.
Returning Values
A function communicates results back to the caller with return. Without return, the function returns None.
def celsius_to_fahrenheit(c): return (c * 9 / 5) + 32
temp_f = celsius_to_fahrenheit(100)print(temp_f) # 212.0
# Store, use in expressions, pass to other functionsprint(f"Boiling point: {celsius_to_fahrenheit(100)}°F")The moment Python executes return, the function stops. Any code after return is unreachable.
def classify(n): if n > 0: return "positive" # function exits here for positive n if n < 0: return "negative" # function exits here for negative n return "zero" # function exits here for n == 0Using return early (rather than else chains) often makes functions easier to read, especially when there are several possible exit conditions.
Multiple Return Values
Python functions can return multiple values as a tuple:
def min_max(numbers): return min(numbers), max(numbers)
low, high = min_max([3, 1, 7, 2, 8, 4])print(low, high) # 1 8
# Or capture as a tupleresult = min_max([3, 1, 7, 2, 8, 4])print(result) # (1, 8)print(result[0]) # 1Returning multiple values is really returning a single tuple — Python’s tuple packing handles the rest.
Docstrings: Documenting What a Function Does
A docstring is a string literal placed immediately inside the function, before any other code. It explains what the function does, what parameters it expects, and what it returns.
def compound_interest(principal, rate, years): """ Calculate compound interest.
Args: principal: The initial amount (float or int). rate: Annual interest rate as a decimal (e.g., 0.05 for 5%). years: Number of years (int).
Returns: The final amount after compound interest is applied. """ return principal * (1 + rate) ** years
help(compound_interest) # prints the docstringprint(compound_interest.__doc__) # accesses it directlyIDEs, documentation generators, and help() all read docstrings. Write them for any function that someone else (or you, six months from now) will need to understand.
Functions as First-Class Objects
In Python, functions are objects. You can assign them to variables, pass them as arguments, and return them from other functions.
def apply(func, value): return func(value)
def double(x): return x * 2
def square(x): return x ** 2
print(apply(double, 5)) # 10print(apply(square, 5)) # 25
# Functions in a listoperations = [double, square, abs]for op in operations: print(op(-4))# -8# 16# 4This enables powerful patterns: passing behaviour into a function rather than hard-coding it.
Scope: Where Variables Live
A variable defined inside a function is local to that function. It does not exist outside.
def calculate(): result = 42 # local variable return result
# print(result) # NameError — result doesn't exist out hereprint(calculate()) # 42
# Global variables are accessible but not modifiable without globalcount = 0
def increment(): global count # declares intent to modify the global count += 1
increment()increment()print(count) # 2Avoid global where possible. Passing values as parameters and returning results is cleaner and easier to test.
Practical Example: A Data Processing Pipeline
def load_csv_rows(path): """Read a CSV file and return rows as a list of dicts.""" import csv with open(path) as f: return list(csv.DictReader(f))
def filter_active(rows, status_field="status"): """Keep only rows where the status field is 'active'.""" return [row for row in rows if row.get(status_field) == "active"]
def extract_emails(rows, email_field="email"): """Extract email addresses from rows.""" return [row[email_field] for row in rows if email_field in row]
def run_pipeline(path): rows = load_csv_rows(path) active = filter_active(rows) emails = extract_emails(active) return emails
# Each function does one thing; composing them handles the full taskEach function is small, named, and testable in isolation. The pipeline is readable because each step has a descriptive name.
Common Mistakes
Forgetting return and wondering why the result is None. A function with no return statement returns None. If you assign the result of such a function to a variable, you get None.
Returning inside a loop when you meant to return after it. Placing return inside a for or while loop causes the function to exit on the first iteration.
def find_all_evens(numbers): evens = [] for n in numbers: if n % 2 == 0: evens.append(n) return evens # return AFTER the loop, not inside itUsing a function name that shadows a built-in. Naming your function list, print, or input replaces the built-in with your function in the current scope.