Learn data science and machine learning by building real-world projects on Jovian

Sign up to execute **python-functions-and-scope** and 150,000+ data science projects. Build your own projects and share them online!

Created a month ago

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:

- First Steps with Python and Jupyter
- A Quick Tour of Variables and Data Types
- Branching using Conditional Statements and Loops
- Writing Reusable Code Using Functions
- Reading from and Writing to Files
- Numerical Computing with Python and Numpy
- Analyzing Tabular Data using Pandas
- Data Visualization using Matplotlib & Seaborn
- 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

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

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.

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 ofcells. 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.

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?
```

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]`

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
```

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 calledlocal variables. Variables that are available everywhere are calledglobal 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
```

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`

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`

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`

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 isnamespaces: 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
```

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.
```

`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.
```

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)
```

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.

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:

- If you're planning a 1-week long trip, which city should you visit to spend the least amount of money?
- How does the answer to the previous question change if you change the trip's duration to four days, ten days or two weeks?
- 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? - 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 [ ]:

` `

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:

- Python Tutorial at W3Schools: https://www.w3schools.com/python/
- Practical Python Programming: https://dabeaz-course.github.io/practical-python/Notes/Contents.html
- Python official documentation: https://docs.python.org/3/tutorial/index.html

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

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

- What is a function?
- What are the benefits of using functions?
- What are some built-in functions in Python?
- How do you define a function in Python? Give an example.
- What is the body of a function?
- When are the statements in the body of a function executed?
- What is meant by calling or invoking a function? Give an example.
- What are function arguments? How are they useful?
- How do you store the result of a function in a variable?
- What is the purpose of the
`return`

keyword in Python? - Can you return multiple values from a function?
- Can a
`return`

statement be used inside an`if`

block or a`for`

loop? - Can the
`return`

keyword be used outside a function? - What is scope in a programming region?
- How do you define a variable inside a function?
- What are local & global variables?
- Can you access the variables defined inside a function outside its body? Why or why not?
- What do you mean by the statement "a function defines a scope within Python"?
- Do for and while loops define a scope, like functions?
- Do if-else blocks define a scope, like functions?
- What are optional function arguments & default values? Give an example.
- Why should the required arguments appear before the optional arguments in a function definition?
- How do you invoke a function with named arguments? Illustrate with an example.
- Can you split a function invocation into multiple lines?
- Write a function that takes a number and rounds it up to the nearest integer.
- What are modules in Python?
- What is a Python library?
- What is the Python Standard Library?
- Where can you learn about the modules and functions available in the Python standard library?
- How do you install a third-party library?
- What is a module namespace? How is it useful?
- What problems would you run into if Python modules did not provide namespaces?
- How do you import a module?
- How do you use a function from an imported module? Illustrate with an example.
- Can you invoke a function inside the body of another function? Give an example.
- What is the single responsibility principle, and how does it apply while writing functions?
- What some characteristics of well-written functions?
- Can you use if statements or while loops within a function? Illustrate with an example.
- What are exceptions in Python? When do they occur?
- How are exceptions different from syntax errors?
- What are the different types of in-built exceptions in Python? Where can you learn about them?
- How do you prevent the termination of a program due to an exception?
- What is the purpose of the
`try`

-`except`

statements in Python? - What is the syntax of the
`try`

-`except`

statements? Give an example. - What happens if an exception occurs inside a
`try`

block? - How do you handle two different types of exceptions using
`except`

? Can you have multiple`except`

blocks under a single`try`

block? - How do you create an
`except`

block to handle any type of exception? - Illustrate the usage of
`try`

-`except`

inside a function with an example. - What is a docstring? Why is it useful?
- How do you display the docstring for a function?
- What are *args and **kwargs? How are they useful? Give an example.
- Can you define functions inside functions?
- What is function closure in Python? How is it useful? Give an example.
- What is recursion? Illustrate with an example.
- Can functions accept other functions as arguments? Illustrate with an example.
- Can functions return other functions as results? Illustrate with an example.
- What are decorators? How are they useful?
- Implement a function decorator which prints the arguments and result of wrapped functions.
- What are some in-built decorators in Python?
- What are some popular Python libraries?

In [ ]:

` `