Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Defining and Calling Python Functions with Parameters, Returns, and Docstrings

Hello! Last time, you used conditionals to choose what a program does and loops to apply that logic to several tasks. The next step is to give a useful piece of logic a name, so you can run it with different inputs instead of copying its code.

In this lesson, you’ll define and call functions, pass values through parameters, use return values, and write docstrings that explain a function’s behavior. The final example packages the task decision from the previous lesson into a reusable function. Allow about 40 minutes to watch the selected video segments, read the short documentation excerpt, and run the code in Colab.


Give a piece of code a name

A function definition begins with def. Its indented body describes what should happen when the function is called; defining it does not run the body immediately.

Watch the opening of Python Functions | Python Tutorial for Absolute Beginners #1 by Programming with Mosh for the definition-and-call syntax, then for the distinction between parameters and arguments.

Python Functions | Python Tutorial for Absolute Beginners #1

Programming with Mosh introduces the syntax of a function and shows how inputs make one definition reusable.

Watch define and call for def, parentheses, the colon, and indentation. Then watch passing inputs; focus on which names appear in the definition and which values are supplied at each call.

Here is a function that makes a readable label from two pieces of task data:

def task_label(action, status):
    """Return a readable action and status label."""
    return f"{action}: {status}"


first_label = task_label("draft", "pending")
second_label = task_label("review", "done")

print(first_label)   # draft: pending
print(second_label)  # review: done

action and status are parameters: names in the definition that stand for incoming values. "draft" and "pending" are arguments: the actual values supplied in the first call. On that call, the function uses "draft" as action and "pending" as status. On the next call, the same body runs with different values.

The parentheses matter. task_label("draft", "pending") calls the function; task_label by itself refers to the function without calling it.

The calls above supply arguments by position, so their order must match the definition. You can instead name the parameters at the call site:

print(task_label(status="pending", action="schedule"))
# schedule: pending

These are keyword arguments. Their names establish which value goes where, regardless of the order in the call. Every required parameter still needs a value: calling task_label("draft") would produce a TypeError because status is missing.


Return a value rather than only displaying it

A function can print inside its body, but printed text is not automatically a value the caller can save and reuse. Watch the video’s comparison of printing and returning, including what happens when a function has no explicit return.

Python Functions | Python Tutorial for Absolute Beginners #1

The same functions video now distinguishes an effect you can see on screen from a value your program receives.

Watch returning values, concentrating on what the caller can do with the result. Then watch implicit None for the result of a function that only prints.

Compare these two functions:

def show_status(status):
    print(f"Status: {status}")


def format_status(status):
    return f"Status: {status}"


printed_value = show_status("pending")  # Displays: Status: pending
print(printed_value)                    # Displays: None

saved_value = format_status("pending")  # Displays nothing yet
print(saved_value.upper())              # Displays: STATUS: PENDING

show_status() has an observable effect: it prints. Because it has no return statement, its call also produces the value None, which represents the absence of a useful returned value here. format_status() instead hands a string back to the caller. You decide whether to display that string, transform it, or use it elsewhere. return also ends that call immediately; code later in the same function body will not run after it.

Parameters and variables created inside a function are local to that call. In format_status(), the name status is available within the function, while saved_value is a separate name assigned outside it to the returned result. Passing inputs in and returning results out gives you a clear account of where the data comes from.


Make a sensible input optional

Sometimes a parameter has a useful usual value. A default parameter value lets the caller omit that argument while retaining the option to override it.

Watch the short segments on keyword calls and defaults in the same video.

Python Functions | Python Tutorial for Absolute Beginners #1

The final selected segments show how named arguments clarify a call and how a default changes which arguments are required.

Watch keyword calls for readability, then default values. Notice where the optional parameter appears in the definition.

If most new task labels have a pending status, you could define a more convenient version:

def new_task_label(action, status="pending"):
    """Return a readable label, defaulting to pending status."""
    return f"{action}: {status}"


print(new_task_label("draft"))                  # draft: pending
print(new_task_label("review", "done"))         # review: done
print(new_task_label("schedule", status="done"))  # schedule: done

action is required; status is optional because it has a default. Put required parameters before parameters with defaults, as shown here. A default is helpful when it reflects a genuine usual case—not merely to avoid typing an argument whose value the caller ought to choose.


Document the function’s contract

The triple-quoted string immediately after a function’s header is its docstring. It tells a reader what to expect when calling the function. Python also makes it available through help(function_name). A docstring is different from a comment beside the code: it is attached to the function itself.

Read the concise convention in the official Python tutorial before looking at a longer example.

4. More Control Flow Tools — Python 3.14.0 documentation

The Python documentation gives a practical convention for a clear summary line and for adding detail when a one-line docstring is insufficient.

In Section 4.9.7, “Documentation Strings,” under “More on Defining Functions,” read the formatting guidance. Start at the section’s opening sentence and stop after the paragraph about calling conventions and side effects, just before the indentation note. Focus on the short first-line summary and the blank line before further explanation.

The illustration shows that Python docstrings can document functions, classes, and modules, can be one-line or multiline, and should avoid vague descriptions and missing explanations of inputs or results. The named documentation formats are options, not additional syntax you need for this lesson.

A short function such as task_label() may need only one summary sentence. For a function with several possible outcomes, document its expected inputs and its result as well. Here, the docstring describes a small contract: what task data the function expects and what it returns.

def task_decision(task, permitted_actions):
    """Describe whether a simulated task can be processed.

    Args:
        task: A dictionary with action, status, and confirmed fields.
        permitted_actions: A collection of allowed action names.

    Returns:
        A short explanation of the task's processing status.
    """
    if task["action"] not in permitted_actions:
        return "Unknown action"
    elif task["status"] == "done":
        return "Already completed"
    elif not task["confirmed"]:
        return "Wait for confirmation"
    else:
        return "Ready for simulated processing"


permitted_actions = {"draft", "review", "schedule"}
tasks = [
    {"action": "draft", "status": "pending", "confirmed": True},
    {"action": "send", "status": "pending", "confirmed": True},
    {"action": "schedule", "status": "pending", "confirmed": False},
]

for task in tasks:
    decision = task_decision(task, permitted_actions)
    print(f"{task['action']}: {decision}")

The loop displays:

draft: Ready for simulated processing
send: Unknown action
schedule: Wait for confirmation

You have already seen the conditional logic; the new feature is its boundary. Each call receives one task and the permitted actions, then returns one explanation. The loop decides what to do with that explanation—in this case, print it. No external task is performed.

Each branch returns, so the function exits as soon as it reaches a decision. All possible paths in this example return a string. That consistency makes the function easier to use: the caller does not need to handle an unexpected None. In Colab, you can also run help(task_decision) in a cell to see how its docstring presents the function to someone using it.


Takeaway

A function defined with def packages code you can call repeatedly. Parameters name its inputs; arguments supply their values, either by position or by keyword. return sends a value back for further use, whereas print only displays it. Defaults make selected inputs optional, and a clear docstring explains what callers should provide and expect.

You can now turn task-processing decisions into reusable building blocks. Next, you’ll learn to import modules and install packages in Colab, so your functions can work alongside code provided by Python and other libraries.

Can't find a good explanation? Sign up and we'll make it for you

Sign up