Functions

Functions are probably the most useful tool for organizing Python code. They let you give a name to a piece of behavior, reuse it in multiple places, and hide implementation details behind a clear interface.

Well-designed functions make your code easier to read, test, and change. Poorly designed ones quickly become a source of bugs and confusion.

When designing functions, the following best practices lead to clearer and more testable code:

  • Give each function a single, clear responsibility. Aim for functions that do one thing rather than mixing multiple or unrelated behaviors. If you find yourself adding many ands to a function name, consider splitting the function into separate steps. Smaller, focused functions are more understandable, testable, and reusable.
  • Use descriptive names that start with a verb. Choose descriptive function names that start with a verb because functions perform actions. Write names like write(), remove_user(), or validate_user_input(), rather than data(), user(), or user_input(). This practice makes your code more readable by providing context about what a function does.
  • Stick to Python naming conventions. Honor Python’s naming conventions, such as using snake case for function names and a leading underscore (_) for non-public helper functions.
  • Favor pure functions when possible. When working on core business logic, prefer pure functions that depend only on their inputs and have no side effects, such as hidden I/O operations or modifications to global state. Pure functions are easier to understand and test because they always produce the same output for a given input. Side effects are often unavoidable at system boundaries—for example, file I/O or network calls—but keeping them isolated improves maintainability.
  • Limit the number of parameters. Functions with many parameters are harder to understand and call correctly. As a general guideline, if a function needs more than three to five parameters, consider grouping related values into a data class or another compound object. This approach improves readability and reduces the risk of passing arguments in the wrong order.
  • Use keyword-only arguments with sensible defaults. For optional or configuration-style parameters, prefer keyword-only arguments with sensible defaults over long positional argument lists. This approach makes calls self-documenting and reduces the likelihood of mistakes.
  • Return consistent types. Design functions so that they always return the same type of object. Avoid functions that sometimes return a useful value and sometimes None, or that mix unrelated return values. Consistent return types make your code easier to use, test, and type-check.
  • Avoid hidden work and surprises. Avoid hiding expensive operations, such as network calls or file I/O, in functions with innocuous names like get_config() unless that behavior is clearly expected. Callers should have a clear understanding of a function’s purpose based on its name, parameters, and docstring.

To see some of these best practices in action, consider a function that filters users, writes a CSV report, and optionally emails it:

🔴 Avoid this:

Python
import csv

def process_users(users, min_age, filename, send_email):
    adults = []
    for user in users:
        if user["age"] >= min_age:
            adults.append(user)

    with open(filename, mode="w", newline="", encoding="utf-8") as csv_file:
        writer = csv.writer(csv_file)
        writer.writerow(["name", "age"])
        for user in adults:
            writer.writerow([user["name"], user["age"]])

    if send_email:
        # Emailing logic here...

    return adults, filename

This function performs several tasks. It filters data, writes it to a file, emails the report, and returns a tuple of mixed and unrelated values. It also relies on multiple positional arguments and a Boolean flag, send_email, that alters the function’s behavior, making calls harder to read and easier to misuse.

Favor this:

Python
import csv

def filter_adult_users(users, *, min_age=18):
    """Return users whose age is at least min_age."""
    return [user for user in users if user["age"] >= min_age]

def save_users_csv(users, filename):
    """Save users to a CSV file."""
    with open(filename, mode="w", newline="", encoding="utf-8") as csv_file:
        writer = csv.writer(csv_file)
        writer.writerow(["name", "age"])
        for user in users:
            writer.writerow([user["name"], user["age"]])

def send_users_report(filename):
    """Send the report."""
    # Emailing logic here...

In this updated version, you split the code into three functions with a single responsibility each. One function filters users, one handles file output, and one manages email delivery. Each function has a clear purpose, predictable behavior, and a focused interface.

Taken together, these changes illustrate how smaller, well-named, and focused functions are easier to understand, test, and reuse than a single large function that performs many different tasks.

Tutorial

Defining Your Own Python Function

Learn how to define your own Python function, pass data into it, and return results to write clean, reusable code in your programs.

basics python

For additional information on related topics, take a look at the following resources:


By Leodanis Pozo Ramos • Updated Dec. 23, 2025 • Reviewed by Brenda Weleschuk and Bartosz Zaczyński