Sooner or later every beginner does it: a piece of code works, you need the same thing again a few lines later, so you select it, copy, paste, and change a name. It works. Then you need it a third time. Then the rules change and you have to hunt down every copy. A function is the cure: you write the code once, give it a name, and use that name as often as you like. This page shows what a function really is, how values get in and out of one, and why "change it in one place" is one of the most useful habits in programming.

The copy-paste trap

Say you're building a tiny weather app that shows temperatures in both Celsius and Fahrenheit. The formula is °F = °C × 9 / 5 + 32. Here is the first, copy-pasted version in Python. It runs, and it prints the right thing:

amsterdam = 18
f = amsterdam * 9 / 5 + 32
print("Amsterdam:", f, "°F")

cairo = 35
f = cairo * 9 / 5 + 32
print("Cairo:", f, "°F")

oslo = -5
f = oslo * 9 / 5 + 32
print("Oslo:", f, "°F")
Amsterdam: 64.4 °F
Cairo: 95.0 °F
Oslo: 23.0 °F

(The .0 appears because / in Python always gives a float, a number with a decimal point, even when the division comes out even.)

Nothing is wrong with this code today. The trouble starts tomorrow. The formula now lives in three places. If you want to round the results, or you spot a mistake in the formula, you must fix it three times, and you must not miss one. Add ten more cities and it's thirteen places. Copy-pasted code doesn't fail when you write it; it fails when you change it.

Define once, call many times

Here's the same program with the formula moved into a function:

def to_fahrenheit(celsius):
    return celsius * 9 / 5 + 32

def report(city, celsius):
    print(city + ":", to_fahrenheit(celsius), "°F")

report("Amsterdam", 18)
report("Cairo", 35)
report("Oslo", -5)

It prints exactly the same three lines. But the formula now exists in one place, and every city line is short and says what it means. Let's read the first function piece by piece:

defShort for define. It tells Python "I'm creating a function".
to_fahrenheitThe function's name. Same naming rules as variables: lowercase words joined with underscores.
(celsius)The parameter: a placeholder name for the value the function will be given.
:The colon ends the header. Forget it and Python stops with a SyntaxError.
indented linesThe body: the code that runs each time. Python knows where the body ends because the indentation stops (4 spaces is the convention).
return …Sends a value back to whoever asked for it, and ends the function.

One thing surprises almost everyone: defining a function does not run it. The def block only teaches Python a new word. Nothing inside it happens until you call the function, which you do by writing its name followed by parentheses: to_fahrenheit(18). Calling is like pressing the button on a machine you built earlier; you can press it as often as you want.

Parameters vs arguments

These two words get mixed up constantly, even by experienced programmers, but the difference is simple:

When the call happens, Python fills each parameter with its argument, in order, as if it had run celsius = 18 right at the top of the body. With two parameters, the first argument goes into the first slot and the second into the second: in report("Oslo", -5), city gets "Oslo" and celsius gets -5.

Those parameter names exist only inside the function, and only while it runs. They're called local variables. When the function finishes they're gone, so code outside can't see celsius. That's a feature: you can use simple names inside a function without worrying that they'll clash with names elsewhere.

If you pass the wrong number of arguments, Python tells you exactly what's missing:

def area(width, height):
    return width * height

area(3)
TypeError: area() missing 1 required positional argument: 'height'

Now try it yourself. Pick a function, type some arguments, and press Call. Then step through and watch the arguments drop into the parameters, the body run, and the return value come out of the bottom.

The function machine

arguments going in
▼
inside: parameters (local)
▼
return value coming out

Return vs print: the most common mix-up

Did you try shout in the machine above? It shows text on the screen, yet result ends up as None. That's the difference between print and return, and it trips up nearly every beginner:

A function with no return still gives something back: the special value None, Python's way of saying "nothing here". That's harmless until you try to use the result. Compare these two versions:

def to_f_print(celsius):
    print(celsius * 9 / 5 + 32)

def to_f_return(celsius):
    return celsius * 9 / 5 + 32

print(to_f_return(18) + 1)   # 65.4
print(to_f_print(18) + 1)    # prints 64.4, then crashes:
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

The print version did show 64.4, so it looked like it worked. But the value only went to the screen; the code received None, and None + 1 makes no sense. A good rule of thumb: functions that calculate something should return it, and leave the printing to whoever called them. That's exactly why our weather program has two functions: to_fahrenheit calculates and returns, report does the printing.

return also ends the function immediately. Any lines after it in the same block never run. You can have several return lines (say, one inside an if), but each call only ever goes through one of them.

Change it in one place

Time for the real payoff. Your boss sends two requests: round the temperatures to whole degrees, and add Tokyo (25 °C). Flip between the copy-pasted version and the function version, apply the changes, and count how many lines you'd have to touch. Orange lines are the ones you'd edit or add.

Refactor toggle

0lines edited, copy-pasted
0lines edited, with functions

With three cities the difference is small. But it grows with every copy: in the copy-pasted version, each change costs one edit per copy, while in the function version it costs one edit, full stop. And the "tired Friday" button shows the nastier problem: copy-pasted code can quietly get out of sync. Nothing crashes; one city is just formatted differently from the others, and nobody notices until a user does. In the function version there's nothing to forget, because there is only one copy of the formula.

Turning repeated code into a function, without changing what the program does, is called refactoring. You did it above: same output, better structure.

DRY: Don't Repeat Yourself

Programmers have a name for this idea: DRY, short for Don't Repeat Yourself. It was popularised by the 1999 book The Pragmatic Programmer by Andy Hunt and Dave Thomas, who put it as: every piece of knowledge should have a single, clear home in your code. The temperature formula is a piece of knowledge, so it should live in exactly one place.

Some signs that it's time to write a function:

Don't overdo it, though. Two lines that merely look alike but mean different things (a tax rate and a discount that happen to both be 0.2) shouldn't be forced into one function, because they'll change for different reasons. A common rule of thumb: the first time, just write it; the second time, notice; the third time, make a function.

Check yourself

Predict the answer, then click.

What does this program print?

def greet():
    print("hi")

The def only defines the function. Nobody ever calls greet(), so its body never runs.

What does this print?

def double(n):
    return n * 2

print(double(4) + 1)

double(4) runs first and is replaced by its return value, 8. Then 8 + 1 is 9.

What appears on screen?

def add(a, b):
    print(a + b)

x = add(2, 3)
print(x)

Calling add prints 5. But it has no return, so it hands back None, and that's what x holds and what the second print shows.

In def area(width, height) and the call area(3, 4), which are the arguments?

Arguments are the actual values passed in a call. width and height are the parameters: the named slots those values land in.

The short version

Next time your finger hovers over Ctrl+V, pause and ask: what would I call this? If you can answer, you've just named your next function.