Every program past a certain size needs a way to package up repeated logic into something reusable. That's what python functions are for — this article covers how to define one with the def keyword, how to call it, and how the return statement sends a result back to whoever called it.
Introduction: Why Functions Matter
A function is a reusable, named block of code that performs a specific task. Instead of writing the same logic over and over throughout your program, you write it once, give it a name, and invoke that name whenever you need it.
Core benefit
Functions solve two related problems at once: they eliminate repeated code (write the logic once, use it everywhere), and they organize a program into logical, testable pieces — instead of one long, undifferentiated block of instructions, your program becomes a collection of clearly named, individually understandable steps.
A real-world framing
Think of a function like a custom setting on an appliance — a "delicate cycle" on a washing machine, say. You configure exactly what that setting does once, give it a name, and from then on you just select it whenever you need that specific behavior, without re-specifying every detail each time.
Defining a Function
Basic syntax
def function_name():
# indented body — the code that runs when the function is calledFour pieces make up a function definition: the def keyword, the function's name, parentheses (which may or may not contain parameters, covered next), and a colon — followed by an indented block, exactly the same indentation rules covered in the earlier syntax article.
Naming rules
Function names follow the same rules and conventions covered in the earlier variables article: they must start with a letter or underscore, contain only letters/numbers/underscores, and by PEP 8 convention, use snake_case — descriptive, lowercase, words separated by underscores.
def calculate_total(): # good — descriptive, snake_case
pass
def calc(): # works, but too vague to be useful later
passA simple example with no parameters
def greet():
print("Hello there!")Important: defining doesn't execute
This is genuinely worth pausing on, because it trips up nearly every beginner at least once: writing a function definition doesn't run any of the code inside it. The body only executes when the function is actually called.
def greet():
print("Hello there!")
# Nothing has printed yet — the function was only defined, not calledIf you run a script containing just that definition and nothing else, it produces no output at all. The function exists, ready to be used, but nothing inside it has happened yet.
Calling a Function and Passing Arguments
Basic call syntax
To actually run a function's code, you call it by name, followed by parentheses:
def greet():
print("Hello there!")
greet() # Hello there!Parameters vs. arguments
This distinction is a genuinely common point of beginner confusion, and it's worth being precise about: parameters are the placeholder names listed in a function's definition. Arguments are the actual values you pass in when calling the function.
def greet(name): # 'name' is a parameter
print(f"Hello, {name}!")
greet("Alex") # "Alex" is the argumentIn casual conversation, people often use "parameter" and "argument" interchangeably, and in practice it rarely causes real confusion — but knowing the precise distinction helps when reading more formal documentation or error messages, which are usually careful about which term they use.
Argument order matters
When you call a function with multiple arguments and don't specify which parameter each one belongs to, Python matches them up by position — the first argument fills the first parameter, the second fills the second, and so on:
def describe_pet(name, animal_type):
print(f"{name} is a {animal_type}")
describe_pet("Rex", "dog") # Rex is a dog
describe_pet("dog", "Rex") # dog is a Rex — technically valid, but clearly wrongBoth calls run without error, but only the first one produces the intended result — the second silently swaps the meaning of the two arguments, since Python has no way to know your actual intent from position alone. (A later article in this series covers keyword arguments, which sidestep this exact problem by letting you name which parameter each value goes to.)
Returning Values with return
What return does
The return statement sends a value back to wherever the function was called, and immediately ends the function's execution — nothing after a return statement (within that function call) ever runs.
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 8Functions without an explicit return
If a function never hits a return statement — or hits a bare return with nothing after it — it implicitly returns None, a behavior covered in more detail in the earlier article on the None type:
def greet(name):
print(f"Hello, {name}!")
# no return statement
result = greet("Alex")
print(result) # None — even though the function did run and print somethingThis is worth remembering: a function doing something (like printing) is entirely separate from a function returning something. A function can do plenty of visible work and still hand back None to its caller, if it never explicitly returns a value.
Returning multiple values at once
Python lets you return more than one value from a single function — under the hood, this actually packs the values into a tuple, which you can then unpack at the call site:
def get_min_max(numbers):
return min(numbers), max(numbers)
lowest, highest = get_min_max([4, 7, 1, 9, 3])
print(lowest, highest) # 1 9return min(numbers), max(numbers) is really returning a single tuple, (1, 9) — and the multiple assignment on the calling side (covered in the earlier assignment operators article) unpacks that tuple directly into lowest and highest.
Calculating vs. printing: a critical distinction
# This function calculates and returns a value — the caller decides what to do with it
def add_tax(price, tax_rate):
return price * (1 + tax_rate)
total = add_tax(50, 0.08)
print(total) # 54.0
# This function only prints — it doesn't hand anything back for further use
def show_total(price, tax_rate):
print(price * (1 + tax_rate))
result = show_total(50, 0.08) # prints 54.0
print(result) # None — nothing was actually returnedThe first version is generally more useful and flexible — the caller gets an actual value back and can do whatever it wants with it (store it, pass it to another function, format it for display). The second version locks the function into one specific behavior (printing), and gives the caller nothing usable in return.
Practical Example and Common Pitfalls
A more complete example
Here's a function that combines a parameter, a loop, and a return value — filtering a list down to just its even numbers:
def get_even_numbers(numbers):
evens = []
for number in numbers:
if number % 2 == 0:
evens.append(number)
return evens
result = get_even_numbers([1, 2, 3, 4, 5, 6])
print(result) # [2, 4, 6]This small example brings together several concepts from earlier articles in this series — a for loop, an if condition, list building with .append() — all wrapped inside a reusable, named function with a clear input and a clear output.
Common beginner mistakes
Forgetting return and expecting output. This is probably the single most common early mistake:
def add(a, b):
a + b # calculated, but never returned!
result = add(3, 5)
print(result) # None — not 8The calculation happened, but without return, the result was simply discarded the moment the function finished — Python doesn't automatically hand back the value of the last expression the way some other languages do.
Mismatched argument counts. Calling a function with too few or too many arguments raises an error immediately:
def greet(name):
print(f"Hello, {name}!")
greet()
# TypeError: greet() missing 1 required positional argument: 'name'Confusing print() inside a function with actually returning a value. As covered above, a function can print something and still return None — these are two entirely separate behaviors, and mixing them up (assuming a function's printed output is also its return value) is a genuinely common source of confusing bugs.
A preview: default argument values
One more thing worth flagging before the next article in this series, which covers parameters and arguments in much more depth: parameters can have default values, letting a caller omit an argument entirely if the default is acceptable:
def greet(name="stranger"):
print(f"Hello, {name}!")
greet() # Hello, stranger!
greet("Alex") # Hello, Alex!This is genuinely useful — but it comes with a well-known gotcha worth knowing about early: using a mutable object (like a list or dictionary) as a default value can cause that same object to be unexpectedly shared and mutated across multiple separate calls to the function, in ways that surprise almost everyone the first time they hit it. The next article covers this in full, along with the standard fix — but for now, the safe rule of thumb is: stick to immutable defaults (numbers, strings, None, True/False) unless you specifically understand why a mutable one would be safe in your situation.