# Functions

## What is a Function?

Function is a set of Python codes that performs a task or returns a result.

So far, you have learnt how to use some of the builtin function in Python, such as `print()`, `len()`, `range()`, `min()`, `max()` etc.

You can write lines of codes to perform a task or return results. But the lines of code gets larger from few lines to 100s or 1000s of lines of code. It is recommended to be break down the code to smaller chunks. This is helpful to make your code more **maintainable** and **readable** as well as **avoid repetition** by **reusing** chunks of codes. These reusable chunks are called **Functions**.

## What are the Types of Functions?

There are generally two types of functions:

1. Perform a task
2. Return a value(s) or an expression(s)

## What is a Function Syntax?

In Python, the building block of a function is as follow:

1. Function starts with `def` keyword
2. A uniquely identified name for the function
3. **Parameters** to pass the value(s) of the function into the function. This is ***optional***.
4. A way to define where the function header finishes: **`colon :`**&#x20;
5. Multi-line string to document what the function input and output is or performs. This is also ***optional*** but it is recommended.
6. Python statement(s) that performs a task
7. If you want the function to return an output, you can use `return` statement. It is also ***optional***.

```python
def function_name(inputs):
    """
    docstrings   
    """
    # Python statements
    
    return outputs
```

### What is `return` Statement?

The `return` statement is used to exit a function and return an output. The output can be assigned to a variable with assignment operator. The `return` statement is:

```python
return output(s)
```

The `return` statement evaluates the function and returns the output. It will return a `None` object where there is either no expression to `return` or no return statement is present.

## An Example of a Function

An example of a function that takes two integers and return the sum of the values. The function is created below:

```python
def take_sum(value_1_int, value_2_int):
    """
    inputs:
    
    output:
    """
    sum_of_values = value_1_int + value_2_int
    
    return sum_of_values
```

## Semantics: Arguments vs Parameters

In the context of `take_sum()` function, `value_1_int` and `value_2_int` are called **parameters** of `take_sum()` functio&#x6E;**.**  `value_1_int` and `value_2_int` are variables locally connected to the `take_sum()` function.

You can call the function name followed by a parentheses () with the arguments in between.

```python
value_1_int = 2
value_2_int = 10

take_sum(value_1_int, value_2_int)

#output
12
```

Now, `value_1_int` and `value_2_int` are called the **arguments** of `take_sum()` and you also passed `value_1_int` and `value_2_int` by reference. i.e. memory addresses of the `value_1_int` and `value_2_int` are passed to the function.

A function can be created without parameters.

{% tabs %}
{% tab title="Function" %}

```python
def greeting():
    print('Good day')
    print('Stay safe and healthy')
```

{% endtab %}

{% tab title="Call the function" %}

```python
greeting()

# output
Good day
Stay safe and healthy
```

{% endtab %}
{% endtabs %}

## What is Positional Arguments?

Positional Arguments is the order in which parameters are passed into a function. It is the most common way of assigning arguments to parameters.

```python
take_sum(2, 10) #--> value_1_int = 2, value_2_int = 10

take_sum(10, 2) #--> value_1_int = 10, value_2_int = 2

take_sum(value_1_int = 10, value_2_int = 2)
```

## What is a Default Values in a Function?

A positional arguments can be made optional by specifying a default value for the corresponding parameter. Let's take the `take_sum()` function as an example.

{% tabs %}
{% tab title="default value" %}

```python
def take_sum(value_1_int, value_2_int=10):
    sum_of_values = value_1_int + value_2_int
    
    return sum_of_values
    
    
take_sum(2)

# output: 12
```

{% endtab %}

{% tab title="overwrite default value" %}

```python
x = 2
y = 100 # it was 10

    
take_sum(x, y)
take_sum(2, 100)
# output: 102
```

{% endtab %}
{% endtabs %}

The default value always comes as a last parameter in the function or any parameter that comes after it has to be assigned with default value. Let's modify take\_sum() function to have three parameters.

{% tabs %}
{% tab title="three values sum" %}

```python
def take_sum(value_1_int, value_2_int=10, value_3_int):
    sum_of_values = value_1_int + value_2_int + value_3_int
    
    return sum_of_values
    
# output
#  File "<ipython-input-3-4a4bb3b6c326>", line 1
#    def take_sum(value_1_int, value_2_int=10, value_3_int):
#                ^
# SyntaxError: non-default argument follows default argument
```

{% endtab %}

{% tab title="three values sum corrected" %}

```python
def take_sum(value_1_int, value_2_int=10, value_3_int=20):
    sum_of_values = value_1_int + value_2_int + value_3_int
    
    return sum_of_values

# or

def take_sum(value_1_int, value_3_int, value_2_int=10):
    sum_of_values = value_1_int + value_2_int + value_3_int
    
    return sum_of_values
```

{% endtab %}
{% endtabs %}

But what if you want to specify the 1st and 3rd arguments, but omit the 2nd argument? \
i.e. you want to specify values for `value_1_int` and `value_3_int`, but let `value_2_int` take on its default value.

```python
take_sum(value_1_int=2, value_3_int=30)
take_sum(2, 30)
# or

take_sum(2, value_3_int=30)
```

### Keyword arguments

Optionally, positional arguments can be specified by using the parameter name regardless of the parameters with or without default values.

```python
take_sum(2, 10, 20)
# or
take_sum(2, 10, value_3_int=20)
# or
take_sum(value_1_int=2, value_2_int=10, value_3_int=20)
# or
take_sum(value_3_int=20, value_1_int=2, value_2_int=10)
```

When you use a named argument, it is required to use named arguments for all arguments after it.

```python
take_sum(value_3_int=20, 10, 2)
# or
take_sum(2, value_3_int=20, 10)


# output
#  File "<ipython-input-8-86d09888c0c5>", line 6
#    take_sum(value_3_int=20, 10, 2)
#                            ^
#SyntaxError: positional argument follows keyword argument
```

You can also not call the named arguments when you are calling your function

```python
take_sum(2) # value_1_int=2, value_2_int=10, value_3_int=20
take_sum(2, 100) # value_1_int=2, value_2_int=100, value_3_int=20
take_sum(2, 100, 200) # value_1_int=2, value_2_int=100, value_3_int=200
```

## What is `*args`?

Remember the iterable unpacking

```python
x, y, z = 1, 2, 3
```

Similar things happens when positional arugments are passed to a function

```python
take_sum(value_1_int, value_2_int, value_3_int)

take_sum(2, 4, 6)
```

There is a handy trick that can be used during [tuple unpacking](/datascience/tuples.md#tuple-unpacking) when you have imbalance on right and left of the assignment operator. You can use a **\* operator** to create a tuple/list from several values.

{% tabs %}
{% tab title="Code" %}

```python
x, y, z = 1, 2, 3, 4
```

{% endtab %}

{% tab title="Output" %}

```python
#output
ValueError                                Traceback (most recent call last)

<ipython-input-9-751b9158939d> in <module>()
----> 1 x, y, z = (1, 2, 3, 4)

ValueError: too many values to unpack (expected 3)
```

{% endtab %}
{% endtabs %}

The above code gives an error. But we can overcome that with **\* operator**. The first and second will be assigned to x and y respectively. The third and fourth will be a list and assigned to z

{% tabs %}
{% tab title="Code" %}

```python
x, y, *z = 1, 2, 3, 4

print(x)
print(y)
print(z)
print(type(z))
```

{% endtab %}

{% tab title="Output" %}

```python
#print(x)
1
#print(y)
2
#print(z)
[3, 4]
#print(type(z))
<class 'list'>
```

{% endtab %}
{% endtabs %}

Let's define the function again, notice the start at the start of third value.

```python
def take_sum(value_1_int, value_2_int, *value_3_int):
    print(value_1_int)
    print(value_2_int)
    print(value_3_int)
    
    
take_sum(1, 2, 'string1', 'string2')
        # value_1_int = 1
        # value_2_int = 2
        # value_3_int = ('string1', 'string2') # a tuple (a minor difference)
```

The  parameter name `value_3_int` is arbitrary. You can choose any name you would like, but it is customary to name it `*args`. So your function can be updated as below.

```python
def take_sum(value_1_int, value_2_int, *args):
    print(value_1_int)
    print(value_2_int)
    print(args)
```

`*args` will not allow anymore positional arguments. But there is a way around it.

```python
def take_sum(value_1_int, value_2_int, *args, value_3_int):
    print(value_1_int)
    print(value_2_int)
    print(args)
    print(value_3_int)
```

This will **not** work

```python
take_sum(2, 10, 30, 50, 20)
```

This will work

```python
take_sum(2, 10, 30, 50, value_3_int = 20)
```

### Unpacking arguments

You can pass an iterable argument into a function with the **`* operator`**.

```python
def take_sum(value_1_int, value_2_int, value_3_int):
    sum_of_values = value_1_int + value_2_int + value_3_int
    
    return sum_of_values
 

values = [2, 10, 20] 
take_sum(*values)
# or
take_sum(2, 10, 20)
# output
#32
```

## What is Keyword Arguments?

The positional parameters can, optionally, be passed as named (keyword) arguments. For example, if we define a function with three positional arguments:

```python
def get_sum(int_1, int_2, int_3):
    total = int_1 + int_2 + int_3
    return total
```

We can call the function `get_sum()` and pass the arguments. The named arguments is optional and it is up to you.

```python
# you can call the function
get_sum(2, 3, 4)
#or
get_sum(int_1=2, int_3=4, int_2=3)
```

You can also rewrite the function above to make more intuitive and practical. Using `*args`, you can pass many values at the same time.

```python
def get_sum(*args):
    total=0
    for value in args:
        total += value
    return total    
```

### Mandatory Keyword Arguments

Sometimes, you need to make the keyword arguments mandatory and force the user to call the named arguments. You can do so by **exhausting** all the positional arguments, and specify another additional parameter or parameters in the function definition.

```python
def get_sum(int_1, int_2, *args, int_last):
    total = int_1 + int_2 + int_last
    return total, args
```

The presence of `*args` exhausts all positional arguments and `int_3` **MUST** be passed as a named argument.

In the function `get_sum()`, you will need to pass two positional arguments, one or more additional arguments (optional), and a mandatory keyword argument which goes into `int_3`. The `int_3` argument can **only** be passed to the function using a named (keyword) argument.

This will not work

{% tabs %}
{% tab title="Code" %}

```python
get_sum(2, 3, 22, 33, 44, 4)
```

{% endtab %}

{% tab title="Output" %}

```python
TypeError                                 Traceback (most recent call last)
<ipython-input-9-91cc736c70b0> in <module>()
----> 1 get_sum(2, 3, 22, 33, 44, 4)

TypeError: get_sum() missing 1 required keyword-only argument: 'int_3'
```

{% endtab %}
{% endtabs %}

But using named argument for `int_3`, it works.

```python
get_sum(2, 3, 22, 33, 44, int_3=4)

#or

get_sum(1, 2,int_3=5)
```

You can even define a function that has only optional positional arguments and mandatory keyword arguments, i.e. you can force no positional arguments at all.&#x20;

```python
def get_sum(*args, max):
    total=0
    for value in args:
        total += value
    total += max
    return total
```

The function `get_sum()` can be rewritten to provide no positional arguments at all.

```python
def get_sum(*, max):
    total=0
    for value in args:
        total += value
    total += max
    return total
```

Now, you have only \* without specifying the name (usually ***args***). The \* indicates the **end** of positional arguments. You tell your function that you are not looking for positional arguments, effectively, there is no positional arguments.

if you call the function and provide positional arguments, Python raises an Exception error.

{% tabs %}
{% tab title="Code" %}

```python
get_sum(2, 3, 22, 33, 44, max=4)
```

{% endtab %}

{% tab title="Output" %}

```python
TypeError                                 Traceback (most recent call last)
<ipython-input-14-a7f9508d7a76> in <module>()
----> 1 get_sum(2, 3, 22, 33, 44, max=4)

TypeError: get_sum() takes 0 positional arguments but 5 positional arguments (and 1 keyword-only argument) were given
```

{% endtab %}
{% endtabs %}

Note: The aims is to showcase no positional arguments and the `get_sum()` function has to be fixed.

In nutshell: \***args** vs **\***

```python
# Function 1
def func(x, y=1, *args, z, factor=True):
    # statements


# Function 2
def func(x, y=1, *, z, factor=True):
    # statements
```

1. **x:** mandatory positional argument (may be specified using a named argument)
2. **y:** optional positional argument (may be specified positionally, as a named argument, or not at all), defaults to 1
3. **args vs \***
   1. **args**: catch-all for any (optional) additional positional arguments
   2. **\***: no additional positional arguments allowed
4. **z:** mandatory keyword argument
5. **factor**: optional keyword argument, defaults to True

## What is \*\*kwargs?

You have a clear understanding what[`*args`](/datascience/functions.md#what-is-args) is by now. `**kwargs` works very similar [`*args`](/datascience/functions.md#what-is-args), but instead of accepting positional arguments ([`*args`](/datascience/functions.md#what-is-args) returns a [tuple](/datascience/tuples.md)) and accepts **named** arguments.&#x20;

`**kwargs` is used to accept a variable amount of remaining keyword arguments. It returns a [`dictionary`](/datascience/disctionary.md).

`**kwargs` can be specified even if the positional arguments have **NOT** been **exhausted.** This is opposite to [`*args`](/datascience/functions.md#what-is-args).

Let's define a function:

{% tabs %}
{% tab title="Code" %}

```python
def new_func(*, int_1, **kwargs):
  print(int_1)
  print(kwargs)
  
  new_func(int_1=1, x=2, y=4)
```

{% endtab %}

{% tab title="Output" %}

```python
#print(int_1)
1

#print(kwargs)
{'x': 2, 'y': 4}
```

{% endtab %}
{% endtabs %}

You can also use it by passing only the named argument int\_1

```python
new_func(int_1=1)
```

This will print only the mandatory positional argument and an empty dictionary.

```python
# Output
1
{}
```

You can also create a function by just passing a `**kwargs`. For example:

{% tabs %}
{% tab title="Code" %}

```python
def new_func(**kwargs):
  print(kwargs)
  
new_func(x=2, y=4, z=6)
```

{% endtab %}

{% tab title="Output" %}

```python
{'x': 2, 'y': 4, 'z': 6}
```

{% endtab %}
{% endtabs %}

You can even create a function with both `*args` and `**kwargs`.

{% tabs %}
{% tab title="Code" %}

```python
def new_func(*args, **kwargs):
  print(args)
  print(kwargs)
  
new_func(11, 22, 33, x=2, y=4, z=6)
```

{% endtab %}

{% tab title="Output" %}

```python
#print(args)
(11, 22, 33)

#print(kwargs)
{'x': 2, 'y': 4, 'z': 6}
```

{% endtab %}
{% endtabs %}

Can we mix args and kwargs as input to a function?

You can even call the function without providing any arguments. This result in returning an empty tuple `()` and an empty dictionary `{}`.

```python
new_func()

# Output
()
{}
```

You can check the Jupyter Notebook in Colab


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://zmnako.gitbook.io/datascience/functions.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
