Learn Python in 7 days: Day 5 - Functions & Error handlers

Shashank Shetty
Shashank Shetty

Comment section

You need to be a subscriber to comment


Functions & Error handlers

List of contents:

  1. Functions
  2. Error Handlers
  3. More to know

Functions

Functions are blocks of code that are executed only when you tell them to. This "tell the computer to run a function" is called as "calling a function". What's great about functions is that you can have certain variables (called parameters) which can easily be given different values (called arguments) when calling them.

Functions are defined with the def keyword followed by a pair of brackets. These brackets contain parameters, the variables that can be given different values. "Giving a value to a parameter of a function" is called "passing an argument". Indentation is necessary in functions as well.


# Define a function named greet. Brackets are empty as this takes no arguments
def greet():
    print("Hello there!")
    name = input("What's your sweet name? ")
    print(f"A wonderful day to you, {name}!")

# None of the above code will run until the function is called.
print("This text will be printed first, even though it is typed after the function")

greet()  # Call the function greet
        

# Defining a function with a parameter. Type hinting is used.
def cube(number: int):
    print(f"The cube of {number} is {number ** 3}.")

# The parameter can be used like a normal variable in the function.

cube(2)  # 2 is the argument passed to the function cube
cube(3)  # 3 is the argument
cube(99)  # 99 is the argument

# The scope of parameters is limited to the function. Uncomment the following line and try:
# print(number)
        

Now, you may realise that you have already been using some functions. print() and type() are functions as well. Just typing type(something) will not give you anything; you have to print it: print(type(something)). Most functions have some functionality and they do not print anything to the console. They "return" the result which can be used for anything by the program. A simple way to think about it is the returned value takes place of the function call.


# Factorial of a number is the product of all numbers from 1 to that number
# 4 factorial (represented as 4!) = 4 x 3 x 2 x 1 = 24

def factorial(n: int) -> int:  # The return type is also annotated after '->''
    product = 1
    for i in range(1, n+1):
        product *= i
    return product
# The product is returned after the execution of the code above it
# Anything written after the return statement in the function will not be run

print(factorial(6))  # We are printing the output of the function
# Without the print statement, you won't see anything, even though the calculation has been completed
        

Function can take more than one parameters, and they can be of different types as well.


def repeat(text: str, n: int) -> str:
    return f"You are repeating {text} {n} times:\n{(text+' ') * n}"

print(repeat("Hello", 5))
# The order of arguments matters. It must be the same in which the parameters are declared in the function
# print(repeat(3, "Ohh!")) will not work

# BUT, you can change the order if you specify which argument is for which parameter
print(repeat(n = 3, text = "Ohh!"))
        

The values passed directly are called arguments, args and the ones passed with the parameter name are called keyword arguments, kwargs. If your functions expects an unknown number of arguments, you can use the arbitrary arguments *args or the arbitrary keywords **kwargs. It's not the word, it's the asterisks that makes them arbitrary.


def cosplay_party(*guests, **details) -> None:
    print(f"Guests are received as {type(guests)}\nDetails are received as {type(details)}\n\n\n")

    print("Guests:")
    for guest in guests:
        print(f"> {guest}")
            
    print("\nDetails:")
    for key, value in details.items():
        print(f"{key}:- {value}")
        
cosplay_party("Ashok", "Bismil", "Chuck", Date="April 20, 2025", Time="6:00 PM", Location="221B Baker st.", Theme="Sherlock Holmes")
        

Error handlers

By now, you must have faced some errors. Errors, or "exceptions" as they are technically called, occur when there is something wrong in the code. The great thing about computers is that 'Computers only do WHAT WE TELL them to do'; the worst thing about computers is that 'Computers ONLY do what we tell them to do'. They can't understand what you intend to do with the code and excuse some slight typo; they will throw an error. It is our work as coders to instruct computers to execute our vision without any ambiguity.

Anyway, when the interpreter encounters some mistake, something it cannot execute successfully, it will throw an exception and the program will terminate. It will tell you exactly where and what the problem is. However, you might be handling some user input or non-reliable data that might cause an error but you want the program to handle them safely, without crashing, and move on and report. That's where try and except come in.


# A function that returns the decimal value of the unit fraction (1/x)
def unit_fraction(n: int) -> float:
    return round(1/n, 6)  # The round function takes a float and rounds it to a given number of decimals

while True:  # An infinite loop that asks for user input
    number: str = input("Enter a number: ")
    print(f"The unit fraction of {number} is {unit_fraction(int(number))}\n")
        

Run the above code and give it some numbers like 3, 5, 45, etc.. Now punch in 0; basic maths dictates that dividing by zero is not possible and you will get an exception. Run it again and type "BUG!"; and it will throw another exception. You cannot convert a text into an integer. We can use error handling to handle this problem without crashing the program when we receive incorrect input.


def unit_fraction(n: int) -> float:
    return round(1/n, 6)

while True:
    number: str = input("Enter a number: ")
    try:  # The code block under this will be executed. If it encounters any error, it will go on to the except block
        print(f"The unit fraction of {number} is {unit_fraction(int(number))}\n")
    except Exception as e:  # This will only run if there is an exception in the try block
        print(f"Uh oh! Something went wrong: {e}\n")
        

Now, even if you put in incorrect values, it will just warn you and go on to ask for another number (oh, type Ctrl + C if you want to end the program). An instance, with a particular error, of the class called Exception() is created in the except clause (you'll understand it later) and it is common convention to acquire it as e. This can be used to print out what the exception exactly is.

In Python, we follow a principle called EAFP. It differs from LBYL used in many other languages. Here is an example to illustrate LBYL:


def unit_fraction(n: int) -> float:
    return round(1/n, 6)

while True:
    number: str = input("Enter a number: ")
    if type(number) == int and number != 0:
        print(f"The unit fraction of {number} is {unit_fraction(int(number))}\n")
    else:
        print(f"Uh oh! Something went wrong.\n")
        

This works flawlessly! Yet, Python programmers don't really use this approach. Firstly, you can only handle a fixed number of issues that you can predict, which in large functions might be unreasonable or impossible. Secondly, checking the validity of something and then using it is just unnecessary. So, we assume that everything is valid; but if we get something invalid, just warn and move ahead, Python-ers are lazy!

The try: except: clause can also have a finally: block. Code under this block will run NO MATTER WHAT. If the program crashes, it will finish the finally block and then crash. It might not be useful in all cases, as the code after the except block will run even if it is not in a finally block, but it is a way to absolutely ensure that it does happen; such as when there might be an error in the except block itself. You can safely exit some application or close a file in the finally block.

More to know

  • You can directly unpack a list as args and dictionary as kwargs to pass arguments to a function. This example might be a bit complex, but I want to give practical examples.
  • 
     def fruit_type(*fruits, **fruit_types) -> dict:  # Takes any number of fruits and any number of types
        result: dict = {}  # Initialize result dictionary with empty lists for each fruit type
        for key in fruit_types.keys():
            result[key] = []
    
        for fruit in fruits:
            fruit = fruit.lower()  # Convert to lowercase for case-insensitive comparison
            for fruit_type in list(fruit_types.keys()):
                if fruit in fruit_types[fruit_type]:
                    result[fruit_type].append(fruit)
    
        return result
    
    fruit_types: dict[str | list] = {"Citrus": ['orange', 'lemon', 'sweet lime', 'lime', 'grapefruit'], 
                                     "Berries": ['banana', 'grape', 'kiwi', 'guava', 'watermelon']}
    basket: list = ['orange', 'banana', 'kiwi', 'grapefruit', 'watermelon']
    
    print(fruit_type(*basket, **fruit_types))
    # Unpacking the list into the args with *
    # And the dictionary into the kwargs with **
                
  • A mind-boggling thing you can do with functions is that you can call the function, from the same function! This can a bit difficult to understand but trying trace the execution on paper might help. The use case of recursions is fairly limited as they are easy to mess up and they are not very efficient. Here is an example that calculates the factorial using recursion:
  • 
    def factorial(n: int) -> int:
        if n >= 1:
            return n * factorial(n-1)
        else:
            return 1
    # This returns the product of the current number with the factorial of the previous number
    #    5! = 5x4! -> 4! = 4x3! -> 3! = 3x2! -> 2! = 2x1
    # Ultimately, 5! = 5x4x3x2x1
    
    print(factorial(5))
                
  • Except blocks can be configured to handle specific exceptions. This is considered more professional as it indicates that you know what kinds of exceptions might arise in a particular situation and it gives proper information to the user as well.
  • 
    def unit_fraction(n: int) -> float:
        return round(1/n, 6)
    
    while True:
        number: str = input("Enter a number: ")
        try:
            print(f"The unit fraction of {number} is {unit_fraction(int(number))}\n")
        except ZeroDivisionError:
            print("You can't divide numbers by 0 mate! Who taught you maths???\n")
        except ValueError:
            print("I asked you for 'a number', words can't be converted to numbers!\n")
        except Exception as e:
            print(f"Uh oh! Something else went wrong: {e}\n")  # It is still recommended to have a general except block