This notebook introduces some important aspects of the Python programming language. For more comprehensive tutorials, we refer to the following sources:

- The Python Tutorial introduces the reader informally to the basic concepts and features of the Python language and system.
- The Scipy Lecture Notes is a tutorial on the scientific Python ecosystem including libraries such as Numpy, Scipy, and Matploblib.

- Python is an interpreted (not compiled), open-source, multi-platform programming language.
- There exist a set of modules for scientific computing (e.g.
`numpy`

,`scipy`

,`matplotlib`

,`librosa`

) in Python. - Python uses indentation (and not brackets or
`end`

-commands) to separate blocks of code. - Comment lines start with the character
`#`

. - Useful functions for help:
- Invoke the built-in help system:
`help()`

- List objects in namespace:
`dir()`

- Show global and local variables:
`globals()`

,`locals()`

- Invoke the built-in help system:

Let us start with some basic facts on Python variables:

- Variables do not need to be declared; neither their type.
- Variables are created automatically when they are first assigned.
- A variable name may contain letters (
`a`

,`b`

, ...,`Y`

,`Z`

) and the underscore (`_`

). - Variable names are
**case sensitive**. - All but the first character can also be positive integer numbers.
- Usually one uses lower case letters and underscores to separate words.

A string is given in single ticks (`'`

) or double ticks (`"`

). If there is no other reason, we recommend single ticks. The following code assigns a string to a variable and prints it using the `print`

-command.

In [1]:

```
string_variable = 'Welcome to the Python Tutorial'
print(string_variable)
```

Here some basic string formatting:

In [2]:

```
print('This is an integer: %d.' % 17)
print('This is string1: %s and this is string2: %6s.' % ('ABCD', '1234'))
print('This is a floating point number: %06.3f.' % 3.14159265359)
```

Some basic math:

In [3]:

```
n = 3
print('n + 1 =', n + 1)
print('n - 1 =', n - 1)
print('n * 2 =', n * 2)
print('n / 2 =', n / 2)
print('n ^ 2 =', n ** 2)
```

Division always results in a floating point number, even if the number is divisible without remainder. (Note that there are differences between **Python 2** and **Python 3** in using `/`

). If the result should be an integer (e.g., when using it as an index), one may use the `//`

operator. The `%`

yields the remainder.

In [4]:

```
n = 8
print('Normal division:', n / 2)
print('Integer division:', n // 2)
print('Normal division:', n / 5)
print('Integer division:', n // 5)
print('Remainder of integer division:', n % 5)
```

For re-assigning a variable, one may use the following conventions:

In [5]:

```
n = 7
n += 11
print(n)
n *= 2
print(n)
n /= 18
print(n)
n **= 0.5
print(n)
```

The basic compound data types in Python are **lists** and **tuples**. A list is enclosed in square brackets and a tuple is enclosed in round brackets. Both are indexed with square brackets (with indexing starting with $0$). The `len`

function gives the length of a tuple or a list.

In [6]:

```
var_lis = ['I', 'am', 'a', 'list']
var_tup = ('I', 'am', 'a', 'tuple')
print(var_lis)
print(var_tup)
print(var_lis[0], var_tup[1], 'generated from',
var_tup[2], var_tup[3], 'and', var_lis[2], var_lis[3])
print(len(var_tup))
print(len(var_lis))
print(type(var_lis))
print(type(var_tup))
```

What is the difference between a list and a tuple? Tuples are **immutable** objects (i.e., their state cannot be modified after they are created) and a bit more efficient. Lists are more flexible. Here are some examples for list operations:

In [7]:

```
var_lis = [1, 2, 3]
var_lis[0] = -1
var_lis.append(10)
var_lis = var_lis + ['a', '12', [13, 14]]
print(var_lis)
print(var_lis[-1])
```

One can index a list with start, stop, and step values (`[start:end:step`

). Note that, in Python, the last index value is `end-1`

. Negative indices are possible with `-1`

referring to the last index. When not specified, `start`

refers to the first item, `end`

to the last item, and `step`

is set to $1$.

In [8]:

```
var_lis = [11, 12, 13, 14, 15]
print('var_lis[0:3] =', var_lis[0:3])
print('var_lis[1:3] =', var_lis[1:3])
print('var_lis[-1] =', var_lis[-1])
print('var_lis[0:3:2] =', var_lis[0:4:2])
print('var_lis[0::2] =', var_lis[0::2])
print('var_lis[::-1] =', var_lis[::-1])
```

The following examples shows how the elements of a list or tuple can be assigned to variables (called **unpacking**):

In [9]:

```
var_lis = [1, 2]
[a, b] = var_lis
print(a, b)
var_tup = (3, 4)
[c, d] = var_tup
print(c, d)
```

Leaving out brackets, tuples are generated.

In [10]:

```
t = 1, 2
print(t)
a, b = t
print(a, b)
```

The `range`

-function can be used to specify a tuple or list of integers (without actually generating these numbers):

In [11]:

```
print(range(9))
print(range(1, 9, 2))
```

A range can then be converted into a tuple or list as follows:

In [12]:

```
print(list(range(9)))
print(tuple(range(1, 9, 2)))
print(list(range(9, 1, -1)))
```

Boolean values in Python are `True`

and `False`

. Here are some examples for basic comparisons:

In [13]:

```
a = 1
b = 2
print(a < b)
print(a <= b)
print(a == b)
print(a != b)
```

The `bool`

function converts an arbitrary value into a boolean value. Here, are some examples:

In [14]:

```
print(bool('a'))
print(bool(''))
print(bool(1))
print(bool(0))
print(bool(0.0))
print(bool([]))
print(bool([4, 'hello', 1]))
```

There are also other data types in Python, which we want to mention here. In the following, we introduce **sets**, which are unordered collections of unique elements. Furthermore, we apply some basic set operations.

In [15]:

```
s = {4, 2, 1, 2, 5, 2}
print('Print the set s:', s)
print('Union of sets:', {1, 2, 3} | {2, 3, 4})
print('Intersection of sets:', {1, 2, 3} & {2, 3, 4})
s.add(7)
print('Adding an element', s)
s.remove(2)
print('Removing an element', s)
```

Another convenient data type are **dictionaries**, which are indexed by **keys** (rather than by a range of numbers as is the case for lists or arrays). The following code cell gives an examples and introduce some basic operations.

In [16]:

```
dic = {'a': 1, 'b': 2, 3: 'hello'}
print('Print the dictionary dic:', dic)
print('Print the keys of dic:', list(dic.keys()))
print('Access the dic via a key:', dic['b'])
print('Print the values of dic:', list(dic.values()))
```

For control structures such as `if`

, `for`

or `while`

one has to use indentations (as part of the syntax). A typical Python convention is to use four spaces. For example, an `if`

-statement is written as follows:

In [17]:

```
n = 2
if n == 2:
print('True')
else:
print('False')
```

The next example shows how to use a `for`

-loop. Note that an iterable may be specified by a range, a list, a tuple, or even other structures.

In [18]:

```
for i in range(3):
print(i, end='-')
print()
for i in ['a', 2, 'c', 'def']:
print(i, end='-')
print()
for i in 'abcd':
print(i, end='-')
print()
```

A `while`

-loop is written as follows:

In [19]:

```
a = 0
while a < 5:
print(a, end='-')
a += 1
```

One defines functions with the `def`

-keyword. As variable names, function names may contain letters (`a`

, `b`

, ..., `Y`

, `Z`

) and the underscore (`_`

). All but the first character can also be positive integer number. Usually one uses lower case letters and underscores to separate words. The following function is named `add`

. It has three arguments `a`

, `b`

, and `c`

(with `b`

and `c`

having a default value). The `return`

keyword is succeeded by the return value.

In [20]:

```
def add(a, b=0, c=0):
"""Function to add three numbers
| Notebook: B/B_libfmp.ipynb and
| Notebook: B/B_PythonBasics.ipynb
Args:
a (float): First number
b (float): Second number (default: 0)
c (float): Third number (default: 0)
Returns:
d (float): Sum
"""
d = a + b + c
print('Addition: ', a, ' + ', b, ' + ', c, ' = ', d)
return d
print(add(5))
print(add(5, 2, 1))
print(add(5, c=4))
```

There can also be multiple return values (which are returned as a tuple):

In [21]:

```
def add_and_diff(a, b=0):
"""Function to add and substract two numbers
Notebook: B/B_PythonBasics.ipynb
Args:
a: first number
b: second number (default: 0)
Returns:
first: a + b
second: a - b
"""
return a + b, a - b
x = add_and_diff(3, 5)
print(x)
```

Python has several useful built-in packages as well as additional external packages. One such package is **NumPy**, which adds support for multi-dimensional arrays and matrices, along with a number of mathematical functions to operate on these structures, see NumPy Reference Manual for details. The NumPy package is imported as follows:

In [22]:

```
import numpy as np
```

It is convenient to bind a package to a short name (for example `np`

as above). This short name appears as prefix when calling a function from the package. This is illustrated by the following `array`

-function provided by numpy:

In [23]:

```
x = np.array([1, 2, 3, 3])
print(x)
```

Each array has a shape, a type, and a dimension.

In [24]:

```
print('Shape:', x.shape)
print('Type:', x.dtype)
print('Dimension:', x.ndim)
```

In this example, note that `x.shape`

produces a one-element tuple, which is encoded by `(4,)`

for disambiguation. (The object `(4)`

would be an integer of type `int`

rather than a tuple.) Multi-dimensional arrays are created like follows:

In [25]:

```
x = np.array([[1, 2, 33], [44, 5, 6]])
print('x = ', x, sep='\n')
print('Shape:', x.shape)
print('Type:', x.dtype)
print('Dimension:', x.ndim)
```

There are a couple of functions for creating arrays:

In [26]:

```
print('Array of given shape and type, filled with zeros: ', np.zeros(2))
print('Array of given shape and type, filled with integer zeros: ', np.zeros(2, dtype='int'))
print('Array of given shape and type, filled with ones: ', np.ones((2, 3)), sep='\n')
print('Evenly spaced values within a given interval: ', np.arange(2, 8, 2))
print('Random values in a given shape: ', np.random.rand(2, 3), sep='\n')
print('Identity matrix: ', np.eye(3), sep='\n')
```

Reshaping of an array is possible like follows:

In [27]:

```
x = np.arange(2 * 3 * 4)
print('Shape:', x.shape)
print(x)
y = x.reshape((3, 8))
print('Shape:', y.shape)
print(y)
y = x.reshape((3, 2, 4))
print('Shape:', y.shape)
print(y)
print('Element y[0, 1, 2] = ', y[0, 1, 2])
```

NumPy allows for giving one of the new shape parameter as `-1`

. In this case, NumPy automatically figures out the unknown dimension. Note that in the following example the difference between the shape `(6,)`

and the shape `(6, 1)`

.

In [28]:

```
x = np.arange(6)
print('Shape: %6s; dim: %s' % (x.shape, x.ndim))
x = x.reshape(-1, 2)
print('Shape: %6s; dim: %s' % (x.shape, x.ndim))
x = x.reshape(-1, 1)
print('Shape: %6s; dim: %s' % (x.shape, x.ndim))
```

Applied to arrays, many operations are conducted in an element-wise fashion:

In [29]:

```
x = np.arange(5)
print('x + 1 =', x + 1)
print('x * 2 =', x * 2)
print('x > 2 =', x > 2)
```

Similarly, applied to two arrays of the same shape, these operations are conducted in an element-wise fashion. Matrix multiplication is performed by using the function `np.dot`

:

In [30]:

```
a = np.arange(0, 4).reshape((2, 2))
b = 2 * np.ones((2, 2))
print(a)
print(b)
print(a + b)
print(a * b)
print(np.dot(a, b))
```

Note that arrays and lists may behave in a completely different way. For example, addition leads to the following results:

In [31]:

```
a = np.arange(4)
b = np.arange(4)
print(a + b, type(a + b))
a = list(a)
b = list(b)
print(a + b, type(a + b))
```

The sum of an array's elements can be computed as follows:

In [32]:

```
x = np.arange(6).reshape((2, 3))
print(x)
print(x.sum())
print(x.sum(axis=0))
print(x.sum(axis=1))
```

There are many ways for accessing and manipulating arrays:

In [33]:

```
x = np.arange(6).reshape((2, 3))
print(x)
print(x[1, 1])
print([x > 1])
print(x[x > 1])
print(x[1, :])
print(x[:, 1])
```

NumPy offers many different numerical types and methods for type conversion. Specifying the exact type is often important when using packages such as `numba`

for optimizing machine code at runtime. In the following example, we give an example where a wrong initialization leads to an error when computing the square root of a negative number.

In [34]:

```
print('=== Initialization with \'int32\' leading to an error ===', flush=True)
x = np.arange(-2, 2)
print(x, x.dtype)
x = np.sqrt(x)
print(x)
```

In [35]:

```
print('=== Initialization with \'complex\' ===', flush=True)
x = np.arange(-3, 3, dtype='complex')
print(x, x.dtype)
x = np.sqrt(x)
print(x)
```