Python functions practice — a temperature converter, a modular calculator and rock, paper, scissors (and the menu that stopped asking for data)

The first version of my FP1 calculator had a lovely menu and one problem: to check whether division by zero worked, I had to run the program, choose option 4, type 7, type 0… and repeat it every time I changed a line. By the tenth time I hit the wrong key, the program crashed somewhere else and I no longer knew what I was testing. At the review, the lecturer pointed at the divide function and asked me: “and why does this function read from the keyboard?”. I had no answer. The lesson I took away is the one behind this whole practice: functions that calculate shouldn’t ask for data or print it. They receive, calculate and return. Everything else goes elsewhere.

In the Python functions theory you saw def, parameters, return, the call stack and scope. Here we use all of it in three complete, medium-level programs, the usual ones but rebuilt: a temperature converter where some functions lean on others, a modular calculator that separates calculation from presentation and a rock, paper, scissors game that cleans up what the player “types”. None of them uses input(): the data is written at the end of each program, as if it were what the user typed (with capitals, spaces and the odd typo), so you can run them and get exactly the same output as here.

Before you start

It’s worth being clear on:

  • How to define and call a function, and that def doesn’t run the body.
  • The difference between print (showing) and return (handing back), and that without return you get None.
  • That a function can call others and that each call has its own local variables.
  • Conditionals (if, elif, else) and for loops.

If any of these feels shaky, review the theory before going on.

Create a functions_practice folder with one file per program (converter.py, calculator.py, rps.py) and open them in VS Code or Thonny. Run each one before reading its explanation and compare it with the article’s output.

Program 1: modular temperature converter

A converter between Celsius, Fahrenheit and Kelvin with six options: °C to °F (1), °C to K (2), °F to °C (3), K to °C (4), a full conversion from °C (5) and °F to K (6). What makes it tricky: it has to reject options that don’t exist and impossible temperatures (below absolute zero), and we want no formula to be written twice.

The idea

We split the work into three kinds of functions:

  1. Calculation: one per formula. They take a number and return another. They don’t print.
  2. Checking: they answer a question (which scale does this option start from? is this temperature possible?) and return the answer.
  3. Presentation: convert(option, value) decides what to do and returns the text that will be shown. Only the main program prints.

And a key idea: to go from °F to K we don’t write a new formula. We already know how to go from °F to °C and from °C to K: we chain the two functions. That’s called composition.

Before coding, think about what you do more than once. Here, converting °C to °F appears in option 1 and in the full conversion: that’s a function.

The code

# Program 1 · Modular temperature converter
# No input(): the tests go at the end, each with its option and its temperature.

# --- Calculation functions: they take a number and RETURN another (they print nothing) ---
def celsius_to_fahrenheit(celsius):
    return round(celsius * 9 / 5 + 32, 2)


def celsius_to_kelvin(celsius):
    return round(celsius + 273.15, 2)


def fahrenheit_to_celsius(fahrenheit):
    return round((fahrenheit - 32) * 5 / 9, 2)


def kelvin_to_celsius(kelvin):
    return round(kelvin - 273.15, 2)


def fahrenheit_to_kelvin(fahrenheit):
    # composition: no copied formulas, we chain two functions that already work
    return celsius_to_kelvin(fahrenheit_to_celsius(fahrenheit))


# --- Checking functions: they answer a question and return the answer ---
def source_scale(option):
    """Options 1, 2 and 5 start from °C; 3 and 6, from °F; 4, from K."""
    if option == 3 or option == 6:
        return "°F"
    if option == 4:
        return "K"
    return "°C"


def is_valid_temperature(value, scale):
    """Nothing can be below absolute zero (−273.15 °C = −459.67 °F = 0 K)."""
    if scale == "°C":
        return value >= -273.15
    if scale == "°F":
        return value >= -459.67
    return value >= 0


# --- Presentation functions: they prepare the text that will be shown ---
def full_conversion(celsius):
    """The three scales at once and a round-trip check (°C → °F → °C)."""
    fahrenheit = celsius_to_fahrenheit(celsius)
    back = fahrenheit_to_celsius(fahrenheit)    # if everything is right, it comes back to the starting value
    return f"{celsius} °C = {fahrenheit} °F = {celsius_to_kelvin(celsius)} K · round trip: {back} °C"


def convert(option, value):
    """Returns the text of the chosen conversion (1-6) or a warning if something is off."""
    if option < 1 or option > 6:
        return f"Option {option} not valid: choose from 1 to 6"
    scale = source_scale(option)
    if not is_valid_temperature(value, scale):
        return f"{value} {scale} is below absolute zero"
    if option == 1:
        return f"{value} °C = {celsius_to_fahrenheit(value)} °F"
    if option == 2:
        return f"{value} °C = {celsius_to_kelvin(value)} K"
    if option == 3:
        return f"{value} °F = {fahrenheit_to_celsius(value)} °C"
    if option == 4:
        return f"{value} K = {kelvin_to_celsius(value)} °C"
    if option == 6:
        return f"{value} °F = {fahrenheit_to_kelvin(value)} K"
    return full_conversion(value)               # only option 5 is left


# --- Main program: each line is what the user used to type ---
print("=== TEMPERATURE CONVERTER ===")
print(convert(1, 100))
print(convert(2, -40))
print(convert(3, 98.6))
print(convert(4, 0))
print(convert(5, 37))
print(convert(6, 212))
print(convert(1, -300))
print(convert(9, 20))

Output

=== TEMPERATURE CONVERTER ===
100 °C = 212.0 °F
-40 °C = 233.15 K
98.6 °F = 37.0 °C
0 K = -273.15 °C
37 °C = 98.6 °F = 310.15 K · round trip: 37.0 °C
212 °F = 373.15 K
-300 °C is below absolute zero
Option 9 not valid: choose from 1 to 6

Checking by hand: 100 × 9 / 5 + 32 = 180 + 32 = 212; −40 + 273.15 = 233.15; (98.6 − 32) × 5 / 9 = 66.6 × 5 / 9 = 37.0; 0 − 273.15 = −273.15. Option 5 does all three at once with 37 °C: 37 × 9 / 5 + 32 = 98.6 °F and 37 + 273.15 = 310.15 K, and the trip back from 98.6 °F to °C gives 37.0 again, a sign that both formulas agree with each other. Option 6 takes 212 °F to 100.0 °C and from there to 373.15 K. The last two lines are the awkward cases: −300 °C doesn’t exist and neither does option 9.

The deciding function: convert

It’s the longest one, so we look at it on its own:

def convert(option, value):
    """Returns the text of the chosen conversion (1-6) or a warning if something is off."""
    if option < 1 or option > 6:
        return f"Option {option} not valid: choose from 1 to 6"
    scale = source_scale(option)
    if not is_valid_temperature(value, scale):
        return f"{value} {scale} is below absolute zero"
    if option == 1:
        return f"{value} °C = {celsius_to_fahrenheit(value)} °F"
    if option == 2:
        return f"{value} °C = {celsius_to_kelvin(value)} K"
    if option == 3:
        return f"{value} °F = {fahrenheit_to_celsius(value)} °C"
    if option == 4:
        return f"{value} K = {kelvin_to_celsius(value)} °C"
    if option == 6:
        return f"{value} °F = {fahrenheit_to_kelvin(value)} K"
    return full_conversion(value)            # only option 5 is left

It goes in this order, and the order matters:

  1. First, the option. If it isn’t between 1 and 6, it returns the warning and ends (return cuts the function short: nothing else is checked).
  2. Then, the temperature. It asks source_scale which scale the option starts from and is_valid_temperature whether the value is possible. If not, another warning and out.
  3. Finally, the conversion. Each if has its own return, so no elif is needed: as soon as one matches, the function ends. If none matched, it can only be option 5.

Trace of convert(1, -300):

StepCallReturnsConsequence
1option < 1 or option > 6 → 1 < 1 or 1 > 6Falsecarries on
2source_scale(1)"°C"scale = "°C"
3is_valid_temperature(-300, "°C") → -300 >= -273.15Falsenot False is True
4return f"{value} {scale} is below…"the warningthe function ends

And this is how the functions share the work when you choose option 5 with 37 °C. Each box is a call and the coral value is what it returns:

The calls made by convert(5, 37)convert(5, 37) calls source_scale, which returns °C, is_valid_temperature, which returns True, and full_conversion, which in turn calls celsius_to_fahrenheit (98.6), fahrenheit_to_celsius (37.0) and celsius_to_kelvin (310.15).convert(5, 37)source_scale(5)→ "°C"is_valid_temperature(37, "°C")→ Truefull_conversion(37)→ "37 °C = 98.6 °F = …"celsius_to_fahrenheit(37)→ 98.6fahrenheit_to_celsius(98.6)→ 37.0celsius_to_kelvin(37)→ 310.15Each function does one single thing and returns its result; convert just chains them.
One menu option puts six functions to work; each coral value is a return travelling back up.

Questions you might ask

Why do the calculation functions round with round(…, 2)? Because decimals in binary aren’t exact (in Python, 0.1 + 0.2 gives 0.30000000000000004) and a conversion can drag ugly tails like that along. Rounding inside the calculation function makes every conversion come out clean without having to remember to do it outside.

Why does convert return the text instead of printing it? Because that way it’s useful for more things: the main program prints it, but you could save it to a file, show it in a window or check it in a test (convert(1, 100) == "100 °C = 212.0 °F"). A function that prints is only good for printing.

Why are source_scale and is_valid_temperature separate functions? Because they answer questions convert has no reason to know how to solve. If tomorrow you add the Rankine scale, you change those two functions and convert barely changes.

What does fahrenheit_to_kelvin gain by calling two others? That if you ever fix the °F to °C formula, the °F to K one fixes itself. With the formula copied, you’d have two places to fix and you’d forget one.

Checking the temperature inside each conversion if. You end up with the same check copied six times. Check it once, before converting, as convert does.

Try it yourself

  • Add option 7, K to °F, without writing any new formula: compose kelvin_to_celsius and celsius_to_fahrenheit.
  • Change full_conversion so it also says whether the temperature is freezing (≤ 0 °C), normal or boiling (≥ 100 °C), with a new function that returns that word.

Program 2: modular calculator

A calculator with six operations (addition, subtraction, multiplication, division, power and modulo). The usual brief, with one new demand: the functions that calculate never print and the ones that print never calculate. And it has to survive two awkward cases: dividing (or taking the modulo) by zero and choosing an option that doesn’t exist.

The idea

Three layers, each with its own responsibility:

  1. Calculate: add, subtract, multiply, divide, power and modulo. They take two numbers and return the result. If there’s no possible result (dividing by zero), they return None.
  2. Show: show_menu, symbol and show_result. They’re the only ones that know what the calculator looks like.
  3. Coordinate: calculate picks the calculation function for the option and run puts a whole operation together: it checks, calculates and shows.

Before coding, think about what a function returns when it “can’t”. Here, None: it isn’t a number, so nobody will mistake it for a valid result.

The code

# Program 2 · Modular mini calculator
# --- Calculation functions: they only calculate and return; they never print ---
def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


def multiply(a, b):
    return a * b


def divide(a, b):
    if b == 0:
        return None          # None = "no result": the caller decides what to say
    return round(a / b, 4)


def power(a, b):
    return a ** b


def modulo(a, b):
    if b == 0:
        return None
    return a % b


# --- Interface functions: they only deal with what you see ---
def show_menu():
    print("=== MODULAR CALCULATOR ===")
    print("1. Add  2. Subtract  3. Multiply  4. Divide  5. Power  6. Modulo")


def symbol(option):
    if option == 1:
        return "+"
    if option == 2:
        return "-"
    if option == 3:
        return "×"
    if option == 4:
        return "/"
    if option == 5:
        return "^"
    return "%"


def show_result(a, sign, b, result):
    if result is None:
        print(f"{a} {sign} {b} → error: cannot divide by zero")
    else:
        print(f"{a} {sign} {b} = {result}")


# --- Coordinating functions ---
def calculate(option, a, b):
    """Picks the calculation function for the option and RETURNS its result."""
    if option == 1:
        return add(a, b)
    if option == 2:
        return subtract(a, b)
    if option == 3:
        return multiply(a, b)
    if option == 4:
        return divide(a, b)
    if option == 5:
        return power(a, b)
    return modulo(a, b)


def run(option, a, b):
    """One complete operation: checks, calculates with one function and shows with another."""
    if option < 1 or option > 6:
        print(f"Option {option} not valid")
        return                                   # return with no value: leaves the function now
    result = calculate(option, a, b)
    show_result(a, symbol(option), b, result)


# --- Main program: the operations that used to be chosen in the menu ---
show_menu()
run(1, 7, 2)
run(4, 7, 2)
run(4, 7, 0)
run(5, 2, 10)
run(6, 17, 5)
run(8, 1, 1)

Output

=== MODULAR CALCULATOR ===
1. Add  2. Subtract  3. Multiply  4. Divide  5. Power  6. Modulo
7 + 2 = 9
7 / 2 = 3.5
7 / 0 → error: cannot divide by zero
2 ^ 10 = 1024
17 % 5 = 2
Option 8 not valid

Checking: 7 + 2 = 9; 7 / 2 = 3.5; 2¹⁰ = 1024; 17 = 3 × 5 + 2, so 17 % 5 = 2. Division by zero doesn’t break the program: divide returns None and show_result turns it into a message. Option 8 is rejected before anything is calculated.

Zoom: divide and run

The piece that makes everything work is this contract of divide:

def divide(a, b):
    if b == 0:
        return None          # None = "no result": the caller decides what to say
    return round(a / b, 4)

divide doesn’t decide what to tell the user: it only signals that there’s no result by returning None. The one that decides which text to show is show_result, which asks if result is None. We use is None (and not == None) because None is a unique value and is checks exactly that: whether it’s that object.

And this is the coordinating function:

def run(option, a, b):
    """One complete operation: checks, calculates with one function and shows with another."""
    if option < 1 or option > 6:
        print(f"Option {option} not valid")
        return                                   # return with no value: leaves the function now
    result = calculate(option, a, b)
    show_result(a, symbol(option), b, result)

Notice the bare return, with no value: it’s there to leave the function as soon as the option is invalid, without wrapping the rest of the code in an else. It returns None, but nobody here uses what run returns, so it doesn’t matter.

Trace of run(4, 7, 0):

StepWhoWhat it doesReturns
1run4 < 1 or 4 > 6 is False: carries on—
2calculate(4, 7, 0)option 4 is divide: calls divide(7, 0)—
3divide(7, 0)b == 0None
4calculatereturns what divide gave itNone
5symbol(4)—"/"
6show_result(7, "/", 0, None)result is None: prints the errorNone

The diagram shows the pattern with that same call: at the top it coordinates, on the left it calculates (and returns) and on the right it shows (and prints):

The calculator pattern: coordinate, calculate and showrun(4, 7, 0) calls calculate, which calls divide(7, 0) and returns None. Then it calls symbol(4), which returns the slash, and show_result, the only one that prints: 7 / 0 → error: cannot divide by zero.Coordinates · run(4, 7, 0)Calculates · returns, never printscalculate(4, 7, 0)→ divide(7, 0)→ return NoneShows · prints, never calculatessymbol(4) → "/"show_result(7, "/", 0, None)1. asks for the result2. passes it on7 / 0 → error: cannot divide by zero
The calculation functions return (here None, «no result»); only the presentation functions touch the screen.

Questions you might ask

Why so many functions for something so small? Because each one can be tested on its own. divide(7, 0) must return None and divide(7, 2) must return 3.5: you check it in two lines, with no menu, no keyboard and no reading the screen. Exactly what I couldn’t do with my first version.

Why is symbol a function and not an if inside show_result? Because the symbol is presentation data that the menu or a history could also use. Besides, show_result stays short and easy to read.

Wouldn’t it be easier for divide to print the error itself? Easier, yes; better, no. If divide prints, you can no longer use it in a windowed program or a test without it messing up the screen. And what would it return? It would still have to return something.

What if the power is huge? Python works with integers of any size: 2 ** 1000 works. With decimals (2.0 ** 10000) you would get an OverflowError. For FP1 it’s enough to know it exists.

Returning an error text from a calculation function (return "Error"). The caller has to compare texts to know whether it worked, and if it ever adds up that “result”, you’ll get a TypeError miles away from its cause. Return None (or, once we reach them, raise an exception).

Try it yourself

  • Add operation 7, floor division (//), with its own function that returns None if the divisor is 0. How many functions did you have to touch?
  • Make show_result round non-integer results to 2 decimals, without touching any calculation function.

Program 3: Rock Paper Scissors

The game against the computer. The player writes their move however they like (Rock, " SCISSORS ") and sometimes with typos (scissor). We have to clean what they type, reject anything that isn’t a move, decide who wins each round and keep the score. The computer picks at random.

The idea

One function per responsibility, and a main function, play, that only coordinates:

  1. clean_move(text): lowercases, strips spaces and returns the move or None.
  2. computer_move(): returns a random move.
  3. determine_winner(player, computer): returns "draw", "player" or "computer". It doesn’t print.
  4. show_round and show_final: the only ones that print.

To decide who wins we use a dictionary, BEATS, which says which move each one beats. You’ll study dictionaries properly in associative containers; here it’s enough to know that BEATS["rock"] is "scissors".

Before coding, think about which parts of the game don’t depend on chance. Those (cleaning, deciding the winner) are the easiest to test.

The code

# Program 3 · Rock, paper, scissors with functions
import random

random.seed(7)    # fixed seed: the computer always plays the same (remove it to play for real)

OPTIONS = ("rock", "paper", "scissors")
BEATS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}    # each move → the one it beats
EMOJI = {"rock": "🪨", "paper": "📄", "scissors": "✂️"}


def computer_move():
    return random.choice(OPTIONS)


def clean_move(text):
    """Lowercases and strips spaces. Returns the move, or None if it isn't valid."""
    move = text.lower().strip()
    if move in OPTIONS:
        return move
    return None


def determine_winner(player, computer):
    """Returns "draw", "player" or "computer". Prints nothing."""
    if player == computer:
        return "draw"
    if BEATS[player] == computer:
        return "player"
    return "computer"


def show_round(number, player, computer, winner):
    print(f"Round {number}: you {EMOJI[player]} {player} · computer {EMOJI[computer]} {computer}")
    if winner == "draw":
        print("  → Draw")
    elif winner == "player":
        print("  → You win!")
    else:
        print("  → The computer wins")


def show_final(wins, losses, draws):
    print(f"Score: you {wins} · computer {losses} · draws {draws}")
    if wins > losses:
        print("You won the game! 🏆")
    elif losses > wins:
        print("The computer wins the game 🤖")
    else:
        print("Perfect draw 🤝")


def play(entries):
    """One round per valid entry; keeps the score and shows it at the end."""
    wins = losses = draws = 0
    round_no = 0
    for text in entries:
        player = clean_move(text)
        if player is None:
            print(f"«{text}» isn't a valid move: it doesn't count")
            continue
        round_no += 1
        computer = computer_move()
        winner = determine_winner(player, computer)
        show_round(round_no, player, computer, winner)
        if winner == "player":
            wins += 1
        elif winner == "computer":
            losses += 1
        else:
            draws += 1
    show_final(wins, losses, draws)


# What the player "types": with capitals, spaces and a typo, like in real life
print("=== ROCK, PAPER, SCISSORS ===")
play(["Rock", "scissor", "paper", " SCISSORS ", "scissors"])

Output

=== ROCK, PAPER, SCISSORS ===
Round 1: you 🪨 rock · computer 📄 paper
  → The computer wins
«scissor» isn't a valid move: it doesn't count
Round 2: you 📄 paper · computer 🪨 rock
  → You win!
Round 3: you ✂️ scissors · computer 📄 paper
  → You win!
Round 4: you ✂️ scissors · computer ✂️ scissors
  → Draw
Score: you 2 · computer 1 · draws 1
You won the game! 🏆

Checking round by round: rock against paper, paper wins (the computer); "scissor" isn’t any of the three moves and doesn’t count (notice it does not use up a computer move, because computer_move() is called after the continue); paper against rock, you win; " SCISSORS " is cleaned to scissors and beats paper; scissors against scissors, a draw. Score 2-1 with one draw.

The line random.seed(7) fixes the “seed” of the randomness, so the computer always plays paper, rock, paper and scissors, and your output will match the article. To play for real, delete it.

Zoom: cleaning and deciding

def clean_move(text):
    """Lowercases and strips spaces. Returns the move, or None if it isn't valid."""
    move = text.lower().strip()
    if move in OPTIONS:
        return move
    return None


def determine_winner(player, computer):
    """Returns "draw", "player" or "computer". Prints nothing."""
    if player == computer:
        return "draw"
    if BEATS[player] == computer:
        return "player"
    return "computer"

clean_move chains lower() (everything to lowercase) and strip() (removes spaces at the start and end). You’ll see string methods in the strings topic; here we use them as tools. What matters is the shape: it returns the clean move or None, and the caller decides what to do with a None.

determine_winner settles the game in three lines thanks to the dictionary. The diagram shows the cycle stored in BEATS:

Who beats whom in BEATSA cycle of three moves: rock beats scissors, scissors beats paper and paper beats rock. On the left, the three entries of the BEATS dictionary; on the right, the rule determine_winner uses. rock scissors paperbeatsbeatsbeatsBEATS["rock"] → "scissors"BEATS["scissors"] → "paper"BEATS["paper"] → "rock"If BEATS[player] == computer,you win. If they're equal, a draw.Otherwise, the computer wins.
Each arrow is one dictionary entry: a single lookup, BEATS[player] == computer, replaces six comparisons.
playercomputerEqual?BEATS[player]== computer?Returns
"rock""paper"no"scissors"no"computer"
"paper""rock"no"rock"yes"player"
"scissors""paper"no"paper"yes"player"
"scissors""scissors"yes——"draw"

Questions you might ask

Why does play receive a list of entries instead of asking for them with input()? So the program can be run and checked the same way every time. To play for real, you’d swap the for text in entries loop for an input() inside a while, and no other function would have to change. That’s the beauty of separating everything.

Why are wins, losses and draws local to play? Because only play needs them while the game lasts. If they were global, any function could touch them and a second game would start with a dirty score.

What does wins = losses = draws = 0 do? It assigns 0 to all three variables in one line. It works because numbers can’t be modified “from inside”; don’t do it with lists, which would end up being the same list.

Why continue on an invalid move? Because that entry mustn’t count as a round: continue jumps to the next pass of the for without going through the rest. That’s why rounds are numbered 1, 2, 3 and 4 even though there are five entries.

Calling computer_move() before checking whether the player’s move is valid. The computer “spends” moves on rounds that don’t exist and, with the fixed seed, every following round changes.

Try it yourself

  • Accept abbreviations: make clean_move turn "r", "p" and "s" into "rock", "paper" and "scissors", without touching any other function.
  • Add lizard and Spock (each move beats two and loses to two). Hint: the value in BEATS can be a list of two moves and the check becomes computer in BEATS[player].

What these three programs have in common

All three programs have the same shape, and it’s the one you’ll use in every GCID lab from now on:

Kind of functionWhat it doesWhat it never doesIn the programs
Calculationreceives data and returns a result (or None)print or ask for datacelsius_to_fahrenheit, divide, determine_winner
Checkinganswers a question with True/False or a valuechange anythingis_valid_temperature, clean_move
Presentationreceives results and shows themcalculateshow_result, show_round
Coordinatorcalls the others in ordercontain formulasconvert, run, play

When you’re unsure where a line belongs, ask yourself: does this calculate or does this show? If it does both, split the function in two.

Common mistakes in this practice

  • Calculation functions that print. They work once and get in the way forever: they can’t be tested or reused.
  • Using the result of a function that only prints. The variable is None and the error jumps out on another line.
  • Copying a formula instead of composing functions. When you fix one, the copy stays wrong.
  • Returning error texts from the calculation. Better None, and let presentation decide the message.
  • Checking with == None. It works almost always, but the correct way is is None.
  • Global scores or counters. Put them in the function that uses them and pass them or return them when needed.

Visualise it with Python Tutor

Python Tutor draws each call as a box with its variables. Choose Python 3 and paste this minimal version of the game, with two fixed rounds:

BEATS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}


def determine_winner(player, computer):
    if player == computer:
        return "draw"
    if BEATS[player] == computer:
        return "player"
    return "computer"


def add_point(winner, wins):
    if winner == "player":
        wins = wins + 1
    return wins


wins = 0
g = determine_winner("paper", "rock")
wins = add_point(g, wins)
g = determine_winner("rock", "paper")
wins = add_point(g, wins)
print(g, wins)

Expected output:

computer 1

What to watch step by step:

  1. When determine_winner("paper", "rock") is called, a box opens with player and computer. Watch how, on the function’s third line, the Return value is "player" and the box closes.
  2. In add_point(g, wins) there are two variables called wins: the one in the Global frame (0) and the box’s local one. When the local one becomes 1, the global one stays at 0 until return and the assignment wins = add_point(...) update it.
  3. In the second round, BEATS["rock"] is "scissors", which isn’t "paper": the function leaves through the last return with "computer", and add_point returns the wins it received, unchanged.
  4. Notice that BEATS lives in the Global frame and the functions read it without it being passed as a parameter: it’s a constant, nobody changes it.

Summary and next step

  • Calculation functions receive, calculate and return: no input() and no print.
  • Composing functions (celsius_to_kelvin(fahrenheit_to_celsius(f))) avoids copying formulas.
  • When no result is possible, return None and check it with is None.
  • A return inside an if cuts the function short: branches that each have their own return don’t need elif.
  • A coordinating function calls the others in order and is the only one that knows the whole flow.
  • If the data comes in as parameters, the program can always be tested the same way.

Checklist: have you practised it?

  • Have you run the three programs and checked the outputs?
  • Could you say which functions in the three programs print and which return?
  • Can you explain why fahrenheit_to_kelvin has no formula of its own?
  • Do you understand why divide returns None and who decides the error message?
  • Have you tried the “Try it yourself” tasks without looking at the theory?
  • Could you switch the game’s input to input() without touching determine_winner?

If you answered yes to almost everything, you’re ready for the Python functions exercises, where I don’t give you the code any more. See you there!

Next step: exercisesPython functions exercises — grades, a mini bank and dice with statistics (and the return that gave up on the first roll)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

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