Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

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 arguments
send_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 functions
print(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 == 0

Using 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 tuple
result = min_max([3, 1, 7, 2, 8, 4])
print(result) # (1, 8)
print(result[0]) # 1

Returning 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 docstring
print(compound_interest.__doc__) # accesses it directly

IDEs, 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)) # 10
print(apply(square, 5)) # 25
# Functions in a list
operations = [double, square, abs]
for op in operations:
print(op(-4))
# -8
# 16
# 4

This 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 here
print(calculate()) # 42
# Global variables are accessible but not modifiable without global
count = 0
def increment():
global count # declares intent to modify the global
count += 1
increment()
increment()
print(count) # 2

Avoid 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 task

Each 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 it

Using 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.