Defining and Calling Functions
Context
A function is a named block of code that takes values in, does something with them, and hands a value back. Once defined it can be called from anywhere in the script, as many times as needed.
This is the unit that every real pipeline is built from. Loading a file, cleaning a column, computing a metric, and formatting a report are each one function, written once and called wherever needed. It is also what makes a step testable, because a function that takes rows and returns a count can be checked against a handful of rows you made up, without running the whole pipeline to find out whether it works.
Without functions a script becomes one long sequence where the same fifteen lines of cleaning appear three times, and fixing a bug means finding and fixing all three, which nobody ever does completely. The specific trap is the function that computes the right answer and never returns it, handing back nothing at all, so the caller receives an absence and the failure surfaces several steps later in a place that looks unrelated. The previous section made code repeat and branch, this lesson makes it reusable.
Goal
- Define a function with
def, give it parameters, and run its body by calling it. - Hand a computed result back to the caller with
return, instead of only printing it. - Distinguish a parameter (a name in the definition) from an argument (a value at the call).
- Distinguish defining a function (writing it) from calling a function (running it).
- Explain precisely why a function that prints is not the same as a function that returns.
Prerequisites
Neither of these is required. The lesson stands on its own, and they are here only if you want the picture before you start.
- YouTube - "Python Full Course for Beginners" by Programming with Mosh, the functions segment. Watch for the single picture of "define once, call many times."
- Official docs - The Python Tutorial, "Defining Functions". The canonical first description of
def, parameters, andreturn.
Table of Contents
- Function
- def (Function Definition)
- Parameter
- Argument
- Return Value
- Function Call
Theory
Nothing needs installing for this lesson. def and return are keywords built into Python itself.
Function
What. A function is a named, reusable unit of logic, a block of code you write once, give a name, and run again whenever you need it. Why. Without functions, a program is one long script, and any logic you need twice must be copied. Copied code drifts out of sync and multiplies bugs. A function lets you write the logic in exactly one place and reuse it by name. When. Reach for a function the moment a chunk of code has a clear job you can name, or the instant you are about to copy-paste logic you already wrote.
Up to now in this module a program has been a single top-to-bottom list of statements. With functions, a program becomes an arrangement of named units, you build small pieces, then assemble them. The program still runs top to bottom, but most of that top-level code becomes short calls to functions defined elsewhere.
# Without a function: the same formula is copied twice and easy to get wrong.
score_a = (88 - 50) / 10 # normalize first score
score_b = (73 - 50) / 10 # same formula again, change '50' and you must fix every copy
print(score_a, score_b)# With a function: the formula lives in ONE named place.
def normalize(value): # 'normalize' is the function's name
return (value - 50) / 10 # the single source of truth for the formula
print(normalize(88)) # reuse by name
print(normalize(73)) # reuse again, no copyDON'T:
- DON'T create a function for a one-off line you will never reuse and that has no name worth giving, naming trivial code adds noise without payoff.
- DON'T think of a function as "code that runs when the file loads", defining it does not run it, as the next subtitle shows.
def (Function Definition)
What. def is the statement that creates a function, it binds a name to a block of code (the body) without running that code.
Why. You need a way to tell Python "remember this logic under this name for later." def is that instruction, separating the moment you write logic from the moment you use it.
When. Use def once per function, at the point in the file before any code that calls it runs.
The shape is fixed: the keyword def, the function name, parentheses holding any parameters, then a colon. Everything indented beneath the colon is the body, and where that indentation stops, the body stops, exactly as it does for a loop or an if.
# 'def' creates the function but does NOT run the body yet.
def greet_model(): # name is 'greet_model', empty parentheses = no inputs
print("training started") # this line is the body, indented under the colon
print("file loaded") # this runs, the body above has NOT run yet# A definition can hold many lines, all indented lines belong to the body.
def describe_split(): # function with a multi-line body
print("train: 80%") # body line 1
print("test: 20%") # body line 2
print("definition complete") # NOT part of the body (not indented)For a body written on the lines below the def, indentation alone decides what is inside the function and what is not.
DON'T:
- DON'T forget the colon after the parentheses,
def f()without:is a syntax error. - DON'T expect the body to run just because you wrote the
def, without a call (shown later) the body is dormant.
Parameter
What. A parameter is a name listed inside the parentheses of a definition that stands for an input the function will receive. Why. A function that can only work on fixed values is barely reusable. Parameters let one definition operate on different inputs each time it runs, which is what makes the logic general. When. Add a parameter for every piece of information the function needs from the outside to do its job.
A parameter behaves like a variable that comes pre-filled when the function runs. Inside the body you use the parameter name as if it were already assigned, because it will be, by the time the body runs.
# 'value' is a parameter: a stand-in for an input not known yet.
def scale(value): # 'value' will hold whatever the caller passes in
return value * 2 # use the parameter as a normal variable
print(scale(10)) # here 'value' becomes 10
print(scale(7)) # here 'value' becomes 7# A function can take more than one parameter, separated by commas.
def weighted(score, weight): # two parameters, in this order
return score * weight # both are usable inside the body
print(weighted(80, 0.5)) # 'score'=80, 'weight'=0.5Order matters, the first value passed by position fills the first parameter, and the precise rules for that matching are the next lesson's subject.
DON'T:
- DON'T confuse the parameter (the name in the definition) with the value it will hold, the parameter is the empty slot, not the contents.
- DON'T reuse an outside variable name as a parameter expecting them to be linked, a parameter is its own local name, covered fully in Lesson 1.3.3.
Argument
What. An argument is the actual value you supply inside the parentheses when you call a function. Why. Parameters are empty slots, arguments are what fills them. A parameter left without an argument has nothing to work on, unless the definition gives it a default, the fallback this lesson's Assignment adds and Lesson 1.3.2 covers. When. Provide one argument for each parameter the function requires, at the moment you call it.
The cleanest way to keep the two words straight: parameters live in the definition, arguments live in the call. The argument is the concrete thing, the parameter is the placeholder that receives it.
# 'epochs' is the PARAMETER (definition side).
def train(epochs): # placeholder declared here
print("training for", epochs, "epochs")
train(5) # 5 is the ARGUMENT (call side), it fills 'epochs'# Arguments can be expressions, not just literals.
def report(accuracy): # one parameter
print("accuracy is", accuracy)
correct = 92 # an ordinary variable
total = 100
report(correct / total) # the argument is computed first, then passed inA function never sees the expression you wrote, only the value it produced, so accuracy becomes 0.92 and the division is already over by the time the body starts.
DON'T:
- DON'T supply the wrong number of arguments, calling
train()with no argument when one is required raises aTypeError. - DON'T assume the argument and parameter must share a name,
report(correct / total)fillsaccuracyeven though noaccuracyvariable exists outside.
Return Value
What. A return value is the result a function hands back to its caller using the return statement, so the call itself becomes that value.
Why. A function that only prints leaves you nothing to use, the number flashes on screen and is gone. A function that returns gives you a value you can store, pass on, or combine. Returning is how functions feed each other.
When. Use return whenever the caller needs the result for anything beyond looking at it, which is almost always.
This is the single most confused point for beginners, so be exact about it. print shows a value to a human and produces nothing usable. return does not show anything, it makes the call evaluate to a value that your code can capture.
# RETURN: the call becomes the value, so you can store it.
def normalize(value): # computes and HANDS BACK a result
return (value - 50) / 10 # 'return' sends this value to the caller
result = normalize(88) # 'result' now holds the returned number
print(result * 100) # we can keep computing with it# PRINT-ONLY: nothing is handed back, so there is nothing to store.
def normalize_print(value): # only shows the result
print((value - 50) / 10) # displays it, returns nothing
result = normalize_print(88) # this PRINTS 3.8, but...
print(result) # ...prints None, nothing was returnedNone is Python's "no value" placeholder, what a def function that ends without a return hands back. Printing is for humans, returning is for code, and if any later line needs the number you must return it.
A return also ends the function immediately, any lines after a return that runs are skipped, just like break leaves a loop. The one exception is a finally block, which still runs on the way out and is covered in Lesson 1.6.2.
# 'return' exits the function at once.
def classify(score): # returns a label and stops
if score >= 50:
return "pass" # function ends here if this runs
return "fail" # only reached when 'score >= 50' is False
print(classify(80)) # pass
print(classify(30)) # failDON'T:
- DON'T use
printwhen the caller needs the value, you will getNoneback and a confusing bug. - DON'T put live code directly under an unconditional
returnin the same block, it can never run, because control leaves the block at thereturn.
Function Call
What. A function call is the act of invoking a defined function by writing its name followed by parentheses, which runs its body. Why. A definition is dormant. The call is what actually executes the logic. Defining and calling are two separate events, and only the call runs the body. When. Call a function every time you want its logic to run, once, or as many times as you need.
The parentheses are the trigger. The name alone refers to the function without running it, the name with parentheses runs it. This define-now, call-later split is what lets you write a function near the top of a file and use it many times below.
# Define once...
def square(value): # definition: stored, not run
return value * value
# ...call many times, each call runs the body fresh.
print(square(2)) # 4
print(square(5)) # 25
print(square(square(3))) # call result fed into another call: 81# The name WITHOUT parentheses does not run the function.
def status(): # simple function
return "ready"
print(status) # prints something like <function status ...>, NOT run
print(status()) # prints ready, the () runs itThe parentheses are the difference between talking about a function and running it.
Many transformations you apply to data later, scale this column, drop these rows, encode these labels, will be a function, yours or a library's, defined once and called wherever it is needed.
DON'T:
- DON'T write the function name without parentheses when you mean to run it, you will get the function object, not its result.
- DON'T call a function before its
defhas run earlier in the file, the name is not bound to it yet, so you get aNameError, or whatever that name meant before, such as an older function of the same name.
Lab Exercises
Lab Exercise 1: Define and Call
Define a function named to_celsius that takes one parameter fahrenheit and returns the Celsius value using the formula (fahrenheit - 32) * 5 / 9. Call it with 212 and 32, storing each result in a variable, then print both stored results. Do not print inside the function.
Lab Exercise 2: Return Versus Print, Then Reuse
Two parts in one file.
First, write add_print(a, b) that prints a + b, and add_return(a, b) that returns a + b. Call each with 4 and 6, store both calls' results in variables, and print those two variables. Add a comment stating why one of the stored variables is None.
Then define weighted_score(score, weight) that returns score * weight, and final_grade(midterm, final) that calls weighted_score twice, midterm at weight 0.4 and final at weight 0.6, and returns the sum of the two returned values. Print final_grade(70, 90). The multiplication must appear exactly once in your file.
Solution 1: Define and Call
# Define the converter, it RETURNS, never prints.
def to_celsius(fahrenheit): # 'fahrenheit' is the parameter
return (fahrenheit - 32) * 5 / 9 # hand the computed value back
boiling = to_celsius(212) # store the returned value
freezing = to_celsius(32) # call again, store again
print(boiling) # 100.0
print(freezing) # 0.0Returning the value instead of printing it is what makes each result reusable.
DON'T:
- DON'T
printinsideto_celsiusin place of thereturn, that would make the result un-storable for any later math. - DON'T write
to_celsiuswithout()when calling,to_celsius 212is a syntax error andto_celsiusalone never runs.
Solution 2: Return Versus Print, Then Reuse
# Prints the sum, hands back nothing.
def add_print(a, b): # two parameters
print(a + b) # shows the value, no 'return'
# Returns the sum, shows nothing on its own.
def add_return(a, b):
return a + b # hands the value back to the caller
p = add_print(4, 6) # PRINTS 10, but 'p' gets None (no return)
r = add_return(4, 6) # returns 10 into 'r', prints nothing yet
print(p) # None
print(r) # 10
# 'p' is None because a function with no 'return' yields None.# Single-purpose helper: the ONLY place the multiplication is written.
def weighted_score(score, weight): # both inputs needed
return score * weight # return the weighted value
# Composes two calls to the helper above.
def final_grade(midterm, final): # higher-level function
m = weighted_score(midterm, 0.4) # 70 * 0.4 = 28.0
f = weighted_score(final, 0.6) # 90 * 0.6 = 54.0
return m + f # combine and hand back
print(final_grade(70, 90)) # 82.0final_grade never repeats the formula, it calls weighted_score twice and adds what came back. That only works because each call returns, so m and f hold real numbers rather than None.
DON'T:
- DON'T assume seeing
10on screen means you captured10,add_printshows it but returnsNone. - DON'T inline
score * weightinsidefinal_grade, reusing the helper keeps the formula in one place.
Theory Assessment
Q1: Defined, Named, Called
A file contains only def warm_up(): print("go") and nothing else, and running it prints nothing. A teammate adds the line warm_up at the bottom and it still prints nothing. What is missing, and what would print(warm_up) show instead?
Hint: Storing a body, referring to a name, and running a body are three separate events. Of the last two, only running involves parentheses.
Q2: Name the Pieces, Then Explain the Difference
In def scale(value): return value * 2 followed by scale(7), name which part is the parameter and which is the argument. Then explain how scale(2) and scale(5) can give different answers when scale is defined exactly once.
Hint: Parameters live in the definition, arguments live in the call. Ask what is fixed when you write the function and what is only filled when you run it.
Q3: The None and the Unreachable Line
A teammate writes def mean(a, b): print((a + b) / 2) then m = mean(4, 8) and is surprised print(m) shows None. Explain exactly why. Then say whether a print("done") written directly under an unconditional return, in the same block, ever runs, and why.
Hint: One question is about what a function hands back when nothing is returned. The other is about what return does to the function the instant it executes.
Q4: What Does This Print, In What Order?
Read the following without running it, then state every line of output in order.
def scale(value):
print(value * 2)
def report(x):
return "result: " + str(x)
print(report(scale(5)))Explain why the output is what it is, naming which call happens first and what value moves between the two functions.
Hint: An argument is fully evaluated before the call it belongs to happens, so work out what scale(5) evaluates to rather than what it displays. Those are two different things here.
Answer 1:
Parentheses are missing. def warm_up(): ... stores the body under the name, and the bare line warm_up only refers to that stored function without running it, so still nothing executes. Adding warm_up() is what calls it and prints go. print(warm_up) shows the function object itself, something like <function warm_up at 0x...>, because printing the name displays the function rather than running it.
DON'T write a function's name alone and expect the body to run, the () is the trigger and nothing else is.
Answer 2:
value is the parameter, the name inside the definition's parentheses. 7 is the argument, the value supplied at the call. The parameter is the placeholder, the argument is what fills it.
They give different answers because only the body is fixed at definition time. The parameter value is empty until a call fills it, so scale(2) fills it with 2 and scale(5) fills it with 5, and the same body value * 2 produces 4 then 10. The definition is the template, each call supplies the data.
DON'T swap the terms, "parameter" belongs to the definition and "argument" belongs to the call.
Answer 3:
mean uses print, not return, so it displays 6.0 on screen and hands back nothing. A function that finishes without a return evaluates to None, so m = mean(4, 8) stores None. The 6.0 flashed during the call and was never captured. Changing the body to return (a + b) / 2 fixes it.
The print("done") never runs. Once a return executes, no later line of its block runs, and control is on its way back to the caller.
DON'T conclude the value was lost to a bug, it was printed instead of returned, which is a different mistake with a different fix.
Answer 4:
The output is two lines:
10
result: Nonereport(scale(5)) cannot call report until it knows what to pass it, so scale(5) runs first. Inside it, print(value * 2) displays 10, which is the first line. But scale has no return, so the call evaluates to None, and that None is what is handed to report as x. report then returns the string "result: None", which the outer print displays as the second line.
The trap is that 10 appears on screen, so it looks like 10 is what moved between the functions. It is not. What moved was None, because displaying a value and handing a value back are unrelated actions, and only the second one fills a parameter.
DON'T read a printed number as evidence that a value was passed on, the only thing that reaches the next function is what return sent.
Assignment
Assignment: A Rounding Helper and a Report That Survives Composition
Build the following in grades.py, in this order.
Define round_to(value, places) returning value rounded to places decimals with the built-in round. Then give places a default of 2 in the signature, so the second argument becomes optional, and call round_to both with and without it.
Define weighted_score(value, weight) returning value * weight, and course_total(quiz, project, exam) that calls weighted_score three times, quiz at 0.2, project at 0.3 and exam at 0.5, and returns the total. The multiplication is written exactly once in your file.
Define summary(score) returning a single line of text built with an f-string, holding both score and round_to(score / 3) with the default in play. Print summary(course_total(50, 80, 70)), composing all four functions in one expression.
Finally, demonstrate the failure rather than describing it. Write broken_total identical to course_total except that its last line prints the total instead of returning it, then call summary(broken_total(50, 80, 70)) and print what comes out. Add a comment stating what summary actually received and why.
Hints:
- A default is written in the signature as
def round_to(value, places=2):, after whichround_to(3.14159)supplies2on its own, and an f-string is a string prefixed withfwhere{...}holds an expression that is evaluated and dropped into the text. - For the last part, work out what
broken_total(50, 80, 70)evaluates to rather than what it displays, then trace that value intosummary's parameter before you run anything.
Solution:
# 'round' is a built-in function: no import required.
# 'places=2' is a DEFAULT, used when the caller omits the second argument.
def round_to(value, places=2): # default parameter
return round(value, places) # round to the requested decimals
# One weighted term, the ONLY place the multiplication is written.
def weighted_score(value, weight): # helper
return value * weight
# Sums three weighted terms by calling the helper three times.
def course_total(quiz, project, exam): # composes three calls
q = weighted_score(quiz, 0.2) # 50 * 0.2 = 10.0
p = weighted_score(project, 0.3) # 80 * 0.3 = 24.0
e = weighted_score(exam, 0.5) # 70 * 0.5 = 35.0
return q + p + e # 69.0
# An f-string: the 'f' prefix makes each {...} evaluate and drop into the text.
def summary(score): # one parameter
# default 'places' applies
return f"score={score}, third={round_to(score / 3)}"
print(round_to(3.14159, 4)) # 3.1416
print(round_to(3.14159)) # 3.14
print(summary(course_total(50, 80, 70))) # score=69.0, third=23.0# Identical to 'course_total' except the last line PRINTS instead of returning.
def broken_total(quiz, project, exam): # the one-word difference that breaks it
q = weighted_score(quiz, 0.2)
p = weighted_score(project, 0.3)
e = weighted_score(exam, 0.5)
print(q + p + e) # displays 69.0, hands back nothing
print(summary(broken_total(50, 80, 70)))
# 'summary' receives None, because 'broken_total' printed 69.0 and returned nothing.Running that second block prints the total, then dies:
69.0
Traceback (most recent call last):
File "/home/goku/dev/bootcamp/grades.py", line 33, in <module>
print(summary(broken_total(50, 80, 70)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/goku/dev/bootcamp/grades.py", line 20, in summary
return f"score={score}, third={round_to(score / 3)}"
~~~~~~^~~
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'round(value, places) with places=2 in the signature is the whole default mechanism, the caller's second argument overrides it and its absence supplies it.
The traceback is the point of the assignment. broken_total computes the correct total and displays it, so 69.0 appears and everything looks like it worked. But print is not return, so the call evaluated to None, summary bound score to None, and score / 3 is where it finally broke. Read the two file positions in the traceback: the error is reported inside summary, on a line that is completely correct, while the actual mistake is the missing return in broken_total, which no frame shows at all, because it had already returned before anything failed. The frame above, line 33, only shows the call that fed its None into summary. That gap is the whole reason this trap costs people hours, the stage that prints instead of returning severs the chain silently and the damage surfaces in whatever consumes it.
DON'T trust a correct-looking number on screen as proof a pipeline stage works, a stage that prints instead of returning hands None to whatever consumes it, and the error surfaces in the consumer rather than in the stage that caused it.
Additional Resources
Optional, and nothing later depends on either one. Worth a look if this lesson interested you.
- YouTube - "Python Tutorial for Beginners 8: Functions" by Corey Schafer. A slightly deeper walkthrough of
def, parameters, andreturnwith live examples. - Official docs - Python Language Reference, "The return statement". The precise rule for what
returndoes, including theNonea barereturnhands back.
Summary
A function is the first real tool for managing complexity. Before this lesson, in this module, a program was one long, top-to-bottom script, and any logic you needed twice had to be copied, with every copy a fresh chance for the two to drift apart. With def you give a block of logic a name and store it for later, turning a program from a single script into an arrangement of named, reusable units. That shift, from writing everything inline to assembling small named pieces, is the quiet beginning of every large program you will ever build.
The two ideas worth carrying forward are subtle precisely because they are invisible. Defining a function is not running it, the body sits dormant until a call with parentheses triggers it, which is why a file full of def statements can print nothing at all. And returning a value is not the same as printing one, printing speaks to a human and hands your code back nothing, while returning makes the call itself become a value you can store, combine, and pass onward. The beginner who confuses these two spends hours puzzled by a None that is not a bug at all.
Everything ahead builds on this. The next lesson refines how arguments bind to parameters, giving you keyword and default arguments. After that comes scope, where you will see exactly which names a function can and cannot touch. And then composition, where functions that return cleanly snap together into pipelines. Each step assumes the reflex you built here: define once, call by name, and hand results back so the rest of your program can use them.
Cheat Sheet
| Concept | Syntax | Notes | When to Avoid |
|---|---|---|---|
| Define | def name(params): |
Stores the body, does not run it | Never, it is how a function is defined by name |
| Parameter | def f(x): |
Placeholder name in the definition | When the function needs no input |
| Argument | f(7) |
Actual value supplied at the call | When the parameter has a usable default |
| Return | return value |
Call becomes the value, exits function | When the caller only needs a printed display |
| Call | f(7) |
Name + () runs the body |
Before the def has run in the file |
| No return | (omit return) |
Function yields None |
When the caller needs the result |
def normalize(value, center): # define: parameters 'value', 'center'
return (value - center) / 10 # hand result back to caller
r = normalize(88, 50) # call with arguments, 'r' = 3.8
print(r) # use the returned value
def show(x): # print-only function
print(x) # shows it, returns None
s = show(5) # prints 5, 's' is None
print(normalize(73, 50)) # define once, call many times