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")
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.
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 **
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))
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