10xdev Book: Programming from Zero to Pro with Python
Chapter 6: Functions in Python: Building Reusable Code Blocks
Chapter Introduction
Okay, your programs can now make decisions and repeat themselves. That’s a huge leap! But as you write more code, you’ll start noticing something: you’re often writing the same (or very similar) blocks of code over and over again in different places.
This repetition is a major red flag in programming. It makes your code longer, harder to read, and a real pain to update (fix a bug in one place? You have to find and fix it everywhere you copied it!). This leads us to a core principle: DRY – Don’t Repeat Yourself.
Functions are Python’s primary tool for staying DRY. A function is simply a named block of code designed to perform a specific task. You write the code once, give it a name, and then you can call that name whenever you need that task done, potentially with different inputs. Think of it as creating your own custom command or a reusable recipe. 🧑🍳
1. What is a Function and How Do We Define One?
To create (or define) a function, you use the def keyword.
Basic Structure:
def function_name(parameter1, parameter2): # Parameters are optional inputs
"""
Optional docstring: Explains what the function does.
Good practice!
"""
# Indented code block: The function's actions
print(f"Executing function_name with {parameter1} and {parameter2}")
result = parameter1 + parameter2 # Example action
return result # Optional: Send a value back
def: Tells Python you’re defining a function.function_name: Your chosen name (usesnake_case).(): Parentheses are required. They hold parameters (input variables) if the function needs them.:: The colon marks the start of the indented function body.- Docstring (
"""..."""): A string literal on the first line explaining the function. Crucial for documentation and help tools. - Function Body: The indented code that runs when the function is called.
return: (Optional) Sends a value back to the place where the function was called.
Calling a Function
Defining a function doesn’t run it. You need to call it by using its name followed by parentheses, providing any required arguments (the actual values for the parameters).
def greet(name: str): # Added a type hint for clarity
"""Prints a simple greeting."""
print(f"Hello, {name}!")
# Calling the function with the argument "Alice"
greet("Alice")
greet("Bob") # Call it again with a different argument
2. Inputs: Parameters vs. Arguments
These terms are often confused:
- Parameter: The variable name inside the function definition’s parentheses (e.g.,
nameindef greet(name):). It’s the placeholder for the input. - Argument: The actual value you pass in when you call the function (e.g.,
"Alice"ingreet("Alice")). It’s the data that fills the placeholder.
3. Outputs: print vs. return
This difference is critical:
print(): Displays output to the user on the console. The function itself doesn’t produce a value that your code can use later.return: Sends a value back to the code that called the function. This allows you to store the result, use it in calculations, or pass it to another function.
def add_numbers(num1: float, num2: float) -> float: # Hinting input and output types
"""Adds two numbers and returns the sum."""
calculation = num1 + num2
return calculation # Send the result back
# Call the function and store the returned value
sum_result = add_numbers(10.5, 5.0)
print(f"The print function just displays: {sum_result}")
# We can use the returned value in further operations
print(f"Result multiplied by 2: {sum_result * 2}")
# A function without a return statement implicitly returns None
def print_greeting(name: str):
print(f"Hi, {name}!")
result_of_print = print_greeting("Charlie")
print(f"The print_greeting function returned: {result_of_print}") # Output: None
Functions that return a value are generally more reusable and flexible than those that only print.
4. Flexible Arguments
Default Parameter Values
You can make parameters optional by providing a default value in the def statement.
def power(base: float, exponent: float = 2.0) -> float: # exponent defaults to 2
"""Calculates base raised to the exponent power."""
return base ** exponent
print(power(5)) # Uses default exponent 2 -> 25.0
print(power(5, 3)) # Overrides default, uses exponent 3 -> 125.0
Keyword Arguments
When calling a function, you can specify arguments by parameter name. This makes the order irrelevant and improves readability, especially for functions with many parameters.
def describe_pet(animal_type: str, pet_name: str):
print(f"I have a {animal_type} named {pet_name}.")
describe_pet(pet_name="Whiskers", animal_type="cat") # Order doesn't matter here
5. Variable Scope: Local vs. Global
Where can you access a variable? That’s determined by its scope.
-
Local Scope: Variables created inside a function (including parameters) exist only within that function. They are local and disappear once the function finishes. This prevents functions from accidentally interfering with each other.
def my_func(): local_var = 100 # Local variable print(f"Inside function: {local_var}") my_func() # print(local_var) # This would cause a NameError! local_var doesn't exist here. -
Global Scope: Variables created outside any function exist in the global scope and can generally be read from anywhere (including inside functions).
global_var = "I'm global" def read_global(): print(f"Inside function, reading global: {global_var}") read_global() print(f"Outside function: {global_var}")
Best Practice: Avoid modifying global variables from inside functions if possible (using the global keyword makes code harder to understand and debug). Pass data into functions via parameters and get results back via return.
6. Applied Project: An Organized Calculator (Refined)
Let’s revisit our calculator, making it cleaner using functions that return values.
# --- Organized Calculator with Functions ---
# Function definitions (the tools)
def add(x: float, y: float) -> float:
"""Returns the sum of x and y."""
return x + y
def subtract(x: float, y: float) -> float:
"""Returns the difference of x and y."""
return x - y
def multiply(x: float, y: float) -> float:
"""Returns the product of x and y."""
return x * y
def divide(x: float, y: float) -> float | str: # Can return float or error string
"""Returns the division of x by y, handles division by zero."""
if y == 0:
return "Error: Cannot divide by zero."
return x / y
# Main part of the program (the user interface)
def run_calculator():
print("Select operation:")
print("1. Add")
print("2. Subtract")
print("3. Multiply")
print("4. Divide")
while True: # Loop for valid choice input
choice = input("Enter choice (1/2/3/4): ").strip()
if choice in ['1', '2', '3', '4']:
break
else:
print("Invalid choice. Please enter 1, 2, 3, or 4.")
try:
num1 = float(input("Enter first number: "))
num2 = float(input("Enter second number: "))
except ValueError:
print("Invalid input. Please enter numbers.")
return # Exit if input is bad
# Call the appropriate function based on choice
result: float | str # Declare type hint for result
if choice == '1':
result = add(num1, num2)
op_symbol = "+"
elif choice == '2':
result = subtract(num1, num2)
op_symbol = "-"
elif choice == '3':
result = multiply(num1, num2)
op_symbol = "*"
elif choice == '4':
result = divide(num1, num2)
op_symbol = "/"
# Display the result (check if it's an error string)
if isinstance(result, str): # Check if the result is the error message
print(result)
else:
print(f"{num1} {op_symbol} {num2} = {result:.2f}") # Format float result
# Run the calculator if this script is executed directly
if __name__ == "__main__":
run_calculator()
This structure clearly separates the calculation logic (in the functions) from the user interaction (in run_calculator). It’s much easier to test, modify, and understand.
7. Chapter Summary
- Functions (
def) let you package code into named, reusable blocks, promoting the DRY principle. - Use docstrings to explain what your functions do.
- Pass data in via parameters (in definition) and arguments (in call). Use keyword arguments for clarity. Provide default values for optional parameters.
- Use
returnto send a value back from a function; otherwise, it returnsNone. - Variables inside functions have local scope. Avoid over-reliance on global variables.
- Use type hints (
param: type -> return_type) for better code documentation and tooling support.
Functions help organize actions, but how do you organize collections of related data? Let’s explore Python’s powerful built-in data structures: lists, tuples, and dictionaries.