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