There are a number of basic variable types in Python.

- Booleans:
`True`

,`False`

- Numeric variables: int, float, long, complex

Use `#`

to demarcate a comment, and use `=`

to assign a value to a variable, e.g,:

In [1]:

```
v = True # Boolean with value True
w = False # Boolean with value False
x = 5 # int
y = 5.0 # float
```

`print()`

to display the value of a variable, or you can just type the variable and hit return:

In [2]:

```
print(v)
```

In [3]:

```
v
```

Out[3]:

In [4]:

```
print(w)
```

In [5]:

```
print(x)
```

In [6]:

```
print(y)
```

Some variables are sequences of smaller units.

- strings
- lists
- tuples

A simple way to create a string is with (single or double) quotation marks:

In [7]:

```
s = 'this is a string'
print(s)
```

In [8]:

```
l = [v,w,x,s]
print(l)
```

In [9]:

```
t = (v,w,x,s)
print(t)
```

Strings, lists, and tuples support indexing and slicing, which means that you can pick out bits and pieces of these variables. Indexing means indicating a single element of a string/list/tuple, and slicing means indicating multiple elements.

Python uses zero-based indexing (i.e., it starts counting indices at 0). It is useful to think of indices pointing to the space between and to the left of elements. When slicing, the element indicated by the starting index is included in the slice, whereas the element indicated by the ending index is not included. The starting and ending indices are separated by a colon.

So, for example, if we wanted to pick out the first `t`

from our string `s`

and assign it to a new variable `r`

, we would do this:

In [10]:

```
r = s[0] # indexing the first element of s, assigning it to r
print(s)
print(r)
```

If we want to pick out the sequence `is a`

from `s`

and assign it to `r`

, we would do this:

In [11]:

```
r = s[5:9] # slicing elements 5 through 8 of s, assigning to r
print(r)
```

In [12]:

```
print(s)
print(s[3:12]) # slice from 3 up to, but not including, 12
print(s[3]) # index 3
print(s[12]) # index 12
```

Lists and tuples support indexing and slicing, too:

In [13]:

```
print(l) # list l assigned above
print(l[1]) # the second element of l
print(l[1:3]) # the second and third elements of l
```

`0`

, or if your ending index is `-1`

(i.e., the last element), you can leave it out:

In [14]:

```
print(l[:3]) # slice from 0 to 3
print(l[2:]) # slice from 2 to the end
print(l[:]) # the whole list
```

*mutable*, whereas tuples are not. This means that you can change lists (e.g., delete an element, make the list longer). You can't do this with tuples.

In [15]:

```
print(l)
l[2] = 7 # replace the third element with the integer 7
print(l)
```

In [16]:

```
print(t)
t[2] = 7 # try the same thing with the tuple t, get an error
```

In [17]:

```
k = [l, t, x] # a list, a tuple, and a number
print(k)
```

In [18]:

```
l[2] = 50 # change the third element of l to the number 50
print(k) # print the list k, the first element of which is the list l
```

`l`

, and this showed up in the list `k`

, which contains `l`

as its first element. That is, the list that is the first element of `k`

and the list `l`

are both referring to the same underlying value. This doesn't happen with numeric types:

In [19]:

```
x = 10 # change the value of x
print(x)
print(k) # print the list k, which contains (the old) x
```

In [20]:

```
d = {'fred':5, 'bill':l, 'kurt':w}
print(d)
print(d['kurt'])
```

`set`

data type, which are unordered collections of unique elements. Because they are unordered, indexing and slicing don't work, but you can add or remove elements from a set using the `add()`

and `discard()`

or `remove()`

methods (we'll talk about methods below). The elements of sets cannot be mutable.

In [21]:

```
u = set((1,2,2,3,'a','a','b','c','c','c'))
print(u)
u.add(5) # add 5 as an element
print(u)
u.discard('c') # get rid of 'c' as an element
print(u)
```

There are a number of basic operations that can be performed on different data types.

Basic math operators

addition: `+`

subtraction: `-`

multplication: `*`

division: `/`

division with flooring (rounding down): `//`

modulus: `%`

exponentiation: `**`

matrix multiplication: `@`

(we'll come back to this later)

In [22]:

```
5 + 3
```

Out[22]:

In [23]:

```
5 - 3
```

Out[23]:

In [24]:

```
5 * 3
```

Out[24]:

In [25]:

```
5 / 3
```

Out[25]:

In [26]:

```
5 // 3
```

Out[26]:

In [27]:

```
5 % 3
```

Out[27]:

In [28]:

```
5**3
```

Out[28]:

The `+`

and `*`

operators also work with strings, lists, and tuples:

In [29]:

```
s + ' ' + s[5:]
```

Out[29]:

In [30]:

```
s*5
```

Out[30]:

In [31]:

```
e = {'a':500000000}
h = ['hello',13,e]
k = h + l
print(h)
print(l)
print(k)
```

In [32]:

```
print(h*3)
```

In [33]:

```
print(t*3)
```

In [34]:

```
print(t+t)
```

`==`

to test for equality, or `!=`

to test for unequal values.

In [35]:

```
x = 5
y = 6
x == y # test for equality of x and y, returns a Boolean
```

Out[35]:

In [36]:

```
x == y-1
```

Out[36]:

In [37]:

```
x != y # tests for inequality of x and y, returns a Boolean
```

Out[37]:

You can use `<`

, `<=`

, `>`

, and `>=`

to test for particular types of (strict or not) inequality.

In [38]:

```
x < y
```

Out[38]:

In [39]:

```
x > y
```

Out[39]:

In [40]:

```
x >= y-1
```

Out[40]:

The keywords `and`

and `or`

are reserved for indicating intersections and unions, respectively.

In [41]:

```
print(w)
print(v)
```

In [42]:

```
t = True
u = False
print(t and v)
print(t and u)
```

In [43]:

```
print(w or v)
print(w or u)
```

`0`

and `''`

(i.e., an empty string) both evaluate to `False`

. Any other number and any non-empty string all evaluate to `True`

. Note, too, that Python checks the elements in order from left to right, returning a value as soon as it can determine the value of the whole expression. We can illustrate these facts here, but they won't be particularly important until later, when we talk about control flow.

In [44]:

```
0 and 5 # 0 is False, so 0 and 5 is False as soon as Python sees 0
```

Out[44]:

In [45]:

```
5 and 0 # 5 is True, but 0 is False, so Python doesn't know 5 and 0 is False until it gets to 0
```

Out[45]:

In [46]:

```
6 or 0 # 6 is True, so 6 or 0 is True
```

Out[46]:

In [47]:

```
0 or 6 # 0 is False, but 0 or 6 is True because 6 is True
```

Out[47]:

In [48]:

```
5 and 6 # 5 is True, but Python has to check both elements to be sure 5 and 6 is True
```

Out[48]:

In [49]:

```
5 or 6 # 5 is True, so 5 or 6 is True
```

Out[49]:

Python is an object-oriented programming language. Part of what this means is that a many variables have methods, which are (pretty much) functions that "belong" to particular classes of variables (e.g., strings, tuples, dictionaries, etc).

You invoke a method by typing a `.`

after a variable name and then typing the name of the method. In a Jupyter notebook, or in IPython, you can see what methods a variable has by typing `.`

and hitting tab.

In [50]:

```
# list methods illustrated here with l.<tab>
```

`copy()`

method. A copy will not automatically inherit changes to the original list.

In [51]:

```
l = [10,'hello',e] # make a new list called l
m = l.copy() # copy it, assign to m
n = l # assign l to n
m # look at the copy
```

Out[51]:

The `append()`

method appends an input argument to a list:

In [52]:

```
l[2] = 30 # redefine third element of l
l.append(5**4) # append 5**4 to l
print(l)
print(m) # the copy
print(n) # not a copy
```

`count()`

method:

In [53]:

```
l.append(False)
l.append(True)
print(l)
l.count(False)
```

Out[53]:

The `index()`

method returns the index of the input argument:

In [54]:

```
l.index(30)
```

Out[54]:

`keys()`

returns the keys of the dictionary, and `values()`

returns the values:

In [55]:

```
d.keys()
```

Out[55]:

In [56]:

```
d.values()
```

Out[56]:

We will use methods quite a bit as we go on. For now, I just want you to know that they exist.

In a notebook or IPython terminal, you can type `%who`

to see all of your variables:

In [57]:

```
%who
```

You can add variable types to see only that type of variable:

In [58]:

```
%who int
```

In [59]:

```
%who str
```

In [60]:

```
%who dict
```

You can type `%whos`

to get more information about the variables:

In [61]:

```
%whos
```

You can type `%ls`

to see what's in the directory you're working in on your computer:

In [62]:

```
%ls
```

You can type `%pwd`

to see what directory you're working in:

In [63]:

```
%pwd
```

Out[63]:

Here are all of the IPython "magics", for future reference.

We've already seen a few functions, e.g., `print()`

, some list and dictionary methods. A function is essentially just a block of reusable code that performs an action. Functions may or may not take inputs, and they may or may not return outputs.

It's easy to define your own functions. Here's a very simple function that takes two numbers, adds them together, and returns the sum:

In [64]:

```
def add_two_nums(n,m):
'''Returns the sum of inputs n and m'''
print(n)
print(m)
o = n + m
return o
```

The keyword `def`

tells Python that what follows is a function definition. The next bit is the name of the function (i.e., the code that you will use to call that function). Any input arguments appear in the parentheses immediately after the function name, separated by commas if there are more than one. The `def`

line ends with a colon, and the code inside the function must be indented - indentation is how Python knows what is inside the function and what is not. The string immediately below the name is the `docstring`

, or the description of what the function does. Finally, the `return`

statement tells Python what to give back as output.

IPython allows you to get docstrings very easily:

In [65]:

```
?add_two_nums
```

In [66]:

```
z = add_two_nums(x,y)
print(z)
```

`add_two_nums()`

*requires* input values. We can give a function default values if we want. If a function has any arguments with default values, they have to come after any that do not. Here's an example of a function with one required input and two inputs with default values.

In [67]:

```
def add_three_nums(a,b=5,c=7):
'''Returns the sum of inputs a, b, and c'''
print(a)
print(b)
print(c)
return a + b + c
```

In [68]:

```
add_three_nums(3)
```

Out[68]:

In [69]:

```
add_three_nums(3,4,2)
```

Out[69]:

`None`

if we want a placeholder rather than a default value. Let's define a new function to illustrate:

In [70]:

```
def fancy_math(a=None,b=None,c=None):
'''returns (a+b)*c'''
return (a+b)*c
```

Using `None`

like this, we have to give the function inputs if we want to avoid an error:

In [71]:

```
fancy_math()
```

We can still give inputs by position if we want:

In [72]:

```
fancy_math(2,3,4)
```

Out[72]:

Or we can give inputs by name in any order:

In [73]:

```
fancy_math(c=10,a=2,b=50)
```

Out[73]: