Python Functions for Beginners
If you've written even a few lines of Python, you've already used functions. print() is one. So are len(), input() and int(). Somebody wrote them, gave each one a name, and now you can use them without ever looking at the code inside.
This article is about writing your own. It's the point where most people stop copying snippets from tutorials and start building things that are actually theirs, so we'll take it slowly. We'll start with a function that does almost nothing, finish with a small program that answers real questions about Mars and the Moon, and stop along the way at the mistakes I see beginners make every week.
Every example below runs as it is. Open the editor and type along rather than just reading. Typing the code yourself sticks far better, and you'll make a few typos that teach you more than the article does.
What a function in Python actually is
A function is a named block of code that does one job. You write it once, and after that you run it just by using its name.
Think of a pre-launch checklist. Nobody at mission control rewrites it before each launch. It lives in one place, it has a name, and when someone says "run the pre-launch checklist", everyone knows exactly which steps that means. Functions work the same way. You describe the steps once, give them a name, and from then on the name is enough.
Why bother? A few reasons:
- You stop repeating yourself. If you've copy-pasted the same five lines into three places, those lines are asking to become a function.
- Fixes happen in one place. Find a bug inside a function and you fix it once. Find a bug in copy-pasted code and you get to hunt down every copy. You will miss one. Everybody does.
- Your code starts to read like a plan. A line like
fuel = calculate_fuel(distance)tells you what's going on. Twenty lines of arithmetic don't.
Your first function
def greet_crew():
print("Good morning, crew.")
print("All systems are go.")
Line by line:
deftells Python you're about to define a function. It's short for "define".greet_crewis the name. The rules are the same as for variable names: lowercase letters, words joined with underscores, no spaces, and it can't start with a number.- The parentheses
()are where inputs will go later. This function doesn't take any, but the parentheses still have to be there. - The colon at the end of the line is required. Forgetting it is probably the most common syntax error in this whole topic.
- Everything indented under the
defline is the body of the function. Python uses that indentation (four spaces is the convention) to work out where the function starts and where it ends.
Now run it. Nothing happens.
That's not a bug, although it confuses almost everyone the first time. Defining a function is like writing a recipe on a card. Writing the card doesn't cook anything. To actually run the code, you call the function by writing its name followed by parentheses:
def greet_crew():
print("Good morning, crew.")
print("All systems are go.")
greet_crew()
greet_crew()
Output:
Good morning, crew.
All systems are go.
Good morning, crew.
All systems are go.
Two calls, two greetings. You could call it a hundred times and you'd still only have written the steps once.
One rule to remember: Python reads your file from top to bottom, so the def has to come before the first call. Put greet_crew() above its definition and you'll get NameError: name 'greet_crew' is not defined, because at that moment Python genuinely hasn't heard of it yet.
Passing data in: parameters and arguments
A function that does exactly the same thing every time has its uses, but most of the time you want to hand it some information and let it work with that.
def greet_astronaut(name):
print(f"Welcome aboard, {name}.")
greet_astronaut("Ada")
greet_astronaut("Yuri")
Output:
Welcome aboard, Ada.
Welcome aboard, Yuri.
Here name is a parameter. Think of it as an empty slot, a variable that only exists while the function is running. When you call greet_astronaut("Ada"), the value "Ada" is the argument. Python drops it into the name slot for that one run, and on the next call the slot gets whatever you pass in next.
People mix up "parameter" and "argument" all the time, including people who've been writing code for years, so don't lose sleep over it. The short version: the parameter is the slot in the definition, the argument is the actual value you send in when you call.
More than one parameter
Separate parameters with commas:
def describe_planet(name, position):
print(f"{name} is planet number {position} from the Sun.")
describe_planet("Mars", 4)
describe_planet("Earth", 3)
Output:
Mars is planet number 4 from the Sun.
Earth is planet number 3 from the Sun.
Arguments are matched to parameters by position: the first argument goes into the first parameter, the second into the second, and so on. Swap them and Python won't complain. It will just do what you told it to:
describe_planet(4, "Mars")
4 is planet number Mars from the Sun.
Python has no idea that "Mars" is supposed to be a name and 4 a number. Keeping the order straight is your job (there's a way to make this safer, and we'll get to it shortly).
Pass the wrong number of arguments, though, and Python stops you straight away:
greet_astronaut()
TypeError: greet_astronaut() missing 1 required positional argument: 'name'
As error messages go, this is a kind one. It tells you which function, how many arguments are missing and what they're called.
return: getting a result back
If you read only one section of this article twice, make it this one. The difference between print and return catches nearly every beginner I've taught. It usually shows up as "my function works, I can see the answer, but I can't do anything with it".
Here's a function that works out what a bathroom scale would show on Mars. Gravity there is about 38% of Earth's, so we multiply by 0.38. (Physics teachers, I know your mass doesn't change. The number on the scale does, and that's what we're calculating.)
def weight_on_mars(earth_weight):
print(earth_weight * 0.38)
weight_on_mars(70)
26.6
Looks fine. Now try to use that number for something, like adding 5 kg of equipment to it:
result = weight_on_mars(70)
print(result + 5)
26.6
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
The function printed 26.6, so the number clearly exists. So why is result empty? Because print() only shows a value on the screen. It doesn't hand anything back to the code that called the function. A function that never uses return gives back a special value called None, which means "nothing here". And you can't add 5 to nothing.
Here's the fixed version:
def weight_on_mars(earth_weight):
return earth_weight * 0.38
mars_weight = weight_on_mars(70)
print(mars_weight)
print(mars_weight + 5)
26.6
31.6
The way I explain it in class: print is mission control reading a number out over the radio. Everyone hears it, nobody writes it down, and a second later it's gone. return is handing someone a sheet of paper with the number on it. They can keep it, do maths with it or pass it on to someone else.
A good rule of thumb: if a function calculates something, it should return the result and let the code that called it decide what happens next, whether that's printing it, saving it or feeding it into another calculation. Functions that keep the calculating separate from the displaying are much easier to reuse.
return also ends the function
The moment Python hits return, the function stops. Anything after it is skipped for that run. That makes it handy for checks:
def check_fuel(level):
if level < 20:
return "Abort: fuel too low"
return "Fuel OK"
print(check_fuel(12))
print(check_fuel(85))
Abort: fuel too low
Fuel OK
Notice there's no else. If the fuel is low, the first return ends the function and the last line never runs. If it isn't low, Python skips the if block and carries on to return "Fuel OK". You'll see this pattern everywhere in real code.
Default values
Sometimes a parameter has a value that's right most of the time. You can make it the default, and then callers only pass it when they want something different:
def weight_on_planet(earth_weight, gravity=0.38):
return round(earth_weight * gravity, 1)
print(weight_on_planet(70)) # no gravity given, so Mars (0.38) is used
print(weight_on_planet(70, 0.165)) # the Moon
26.6
11.6
I've also wrapped the result in round(..., 1). Computers sometimes produce numbers like 26.599999999999998 when multiplying decimals, and rounding to one decimal place keeps the output readable.
One rule: parameters with defaults must come after the ones without. Writing def weight_on_planet(earth_weight=70, gravity): gives you a SyntaxError saying a parameter without a default follows one with a default. If you think about it, Python would have no way of knowing which slot a single argument was meant for.
Keyword arguments
Remember the swapped "Mars" and 4? Keyword arguments solve that. Instead of relying on position, you name the parameter when you call the function:
def launch(rocket, destination, crew_size):
print(f"Launching {rocket} to {destination} with {crew_size} crew.")
launch("Kestrel", "the Moon", 4)
launch(destination="Mars", crew_size=6, rocket="Halcyon")
launch("Kestrel", crew_size=3, destination="the Moon")
Launching Kestrel to the Moon with 4 crew.
Launching Halcyon to Mars with 6 crew.
Launching Kestrel to the Moon with 3 crew.
With keyword arguments the order doesn't matter, and the call explains itself. Six months from now, launch("Halcyon", "Mars", 6) might leave you wondering what the 6 was for. crew_size=6 won't.
You can mix both styles, as in the third call. The only rule is that positional arguments go first.
Returning more than one value
A function can send back several values at once. Here's one that works out how long a radio signal takes to reach Mars:
def signal_delay(distance_km):
speed_of_light = 299_792 # km per second
seconds = distance_km / speed_of_light
minutes = seconds / 60
return seconds, minutes
sec, mins = signal_delay(225_000_000)
print(f"A message to Mars takes about {round(mins, 1)} minutes.")
A message to Mars takes about 12.5 minutes.
A couple of things to point out. The underscores in 299_792 and 225_000_000 are only there to make big numbers easier to read; Python ignores them. And 225 million km is a typical distance to Mars, but the real figure keeps changing as both planets travel around the Sun, from roughly 55 million km to about 400 million. That's why a command sent to a Mars rover can take anything from about 3 minutes to over 20 to arrive.
When you write return seconds, minutes, Python packs both values into a tuple, a small fixed bundle of values. If you printed the whole result, you'd see (750.5203607834766, 12.50867267972461). The line sec, mins = signal_delay(...) unpacks that bundle into two separate variables in one go. Just make sure the number of names on the left matches the number of values coming back.
Scope: what happens in a function stays in it
def calculate_orbit():
altitude = 400
print(f"Inside the function, altitude is {altitude} km")
calculate_orbit()
print(altitude)
Inside the function, altitude is 400 km
NameError: name 'altitude' is not defined
The International Space Station orbits roughly 400 km up, but outside this function, altitude doesn't exist. Variables created inside a function are local. They appear when the function starts and are thrown away when it finishes. That's a good thing. It means two different functions can each have a variable called total without stepping on each other's toes.
The other direction is where people get stuck. A function can read a variable from outside, but if it tries to change one, things go wrong:
fuel = 100
def burn(amount):
fuel = fuel - amount
burn(30)
UnboundLocalError: cannot access local variable 'fuel' where it is not associated with a value
Because there's an assignment to fuel inside the function, Python treats fuel as a local variable for that whole function. Then it finds you're trying to subtract from it before it has a value. You'll find forum answers telling you to fix this with the global keyword. My advice is to skip that for now and do this instead:
def burn(fuel, amount):
return fuel - amount
fuel = 100
fuel = burn(fuel, 30)
print(fuel)
70
Data goes in through parameters, results come out through return. Stick to that habit and your functions stay predictable. You can test each one on its own without worrying about what the rest of the program is up to.
Naming and docstrings
Good function names are usually verbs, or short phrases that start with one, because a function does something: calculate_fuel, get_planet_name, print_report. For functions that answer a yes or no question, starting with is_ or has_ reads nicely: if is_ready_for_launch(crew): is almost plain English. Names like do_stuff, func2 or x will mean nothing to you next month.
Here's a test I like: if you can't describe what a function does without using the word "and", it's probably doing two jobs and would be easier to work with as two functions.
You can also describe a function with a docstring, a string in triple quotes placed right under the def line:
def weight_on_planet(earth_weight, gravity=0.38):
"""Return what something would weigh on another planet.
gravity is the planet's surface gravity compared to Earth's.
Mars is about 0.38, the Moon about 0.165.
"""
return round(earth_weight * gravity, 1)
help(weight_on_planet)
Help on function weight_on_planet in module __main__:
weight_on_planet(earth_weight, gravity=0.38)
Return what something would weigh on another planet.
gravity is the planet's surface gravity compared to Earth's.
Mars is about 0.38, the Moon about 0.165.
Python doesn't run the docstring, it stores it. help() prints it back along with the parameters and their defaults, and most code editors show it when you hover over the function name. For small functions a single line is plenty.
Mistakes I see every week
1. Forgetting the parentheses
greet_crew without parentheses doesn't call the function. It just refers to it. Python won't complain, it simply won't run anything. If you print it, you'll get something cryptic like this:
print(greet_crew)
The number at the end will be different on your machine. If you ever see this in your output, you've left off the () somewhere.
2. "None" showing up in the output
This one is a close relative of the print-versus-return confusion. It happens when you print the result of a function that already prints:
def greet_crew():
print("Good morning, crew.")
print(greet_crew())
Good morning, crew.
None
The function prints its greeting, then returns None, and the outer print dutifully prints that too. Either call the function without the outer print, or change the function so it returns the text instead of printing it.
3. return indented one level too far
def total_fuel(tanks):
total = 0
for tank in tanks:
total = total + tank
return total
print(total_fuel([100, 250, 80]))
100
The answer should be 430. Because return sits inside the for loop, the function ends during the very first pass. Move it back one level so it lines up with for:
def total_fuel(tanks):
total = 0
for tank in tanks:
total = total + tank
return total
Now it waits until the loop has finished. When a function gives you an answer that's suspiciously small, check where the return is indented before anything else.
4. Writing a function and never calling it
"My program runs but nothing happens." Nine times out of ten, the function is defined perfectly and there's simply no line that calls it. Scroll to the bottom of your file and check.
Putting it all together
Here's a small program that uses most of what we've covered. It prints a short report for crew members heading to different destinations: what their scale would read there, and how long a radio message takes to get home.
SPEED_OF_LIGHT = 299_792 # km per second
def weight_on_planet(earth_weight, gravity):
"""Return the weight on another world, rounded to 1 decimal place."""
return round(earth_weight * gravity, 1)
def signal_delay_seconds(distance_km):
"""Return how many seconds a radio signal needs to travel distance_km."""
return distance_km / SPEED_OF_LIGHT
def format_delay(seconds):
"""Turn a number of seconds into readable text."""
if seconds < 60:
return f"{round(seconds, 1)} seconds"
return f"{round(seconds / 60, 1)} minutes"
def mission_report(name, earth_weight, destination, gravity, distance_km):
"""Print a short report for one crew member."""
weight = weight_on_planet(earth_weight, gravity)
delay = format_delay(signal_delay_seconds(distance_km))
print(f"Mission report: {name}")
print(f" Destination: {destination}")
print(f" Scale reading there: {weight} kg")
print(f" Radio delay to Earth: {delay}")
print()
mission_report("Ada", 62, "Mars", 0.38, 225_000_000)
mission_report("Yuri", 75, "the Moon", 0.165, 384_400)
Mission report: Ada
Destination: Mars
Scale reading there: 23.6 kg
Radio delay to Earth: 12.5 minutes
Mission report: Yuri
Destination: the Moon
Scale reading there: 12.4 kg
Radio delay to Earth: 1.3 seconds
Take a minute to read mission_report. It doesn't do any maths itself. It asks the other functions for answers and arranges them on the screen. Each of the smaller functions does one job and can be tested on its own. Try print(format_delay(90)) in the editor and check you get 1.5 minutes before you even look at the report.
This split pays off when things change. Want the delay shown in hours for something as far away as Neptune? You change format_delay and nothing else. Want to add Jupiter? Add one more mission_report call with its gravity (about 2.53) and its distance from Earth, which I'll leave you to look up. Bigger programs are built mostly this way, out of small functions that each do one thing and call each other.
The capital letters in SPEED_OF_LIGHT are a Python convention for values that are set once and never change. Python doesn't enforce it. It's a note to anyone reading your code, future you included.
Projects to try next
Reading about functions only gets you so far. The real learning starts when you're staring at your own code wondering why it returned None. Here are six projects, roughly from easiest to hardest. Do at least the first two, even if they look too simple.
1. Planetary scale
Ask the user for their weight with input(), then show what a scale would read on the Moon, Mars and Jupiter. Write one function that does the calculation and a separate one that prints the results.
Hint: input() always gives you text, so convert it with float() before doing any maths. Stretch goal: add a function that rejects negative numbers and asks again.
2. Countdown
Write countdown(start) that prints the numbers from start down to 1, followed by "Liftoff!". Then add a second parameter for that final message, with "Liftoff!" as the default.
Stretch goal: add a step parameter so that countdown(100, step=10) counts 100, 90, 80 and so on.
3. Temperature converter
Scientists and space agencies often record temperatures in kelvin. Write three small functions: celsius_to_kelvin, kelvin_to_celsius and celsius_to_fahrenheit. Each takes one number and returns one number. No printing inside them.
Hint: kelvin is Celsius plus 273.15, and Fahrenheit is Celsius times 9/5, plus 32. Stretch goal: write kelvin_to_fahrenheit by calling two of the functions you already have, instead of writing a new formula.
4. Callsign generator
Write make_callsign(first_name, last_name) that returns something like "ADA-LOV": the first three letters of each name, in capitals, joined with a dash.
Hint: look up string slicing (name[:3]) and the .upper() method. Stretch goal: use the random module to add two random digits at the end.
5. Go / no-go check
Write is_go_for_launch(fuel, wind_speed, crew_ready) that returns True only if fuel is at least 90%, wind speed is under 30 km/h and the crew is ready. Otherwise it returns False. Then write a second function that calls it and prints a clear "GO" or "NO GO".
Hint: early return statements, like in check_fuel above, keep this tidy. Stretch goal: make the second function also say why a launch is a no-go.
6. Mission quiz
Write ask_question(question, correct_answer) that shows a question, reads the user's answer and returns True or False. Then write run_quiz() that asks five questions, keeps score and prints the result at the end.
Hint: use .strip().lower() on both answers before comparing them, so "Mars", "mars " and "MARS" all count as correct. This project pulls together almost everything in this article, so take your time with it.
FAQ
What's the difference between a parameter and an argument?
A parameter is the name in the function definition, like name in def greet_astronaut(name):. An argument is the actual value you pass when you call the function, like "Ada". In everyday conversation people use the words interchangeably and nobody will mind.
When should I turn code into a function?
Two signals. You've written the same lines, or nearly the same lines, more than once. Or you can give a chunk of code a clear name. Even if print_report() only runs once, moving that code into a function makes the main part of your program shorter and easier to follow. Don't swing too far the other way, though. A function should do a job that's worth naming.
Does every function need a return statement?
No. Functions that do something, like printing a report or saving a file, often don't return anything, and that's fine. Python quietly returns None for you. Functions that work something out should almost always return their result.
Why does my output have "None" in it?
Almost always because you printed the result of a function that doesn't return anything, for example print(greet_crew()). The function does its own printing, then hands back None, and your outer print shows it. See mistake number 2 above.
Can a function call another function?
Yes, all the time. mission_report above calls three of them. A function can even call itself, which is called recursion. It's very useful for certain problems, but it's a topic for later.
How many parameters can a function have?
There's no practical limit, but once you're past four or five, calls get hard to read and easy to get wrong. A long list of parameters is often a sign that the function is trying to do too many jobs. When you really do need several, keyword arguments make the calls much clearer.
What's the difference between a function and a method?
A method is a function that belongs to an object, and you call it with a dot: "mars".upper() or crew.append("Ada"). Under the hood they work the same way. They take arguments and can return values. You'll write your own methods once you get to classes.
What are *args and **kwargs?
They let a function accept any number of positional or keyword arguments. print() works like this, which is why you can pass it as many values as you like. You'll see them in other people's code, but you don't need them yet. Get comfortable with ordinary parameters first.
What is a lambda?
A tiny function without a name, written on one line, like lambda x: x * 0.38. It's mostly used when you need a quick throwaway function, for example when sorting a list. Anything a lambda can do, a normal def can do too, and the def version is usually easier to read. Leave lambdas for later.
Should I use global variables inside functions?
As a beginner, try not to. Reading a fixed value like SPEED_OF_LIGHT is fine. Changing global variables from inside functions is what makes bugs hard to track down, because any function anywhere might have changed the value. Pass data in as arguments and send results back with return.
That's a lot for one sitting, so don't worry if it hasn't all settled yet. If you remember only two things, make it these: a function does nothing until you call it, and return hands a value back while print only shows it. The rest comes with practice, and the projects above will give you plenty of that.
SPEED_OF_LIGHT = 299_792 # km per second
def weight_on_planet(earth_weight, gravity):
"""Return the weight on another world, rounded to 1 decimal place."""
return round(earth_weight * gravity, 1)
def signal_delay_seconds(distance_km):
"""Return how many seconds a radio signal needs to travel distance_km."""
return distance_km / SPEED_OF_LIGHT
def format_delay(seconds):
"""Turn a number of seconds into readable text."""
if seconds < 60:
return f"{round(seconds, 1)} seconds"
return f"{round(seconds / 60, 1)} minutes"
def mission_report(name, earth_weight, destination, gravity, distance_km):
"""Print a short report for one crew member."""
weight = weight_on_planet(earth_weight, gravity)
delay = format_delay(signal_delay_seconds(distance_km))
print(f"Mission report: {name}")
print(f" Destination: {destination}")
print(f" Scale reading there: {weight} kg")
print(f" Radio delay to Earth: {delay}")
print()
mission_report("Ada", 62, "Mars", 0.38, 225_000_000)
mission_report("Yuri", 75, "the Moon", 0.165, 384_400)