Free Ebook cover Python for Absolute Beginners: Variables, Loops, and Small Useful Scripts

Python for Absolute Beginners: Variables, Loops, and Small Useful Scripts

New course

14 pages

Organizing Code with Functions and Reusable Logic

Capítulo 6

Estimated reading time: 10 minutes

+ Exercise

Why Functions Matter (and What Problem They Solve)

As your scripts grow, you quickly run into two common problems: repeated code and hard-to-read “long” programs. Repeated code happens when you copy and paste the same logic in multiple places (for example, formatting a price, validating input, or calculating a discount). Long programs happen when everything is written top-to-bottom in one file, making it difficult to find where a specific behavior is implemented.

Functions solve both problems by letting you package a piece of logic into a named block that you can run whenever you need it. You write the logic once, give it a clear name, and then call it from different places. This makes your code easier to read, easier to test, and easier to change later.

A function is essentially a small “mini-program” inside your program. It can take inputs (called parameters), do some work, and optionally produce an output (called a return value).

Defining and Calling a Function

In Python, you define a function using def, then you call it by writing its name followed by parentheses.

def greet():
    print("Hello!")

greet()

Key ideas:

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

  • Function definition: the code under def greet(): is stored for later use.
  • Function call: greet() runs the stored code.
  • Indentation matters: the function body is the indented block under the def line.

Functions as “named steps”

A helpful mental model is to think of functions as named steps in a recipe. Instead of writing every detail repeatedly, you write a step once (like “make sauce”), then reuse it whenever needed.

Parameters: Passing Data Into a Function

Most useful functions need data to work with. Parameters are variables listed in the function definition that receive values when you call the function.

def greet_person(name):
    print(f"Hello, {name}!")

greet_person("Amina")
greet_person("Diego")

Here, name is a parameter. When you call greet_person("Amina"), the value "Amina" is assigned to name inside the function.

Multiple parameters

def format_full_name(first, last):
    return f"{first} {last}".strip()

full = format_full_name("Ada", "Lovelace")
print(full)

Parameters let you make functions flexible: the same function can work for many different inputs.

Return Values: Getting Results Back

Some functions do something visible (like printing), while others compute a value and hand it back to the caller. The return statement sends a value out of the function.

def add(a, b):
    return a + b

result = add(10, 5)
print(result)

Important details about return:

  • When Python hits return, the function ends immediately.
  • A function can return any type of value (number, string, list, etc.).
  • If a function has no return, it returns None by default.

Printing vs returning

Beginners often mix these up. Printing shows something to the user, but it does not give a value back to the rest of your program. Returning gives a value back so other code can use it.

def area_print(width, height):
    print(width * height)

def area_return(width, height):
    return width * height

x = area_print(3, 4)   # x becomes None
y = area_return(3, 4)  # y becomes 12
print(x, y)

Use return for reusable logic. Use print mainly for user-facing output.

Designing Functions: One Job, Clear Name

A function is easiest to reuse when it does one job and does it well. If a function is doing multiple unrelated tasks (for example, reading input, validating it, calculating, and printing), it becomes harder to reuse in other contexts.

Try to follow these guidelines:

  • Choose a clear verb-based name: calculate_total, is_valid_email, format_receipt_line.
  • Keep it focused: if you find yourself writing “and then…” many times, consider splitting into smaller functions.
  • Prefer returning values from logic functions, and printing in a separate “display” function.

Step-by-Step: Refactoring Repeated Logic Into Functions

Imagine you are writing a small script that prepares a simple receipt line for multiple items. You might start by repeating formatting logic.

Step 1: Notice repetition

item1 = "Notebook"
price1 = 2.5
print(f"{item1:12} ${price1:6.2f}")

item2 = "Pen"
price2 = 1.2
print(f"{item2:12} ${price2:6.2f}")

item3 = "Backpack"
price3 = 29.99
print(f"{item3:12} ${price3:6.2f}")

The formatting pattern is the same each time.

Step 2: Extract a function

def print_receipt_line(item_name, price):
    print(f"{item_name:12} ${price:6.2f}")

print_receipt_line("Notebook", 2.5)
print_receipt_line("Pen", 1.2)
print_receipt_line("Backpack", 29.99)

Now the formatting logic is written once. If you want to change the layout (for example, wider item names), you change it in one place.

Step 3: Separate formatting from printing (more reusable)

Sometimes you want the formatted line to be used elsewhere (saved to a file, combined into a larger string, etc.). Returning the formatted line makes the function more flexible.

def format_receipt_line(item_name, price):
    return f"{item_name:12} ${price:6.2f}"

line = format_receipt_line("Notebook", 2.5)
print(line)

Default Parameter Values

Default values let you make parameters optional. If the caller does not provide a value, the default is used.

def apply_discount(price, percent=0):
    return price * (1 - percent / 100)

print(apply_discount(100))      # 100.0
print(apply_discount(100, 15))  # 85.0

Defaults are useful for “common case” settings. They also help keep function calls simple.

Be careful with mutable defaults

A common beginner pitfall is using a list or dictionary as a default value, because it can be shared across calls. Prefer None and create the list inside.

def add_tag(tags=None, tag="new"):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

print(add_tag())
print(add_tag())

Keyword Arguments: Calling Functions More Clearly

When a function has multiple parameters, keyword arguments can make calls easier to read. You specify parameter names in the call.

def create_user(username, email, is_admin=False):
    return {"username": username, "email": email, "is_admin": is_admin}

u1 = create_user("sam", "sam@example.com")
u2 = create_user(username="lee", email="lee@example.com", is_admin=True)
print(u1)
print(u2)

Keyword arguments help prevent mistakes, especially when parameters have the same type (for example, two strings).

Scope: Where Variables Live

Variables created inside a function are local to that function. That means they exist only while the function runs, and they cannot be accessed directly from outside.

def compute():
    temp = 42
    return temp

value = compute()
print(value)
# print(temp)  # This would cause an error

Local variables are a good thing: they reduce accidental interference between different parts of your program.

Using variables from outside (read-only is common)

A function can read a variable defined outside it, but modifying it is different. In beginner scripts, it is usually better to pass values in as parameters and return results, rather than relying on outside variables.

TAX_RATE = 0.2

def add_tax(price):
    return price * (1 + TAX_RATE)

print(add_tax(10))

This is readable because TAX_RATE is a constant-like setting. For changing values, prefer parameters and return values.

Early Returns and Guard Clauses

Functions often need to handle “invalid” or special cases. An early return (sometimes called a guard clause) can keep your code simpler by handling the special case first.

def safe_divide(a, b):
    if b == 0:
        return None
    return a / b

print(safe_divide(10, 2))
print(safe_divide(10, 0))

Instead of nesting the main logic inside an else, you return early for the exceptional case.

Building Reusable Logic: Small Utility Functions

Many scripts benefit from a few small utility functions that you can reuse across the program. These functions usually:

  • Take simple inputs
  • Return a result without printing
  • Have predictable behavior

Example: Normalizing text

def normalize_name(name):
    name = name.strip()
    if not name:
        return ""
    return name.title()

print(normalize_name("  aDa lovelace "))

Example: Formatting money

def format_money(amount, currency="$"):
    return f"{currency}{amount:.2f}"

print(format_money(3))
print(format_money(3, "€"))

By keeping these functions small and focused, you can combine them in many ways.

Step-by-Step: A Small “Invoice Helper” Using Functions

This example shows how functions help you organize a script into reusable pieces. The goal is to compute line totals, apply tax, and produce formatted lines. Each function has a single responsibility.

Step 1: Write the core calculation functions

def line_total(unit_price, quantity):
    return unit_price * quantity

def add_tax(subtotal, tax_rate):
    return subtotal * (1 + tax_rate)

Step 2: Add formatting functions

def format_line(item, unit_price, quantity):
    total = line_total(unit_price, quantity)
    return f"{item:12} {quantity:3} x ${unit_price:6.2f} = ${total:7.2f}"

Step 3: Use the functions together

items = [
    ("Notebook", 2.50, 4),
    ("Pen", 1.20, 10),
    ("Backpack", 29.99, 1)
]

tax_rate = 0.2

subtotal = 0
for item, unit_price, quantity in items:
    print(format_line(item, unit_price, quantity))
    subtotal += line_total(unit_price, quantity)

total_with_tax = add_tax(subtotal, tax_rate)
print(f"Subtotal: ${subtotal:.2f}")
print(f"Total (tax): ${total_with_tax:.2f}")

Notice what improved:

  • The loop reads like a high-level plan: print each line, add totals.
  • Math logic is isolated in line_total and add_tax.
  • Formatting is isolated in format_line.

If you later change how totals are calculated (for example, rounding rules), you update one function instead of hunting through the script.

Functions That Return Multiple Values

Sometimes you want a function to produce more than one result. In Python, you can return multiple values as a tuple.

def min_max(values):
    return min(values), max(values)

low, high = min_max([3, 1, 9, 2])
print(low, high)

This can make your code cleaner than calling two separate functions when the results are naturally related.

Documenting Functions with Docstrings

A docstring is a short description placed as the first statement inside a function. It helps you (and others) understand what the function expects and returns.

def celsius_to_fahrenheit(c):
    """Convert a temperature from Celsius to Fahrenheit."""
    return c * 9 / 5 + 32

print(celsius_to_fahrenheit(0))

Good docstrings typically mention:

  • What the function does
  • What parameters mean
  • What is returned (especially if it can be None)

Common Beginner Mistakes (and How to Avoid Them)

Writing functions that do too much

If a function reads input, validates it, calculates, and prints, it becomes hard to reuse. Prefer splitting into separate functions: one for calculation, one for formatting, one for user interaction.

Forgetting to return a value

If you expect a result but the function prints instead of returning, you will end up with None when you try to store the result. If the caller needs the value, use return.

Using unclear names

do_stuff() and calc() hide intent. Names are part of readability. Prefer calculate_shipping_cost or format_address.

Relying on global variables

Passing values as parameters and returning results makes functions easier to test and reuse. Globals can be useful for constants, but avoid using them as hidden inputs that change over time.

Organizing a Script with a main() Function

As scripts grow, a common pattern is to put the “top-level” flow into a main() function. This keeps your file organized: helper functions at the top, and the main program logic in one place.

def format_money(amount):
    return f"${amount:.2f}"

def calculate_total(prices):
    return sum(prices)

def main():
    prices = [2.5, 1.2, 29.99]
    total = calculate_total(prices)
    print("Total:", format_money(total))

main()

This structure makes it easier to scan the file and understand what the program does. It also encourages you to keep logic in functions rather than scattered across the script.

Mini Practice Tasks (Use Functions to Keep Them Clean)

Task 1: Reusable text cleaner

Create a function clean_text(text) that trims spaces and converts multiple spaces inside the text into a single space. Then use it on several example strings.

Task 2: Price calculator utilities

Create subtotal(prices), apply_tax(amount, rate), and apply_discount(amount, percent). Combine them to compute a final price. Keep each function small and return values instead of printing.

Task 3: Formatting report lines

Create a function format_report_line(label, value, width=20) that returns a nicely aligned string. Use it to build a small report made of multiple lines.

Now answer the exercise about the content:

Why is returning a value often better than printing inside a function when you want reusable logic?

You are right! Congratulations, now go to the next page

You missed! Try again.

Return sends a computed value back to the calling code so it can be stored, combined, or used elsewhere. Print only displays output and the function result is None if nothing is returned.

Next chapter

Storing and Updating Data with Lists and Dictionaries

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.