Jovian
⭐️
Sign In
Learn data science and machine learning by building real-world projects on Jovian

Writing Reusable Code using Functions in Python

Part 4 of "Data Analysis with Python: Zero to Pandas"

This tutorial series is a beginner-friendly introduction to programming and data analysis using the Python programming language. These tutorials take a practical and coding-focused approach. The best way to learn the material is to execute the code and experiment with it yourself. Check out the full series here:

  1. First Steps with Python and Jupyter
  2. A Quick Tour of Variables and Data Types
  3. Branching using Conditional Statements and Loops
  4. Writing Reusable Code Using Functions
  5. Reading from and Writing to Files
  6. Numerical Computing with Python and Numpy
  7. Analyzing Tabular Data using Pandas
  8. Data Visualization using Matplotlib & Seaborn
  9. Exploratory Data Analysis - A Case Study

This tutorial covers the following topics:

  • Creating and using functions in Python
  • Local variables, return values, and optional arguments
  • Reusing functions and using Python library functions
  • Exception handling using try-except blocks
  • Documenting functions using docstrings

How to run the code

This tutorial is an executable Jupyter notebook hosted on Jovian. You can run this tutorial and experiment with the code examples in a couple of ways: using free online resources (recommended) or on your computer.

Option 1: Running using free online resources (1-click, recommended)

The easiest way to start executing the code is to click the Run button at the top of this page and select Run on Binder. You can also select "Run on Colab" or "Run on Kaggle", but you'll need to create an account on Google Colab or Kaggle to use these platforms.

Option 2: Running on your computer locally

To run the code on your computer locally, you'll need to set up Python, download the notebook and install the required libraries. We recommend using the Conda distribution of Python. Click the Run button at the top of this page, select the Run Locally option, and follow the instructions.

Jupyter Notebooks: This tutorial is a Jupyter notebook - a document made of cells. Each cell can contain code written in Python or explanations in plain English. You can execute code cells and view the results, e.g., numbers, messages, graphs, tables, files, etc., instantly within the notebook. Jupyter is a powerful platform for experimentation and analysis. Don't be afraid to mess around with the code & break things - you'll learn a lot by encountering and fixing errors. You can use the "Kernel > Restart & Clear Output" menu option to clear all outputs and start again from the top.

Creating and using functions

A function is a reusable set of instructions that takes one or more inputs, performs some operations, and often returns an output. Python contains many in-built functions like print, len, etc., and provides the ability to define new ones.

In [1]:
today = "Saturday"
print("Today is", today)
Today is Saturday

You can define a new function using the def keyword.

In [2]:
def say_hello():
    print('Hello there!')
    print('How are you?')

Note the round brackets or parentheses () and colon : after the function's name. Both are essential parts of the syntax. The function's body contains an indented block of statements. The statements inside a function's body are not executed when the function is defined. To execute the statements, we need to call or invoke the function.

In [3]:
say_hello()
Hello there! How are you?

Function arguments

Functions can accept zero or more values as inputs (also knows as arguments or parameters). Arguments help us write flexible functions that can perform the same operations on different values. Further, functions can return a result that can be stored in a variable or used in other expressions.

Here's a function that filters out the even numbers from a list and returns a new list using the return keyword.

In [4]:
def filter_even(number_list):
    result_list = []
    for number in number_list:
        if number % 2 == 0:
            result_list.append(number)
    return result_list

Can you understand what the function does by looking at the code? If not, try executing each line of the function's body separately within a code cell with an actual list of numbers in place of number_list.

In [5]:
even_list = filter_even([1, 2, 3, 4, 5, 6, 7])
In [6]:
even_list
Out[6]:
[2, 4, 6]

Writing great functions in Python

As a programmer, you will spend most of your time writing and using functions. Python offers many features to make your functions powerful and flexible. Let's explore some of these by solving a problem:

Radha is planning to buy a house that costs $1,260,000. She considering two options to finance her purchase:

  • Option 1: Make an immediate down payment of $300,000, and take loan 8-year loan with an interest rate of 10% (compounded monthly) for the remaining amount.
  • Option 2: Take a 10-year loan with an interest rate of 8% (compounded monthly) for the entire amount.

Both these loans have to be paid back in equal monthly installments (EMIs). Which loan has a lower EMI among the two?

Since we need to compare the EMIs for two loan options, defining a function to calculate the EMI for a loan would be a great idea. The inputs to the function would be cost of the house, the down payment, duration of the loan, rate of interest etc. We'll build this function step by step.

First, let's write a simple function that calculates the EMI on the entire cost of the house, assuming that the loan must be paid back in one year, and there is no interest or down payment.

In [7]:
def loan_emi(amount):
    emi = amount / 12
    print('The EMI is ${}'.format(emi))
In [8]:
loan_emi(1260000)
The EMI is $105000.0

Local variables and scope

Let's add a second argument to account for the duration of the loan in months.

In [9]:
def loan_emi(amount, duration):
    emi = amount / duration
    print('The EMI is ${}'.format(emi))

Note that the variable emi defined inside the function is not accessible outside. The same is true for the parameters amount and duration. These are all local variables that lie within the scope of the function.

Scope: Scope refers to the region within the code where a particular variable is visible. Every function (or class definition) defines a scope within Python. Variables defined in this scope are called local variables. Variables that are available everywhere are called global variables. Scope rules allow you to use the same variable names in different functions without sharing values from one to the other.

In [10]:
emi
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-10-e5795bfcf3c1> in <module> ----> 1 emi NameError: name 'emi' is not defined
In [11]:
amount
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-11-9b8f90fabac0> in <module> ----> 1 amount NameError: name 'amount' is not defined
In [12]:
duration
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-12-5b6659652103> in <module> ----> 1 duration NameError: name 'duration' is not defined

We can now compare a 6-year loan vs. a 10-year loan (assuming no down payment or interest).

In [13]:
loan_emi(1260000, 8*12)
The EMI is $13125.0
In [14]:
loan_emi(1260000, 10*12)
The EMI is $10500.0

Return values

As you might expect, the EMI for the 6-year loan is higher compared to the 10-year loan. Right now, we're printing out the result. It would be better to return it and store the results in variables for easier comparison. We can do this using the return statement

In [15]:
def loan_emi(amount, duration):
    emi = amount / duration
    return emi
In [16]:
emi1 = loan_emi(1260000, 8*12)
In [17]:
emi2 = loan_emi(1260000, 10*12)
In [18]:
emi1
Out[18]:
13125.0
In [19]:
emi2
Out[19]:
10500.0

Optional arguments

Next, let's add another argument to account for the immediate down payment. We'll make this an optional argument with a default value of 0.

In [20]:
def loan_emi(amount, duration, down_payment=0):
    loan_amount = amount - down_payment
    emi = loan_amount / duration
    return emi
In [21]:
emi1 = loan_emi(1260000, 8*12, 3e5)
In [22]:
emi1
Out[22]:
10000.0
In [23]:
emi2 = loan_emi(1260000, 10*12)
In [24]:
emi2
Out[24]:
10500.0

Next, let's add the interest calculation into the function. Here's the formula used to calculate the EMI for a loan:

where:

  • P is the loan amount (principal)
  • n is the no. of months
  • r is the rate of interest per month

The derivation of this formula is beyond the scope of this tutorial. See this video for an explanation: https://youtu.be/Coxza9ugW4E .

In [25]:
def loan_emi(amount, duration, rate, down_payment=0):
    loan_amount = amount - down_payment
    emi = loan_amount * rate * ((1+rate)**duration) / (((1+rate)**duration)-1)
    return emi

Note that while defining the function, required arguments like cost, duration and rate must appear before optional arguments like down_payment.

Let's calculate the EMI for Option 1

In [26]:
loan_emi(1260000, 8*12, 0.1/12, 3e5)
Out[26]:
14567.19753389219

While calculating the EMI for Option 2, we need not include the down_payment argument.

In [27]:
loan_emi(1260000, 10*12, 0.08/12)
Out[27]:
15287.276888775077

Named arguments

Invoking a function with many arguments can often get confusing and is prone to human errors. Python provides the option of invoking functions with named arguments for better clarity. You can also split function invocation into multiple lines.

In [28]:
emi1 = loan_emi(
    amount=1260000, 
    duration=8*12, 
    rate=0.1/12, 
    down_payment=3e5
)
In [29]:
emi1
Out[29]:
14567.19753389219
In [30]:
emi2 = loan_emi(amount=1260000, duration=10*12, rate=0.08/12)
In [31]:
emi2
Out[31]:
15287.276888775077

Modules and library functions

We can already see that the EMI for Option 1 is lower than the EMI for Option 2. However, it would be nice to round up the amount to full dollars, rather than showing digits after the decimal. To achieve this, we might want to write a function that can take a number and round it up to the next integer (e.g., 1.2 is rounded up to 2). That would be a great exercise to try out!

However, since rounding numbers is a fairly common operation, Python provides a function for it (along with thousands of other functions) as part of the Python Standard Library. Functions are organized into modules that need to be imported to use the functions they contain.

Modules: Modules are files containing Python code (variables, functions, classes, etc.). They provide a way of organizing the code for large Python projects into files and folders. The key benefit of using modules is namespaces: you must import the module to use its functions within a Python script or notebook. Namespaces provide encapsulation and avoid naming conflicts between your code and a module or across modules.

We can use the ceil function (short for ceiling) from the math module to round up numbers. Let's import the module and use it to round up the number 1.2.

In [32]:
import math
In [33]:
help(math.ceil)
Help on built-in function ceil in module math: ceil(x, /) Return the ceiling of x as an Integral. This is the smallest integer >= x.
In [34]:
math.ceil(1.2)
Out[34]:
2

Let's now use the math.ceil function within the home_loan_emi function to round up the EMI amount.

Using functions to build other functions is a great way to reuse code and implement complex business logic while still keeping the code small, understandable, and manageable. Ideally, a function should do one thing and one thing only. If you find yourself writing a function that does too many things, consider splitting it into multiple smaller, independent functions. As a rule of thumb, try to limit your functions to 10 lines of code or less. Good programmers always write short, simple, and readable functions.

In [35]:
def loan_emi(amount, duration, rate, down_payment=0):
    loan_amount = amount - down_payment
    emi = loan_amount * rate * ((1+rate)**duration) / (((1+rate)**duration)-1)
    emi = math.ceil(emi)
    return emi
In [36]:
emi1 = loan_emi(
    amount=1260000, 
    duration=8*12, 
    rate=0.1/12, 
    down_payment=3e5
)
In [37]:
emi1
Out[37]:
14568
In [38]:
emi2 = loan_emi(amount=1260000, duration=10*12, rate=0.08/12)
In [39]:
emi2
Out[39]:
15288

Let's compare the EMIs and display a message for the option with the lower EMI.

In [40]:
if emi1 < emi2:
    print("Option 1 has the lower EMI: ${}".format(emi1))
else:
    print("Option 2 has the lower EMI: ${}".format(emi2))
Option 1 has the lower EMI: $14568

Reusing and improving functions

Now we know for sure that "Option 1" has the lower EMI among the two options. But what's even better is that we now have a handy function loan_emi that we can use to solve many other similar problems with just a few lines of code. Let's try it with a couple more questions.

Q: Shaun is currently paying back a home loan for a house he bought a few years ago. The cost of the house was $800,000. Shaun made a down payment of 25% of the price. He financed the remaining amount using a 6-year loan with an interest rate of 7% per annum (compounded monthly). Shaun is now buying a car worth $60,000, which he is planning to finance using a 1-year loan with an interest rate of 12% per annum. Both loans are paid back in EMIs. What is the total monthly payment Shaun makes towards loan repayment?

This question is now straightforward to solve, using the loan_emi function we've already defined.

In [41]:
cost_of_house = 800000
home_loan_duration = 6*12 # months
home_loan_rate = 0.07/12 # monthly
home_down_payment = .25 * 800000

emi_house = loan_emi(amount=cost_of_house,
                     duration=home_loan_duration,
                     rate=home_loan_rate, 
                     down_payment=home_down_payment)

emi_house
Out[41]:
10230
In [42]:
cost_of_car = 60000
car_loan_duration = 1*12 # months
car_loan_rate = .12/12 # monthly

emi_car = loan_emi(amount=cost_of_car, 
                   duration=car_loan_duration, 
                   rate=car_loan_rate)

emi_car
Out[42]:
5331
In [43]:
print("Shaun makes a total monthly payment of ${} towards loan repayments.".format(emi_house+emi_car))
Shaun makes a total monthly payment of $15561 towards loan repayments.

Exceptions and try-except

Q: If you borrow $100,000 using a 10-year loan with an interest rate of 9% per annum, what is the total amount you end up paying as interest?

One way to solve this problem is to compare the EMIs for two loans: one with the given rate of interest and another with a 0% rate of interest. The total interest paid is then simply the sum of monthly differences over the duration of the loan.

In [44]:
emi_with_interest = loan_emi(amount=100000, duration=10*12, rate=0.09/12)
emi_with_interest
Out[44]:
1267
In [45]:
emi_without_interest = loan_emi(amount=100000, duration=10*12, rate=0./12)
emi_without_interest
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) <ipython-input-45-b684ffbee02d> in <module> ----> 1 emi_without_interest = loan_emi(amount=100000, duration=10*12, rate=0./12) 2 emi_without_interest <ipython-input-35-ad16168becb0> in loan_emi(amount, duration, rate, down_payment) 1 def loan_emi(amount, duration, rate, down_payment=0): 2 loan_amount = amount - down_payment ----> 3 emi = loan_amount * rate * ((1+rate)**duration) / (((1+rate)**duration)-1) 4 emi = math.ceil(emi) 5 return emi ZeroDivisionError: float division by zero

Something seems to have gone wrong! If you look at the error message above carefully, Python tells us precisely what is wrong. Python throws a ZeroDivisionError with a message indicating that we're trying to divide a number by zero. ZeroDivisonError is an exception that stops further execution of the program.

Exception: Even if a statement or expression is syntactically correct, it may cause an error when the Python interpreter tries to execute it. Errors detected during execution are called exceptions. Exceptions typically stop further execution of the program unless handled within the program using try-except statements.

Python provides many built-in exceptions thrown when built-in operators, functions, or methods are used incorrectly: https://docs.python.org/3/library/exceptions.html#built-in-exceptions. You can also define your custom exception by extending the Exception class (more on that later).

You can use the try and except statements to handle an exception. Here's an example:

In [46]:
try:
    print("Now computing the result..")
    result = 5 / 0
    print("Computation was completed successfully")
except ZeroDivisionError:
    print("Failed to compute result because you were trying to divide by zero")
    result = None

print(result)
Now computing the result.. Failed to compute result because you were trying to divide by zero None

When an exception occurs inside a try block, the block's remaining statements are skipped. The except block is executed if the type of exception thrown matches that of the exception being handled. After executing the except block, the program execution returns to the normal flow.

You can also handle more than one type of exception using multiple except statements. Learn more about exceptions here: https://www.w3schools.com/python/python_try_except.asp .

Let's enhance the loan_emi function to use try-except to handle the scenario where the interest rate is 0%. It's common practice to make changes/enhancements to functions over time as new scenarios and use cases come up. It makes functions more robust & versatile.

In [47]:
def loan_emi(amount, duration, rate, down_payment=0):
    loan_amount = amount - down_payment
    try:
        emi = loan_amount * rate * ((1+rate)**duration) / (((1+rate)**duration)-1)
    except ZeroDivisionError:
        emi = loan_amount / duration
    emi = math.ceil(emi)
    return emi

We can use the updated loan_emi function to solve our problem.

Q: If you borrow $100,000 using a 10-year loan with an interest rate of 9% per annum, what is the total amount you end up paying as interest?

In [48]:
emi_with_interest = loan_emi(amount=100000, duration=10*12, rate=0.09/12)
emi_with_interest
Out[48]:
1267
In [49]:
emi_without_interest = loan_emi(amount=100000, duration=10*12, rate=0)
emi_without_interest
Out[49]:
834
In [50]:
total_interest = (emi_with_interest - emi_without_interest) * 10*12
In [51]:
print("The total interest paid is ${}.".format(total_interest))
The total interest paid is $51960.

Documenting functions using Docstrings

We can add some documentation within our function using a docstring. A docstring is simply a string that appears as the first statement within the function body, and is used by the help function. A good docstring describes what the function does, and provides some explanation about the arguments.

In [52]:
def loan_emi(amount, duration, rate, down_payment=0):
    """Calculates the equal montly installment (EMI) for a loan.
    
    Arguments:
        amount - Total amount to be spent (loan + down payment)
        duration - Duration of the loan (in months)
        rate - Rate of interest (monthly)
        down_payment (optional) - Optional intial payment (deducted from amount)
    """
    loan_amount = amount - down_payment
    try:
        emi = loan_amount * rate * ((1+rate)**duration) / (((1+rate)**duration)-1)
    except ZeroDivisionError:
        emi = loan_amount / duration
    emi = math.ceil(emi)
    return emi

In the docstring above, we've provided some additional information that the duration and rate are measured in months. You might even consider naming the arguments duration_months and rate_monthly, to avoid any confusion whatsoever. Can you think of some other ways to improve the function?

In [53]:
help(loan_emi)
Help on function loan_emi in module __main__: loan_emi(amount, duration, rate, down_payment=0) Calculates the equal montly installment (EMI) for a loan. Arguments: amount - Total amount to be spent (loan + down payment) duration - Duration of the loan (in months) rate - Rate of interest (monthly) down_payment (optional) - Optional intial payment (deducted from amount)

Save and upload your notebook

Whether you're running this Jupyter notebook online or on your computer, it's essential to save your work from time to time. You can continue working on a saved notebook later or share it with friends and colleagues to let them execute your code. Jovian offers an easy way of saving and sharing your Jupyter notebooks online.

In [54]:
# Instal the library
!pip install jovian --upgrade --quiet
In [55]:
# Import the jovian module
import jovian
In [59]:
jovian.commit(project='python-functions-and-scope', environment=None)
[jovian] Attempting to save notebook.. [jovian] Updating notebook "aakashns/python-functions-and-scope" on https://jovian.ai/ [jovian] Uploading notebook.. [jovian] Committed successfully! https://jovian.ai/aakashns/python-functions-and-scope

The first time you run jovian.commit, you'll be asked to provide an API Key to securely upload the notebook to your Jovian account. You can get the API key from your Jovian profile page after logging in / signing up.

jovian.commit uploads the notebook to your Jovian account, captures the Python environment, and creates a shareable link for your notebook, as shown above. You can use this link to share your work and let anyone (including you) run your notebooks and reproduce your work.

Exercise - Data Analysis for Vacation Planning

You're planning a vacation, and you need to decide which city you want to visit. You have shortlisted four cities and identified the return flight cost, daily hotel cost, and weekly car rental cost. While renting a car, you need to pay for entire weeks, even if you return the car sooner.

City Return Flight ($) Hotel per day ($) Weekly Car Rental ($)
Paris 200 20 200
London 250 30 120
Dubai 370 15 80
Mumbai 450 10 70

Answer the following questions using the data above:

  1. If you're planning a 1-week long trip, which city should you visit to spend the least amount of money?
  2. How does the answer to the previous question change if you change the trip's duration to four days, ten days or two weeks?
  3. If your total budget for the trip is $1000, which city should you visit to maximize the duration of your trip? Which city should you visit if you want to minimize the duration?
  4. How does the answer to the previous question change if your budget is $600, $2000, or $1500?

Hint: To answer these questions, it will help to define a function cost_of_trip with relevant inputs like flight cost, hotel rate, car rental rate, and duration of the trip. You may find the math.ceil function useful for calculating the total cost of car rental.

In [57]:
# Use these cells to answer the question - build the function step-by-step
In [ ]:
 
In [ ]:
 
In [ ]:
 

Summary and Further Reading

With this, we complete our discussion of functions in Python. We've covered the following topics in this tutorial:

  • Creating and using functions
  • Functions with one or more arguments
  • Local variables and scope
  • Returning values using return
  • Using default arguments to make a function flexible
  • Using named arguments while invoking a function
  • Importing modules and using library functions
  • Reusing and improving functions to handle new use cases
  • Handling exceptions with try-except
  • Documenting functions using docstrings

This tutorial on functions in Python is by no means exhaustive. Here are a few more topics to learn about:

  • Functions with an arbitrary number of arguments using (*args and **kwargs)
  • Defining functions inside functions (and closures)
  • A function that invokes itself (recursion)
  • Functions that accept other functions as arguments or return other functions
  • Functions that enhance other functions (decorators)

Following are some resources to learn about more functions in Python:

You are ready to move on to the next tutorial: "Reading from and writing to files using Python".

Questions for Revision

Try answering the following questions to test your understanding of the topics covered in this notebook:

  1. What is a function?
  2. What are the benefits of using functions?
  3. What are some built-in functions in Python?
  4. How do you define a function in Python? Give an example.
  5. What is the body of a function?
  6. When are the statements in the body of a function executed?
  7. What is meant by calling or invoking a function? Give an example.
  8. What are function arguments? How are they useful?
  9. How do you store the result of a function in a variable?
  10. What is the purpose of the return keyword in Python?
  11. Can you return multiple values from a function?
  12. Can a return statement be used inside an if block or a for loop?
  13. Can the return keyword be used outside a function?
  14. What is scope in a programming region?
  15. How do you define a variable inside a function?
  16. What are local & global variables?
  17. Can you access the variables defined inside a function outside its body? Why or why not?
  18. What do you mean by the statement "a function defines a scope within Python"?
  19. Do for and while loops define a scope, like functions?
  20. Do if-else blocks define a scope, like functions?
  21. What are optional function arguments & default values? Give an example.
  22. Why should the required arguments appear before the optional arguments in a function definition?
  23. How do you invoke a function with named arguments? Illustrate with an example.
  24. Can you split a function invocation into multiple lines?
  25. Write a function that takes a number and rounds it up to the nearest integer.
  26. What are modules in Python?
  27. What is a Python library?
  28. What is the Python Standard Library?
  29. Where can you learn about the modules and functions available in the Python standard library?
  30. How do you install a third-party library?
  31. What is a module namespace? How is it useful?
  32. What problems would you run into if Python modules did not provide namespaces?
  33. How do you import a module?
  34. How do you use a function from an imported module? Illustrate with an example.
  35. Can you invoke a function inside the body of another function? Give an example.
  36. What is the single responsibility principle, and how does it apply while writing functions?
  37. What some characteristics of well-written functions?
  38. Can you use if statements or while loops within a function? Illustrate with an example.
  39. What are exceptions in Python? When do they occur?
  40. How are exceptions different from syntax errors?
  41. What are the different types of in-built exceptions in Python? Where can you learn about them?
  42. How do you prevent the termination of a program due to an exception?
  43. What is the purpose of the try-except statements in Python?
  44. What is the syntax of the try-except statements? Give an example.
  45. What happens if an exception occurs inside a try block?
  46. How do you handle two different types of exceptions using except? Can you have multiple except blocks under a single try block?
  47. How do you create an except block to handle any type of exception?
  48. Illustrate the usage of try-except inside a function with an example.
  49. What is a docstring? Why is it useful?
  50. How do you display the docstring for a function?
  51. What are *args and **kwargs? How are they useful? Give an example.
  52. Can you define functions inside functions?
  53. What is function closure in Python? How is it useful? Give an example.
  54. What is recursion? Illustrate with an example.
  55. Can functions accept other functions as arguments? Illustrate with an example.
  56. Can functions return other functions as results? Illustrate with an example.
  57. What are decorators? How are they useful?
  58. Implement a function decorator which prints the arguments and result of wrapped functions.
  59. What are some in-built decorators in Python?
  60. What are some popular Python libraries?
In [ ]: