---
# System prepended metadata

title: 11-11 Data Types
tags: [Unit 3 - Data Types]

---

Let us confess to God those things that are wrong in our work:
That the presence of God at work is often overlooked;
That creative people are often subjected to long, boring and unrelenting routines;
That skills are undeveloped through lack of training;
That resources are wasted in shoddy work and the production of unwanted goods;
That the maximisation of profit often excludes concern for people;
That men and women are discriminated against because of age, race, gender, disability, lack of skill and length of employment;
That the poor stand so little chance against the power of the rich, and the world’s destitute are forgotten.
Lord, have mercy upon us. Forgive us our sins and help us to amend our lives. Amen.
(https://www.theologyofwork.org/work-in-worship)

# Python Built-in Types

Full reference is [here](https://docs.python.org/3/library/stdtypes.html). Some of them are:

* **Numeric types**: `int`, `float`, `complex`
* **Boolean types** (logic): `bool` (`True` and `False`)
* **Sequence types**: `list`, `tuple`, `str`, `range`, `bytes`, and others
* **Mapping type**: `dict`
* **Set types**: `set`, `frozenset`

## A summary table

| Type                 | Container             | Subscriptable              | Ordered (sequence) | Mutable |
|---------------|---------------|---------------|---------------|---------------|
| Numbers and booleans | No                    | No                         | No      | No      |
| Lists                | Yes                   | Yes (integers)             | Yes     | Yes     |
| Tuples               | Yes                   | Yes (integers)             | Yes     | No      |
| Strings              | Yes (only characters) | Yes (integers)             | Yes     | No      |
| Dictionaries         | Yes                   | Yes (immutable objects) | No      | Yes     |
| Sets                 | Yes (only immutable objects)                  | No                         | No      | Yes     |

## Container types
- An object that is a collection of other objects.
    - For example, you may have a variable pointing to a single number (a numeric type). But you can also a variable pointing to a collection of numbers, or strings, or even another collections of numbers.

## Subscriptable types
- A container whose internal objects can be accessed by means of a "subscript"
    - If a variable is an address, think like adding the complement to this address: "1234 Smith Ave **Apt 101**"
    - Subscripts in Python are written with `[]` **after a variable name**. (Don't confuse this with a list declaration, which also uses `[]`).
    - For example, a Python `list`:

```{python}
condo = ["room 1", "room 2", "room 3"]
print(condo[0])
```

- Notice that if we try to "subscript" an object which is not subscriptable, we get an error:

```{python}
house = 3
print(house[0])
# ERROR: Non-subscriptable type
```

## Sequence types
- Subscriptables whose **subscripts are integer numbers**, which are called **indexes**.
    -   index 0 is for 1st element, index 1 is for 2nd, and so on...
        - **Attention:** indexes always starts at zero!
    - This also assumes that sequence types always have **ordered** elements (doesn't happen with other types such as dictionaries or sets)
    - This is the case with lists, tuples and strings.



```{python}
condo = "room 1", "room 2", "room 3"
print(condo[0]) # room 1
print(condo[1]) # room 2
print(condo[2]) # room 3
```
-   If the index can't be found, we will have an error!

```{python}
condo = "room 1", "room 2", "room 3"
print(condo[4])
# IndexError: tuple index out of range 
```

### Sequence methods
- The methods below thus work for all of these types: lists, tuples and strings.

| Operation            | Result                                                                           |
|----------------------|----------------------------------------------------------------------------------|
| x in s               | True if an item of s is equal to x, else False                                   |
| x not in s           | False if an item of s is equal to x, else True                                   |
| s + t                | the concatenation of s and t                                                     |
| s * n or n * s       | equivalent to adding s to itself n times                                         |
| s[i]                 | ith item of s, origin 0                                                          |
| s[i:j]               | slice of s from i to j                                                           |
| s[i:j:k]             | slice of s from i to j with step k                                               |
| len(s)               | length of s                                                                      |
| min(s)               | smallest item of s                                                               |
| max(s)               | largest item of s                                                                |
| s.index(x[, i[, j]]) | index of the first occurrence of x in s (at or after index i and before index j) |
| s.count(x)           | total number of occurrences of x in s                                            |

### Unpacking sequences

- All sequence types can also be **unpacked** to multiple variables. For example:

```{python}
s = ["I", "am", "your", "father"]
a, b, c, d = s
print(a)
print(b)
```

```{python}
s = "hi!"
ch1, ch2, ch3 = s
print(ch1, ch2, ch3)
```

- But be careful: you will get an error if you don't match the length!

### What can go in?

* With the exception of strings, which only permit characters, sequences can be a collection of items of any type.

```{python}
x = (1, 3.33333, "hello", True, 4)
```

* You can even make tuples of tuples, lists of lists, lists of tuples...
  -   For example: a 3x3 matrix - a 3-element list of 3-element lists

```{python}
mat = [[1,2,3],
      [4,5,6],
      [7,8,9]]
print(mat[0][1]) # accessing value in row 0 and column 1
```

## Mapping types
-   **Subscripts can be any kind of object** (given that it is an *immutable* object --- e.g., lists are not allowed), which are called, in this case, **keys**.
    -   This is the case of the dictionary type (`dict`):

```{python}
band = {"vocals": "Jon Anderson", "guitar":"Steve Howe", "bass": "Chris Squire", "keyboard": "Rick Wakeman"}
print(band["vocals"]) # access Jon Anderson
print(band["guitar"]) # access Steve Howe
```

## Callable types
-  Some objects in Python can be used to run code. These are "callable". *Functions* and *classes* are examples (check, for example, `type(print)` or `type(math.cos)`)
    -   The syntax for calling them is already known: use parentheses, like `print("hi")`
    -   See, for example, what happens if you try calling a non-callable object:

```{python}
a = 1
a()
# TypeError: 'int' object is not callable
```

## Immutable/Mutable types

### Lists: mutable sequences

- Declared with square brackets: `x = [2, 6, 9]`
    - It is possible to **initialize an empty list** with `x = []`
- List methods:

| Operation             | Result                                                                                |
|-----------------------|---------------------------------------------------------------------------------------|
| `s.append(x)`           | appends x to the end of the sequence (same as `s[len(s):len(s)] = [x]`)                 |
| `s.clear()`             | removes all items from s (same as `del s[:]`)                                           |
| `s.copy()`              | creates a shallow copy of s (same as `s[:]`)                                            |
| `s.extend(t)` or `s += t` | extends s with the contents of t (for the most part the same as `s[len(s):len(s)] = t)` |
| `s *= n`                | updates s with its contents repeated n times                                          |
| `s.insert(i, x)`        | inserts x into s at the index given by i (same as `s[i:i] = [x]`)                       |
| `s.pop()` or `s.pop(i)`   | retrieves the item at i and also removes it from s                                    |
| `s.remove(x)`          | remove the first item from s where `s[i]` is equal to `x`                                 |
| `s.reverse()`           | reverses the items of s in place                                                      |

- When dealing with mutable objects, it is *very important* to check if an operation is changing the object or making a copy of it.

For example:

```{python}
a = [1,2,3]
b = a
a[1] = 5
print(b)
```

* What happened here? Wasn't `b` supposed to remain `[1,2,3]`?
    - variables **`a` and `b` are pointing to the same object** (the list `[1,2,3]`). If we change something in `a`, we change in `b` and vice-versa.
    - You can check that with the function `id()`, which finds an unique integer identifier for each object.

```{python}
print(id(a))
print(id(b))
```

- Things are different if we make a copy of the object (observe, now, that `id`s of `a` and `b` are different)

```{python}
a = [1,2,3]
b = a.copy()
a[1] = 5
print(a)
print(b)
```

  - So, attention: *mutable objects need to be explicitly copied, otherwise new variables will just point to the same old object. Immutable objects don't need this.*

### Tuples: immutable sequences

- Represented just as values separated with commas (`fruits = "banana", "apple", "orange"`), or separated with commas  and enclosed with parentheses (`fruits = ("banana", "apple", "orange")`)

- **Why using immutables?**
    - They uses less memory and demands less processing.
    - They don't need to be copied.

```python
fruits[1] = "pineapple" # try changing "apple" to "pineapple"
# TypeError: 'tuple' object does not support item assignment
```

- What if I *really* want to change?
    1.  Create a new tuple (with the changes you want), or
    2.  Convert the tuple to a **list**, change the value you want (lists are mutable), and convert it back to a tuple.
        -   In that case, however, just go with a list from the start!

### Strings: immutable sequences of characters

- Specified with `""`.

```{python}
a = "Hello World"
print(a[1])
print(a[4:9])
```

- Important: since they can't change, **all the methods applied to immutable objects are making copies of them!**
    - Which means **you will have to assign the result**. If you just call, like, `a.upper()`, this won't do anything to `a` itself.
    - For example:

```{python}
a = "Hello"
b = a.upper()
print(b)
print(id(a))
print(id(b))  # id of a is different from id of b
```

- See [documentation](https://docs.python.org/3/library/stdtypes.html#string-methods) for all string methods.

# Exercise: **Library Catalog Organizer**

You are tasked with managing a library's catalog. Each book in the catalog has a title, author, and genre. The catalog is stored as a list of tuples, where each tuple contains the title (string), author (string), and genre (string).

```python
catalog = [
    ("To Kill a Mockingbird", "Harper Lee", "Fiction"),
    ("1984", "George Orwell", "Dystopian"),
    ("Moby Dick", "Herman Melville", "Adventure"),
    ("The Great Gatsby", "F. Scott Fitzgerald", "Fiction"),
    ("Brave New World", "Aldous Huxley", "Dystopian")
]
```

Perform the following tasks:

- Extract Genres: Create a list of all the unique genres in the catalog.
- Count Books by Genre: Create a dictionary where each genre is a key, and the value is the count of books in that genre.
- Sort Books by Title: Create a new list of tuples where the books are sorted alphabetically by their title.
- Search for Books by Author: Write a function `search_by_author(catalog, author)` that takes the catalog and an author's name as input and returns a list of book titles written by that author.
- Generate a Summary String: Write a function `generate_summary(catalog)` that returns a formatted string summarizing the catalog, such as:  
   ```
   Library Catalog:
   - Fiction: 2 books
   - Dystopian: 2 books
   - Adventure: 1 book
   ```
- **Bonus Challenge:** Write a function `add_book(catalog, title, author, genre)` that adds a new book (as a tuple) to the catalog, ensuring the catalog remains a list of tuples.