Python functions practice — 3 programs that show their real power
In the previous article we covered Python functions theory. Now it’s time to write real code. In this article we build three programs from scratch in VS Code where functions aren’t optional, they’re the difference between code that’s readable and maintainable and code that’s a mess. Each program is more modular than the last.
Create a folder called functions_practice and a separate file for each program.
Table of Contents
Python functions practice — Program 1: Modular temperature converter
This is a clean version of the temperature converter we built in the operators article, but now every conversion is its own function. Create temperature_converter.py:
# Modular temperature converter
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 fahrenheit_to_kelvin(fahrenheit):
celsius = fahrenheit_to_celsius(fahrenheit)
return celsius_to_kelvin(celsius) # calls another function
def kelvin_to_celsius(kelvin):
return round(kelvin - 273.15, 2)
def kelvin_to_fahrenheit(kelvin):
celsius = kelvin_to_celsius(kelvin)
return celsius_to_fahrenheit(celsius)
def show_all_conversions(value, unit):
unit = unit.upper()
print(f"\n--- Conversions for {value}° {unit} ---")
if unit == "C":
print(f" {value}°C → {celsius_to_fahrenheit(value)}°F")
print(f" {value}°C → {celsius_to_kelvin(value)} K")
elif unit == "F":
print(f" {value}°F → {fahrenheit_to_celsius(value)}°C")
print(f" {value}°F → {fahrenheit_to_kelvin(value)} K")
elif unit == "K":
print(f" {value} K → {kelvin_to_celsius(value)}°C")
print(f" {value} K → {kelvin_to_fahrenheit(value)}°F")
else:
print(f" Unknown unit: {unit}. Use C, F or K.")
def is_valid_temperature(value, unit):
unit = unit.upper()
if unit == "K" and value < 0:
return False, "Kelvin cannot be negative (absolute zero is 0 K)"
if unit == "C" and value < -273.15:
return False, "Celsius cannot be below -273.15°C (absolute zero)"
if unit == "F" and value < -459.67:
return False, "Fahrenheit cannot be below -459.67°F (absolute zero)"
return True, "Valid"
# Main program
print("=== TEMPERATURE CONVERTER ===\n")
value = float(input("Temperature value: "))
unit = input("Unit (C / F / K): ")
valid, message = is_valid_temperature(value, unit)
if valid:
show_all_conversions(value, unit)
else:
print(f"Error: {message}")
Output with 100°C:
=== TEMPERATURE CONVERTER === Temperature value: 100 Unit (C / F / K): C --- Conversions for 100.0° C --- 100.0°C → 212.0°F 100.0°C → 373.15 K
Notice what the function structure gives us here. fahrenheit_to_kelvin doesn’t calculate the conversion directly, it calls fahrenheit_to_celsius first, then celsius_to_kelvin. No code is repeated. If we ever need to fix the Celsius to Kelvin formula, we fix it in one place and all functions that depend on it update automatically.
is_valid_temperature returns two values, a boolean and a message. This is a very common Python pattern: returning a tuple when you need to communicate both success/failure and the reason.
Python functions practice — Program 2: Modular calculator
The most complete version of the calculator we’ve built across several articles — now fully modularised. Create modular_calculator.py:
# Modular calculator
def add(a, b): return round(a + b, 4)
def subtract(a, b): return round(a - b, 4)
def multiply(a, b): return round(a * b, 4)
def divide(a, b):
if b == 0:
return None, "Error: cannot divide by zero"
return round(a / b, 4), "OK"
def floor_divide(a, b):
if b == 0:
return None, "Error: cannot divide by zero"
return int(a // b), "OK"
def modulo(a, b):
if b == 0:
return None, "Error: cannot divide by zero"
return round(a % b, 4), "OK"
def power(a, b): return round(a ** b, 4)
def square_root(a):
if a < 0:
return None, "Error: cannot take square root of negative number"
return round(a ** 0.5, 4), "OK"
def show_menu():
print("\n=== CALCULATOR ===")
print("1. Addition (+)")
print("2. Subtraction (-)")
print("3. Multiplication (×)")
print("4. Division (/)")
print("5. Floor division (//)")
print("6. Modulo (%)")
print("7. Power (^)")
print("8. Square root (√)")
print("0. Exit")
def get_two_numbers():
a = float(input("First number: "))
b = float(input("Second number: "))
return a, b
def get_one_number():
return float(input("Number: "))
def calculate(option):
if option == 1:
a, b = get_two_numbers()
print(f"Result: {a} + {b} = {add(a, b)}")
elif option == 2:
a, b = get_two_numbers()
print(f"Result: {a} - {b} = {subtract(a, b)}")
elif option == 3:
a, b = get_two_numbers()
print(f"Result: {a} × {b} = {multiply(a, b)}")
elif option == 4:
a, b = get_two_numbers()
result, status = divide(a, b)
if status == "OK":
print(f"Result: {a} / {b} = {result}")
else:
print(status)
elif option == 5:
a, b = get_two_numbers()
result, status = floor_divide(a, b)
if status == "OK":
print(f"Result: {int(a)} // {int(b)} = {result}")
else:
print(status)
elif option == 6:
a, b = get_two_numbers()
result, status = modulo(a, b)
if status == "OK":
print(f"Result: {a} % {b} = {result}")
else:
print(status)
elif option == 7:
a, b = get_two_numbers()
print(f"Result: {a} ^ {b} = {power(a, b)}")
elif option == 8:
a = get_one_number()
result, status = square_root(a)
if status == "OK":
print(f"Result: √{a} = {result}")
else:
print(status)
else:
print("Invalid option")
# Main loop
running = True
while running:
show_menu()
option = int(input("\nOption: "))
if option == 0:
print("Goodbye!")
running = False
else:
calculate(option)
The key design decision here: each operation is its own function with a single responsibility. calculate() doesn’t do maths, it reads the input and calls the right function. show_menu() doesn’t do anything except display the menu. This is called separation of concerns and it’s fundamental to writing maintainable code.
Notice how divide, floor_divide, modulo and square_root return a tuple (result, status), the caller can always check whether the operation succeeded before using the result. This pattern prevents crashing on invalid inputs.
Python functions practice — Program 3: Rock Paper Scissors
The most fun program so far — and the one that best shows how functions make a game readable. Create rock_paper_scissors.py:
# Rock Paper Scissors
import random
def get_computer_choice():
choices = ["rock", "paper", "scissors"]
return random.choice(choices)
def get_player_choice():
valid = ["rock", "paper", "scissors", "r", "p", "s"]
choice = input("Your choice (rock/paper/scissors or r/p/s): ").lower()
while choice not in valid:
print("Invalid — choose rock, paper or scissors")
choice = input("Your choice: ").lower()
# Normalise shortcuts
if choice == "r": return "rock"
if choice == "p": return "paper"
if choice == "s": return "scissors"
return choice
def determine_winner(player, computer):
if player == computer:
return "draw"
wins = {
"rock": "scissors",
"paper": "rock",
"scissors": "paper"
}
if wins[player] == computer:
return "player"
return "computer"
def get_emoji(choice):
emojis = {"rock": "🪨", "paper": "📄", "scissors": "✂️"}
return emojis.get(choice, "?")
def show_result(player, computer, winner):
print(f"\nYou: {get_emoji(player)} {player}")
print(f"Computer: {get_emoji(computer)} {computer}")
if winner == "player":
print("→ You win! 🎉")
elif winner == "computer":
print("→ Computer wins 🤖")
else:
print("→ Draw 🤝")
def show_score(wins, losses, draws):
total = wins + losses + draws
print(f"\n--- Score ---")
print(f"Wins: {wins}")
print(f"Losses: {losses}")
print(f"Draws: {draws}")
print(f"Total: {total}")
if total > 0:
win_rate = round(wins / total * 100, 1)
print(f"Win rate: {win_rate}%")
def play_round():
player = get_player_choice()
computer = get_computer_choice()
winner = determine_winner(player, computer)
show_result(player, computer, winner)
return winner
# Main game
print("=== ROCK PAPER SCISSORS ===")
player_wins = 0
computer_wins = 0
draws = 0
playing = True
while playing:
result = play_round()
if result == "player":
player_wins += 1
elif result == "computer":
computer_wins += 1
else:
draws += 1
again = input("\nPlay again? (y/n): ").lower()
if again != "y":
playing = False
show_score(player_wins, computer_wins, draws)
print("\nThanks for playing!")
Output:
=== ROCK PAPER SCISSORS === Your choice (rock/paper/scissors or r/p/s): r You: 🪨 rock Computer: ✂️ scissors → You win! 🎉 Play again? (y/n): y Your choice (rock/paper/scissors or r/p/s): paper You: 📄 paper Computer: 📄 paper → Draw 🤝 Play again? (y/n): n --- Score --- Wins: 1 Losses: 0 Draws: 1 Total: 2 Win rate: 50.0% Thanks for playing!
Read through the main game loop at the bottom. It’s 15 lines. Each line is readable because every complex task is hidden inside a function — play_round() runs a complete round, show_score() displays the statistics. Without functions this program would be 80+ lines of tangled code.
determine_winner uses a dictionary to map each choice to what it beats, wins[player] == computer checks “does the player’s choice beat the computer’s?”. This is more elegant and less error-prone than three if/elif comparisons.
What these three programs have in common
All three programs share the same design philosophy: one function, one responsibility. celsius_to_fahrenheit converts. show_menu displays. get_player_choice reads and validates input. determine_winner decides the winner. None of these functions does more than one thing.
When you follow this principle, debugging becomes easier, if the temperature conversion is wrong you know exactly which function to check. Testing becomes easier, you can test each function independently. Reading becomes easier, the function name tells you what it does.
Visualise it with Python Tutor
Paste the Rock Paper Scissors game (without the while loop, just one round) into pythontutor.com and step through it. Watch how each function call opens a new frame on the right side, play_round calls get_player_choice, which creates its own frame, returns the choice, then the frame closes. Then play_round calls get_computer_choice, another frame opens and closes. Then determine_winner, another frame. You can literally see the functions calling each other as a stack.
Summary and next step
In this article you practised Python functions with three real programs. You used functions to avoid code repetition, to separate responsibilities, to return multiple values as tuples, and to make the main program readable regardless of complexity.
In the next article you’ll find proposed exercises with solutions, including a classic maths challenge and a word analyser.

One Comment