---
# System prepended metadata

title: ACIT 1515 - Lesson 8
tags: [ACIT-1515]

---

<style>
    .markdown-body h1:first-of-type {
        margin-top: 24px;
    }
    .markdown-body h1 {
        margin-top: 64px;
    }
    .markdown-body h1 + h2 {
        margin-top: 32px;
    }
    .markdown-body h2 {
        margin-top: 48px;
    }
    .markdown-body h2.topics {
        font-size: 1.8em;
        border-bottom: none;
    }
    .markdown-body h3 {
        color: cornflowerblue;
    }
    .markdown-body h4 {
        color: cornflowerblue;
    }
    .markdown-body p strong {
        font-weight: normal;
        color: red;
    }
    .exercise {
        font-size: 150%;
        font-weight: bold;
        color: rgb(227,112,183);
    }
    .note {
        color: red;
    }
</style>

# ACIT 1515 - Lesson 8

<h2 class="topics">Topics</h2>

- [Dictionaries](#Dictionaries)
- [JSON](#JSON)
- [File I/O](#File-IO-with-JSON)

## Dictionaries

In lesson 5 we discussed sequences: ordered collections of values. Lists and tuples in particular can hold any type of value, and can be read and updated using square bracket notation:

```python=
letters = ['a', 'b', 'c']

print(letters[0])     # prints a
letters[0] = 'A!'     # lists are mutable 
print(letters[0])     # prints A!
```

A ==dictionary== is also a collection of values, but they are named and _unordered_. This means we **cannot** use numeric positions to read or update a dictionary.

Dictionaries are Python's one and only _mapping_ type, so-called because they map ==keys== to ==values==. Every value in a dictionary must have a named _key_ associated with it.

Keys in a dictionary are typically strings (but can be any immutable type), and the value can be any data type. Multiple key/value pairs must be separated by commas, and there must be a colon between the key and the value.

```python=
dog = {
    'name': 'Charlie',     # key is name, value is the string Charlie
    'age': 12              # key is age, value is the int 12
}

print(type(dog)) # prints <class 'dict'>
```

To read or update a value in a dictionary, we must specify a _key_ inside square brackets rather than a _number_:

```python=
print(dog['name'])     # prints Charlie

dog['name'] = 'Tater'
print(dog['name'])     # prints Tater
```

Attempting to read or update a key that does not exist results in a _KeyError_:

```python=
print(dog['breed'])     # KeyError: 'breed'
```

### Dictionary Methods

We can use the following simple syntax to search for _keys_ in a dictionary:

```python=
print('name' in dog) # True, the dog dictionary has a name key
```

but if we need to search for _values_ we have to use one of Python's dictionary methods.

#### .keys() and .values()

`dog.keys()` returns all the keys in the dog dictionary
`dog.values()` returns all the values

Both of the above methods return a special data type called a _dictview_, which supports the `in` operator and the `len()` method:

```python=
dog = {
    'name': 'Charlie',
    'age': 12
}

values = dog.values() # values is a 'dictview'
number_of_values = len(values) # prints 2

print('Charlie' in values) # True
```

We can also use `.keys()` and `.values()` if we want to loop through all the keys or values in a dictionary:

```python=
# prints all keys in the dictionary, one per line
for k in dog.keys():
    print(k)
 
# prints all values in the dictionary, one per line
for v in dog.vals():
    print(v)
```

#### .items()

Additionally, dictionaries have an `items()` method, which returns a list of tuples, with each tuple containing both a key _and_ a value. Printing `dog.items()` gives us the following result:

```
dict_items([('name', 'Charlie'), ('age', 12)])
```

`dict_items` is also a dictview object. Thankfully, _dictview_ objects can be converted to a list, so with a little extra work we can see more clearly what is returned by `dog.items()`:

```python=
items = list(dog.items())
print(items) # prints [('name', 'Charlie'), ('age', 12)]
```

We can test if a particular key/value pair exists in a dictionary by putting a tuple on the left-hand side of the `in` operator:

```python=
print(('name', 'Charlie') in dog.items()) # True
```

and we can step through the keys and values at the same time, provided we declare a _second variable_ in the loop:

```python=
for k, v in dog.items():
    print(k) # print key from nth tuple
    print(v) # print value from nth tuple
```

#### Adding and deleting values

New key/pair values can be added to an existing dictionary using square bracket notation and specifying a new key name:

```python=
dog = {
    'name': 'Charlie',
    'age': 12
}
dog['breed'] = 'Ridgeback' # adds 'breed' key to dict
```

Key/value pairs can be deleted using the `del` keyword:

```python=
dog = {
    'name': 'Charlie',
    'age': 12
}
print(dog) # prints { 'name': 'Charlie', 'age': 12 }

del dog['age']
print(dog) # prints { 'name': 'Charlie' }
```

## JSON

JSON, short for JavaScript Object Notation, is a format used for storing or sending data as **plain text**. JSON allows us to store information like Python dictionaries in files on a hard drive, or send those dictionaries across a network to be read by another language or application.

JSON looks very similar to Python dictionaries with one exception: **the keys must be strings** (note that keys use _double-quotes_, not single-quotes):

```json 
{
    "name": "Charlie",
    "age": 12
}
```

and we can encode and decode using Python's `json.dumps()` and `json.loads()` methods:

```python=
import json

dog = {
    'name': 'Charlie',
    'age': 12
}

json_dog = json.dumps(dog) # convert dict to JSON string
print(type(json_dog)) # prints <class 'str'> - this is a string in JSON format!

original_dog = json.loads(json_dog) # convert JSON string to dict
print(type(original_dog)) # prints <class 'dict'>

print(dog == original_dog) # True!
```

## File I/O with JSON

To save or read JSON from a file, we can use the `with open() as` syntax to open and close files, combined with the `json.dump()` and `json.load()` methods. 

Note the difference between JSON methods: `dumps()` and `loads()` converts a dictionary to and from a JSON string, whereas `dump()` and `load()` reads or writes to a file.

```python=
grades = {
    'ACIT1620': 80,
    'ACIT1515': 99,
    'ACIT1630': 55
}

# create a file in the same folder as the current script
with open('test.json', 'w') as file:
    json.dump(grades, file)
```

In the above example we pass two arguments to `open()`:

1. The name of the file we want to create, `test.json`
2. The 'mode': `'w'` for write. This will create the file if it does not already exist. Other available modes are `'r'` for read, and `'a'` for append (similar to write, but adds new data to the end of the file instead of overwriting)

`as file` specifies a variable named 'file' that contains a reference to our file, which is necessary for the `json.dump()` method. As always this variable can be named whatever you like. `dump` requires us to pass two pieces of information, the dictionary to write, and the file reference we want to write it to.

Assuming we have an existing file with JSON data we want to read, we can use:

```python=
# read a file in the same folder as the current script
with open('test.json', 'r') as file:
    data = json.load(file)
    
print(type(data)) # prints <class 'dict'>
print(data) # prints {'ACIT1620': 80, 'ACIT1515': 99, 'ACIT1630': 55}
```

With the combination of `open()` and the `json.dump()` and `json.load()` methods, we have persistence _and_ a way to store data that can be easily transferred across a network
