# Python Fundamentals {#py-fun}

```{block, type="learningObj"}

Our learning objectives in this session...

- Learn and use basic functions, methods and packages
- Explore the most common data containers:
  - Lists
  - Dictionaries
  - NumPy Arrays
  
```


Let's begin with some really basic foundations.

## Key types

There are four key data types that we'll see in Python:

| Type    | Name            | Example                                 |
|---------|-----------------|-----------------------------------------|
| `bool`  | Binary          | `True` and `False`                      |
| `int`   | Integer numbers | 7,9,2,-4                                |
| `float` | Real numbers    | 3.14, 2.78, 6.45                        |
| `str`   | String          | All Alphanumeric and special characters |

As we've already seen, just use the assignment operator, `=`, for storing a value in a variable. Let's take a look at the different types. You can also find out the type of an object using the `type()` function.

```{python}
# Types
a = 1
type(a)
```

```{python}
b = 0.25
type(b)
```

```{python}
c = "Berlin"
type(c)
```

```{python}
d = True
type(d)
```

## Arithmetic operators

Python can perform arithmetic operations using the following operators.

Table: Arithmetic operators


| Operator | Description                                                                | Use      |
|----------|----------------------------------------------------------------------------|----------|
| `+`	     | Addition: adds two operands                                              	| `x + y`  | 
| `-`	     | Subtraction: subtracts two operands	                                      | `x - y`  |
| `*`	     | Multiplication: multiplies two operands	                                  | `x * y`  |
| `/`	     | Division (float): divides the first operand by the second	                | `x / y`  |
| `**`     | Exponentiation                                                             | `x ** y` |
| `//`	   | Division (floor): divides the first operand by the second	                | `x // y` |
| `%`	     | Modulus: returns the remainder when first operand is divided by the second	| `x % y`  |

There are _many_ operators in Python. An operator is simply a little function that takes something on the _left_, something on the _right_ and performs some kind of function with them.


```{exercise name="Differences in handling types"}

What do expect when executing the following functions?

`1 + 1`

`'1' + '1'`

`'1' * 5`

`'1' '1'`

```

```{python echo = FALSE, eval = FALSE}
print(1 + 1)

print('1' + '1')

print('1' * 5)

print('1' '1')
```

Notice the curious case of adding `str` objects.

```{python}
w1 = "hello"
w2 = "there"
w1 + w2
```

The same holds true for multiplication

```{python}
w1*6
```

## Functions & Methods

- Python is an _object-oriented_ programming language. This means that objects have:
  - Attributes, and
  - Methods

_Methods_ are functions that an object can call on itself, they are class-specific functions. _Attributes_ are characteristics of an object.  

We'll get to creating our own classes later on. For now, let's understand Functions in Python and see how to apply methods and examine attributes.

## Packages

There are many functions built into Python, but most of what you'll use in data science comes from a suite of packages that we installed and imported in the previous case studies. In the literature you'll find the name package, library and module used interchangeably. 

To import a package we use the `import` keyword, like we saw in the case study:

```{python}
import math
```

What actually happened was this:

```{python}
from math import *
```

We implicitly used the `*` as a wild-card to import _all_ functions in the math package. 

We also saw that we can give a package an _alias_ name that will save us typing later on. These are pretty much standard use for some packages and you shouldn't change them! To do this use the `as` keyword as such:

```{python eval = TRUE}
import numpy as np
import pandas as pd
```

This means that we are loading the entire `NumPy` package and giving it an alias, `np`, to make it easier to access. When we want to use a function in NumPy, we need to preface it with `np`, or whatever alias we have given. Although we can choose any alias we want, there's not good reason not to use the standard naming conventions.

We can import a specific part of the package using the `from` keyword, like so: 

```{python}
from scipy import stats
```

So in this case we're just importing the `stats` module from the whole `scipy` package. We do this when there are potential conflicts or when it's large. This makes it a bit confusing, because as you saw previously, we could also read in just a specific function.

```{python}
from scipy.stats import ttest_ind
from statsmodels.stats.multicomp import pairwise_tukeyhsd
from statsmodels.formula.api import ols
```

So this is pretty convenient. Why should we read in the whole package when we want just a single function? But it can get very confusing very quickly! Keep to conventions and make sure you have all your import calls at the beginning of your script so that it's easy to keep track of what your using.

## Calling functions

Functions have a generic form of:

```
function(args)
```
For example:

```{python}
numb = 8
np.log2(numb)
```

Notice that we use the `.` notation to access a function within a package, keep that in mind because it's exactly what we're going to do to access methods. This also means that you can't use a `.` in a variable or function name. 

Can you predict what the above function will do?

We didn't need to define the variable `numb`, I just did that to show you that we can use a variable as a place-holder. We could have simply used `np.log2(8)`. This is an example of a scalar variable, in that is contains a single value.

Typically, there is more than one way to get an answer, for example:

```{python}
math.log(8, 2)
```

would produce the same result.

## Lists, Data Containers part I

```{exercise name = "Mixing different lengths"}

The first exercise was pretty easy... what would happen in these cases? Try to predict the answer without executing code!

`[1, 6, 9, 36] + 100`

`[1, 6, 9, 36] + [10, 100]`

`[1, 6, 9, 36] + [100, 200, 300, 400]`

`[1, 6, 9, 36] + [100, "dogs", 300, 400]`

```

Let's make a small data set to play with. 

```{python}
heights = [167, 188, 178, 194, 171, 169]
```

`heights` is an example of a list. It's a builtin type and can take many values of different types. We use `[]` to create a list. We'll consider more data containers in detail in the next section.

There are various things we can calculate on the values in a list:

Table: (\#tab:nice-fun) Some common functions.

| Function       | Description              |
|----------------|--------------------------|
| `len()`        | The number of values, n  |
| `np.mean()`    | The mean                 |
| `stats.mode()` | The mode                 |
| `np.median()`  | The median               |
| `np.var()`     | The variance             |
| `np.std()`     | The standard deviation   |
| `stats.iqr()`  | The inter-quartile range |
| `max()`        | The maximum value        |
| `min()`        | The minimum value        |
| `range()`      | The range                |

```{python}
sum(heights)
len(heights)
```

#### Exercises

```{exercise name = "Functions and math"}
Given the definition of the Normal 95% confidence interval
```

$$95\%CI = \bar{x} \pm 1.96 \times s/\sqrt n$$
where $\bar{x}$ is mean, $s$ is the standard deviation and $n$ is the number of observations, calculate the 95% confidence interval for the list of heights.


```{exercise}

Given the `dist` list for some cities:

```

``` 
cities = ['Munich', 'Paris', 'Amsterdam', 'Madrid', 'Istanbul']
dist = [584, 1054, 653, 2301, 2191]
```  

Answer the following questions:

- How many data points are there?
- What is the longest and shortest distance?
- Get the average distance

```{exercise}

Using what we've seen in the Plant Growth case study:

- Visualize the values in the `dist` list as a univariate strip plot or a histogram

```

## Making your own functions

Making your own functions is extremely common in Python and understanding how they're built will help you to understand how they're used. In this section we're going to make some small play example functions to get you used to the syntax and concepts.

To produce a function use the `def` keyword. Give it a name and then define any arguments inside `()`. All the contents of the function are indented with **four spaces**. This is very important -- remember the PEP8 Style Guide!

```
def addNumbs(x, y):
    z = x + y
    return z
```

A function ends with the `return` keyword.

```{python eval=FALSE, echo=TRUE}
def addNumbs(x, y):
    z = x + y
    return z
```

We can call our function as such:

```{python eval = FALSE, echo = TRUE}
addNumbs(4, 6)
```

```{python echo=FALSE, eval=TRUE}
def addNumbs(x, y):
    z = x + y
    return z
    
print(addNumbs(4, 6))
```

Document your functions using _Docstrings_. This is a short description placed in triple quotes `"""..."""` directly below your `def` call.

```{python eval=FALSE, echo=TRUE}
def addNumbs(x, y):
    """Add two numbers together"""
    z = x + y
    return z
```

You can access a function's Docstrings as a method, which we'll encounter later on.

```{exercise, name="Creating functions"}
We already figured out how to calculate the the 95% confidence interval. Here, create a function that takes a list and returns the lower limit. We'll return both the lower and upper limit in a later exercise. Call the function `confInt`. Don't forget to include function documentation.
```

### Returning multiple values as a tuple

The above functions returned a single value. A function may return multiple values, in which you can imagine that we'd return a list. Typically, functions actually return _Tuples_.  which are like lists (e.g. `heights`) in that they can contain multiple values, but in contrast to a list, that are an _immutable_ type -- you cannot change the values. Recall that we used `[]` to make a list, but we use `()` to make a tuple.

In this function, we return a _tuple_ of two numbers.

```{python}

def mathFun(x, y):
    """Add and subtract two numbers together"""
    result = (x + y, x - y)
    
    return result
    
print(mathFun(4, 6))

```

```{exercise, name="Returning tuples"}
Modify the function you made in the previous exercise, `confInt`, so that it returns two values, the lower and upper 95% confidence interval for a list.
```

### Inner functions

We can define a function within a function.

```{python}
# Define three_shouts
def three_shouts(word1, word2, word3):
    """Returns a tuple of strings
    concatenated with '!!!'."""

    # Define inner
    def inner(word):
        """Returns a string concatenated with '!!!'."""
        return word + '!!!'

    # Return a tuple of strings
    return (inner(word1), inner(word2), inner(word3))

```

```{python}
# Call three_shouts() and print
three_shouts('a', 'b', 'c')
```

```{exercise, name = "Inner functions"}
Returning to `confInt`, create a function within this function which manually calculates 
```

$$1.96 \times s/\sqrt n$$
and returns the value for a given list. Call this inner function `calcErr`. The end result should still be the lower and upper limits of the 95% confidence interval for a list of numbers.

### Function generators

We can even have a function return a function when it's called:

```{python}
def raise_value(n):
    """return a function"""

    def inner(x):
        """raise x to the power of n"""  
        raised = x ** n
        return raised
    
    return inner
```

Now when we call `raise_value()` with a numeric argument, we get a function using that argument as its hard-coded default.

```{python}
sq = raise_value(2)
sq(2)
```

```{python}
cu = raise_value(3)
cu(2)
```

```{exercise, name = "Functions that create functions"}
Create a function that creates functions which take a list of numbers as input and returns the 95% confidence interval calculated according to a t-distribution. For example, the correction factor for a sample of 10 observations would be 2.26 instead of 1.96. We'll produce functions which calculate the 95% confidence interval using whatever correction we provide.
```

### The LEGB rule

We can access variables from a nested function using the `nonlocal` keyword. Here, `echo_word` is a variable defined within the function `echo_shout()`. It's used inside the nested function `shout()`, but in that function's environment, it doesn't exists. To access it, we can call it using the `nonlocal` keyword. 

```{python}

# Define echo_shout()
def echo_shout(word):
    """Change the value of a nonlocal variable"""
    
    # Concatenate word with itself: echo_word
    echo_word = word*2
    
    # Print echo_word
    print(echo_word)
    
    # Define inner function shout()
    def shout():
        """Alter a variable in the enclosing scope"""    
        # Use echo_word in nonlocal scope
        nonlocal echo_word
        
        # Change echo_word to echo_word concatenated with '!!!'
        echo_word = echo_word + '!!!'
    
    # Call function shout()
    shout()
    
    # Print echo_word
    print(echo_word)

# Call function echo_shout() with argument 'hello'
echo_shout('hello')

```

If we want to access a variable from the global environment within a function, we can do so using the `global` keyword. Python searches the namespaces in the following order: `Local`, `Enclosing`, `Global`, `Built-in` (aka the LEGB rule).

### Default arguments and flexible arguments

Often, you'll want to specify a default value for a function. You an do this by simply calling assigning with with the `=` operator.

```{python}
# Define shout_echo
def shout_echo(word1, echo = 1):
    """Concatenate echo copies of word1 and three
     exclamation marks at the end of the string."""

    # Concatenate echo copies of word1 using *: echo_word
    echo_word = word1*echo

    # Concatenate '!!!' to echo_word: shout_word
    shout_word = echo_word + '!!!'

    # Return shout_word
    return shout_word

# Call shout_echo() with "Hey": no_echo
no_echo = shout_echo('Hey')

# Call shout_echo() with "Hey" and echo=5: with_echo
with_echo = shout_echo('Hey', 5)

# Print no_echo and with_echo
print(no_echo)
print(with_echo)

```

```{exercise, name="Defining function defaults"}
Returning to `confInt` with the nested `calcErr` function (not generating a function), modify the function so that we can input the error corrrection as an argument.
```

### Many unnamed arguments

If you want to allow many arguments, you can use `*args` to returns all arguments as a tuple. Here, we have a function that allows us to add many values together. We'll get into loop later on, if you're not sure what's happening there, don't worry.

```{python}
def add_all(*args):
    """sum all values in *args together."""
    
    # Initialize sum
    sum_all = 0
    
    # Accumulate the sum
    for num in args:
        sum_all += num
        
    return sum_all

print(add_all(2,4,5))
```

```{exercise}
Modify the previous `confInt` function so that it accepts many lists instead of a single list. It should combine all the values into one large list. 
```

### Many named arguments

We can even allow for many arguments that have names by using `**kwargs`. This is for argument's proceeded by identifiers (key-word arguments, hence `kwargs`). Again, don't worry about the for loop if you're not familiar with it, we'll get there later on.

```{python}
# Define report_status
def report_status(**kwargs):
    """Print out the status of a document."""

    print("\nBEGIN: REPORT\n")

    # Iterate over the key-value pairs of kwargs
    for key, value in kwargs.items():
        # Print out the keys and values, separated by a colon ':'
        print(key + ": " + value)

    print("\nEND REPORT")

# First call to report_status()
report_status(type="proposal", affiliation="Charite", status="incomplete")

# Second call to report_status()
report_status(type="grant", affiliation="MPI", status="approved")

```

Note that in the above example `\n` is a special escape character which represents a carriage return, so that we begin the next output on a new line.

### The lambda keyword

Sometimes, we don't want to actually name a function, but just use it on the fly. For this we have the `lambda` keyword. this is really convenient because it makes code most shorter, we can do everything in one line. This lambda function takes two arguments, `x` and `y`.

```{python}
raise_to_power =  lambda x, y: x ** y
raise_to_power(2, 3)
```

Can you see the relationship to a classical function definition:

```{python eval = FALSE}
def raise_to_power(x, y):
    """exponents"""
    return x ** y
```

Let's take a look at some useful use cases of lambda `functions`.

#### map()

`map()` takes two arguments. The first is a function that will be applied to all elements of the second object. Here, lambda functions can be passed to map without even having a name, i.e. they are anonymous functions.

```{python}
nums = [3, 5, 6, 8, 3]
square_all = map(lambda num: num ** 2, nums)
```

```{python}
# This returns a "map" object
square_all
```

```{python}
# so we need to convert it to a list
list(square_all)
```

#### filter()

The `filter()` function allows you to quickly _filter_ a list according to some specific criteria. Here we use the `len()` function to get all the values larger than 6 characters. We'll see more relational operators later on.

```{python}

# Create a list of strings: fellowship
fellowship = ['frodo', 'samwise', 'merry', 'pippin', 'aragorn', 'boromir', 'legolas', 'gimli', 'gandalf']

# Use filter() to apply a lambda function over fellowship: result
result = filter(lambda member: len(member) > 6, fellowship)
```

```{python}
result
```

```{python}
# We have to convert to a list
list(result)
```

### reduce()

The `reduce()` function is useful for performing some computation on a list and, unlike `map()` and `filter()`, returns a single value as a result. To use reduce(), you must import it from the `functools` module.

```{python}
# Import reduce from functools
from functools import reduce

# apply reduce to the fellowship list, see above
result = reduce(lambda item1, item2: item1 + item2, fellowship)

```

```{python}
# Print the result
result
```

```{python}
list(result)
```


### Catching errors

An error in execution is called an _exception._ To catch an exception use a `try-except` clause. i.e. if the code after try can be executed then proceed, if not execute the code in the except, which can be an error message contained in a `print()` function.

```{python}
def sqrt(x):
    try:
       return x ** 5
    except:
       print('nope')
       
sqrt(3)
sqrt(3.6)
sqrt('yes')
```

When the argument is not a number, we get an error, so the best case it to return a TypeError. The wrong type of a variable is one of the most common errors.

```{python}
def sqrt(x):
    try:
       return x ** 5
    except TypeError:
       print('nope, you have the wrong type')
       
sqrt(3)
sqrt(3.6)
sqrt('yes')
```

```{exercise}
Add a TypeError to the `calcInt` function that returns a message if a list of strings is used as input. 
```

Another type of error is the `ValueError`, when the given value is the right type, but the wrong value. We can _raise_ an error on purpose by using a `Raise` keyword.

```{python}

def sqrt(x):
    if x < 0:
        raise ValueError('x must be positive')
    try:
       return x ** 5
    except TypeError:
       print('nope, you have the wrong type')

# sqrt(-9)

```

```{exercise}
Add a ValueError to the `calcInt` function that returns a message if the maximum value is larger than 100. 
```

## String formatting

Use f-string, `f` or `F`, to format strings for output.

```{python}
name = "Rick"
place = "Misk Academy"

# use {} to display its value in the output
print(f"{name} is visiting the folks at {place}.")


## arithmetic expression
print(f"{23 * 6}")

salutation = "Mr."
def greet(name, salutation):
    return "Hello, " + salutation + " " + name
print(f"{greet(name, salutation)}, how's it going?")
print(greet(name, salutation))
```

```{exercise, name="Composing sentences"}
Sometimes it's really useful to include a sentence as output to a function, especially if you have a function that is just reporting a value or inside a loop (more on that later) which you need to see how it changes. Returning to the `confInt` function, add a line at the end which return a sentence "There is a 95% chance the the true population parameter is covered by the region [XX, YY]" where XX and YY are the lower and upper limits of the 95% confidence interval, respectively.
```


## Methods

### Dot notation

Python uses `.` notation, where periods have very specific meanings and uses. They call methods that are available. If you want to see the methods available to an object, you can just type `.` and IntelliSense should auto-complete with a list of possibilities.

For example, this does _not_ work:

```
l.len()  # not a method
```

because methods are functions that an object _calls_, whereas you pass objects _to_ functions. Here we pass `l` a list to the function `len()`, rather than calling the method `len()` on `l` using `.` notation. Get it?

Let's take a look at an example, _append_ a list.

Call the append method on the list `l` using the dot notation.

```{python eval=FALSE, echo=TRUE}
l = [1, "2", True]
l.append('appended value')
l
```

```{python echo=FALSE, eval=TRUE}
l = [1, "2", True]
l.append('appended value')
print(l)
```

So your not passing `l` to the `append()` function, rather you're calling the `append()` method on list `l`. The append method modifies the object `l` by _appending_ a new value to it.

Methods are special functions that belong to specific objects. For example, we have specific methods for strings:

```{python}
name = "Berlin"
name.lower()
name.upper()
```

You can get a list of valid methods by calling `dir()` on a variable. For example, a numeric list has the following:

- `append()`
- `clear()`
- `copy()`
- `count()`
- `extend()`
- `index()`
- `insert()`
- `pop()`
- `remove()`
- `reverse()`
- `sort()`

For example:

```{python}
# As above:
dist = [584, 1054, 653, 2301, 2191]
```

```{python}
dist.append(560)
dist
```


```{python}
dist.sort()
dist
```

```{python}
dist.pop(3)
dist
```

```{python}
dist.remove(653)
dist
```

```{exercise}
In class we'll explore some of the uses. Write examples for the remaining methods listed above.
```

One of the most fundamental things in any language is know what type of data you are working with. If you have they wrong type, you can't calculate the things that you want to, so you'll need to _coerce_ into a different type, for example `str()` is used to create a _string_.

```{python}
# coercion
numb = 8
type(numb)
numb = str(numb)
type(numb)
```

Similarly, we can coerce to an integer

```{python}
type(numb)
numb = int(numb)
type(numb)
```

or a float

```{python}
numb = float(numb)
type(numb)
```

## Attributes, part I

There are _functions_ and _methods_, and last, we have _attributes_. Attributes are characteristics of a variable. They are also called with a `.` notation. e.g.

```{python}
l.__len__
```

The `()` are optional for attributes. A variable that has a `__len__` attribute can be accessed by using the `len()` function, which is way easier.

```{python}
len(l)
```

Remember the docstrings from our functions earlier? Those are attributes and can be accessed using the following attribute

```{python echo = TRUE, eval = FALSE}
addNumbs.__doc__
```

```
## 'Add two numbers together'
```

The `__` notation is pronounced _dunder_, i.e. "double underscore". We'll see this in more detail in the OOP chapter.

## Dictionaries, Data Containers part II

Lists are fine, but we need some more elegant solutions. That's where dictionaries, arrays (i.e. matrices) and DataFrames come into play. Dictionaries are a builtin data container, but arrays and DataFrames are not in Python by default. They come from `NumPy` (arrays and matrices) and `pandas` (DataFrame).

Let's begin with dictionaries. Lists are, for the most part, 1-dimensional. Dictionaries (aka _associative arrays_) are 2-dimensional & heterogeneous, like many data sources you'll encounter.

Lists are unlabeled, but dictionaries provide `key:value` pairs using `{}` and `:`.

```{python}
d = {'int_value':3, 
     'bool_value':False, 
     'str_value':'hello'}

print(d) # 
print(d['str_value'])
```

```{python}
organizations = {'name': ['Volkswagen', 'Daimler', 'Allianz', 'Charite'],
                 'structure': ['company', 'company', 'company', 'research']}
print(organizations['name'])
```

We can put two lists together to create a dictionary. We do this using `dict(zip())`.

```{python}
heights = [167, 188, 178, 194, 171, 169]
persons = ["Mary", "John", "Kevin", "Elena", "Doug", "Galin"]

heights_persons = dict(zip(persons, heights))
```

There are specific methods to use here also:

```{python}
heights_persons.values()
heights_persons.keys()
```

Notice that the order when printing is different than in definition. Order is not guaranteed! So don't rely on the printed order of the keys.

```{python}
l = [0, 1, 2, 3, 4]
l
```


```{python}
d = {'int_value':3, 'bool_value':False, 'str_value':'hello'}
d
```

```{exercise}
Returning to the two lists containing the cities and their distances from Berlin, above, create a dictionary with two key/value pairs: `city`, containing a list of five cities, and `distance`, containing a list of 5 integers. Assign this to the variable `distDict`.
```

``` 
cities = ['Munich', 'Paris', 'Amsterdam', 'Madrid', 'Istanbul']
dist = [584, 1054, 653, 2301, 2191]
```  


```{python eval = FALSE, echo = FALSE}
n = 34
p = 6

# sequential numbers
foo1 = np.arange(1, 100, 7)
foo1

# use objects in functions
foo2 = np.arange(1, n, p)
foo2

np.log(foo1)

np.log10(foo1)

np.log2(8)

np.sqrt(foo1)

stats.zscore(foo1)

foo1 * 3

sum(foo1)

factorial(6)

len(foo1)

foo1 + 100

foo1 * 3

foo2 + foo2

heights = [167, 188, 178, 194, 171, 169]
type(heights)
heights[0]
heights[0:2]
heights[-1]

heights_persons = {"Mary": 176, "John" : 169, "Jeremy": 194, "Elena": 170}

type(heights_persons)


type(heights)

heights_np = np.array(heights)
heights_np
heights_np + 2

```

## NumPy Arrays, Data Containers part III

Many of the popular packages we use for data science are built on top of `NumPy`.

Here we can use matrices and vectors as data structures, which is exactly what we would like for data science, since most of our data is laid out in tables.

Many mathematical operations are based on matrices, called **NumPy arrays**, in Python you'll see this as an `ndarray`. This just means data grouped in rows and columns.

Arrays are similar to lists, but are more dynamic:

- They can be n-dimensional. The `nd` in `ndarray` stands for _n-dimensional_, we'll only be dealing with 1 and 2-dimensional arrays in this workshop.
- They only take one data type!

```{python}
# lists
xx = [3, 8, 9, 23]
print(xx)
type(xx)
```

```{python}
# but what you ureally want is a NumPy array
import numpy as np
xx = np.array([3, 8, 9, 23])
xx
```

`NumPy` arrays come in all shapes and sizes:

```{python}
np.array([[5,7,8,9,3], 
          [0,3,6,8,2],
          range(5)])
```

When we refer to the rows as axis 0, and the columns as axis 1.

Here we introduce a new function, `range()`. Can you guess what it does?

The primary reason we use NumPy arrays is to _vectorize_ functions over all values. Let's take a look at other common functions from `NumPy` that do this really well in table \@ref(tab:nice-fun).

## Some references

### Builtin functions and types

A summary of the Builtin functions and types is available on the [help](https://docs.python.org/3/library/functions.html) pages. Here is a summary of some common functions and types.

| Function                             | Description                                           |
|--------------------------------------|-------------------------------------------------------|
| `abs()`                              | Return the absolute value of a number |
| `all()`                              | Return True if all elements of the iterable are true |
| `any()`                              | Return True if any element of the iterable is true |
| `_class_ bool()`                     |  Return a Boolean value, i.e. one of True or False |
| `@classmethod()`                     | Transform a method into a class method |
| `dict()`                             | Create a new dictionary |
| `dir([object])`                      | Without arguments, return the list of names in the current local scope. With an argument, attempt to return a list of valid attributes for that object |
| `enumerate(iterable, start=0)`       | Return an enumerate object. iterable must be a sequence, an iterator, or some other object which supports iteration. |
| `filter(function, iterable)`         | Construct an iterator from those elements of iterable for which function returns true. |
| `_class_ float()`                    | Return a floating point number constructed from a number or string x |
| `hash(object)`                       | Return the hash value of the object (if it has one). Hash values are integers. They are used to quickly compare dictionary keys during a dictionary look-up |
| `help([object])`                     | Invoke the built-in help system |
| `input([prompt])`                    | Get user input |
| `_class_ int()`                      | Return an integer object constructed from a number or string x, or return 0 if no arguments are given |
| `iter()`                             | Return an iterator object. |
| `len()`                              | Return the length (the number of items) of an object.  |
| `_class_ list()`                     | a mutable sequence type, see [here](https://docs.python.org/3/library/stdtypes.html#typesseq) |
| `max()`                              | Return the largest item in an iterable or the largest of two or more arguments |
| `min()`                              | Return the smallest item in an iterable or the smallest of two or more arguments |
| `next()`                             | Retrieve the next item from the iterator |
| `print()`                            | Print objects |
| `range()`                            | an immutable sequence type, see [here](https://docs.python.org/3/library/stdtypes.html#typesseq) |
| `repr()`                             | Return a string containing a printable representation of an object |
| `reversed()`                         | Return a reverse iterator |
| `round()`                            | Return number rounded to ndigits precision after the decimal point |
| `_class_ slice(start, stop[, step])`  | Return a slice object representing the set of indices specified by range(start, stop, step). The start and step arguments default to None |
| `sorted()`                           | Return a new sorted list from the items in iterable |
| `@staticmethod()`                    | Transform a method into a static method |
| `sum(iterable[, start])`             | Sums start and the items of an iterable from left to right and returns the total |
| `tuple()`                            | an immutable sequence type, see [here](https://docs.python.org/3/library/stdtypes.html#typesseq) |
| `_class_ type()`                     | return the type of an object |
| `zip()`                              | Make an iterator that aggregates elements from each of the iterables |

### keywords

We saw a lot of key words already. we won't use them all the time, but here is a list

```{python}
import keyword
keyword.kwlist
```

## Wrap-up

```{block, type="chapRecap"}

So far in our Python journey we've covered the core basics of working with data. We saw packages, functions, modules and attributes for calculating and accessing information. In addition we now know the four main types in Python (`bool`, `int`, `float` and `str`) and the most common data containers (lists, dictionaries, NumPy arrays and dictionaries).

```
