Lesson 6 — Functions

By the end of this you can: give a piece of work a name, hand it the values it needs, get an answer back out of it, and change a variable that lives outside it — which is the one a game cannot do without.

The same four lines, three times

Here is a program that works and is still wrong:

health = 100

health = health - 30
if health < 0:
    health = 0
print("Health:", health)

health = health - 45
if health < 0:
    health = 0
print("Health:", health)

health = health - 40
if health < 0:
    health = 0
print("Health:", health)
Health: 70
Health: 25
Health: 0

The same four lines, written out three times. Everything you have learnt so far makes programs longer; this is the lesson where they start getting shorter.

And length is the least of it. Suppose you decide that health should stop at 1 rather than 0. There are three places to change and nothing to remind you of the third — so the bug you ship is a player who can survive two hits and dies on the wrong one.

Writing one

A function is a piece of work with a name on it:

def say_hello():
    print("Hello!")
    print("Ready to play?")


say_hello()
say_hello()
Hello!
Ready to play?
Hello!
Ready to play?

Two separate things happen there, and mixing them up is the commonest confusion in the lesson:

  • def say_hello(): and the indented lines under it define the function. Nothing runs. Python reads it, notes that say_hello now means those two lines, and moves on.
  • say_hello() calls it. Now the lines run.

The shape is the shape you already know from if and for: a line ending in a colon, and an indented block underneath that belongs to it.

The brackets are what make it happen. Written without them, the name is just the function itself sitting there doing nothing:

def say_hello():
    print("Hello!")


say_hello
print("Finished")
Finished

No error, no greeting. A line that is only a name is legal Python and has no effect, which is why a missing pair of brackets is so hard to spot: nothing tells you, the program simply does less than you thought. Keep that one in mind — lesson 7 is built on it.

Giving it something to work with

say_hello says the same thing every time, which is not much use. Put a name in the brackets and it becomes a blank the caller fills in:

def greet(name):
    print("Hello, " + name + "!")


greet("Ada")
greet("Grace")
Hello, Ada!
Hello, Grace!

name is a parameter. Inside the function it behaves like any other variable; the difference is where its value comes from. greet("Ada") puts "Ada" into it and then runs the body.

More than one goes in a comma-separated list, and they are filled in order:

def report(name, score, lives):
    print(name, "has", score, "points and", lives, "lives")


report("Ada", 120, 3)
report("Grace", 80, 1)
Ada has 120 points and 3 lives
Grace has 80 points and 1 lives

Order, not meaning. report(120, "Ada", 3) runs perfectly happily and prints nonsense, because Python has no idea which of them was supposed to be a name.

Give the wrong number and it does stop you:

report("Ada", 120)
TypeError: report() missing 1 required positional argument: 'lives'

Which is a friendlier error than most: it names the function, says how many are missing, and tells you which one.

Getting an answer back

So far these functions do something. Often you want one that works something out and hands it back:

def damage(health, hit):
    left = health - hit
    if left < 0:
        left = 0
    return left


print(damage(100, 30))
print(damage(20, 45))
70
0

return does two things at once: it sends a value back to whoever called, and it stops the function immediately. Nothing after a return that has run will run.

Because it hands a value back, a call can be used anywhere a value can be — stored, printed, added to, passed straight into another call:

def damage(health, hit):
    left = health - hit
    if left < 0:
        left = 0
    return left


health = 100
health = damage(health, 30)
health = damage(health, 45)
health = damage(health, 40)

print("Health:", health)
Health: 0

That is the program from the top of the page, with the rule written once. Move the floor from 0 to 1 and there is exactly one line to change.

print and return are not the same thing, and this is worth being stubborn about. print puts something on the screen for a person to read. return hands a value back to the program. A function that prints its answer instead of returning it gives you nothing to work with:

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


answer = add_up(2, 3)
print("I got:", answer)
5
I got: None

The 5 appears — the function printed it — but answer is None. A function with no return hands back None, which is Python's word for "nothing here". Seeing None turn up where you expected a number nearly always means a missing return.

Where names live

A variable made inside a function belongs to that function, and disappears when it finishes:

def roll():
    result = 6
    print("Inside:", result)


roll()
print("Outside:", result)
Inside: 6
NameError: name 'result' is not defined

This is a feature and not an obstacle. It means you can use i or count or result inside a function without checking whether some other part of the program already used that name. Each function gets its own set, and they cannot collide.

Reading a variable from outside does work:

score = 7


def show():
    print("Score is", score)


show()
Score is 7

global — changing something outside

Reading works. Now try changing:

score = 0


def add_point():
    score = 1


add_point()
print(score)
0

Read that again, because there is no error and the answer is wrong. The function ran. It assigned 1. And score is still 0.

Here is why. Assigning to a name inside a function makes that name local to the function — every time, whether or not a variable of the same name exists outside. So score = 1 did not touch the outer score at all; it made a second, private score, put 1 in it, and threw it away when the function ended.

One word fixes it:

score = 0


def add_point():
    global score
    score = score + 1


add_point()
add_point()
add_point()

print(score)
3

global score says: when I write score in here, I mean the one outside, not a new one. It goes at the top of the function, once, and names the variables — global score, lives for two.

This is the line that makes games work. Everything interesting in a game happens inside a function that the engine calls for you: something is hit, a key goes down, a second passes. Those functions need to change the score, the health, the level. Without global they change a private copy and the game silently never moves — which looks exactly like the collision not being detected, and sends people hunting in completely the wrong place.

You only need it when you assign. Reading needs nothing, and neither does changing something inside an object or a list:

scores = [10, 20]


def add_score(n):
    scores.append(n)


add_score(30)
print(scores)
[10, 20, 30]

No global, and it worked. scores.append(30) is not an assignment to scores — it is a message to the list that scores already points at. Write scores = [10, 20, 30] in there instead and you are back to the silent local.

Now put it on the screen

A scoreboard with one function that knows how to change it:

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])

score = 0
label = add([text("Score: 0", size=32), pos(40, 40)])


def add_points(amount):
    global score
    score = score + amount
    label.text = "Score: " + str(score)


add_points(10)
add_points(25)
add_points(100)

print("Final score:", score)
Final score: 135

Three calls, one rule. The function is the only place that knows a score is kept in score and shown in label, so those two can never drift apart — which they absolutely will if every part of the game updates both by hand.

And it works the same when the caller is the engine rather than you:

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])

score = 0
label = add([text("Score: 0", size=32), pos(40, 40)])


def add_points(amount):
    global score
    score = score + amount
    label.text = "Score: " + str(score)


def tick():
    add_points(1)


loop(0.5, tick)

loop(0.5, tick) hands the engine your function and asks it to call it twice a second. Note the name with no brackets after it: you are giving the engine the function, not the result of running it. loop(0.5, tick()) would run tick once, immediately, and hand the engine the None it returned.

Passing a function by name is worth getting comfortable with now, because lesson 7 is an easier way of writing exactly this.

Watch for

Defining is not calling. def only describes the work; the brackets do it.

A name with no brackets does nothing and says nothing. Look for it when a function seems not to have run.

Arguments go in by position, not by what they are called.

print shows, return hands back. An unexpected None is a missing return.

Assigning inside a function makes a local unless you said global. This one fails silently.

Two blank lines before a def, by convention. Not a rule, but every Python program you meet will be written that way.

Classwork 1 — The Dice Roller

Start here and press Run:

def roll(sides):
    return sides


print(roll(6))
print(roll(20))
6
20

It does not roll anything yet — it hands back the number you gave it. Fix that, one step at a time:

  1. Make it a real roll. from kaypy import * at the top gives you randi(1, sides), a whole number between 1 and sides. Return that instead. Run it a few times; the numbers should change.
  2. Write a second function, roll_twice(sides), that calls roll twice and returns the total. It must call roll — do not write the randomness again.
  3. Write best_of_three(sides), which rolls three times and returns the highest. if from lesson 3, or a list and max() from lesson 5.
  4. Count how often a 20-sided die beats 15. Make wins = 0 outside, and a function that rolls once and adds to wins when it is over 15. Call it fifty times in a loop and print wins.

Step 4 needs global. Try it once without, see the 0, and then add the line — it is worth meeting that failure on purpose, while you know what it is, rather than for the first time inside a game.

Stretch: give roll a second parameter, lucky, that adds 1 to the result when it is True. Then call it both ways from the same program.

Classwork 2 — The Spawner

One function that builds a thing, called over and over.

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])
loadSprite("coin", "images/coin.png")


def make_coin(x, y):
    add([sprite("coin"), pos(x, y), area(), "coin"])


make_coin(120, 200)
make_coin(300, 200)
make_coin(480, 200)
  1. Run it. Three coins. Now add a fourth with one more line, and notice how little that cost.
  2. Replace the three calls with a for loop that makes eight coins in a row, 200 apart, using the loop variable to work out the x.
  3. Give make_coin a third parameter, value, and return the object add gave you. Store it and print how many you made.
  4. Add a total_value variable outside and have make_coin add to it. You will need global. Print the total at the end.

Stretch: write make_row(y, how_many) that calls make_coin in a loop, then call make_row three times at three different heights. A function calling a function is not a special case — it is the whole point.

Check yourself

What does this print?

lives = 3


def lose_one():
    lives = lives - 1
    return lives


print(lives)
3
Why is there no error, and why is it 3?

Because lose_one is never called. The def was read, the function was filed away under that name, and the only line that actually runs is the print. lives has been 3 the whole time.

The interesting part is what would happen if you added lose_one() above the print. Not 2, and not a silent 3 either — an error, because lives = lives - 1 assigns to lives, which makes it local for the whole function, which means the lives being read on the right-hand side is the local one, which has nothing in it yet. Python calls that UnboundLocalError, and the fix is global lives at the top.

So this function has a bug that no amount of running the program will show you, because nothing calls it. Code that is never reached is never tested, which is worth remembering when you write a handler and nothing happens.



← Lesson 5 — Lists · All nine lessons · Lesson 7 — Decorators →