What Are Python Functions and How Do They Work?

Python functions

You are building a small portfolio project after class, during a library study session, or at home on a Windows laptop. The project reads a few task records and prints a status message. At first, copying the logic feels faster than learning a new Python feature. Then the wording changes. One copy gets updated; the other two do not. Your program still runs, but the results no longer agree.

This is the problem functions solve. A function is not merely a way to make a file shorter. It gives a name to one responsibility so that the responsibility can be called, tested, and changed in one place.

This guide uses a refactoring clinic. You will start with repeated code, identify the rule hidden inside it, extract a function, test its boundaries, and decide when the function is helping rather than adding another layer of indirection.

Clinic note 1: find the repeated responsibility

Here is a small script that prints labels for study tasks:

task_name = "read the error message"
task_minutes = 15

if task_minutes <= 20:
    print(f"{task_name}: short task")
else:
    print(f"{task_name}: break into steps")

other_task_name = "rewrite the function"
other_task_minutes = 45

if other_task_minutes <= 20:
    print(f"{other_task_name}: short task")
else:
    print(f"{other_task_name}: break into steps")

The script is valid, but the rule is duplicated. If the boundary changes from 20 minutes to 30, you must remember to update every copy. Duplication is the symptom. The hidden responsibility is:

Given a task name and its estimated duration, produce a useful label.

That sentence is a good candidate for a function because it describes one job with identifiable inputs and an output.

Clinic note 2: extract the rule and give it a name

def label_task(task_name, task_minutes):
    if task_minutes <= 20:
        return f"{task_name}: short task"
    return f"{task_name}: break into steps"

print(label_task("read the error message", 15))
print(label_task("rewrite the function", 45))

Output:

read the error message: short task
rewrite the function: break into steps

The def keyword introduces a function definition. The names inside the parentheses are parameters: placeholders for the values a caller will provide. The calls pass arguments into those placeholders.

The official Python tutorial explains that the function body begins after the definition line and must be indented. Defining the function does not run its body immediately. The body runs when the function is called.

The name label_task tells a reader what the function does. A name such as process would be technically valid but much less useful. Good names reduce the amount of code a reader must hold in their head.

Clinic note 3: return a result instead of printing inside the rule

Notice that the function uses return, not print. That distinction matters.

def label_task(task_minutes):
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

label = label_task(15)
print(f"Current task: {label}")

The function returns a value to the caller. The caller decides where to display it, save it, compare it, or test it. If the function printed the message internally, another part of the program would have less control over the result.

A function with no explicit return still returns a value: None. The Python tutorial documents this behavior and shows that falling off the end of a function produces None. That is useful when the function is meant to perform an action, but surprising when you expected a calculated result.

def label_task(task_minutes):
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

def show_label(task_minutes):
    print(label_task(task_minutes))

result = show_label(15)
print(result)

Output:

short task
None

The display happened, but result did not receive the label. Prefer a returned value when the caller may need to make another decision.

Clinic note 4: use a function inside a loop

Now the same responsibility can be applied to several records without copying the rule:

def label_task(task_minutes):
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

 tasks = [
    {"name": "read the error message", "minutes": 15},
    {"name": "rewrite the function", "minutes": 45},
    {"name": "run the test", "minutes": 10},
]

for task in tasks:
    label = label_task(task["minutes"])
    print(f"{task['name']}: {label}")

There is one small formatting error in the example above: the line beginning with tasks has an accidental leading space. Python treats that as an unexpected indent at the top level. Correct it before running:

def label_task(task_minutes):
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

tasks = [
    {"name": "read the error message", "minutes": 15},
    {"name": "rewrite the function", "minutes": 45},
    {"name": "run the test", "minutes": 10},
]

for task in tasks:
    label = label_task(task["minutes"])
    print(f"{task['name']}: {label}")

When a beginner is reading a tutorial, a visibly broken indentation example can create the wrong lesson. The deliberate diagnosis is useful here because it shows what an indentation error looks like, but the final version is the one to keep in a working file. For loop mechanics, see the Vandutz guide to Python for loops.

Clinic note 5: test the boundary before calling the function finished

The first rule says that 20 minutes is a short task. Test 19, 20, and 21 rather than assuming the comparison is correct:

def label_task(task_minutes):
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

for minutes in [19, 20, 21]:
    print(minutes, label_task(minutes))

Expected output:

19 short task
20 short task
21 break into steps

This small test tells you what the boundary actually means. If the requirement were “less than 20,” the function would need < instead of <=. A function becomes more trustworthy when its important boundaries are examples you can run, not assumptions hidden in your memory.

Clinic note 6: decide what to do with invalid input

What should happen if a task has a negative duration? A tutorial can ignore the question, but a robust beginner project should make the assumption visible.

def label_task(task_minutes):
    if task_minutes < 0:
        return "invalid duration"
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

for minutes in [-5, 0, 20, 45]:
    print(minutes, label_task(minutes))

Output:

-5 invalid duration
0 short task
20 short task
45 break into steps

The invalid case comes first because it must not be mistaken for an ordinary short task. Whether zero should be accepted depends on the rule you are modeling. In this hypothetical exercise, zero is allowed; in a real application, you would define that requirement explicitly.

You can also validate the type before comparing it:

def label_task(task_minutes):
    if not isinstance(task_minutes, int):
        return "duration must be an integer"
    if task_minutes < 0:
        return "invalid duration"
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

print(label_task("twenty"))
print(label_task(15))

This is not a universal input-validation strategy. It is a narrow example showing how a function can turn an assumption into an observable result.

Clinic note 7: default parameters should express a real fallback

Sometimes a caller can omit a value because a documented default makes sense:

def greet_learner(name="there"):
    return f"Hello, {name}!"

print(greet_learner("Alex"))
print(greet_learner())

Output:

Hello, Alex!
Hello, there!

A default parameter is not a way to hide missing information. Use it only when the fallback is meaningful. A default such as minutes=20 could be misleading if the caller forgot to provide the actual duration. The question is not “can Python accept a default?” but “does the domain rule justify this default?”

Clinic note 8: keyword arguments make a call easier to inspect

When a function has several parameters, naming them at the call site can make the relationship clearer:

def describe_task(name, minutes, done):
    status = "done" if done else "open"
    return f"{name}: {status}, {minutes} minutes"

print(describe_task("run the test", 10, False))
print(describe_task(name="run the test", minutes=10, done=False))

Both calls represent the same data. Keyword arguments become more useful when positional order is easy to misunderstand. They do not replace meaningful parameter names, but they give the reader another signal about intent.

Clinic note 9: understand local scope before sharing variables

A function creates a local namespace for its local variables. This is helpful because temporary names inside one function do not automatically overwrite names elsewhere.

def calculate_total():
    tax_rate = 0.08
    return 100 * (1 + tax_rate)

print(calculate_total())
print(tax_rate)

The first print succeeds. The second raises NameError because tax_rate exists inside the function, not in the surrounding module. The Python documentation explains that a function call creates a local namespace and that assignments inside the function normally bind local names.

The usual beginner-friendly solution is not to make every variable global. Pass the value in and return the result:

def calculate_total(amount, tax_rate):
    return amount * (1 + tax_rate)

print(calculate_total(100, 0.08))

This version is easier to test because the inputs are visible at the call site.

A refactoring check: is this function actually helping?

Extracting every two lines into a function can make a small script harder to follow. Before creating a function, ask:

  • Does the code express one responsibility?
  • Does the responsibility have a useful name?
  • Will it be called more than once, tested separately, or changed independently?
  • Can its inputs and output be described clearly?
  • Would the caller benefit from receiving a value instead of only seeing printed output?

A function is probably useful when it gives a repeated or meaningful rule a stable name. It may be unnecessary when it hides one obvious line and is never reused.

Why this habit matters in an American beginner’s first project

If you are preparing a portfolio project, completing a community-college assignment, or reviewing test output at a study group, refactoring changes how you can explain your work. Instead of saying “the script has several copies of the same check,” you can say “this function classifies a task, and these examples test its normal and boundary cases.” That is a more reviewable unit of work.

The U.S. Bureau of Labor Statistics describes software developers as designing applications, while quality-assurance analysts and testers identify problems and report defects. Learning functions does not guarantee a job or replace formal preparation. It does give a beginner a practical way to separate a rule, run it with several inputs, and make a change without hunting through duplicated code.

Final clinic: turn the rule into a small testable unit

def label_task(task_minutes):
    if not isinstance(task_minutes, int):
        return "duration must be an integer"
    if task_minutes < 0:
        return "invalid duration"
    if task_minutes <= 20:
        return "short task"
    return "break into steps"

checks = {
    -1: "invalid duration",
    0: "short task",
    20: "short task",
    21: "break into steps",
}

for minutes, expected in checks.items():
    actual = label_task(minutes)
    print(minutes, actual == expected, actual)

Expected output:

-1 True invalid duration
0 True short task
20 True short task
21 True break into steps

This is not a full testing framework, but it is a useful bridge from “I copied an example” to “I can check whether my rule still behaves as intended.” Change the threshold and rerun the checks. The failed line tells you which expectation needs attention.

Functions are valuable because they make responsibilities visible. They let you name a rule, give it controlled inputs, return a result, and test a boundary without rewriting the surrounding program. That is the difference between code that merely runs once and code that can be reviewed and changed.

Once you are comfortable extracting a function from duplicated code, return to Python if statements for decision logic and Python for loops for repeated data. Together, these three building blocks let you turn a small script into a sequence of named, testable responsibilities.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top