---
# System prepended metadata

title: ACIT 1515 - Lesson 6
tags: [ACIT-1515]

---

<style>
    .markdown-body h1:first-of-type {
        margin-top: 24px;
    }
    .markdown-body h1 {
        margin-top: 64px;
    }
    .markdown-body h1 + h2 {
        margin-top: 32px;
    }
    .markdown-body h2 {
        margin-top: 48px;
    }
    .markdown-body h2.topics {
        font-size: 1.8em;
        border-bottom: none;
    }
    .markdown-body h3 {
        color: cornflowerblue;
    }
    .markdown-body p strong {
        font-weight: normal;
        color: red;
    }
    .exercise {
        font-size: 150%;
        font-weight: bold;
        color: rgb(227,112,183);
    }
    .note {
        color: red;
    }
</style>

# ACIT 1515 - Lesson 6

<h2 class="topics">Topics</h2>

- [Functions](#Functions)
- [The Return Keyword](#The-Return-Keyword)
- [Function Parameters and Arguments](#Function-Parameters-and-Arguments)
- [Variable Scope](#Variable-Scope)

## Functions

Python has many built-in functions, all of which provide a quick way to perform commonly-needed actions. `print()` is a function that takes a string message and outputs it to the terminal. `input()` is a function that outputs a message to the terminal, captures the value the user enters, and returns that value to the script so that we can store and use it. In addition to these built-in functions, we can create our own _custom_ functions.

Generally speaking, functions are a way for us to bundle one or more lines of our code into a named container, so that we can run those lines of code on demand. Typically we create a function whenever we have a series of actions we need to perform more than once, like:
1. prompting a user for a password
2. checking the password
3. logging them in if the password is correct.

In this case we might create a function called 'login' that contains all of these steps, and any time we need to log in a user, call our `login()` function.

A function may run once, never, or many times, but in each case it only runs when we explicitly tell it to.

### Defining a function

Below is a simple function _definition_, meaning that we _define_ the code that we need to run, and assign it a name (so that we can run it later).

```python=
def first_function():
    print('Hello!')
```

The `def` keyword specifies that we are defining a function, and `first_function` is its name. The name must always be followed by parentheses and a colon.

The so-called 'body' of the function is all the lines that are indented below the name. The function is considered finished when the first **unindented** line is encountered.

### Running a function

As a reminder, the example code above is just the 'definition'. It _defines_ what should happen when the function is run, but if you were to copy these two lines into your file and run it you wouldn't see any output!

Running (a.k.a. calling, executing, or invoking) a function involves referring to the name of the function followed by parentheses:

```python=
# define the function
def first_function():
    print('Hello!')
    
first_function() # run the function, Hello! is printed
```

In this case the function is only run once, so the output Hello! only appears once. If we want to run the function multiple times, we need to copy and paste line 5 multiple times:

```python=
# define the function
def first_function():
    print('Hello!')
    
first_function() # run the function, Hello! is printed
first_function() # run the function, Hello! is printed again
first_function() # run the function, Hello! is printed again
first_function() # run the function, Hello! is printed again
```

This example demonstrates how functions run on demand. If lines 5 through 8 are removed, the code inside `first_function` never runs!


## The Return Keyword

<!--
Take the case where we need to ask a user for several values, with the following constraints:

1. Values cannot be blank
2. Values must have a mix of uppercase and lowercase characters
3. Values must be at least 8 characters long

```python=
username = input('Please enter a username: ')

if username == '':
    sys.exit('Username cannot be blank')
    
if username.isupper() or username.islower():
    sys.exit('Username must contain uppercase and lowercase characters')
    
if len(username) < 8:
    sys.exit('Username must be at least 8 characters long')
    
password = input('Please enter a password: ')

if password == '':
    sys.exit('Password cannot be blank')
    
if password.isupper() or password.islower():
    sys.exit('Password must contain uppercase and lowercase characters')
    
if len(password) < 8:
    sys.exit('Password must be at least 8 characters long')
```

The code above is clearly repetitive - we are performing the exact same steps multiple times, but using different text and variable names. Using a function, we can _generalize_ the steps and reduce repetitive code:

```python=
def get_value():
    value = input('Please enter a value: ')
    
    if value == '':
       sys.exit('Value cannot be blank') 
    
    if value.isupper() or value.islower():
        sys.exit('Value must contain uppercase and lowercase characters')
        
    if len(value) < 8:
        sys.exit('Value must be at least 8 characters')
        
    return value
```
-->

The ==return== keyword inside a function allows us to send a value back to the line where the function is called. Another way to say this is we _call_ the function and the function _replies_ with a value.

```python=
def get_value():
    val = input('Please enter a value: ')

    # Later we will additional code here to check
    # - if the user entered a value
    # - if the user entered a valid value
    # - etc.

    return val
```

Because `get_value` _returns_ what the users enters, we could now theoretically call the function twice and store the results in two variables, `username` and `password`.

```python=
# call the get_value() function, store the reply in a variable
username = get_value() 
password = get_value()
```

Three things to note about the return keyword:

1. `return` _always ends a function_ - any statements placed after a return statement will not run
2. `return` is optional, _unless you want the function to reply with a value_
3. We can return any kind of data we want, numbers, strings, None, lists, sets, anything!

Our very simple function works, but the output the user sees is not ideal: regardless of whether we need a username or password, the user only sees 'Please enter a value: '.

Ideally we would customize these messages so the user can tell what it is they should be entering. We can accomplish this using function ==parameters==.

## Function Parameters and Arguments

Function ==parameters== are variables that are created for use inside of a function, and they allow us to pass values into a function when it is called. Below is a simple example:

```python=
def print_something(thing):
    print(thing + ' Python!')
    
print_something('Hello') # prints Hello Python!
print_something('Goodbye') # prints Goodbye Python!
```

Function parameters are declared inside parentheses - they can be named whatever you like, and we can specify multiple parameters by separating them with commas:

```python=
def print_course(name, number):
    print(name + number)
    
print_course('ACIT', '1515') # prints ACIT1515
print_course('ACIT', '1620') # prints ACIT1620
```

The _values_ that we pass _into_ the function when it is called, e.g. 'ACIT', '1515', and 'ACIT', '1620' are called ==arguments==. 

Arguments are _assigned_ to parameters.

```python=
def test(x, y):
    print(x)
    print(y)
    
test(10, 20) # x is assigned 10, y is assigned 20
test(30, 40) # x is assigned 30, y is assigned 40
```

## Named and Default Arguments

### Named Arguments

Typically, when a function is called and arguments are provided, they are assigned to function parameters _in order_. The first argument is assigned to the first parameter, the second argument to the second parameter, and so on. 

```python=
def f(p1, p2, p3):
    print(p1, p2, p3) # prints 10 20 30
    
# p1 is assigned 10, p2 is assigned 20, p3 is assigned 30
f(10, 20, 30) 
```

If necessary, or preferable, we can also pass the arguments _out of order_ if we specify the parameter's _name_ in the function call:

```python=
def f(p1, p2, p3):
    print(p1, p2, p3) # prints 10 20 30
    
f(p3 = 30, p2 = 20, p1 = 10) 
```

### Default Arguments

By default, when a function has _n_ parameters defined, we have to provide _n_ arguments:

```python=
def f(p1, p2, p3, p4):
    print(p1, p2, p3, p4)
    
# f has 4 parameters defined, so we have to provide 4 arguments
f(10, 20, 30, 40)
```

If we fail to provide the correct number of arguments, we get a _TypeError_:

```python=
def f(p1, p2, p3, p4):
    print(p1, p2, p3, p4)
    
f(10, 20) # TypeError: missing 2 required arguments
```

However, we can define ==default arguments== in the function definition - values that are assigned to parameters if no arguments are provided. Using default arguments, we can call a function with a variable number of values:

```python=
def f(p1 = 10, p2 = 20, p3 = 30, p4 = 40):
    print(p1, p2, p3, p4)
    
f() # defaults for all parameters are used, prints 10 20 30 40
f(15, 25) # p1 is assigned 15, p2 is assigned 25, p3 and p4 use defaults
f(15, 25, 35) # prints 15 25 35 40
```

## Variable Scope

It's important to note that function parameters are variables that **can only be used inside the function body**. Trying to reference a function parameter _outside_ of a function will cause your script to crash.

```python=
def test(x, y):
    print(x, y) # OK, referencing parameter inside function body
    
print(x, y) # ERROR! x and y can only be used inside the function body
```

The same is true of variables declared inside a function - they are considered 'local' variables and can only be used inside the function.

```python=
def test(x, y):
    z = 10
    print(z) # OK, still inside the function
  
print(z) # ERROR! z is a 'local' variable and cannot be used here
```