Python Beginners Tutorial: Animal registry
Tags: python, beginners, tutorial, programming
Published: 2021-09-13 23:34:27

We will write a program that will allow us to make a register of animals, with their scientific name and common name, binded by a unique ID. Using that ID we will be able to remove the animal from the register, or simply modify the names already written. Also an option to view the list of animals already registered and an option to exit the program.

The goal is to generate and print a list that can be modified like this:

|      | Animal Registry        |                   |
|------|------------------------|-------------------|
| **ID**   | **Scientific Name**        | **Common Name**       |
| 3Y99 | Canis Lupus Familiaris | Siberian Husky    |
| MT5R | Cebinae                | Capuchin Monkey   |
| 8D6U | Octopoda               | Octopus           |
| YG0E | Anthophila             | Bee               |
| JN14 | Dinastinae             | Rhinoceros Beetle |
| WVTA | Python Regius          | Ball Python       |
| HAZP | Gorilla                | Gorilla           |


We will work with:
- The PrettyTable module.
- Nested dictionaries.
- Flow control.
- Functions.

>You must install PrettyTable via Pip.
>Try with:
>pip install prettytable

First, we will import the necessary modules:
*PrettyTable* for the creation of the table where we will register the animals, *random* and *string* to 
generate a random ID with which we will identify the row in the record and *sys* to exit the program.

```python
from prettytable import PrettyTable
import random
import string
import sys
```

Now we will create an empty dictionary where we will store the animals that we are going to register.

``` python
animal_dic = {}
```

We will write the conditional "if name == "main"" to avoid import errors and for good practice.
and call our main() function.

``` python
if __name__ == "__main__":
    main()
```
>An explanation of how *if* \__name__ == "\__main__" works is beyond the scope of this article.

-----------------------------------------------------------------------------------------------------
## Functions:

Now, let's write the functions that will run our program, these will go **BEFORE** the conditional "*if* \__name__ == "\__main__"" described above. Otherwise Python would try to execute the *main()* function when it **has not** been defined, thus triggering an error.

------------------------------------------------------------------------------------------------
### Main function:
Now, we must bind the option chosen by the user to the functions of our program.

The next step is straightforward: inside *main()* we will force this *loop* to run, with a *while True*, 
it will print the instructions to *add*, *delete*, *update*, *check* and *exit* the program.
We also have an *input()* that will be the option that the user chooses and this will be stored in
the variable *user_input*. Pay attention to the *.lower()* method, transform what the user enters into lower case, so we avoid typing errors. 
Finally, the conditionals. Depending on what the user chooses, a function will be called.

``` python
# ~~~~~~~~~~~~~~~~~~ Functions(): ~~~~~~~~~~~~~~~~~~~
def main():
    # ~~~~~~~~~~~~~~ User's choise ~~~~~~~~~~~~~~~
    while True:
        instrucctions()
        user_input = input("\nWhat do you want to do? "
                           "(a, d, u, l, e): ").lower()
        if user_input == "a":
            add_animal()
        elif user_input == "d":
            delete_animal()
        elif user_input == "u":
            update_animal()
        elif user_input == "l":
            print_register()
        elif user_input == "e":
            exit_program()
        elif not user_input:
            print("please, enter something!")
```

---------------------------------------------------------------------------------------
### Instructions:

We will create a function so that the user can see the options of our program along with a key to press to execute that option, and so that the user knows what to do. The options are: add animal, delete animal, update animal, check list and exit the program.
So, it would be like this:

```python
# ~~~~~~~~~~~~~ Functions ~~~~~~~~~~~~~~
def instrucctions():
    print('\nAnimal registry program:'
          '\n1: Enter A or a to add new animal.'
          '\n2: Enter D or d to delete a animal'
          '\n3: Enter U or u to update animal.'
          '\n4: Enter L or l to check list of animals. '
          '\n5: Enter E or e to exit the program.')
```

We print the options. *'/n'* means a line break.

-----------------------------------------------------------------------------------------------------
### Print the list:

``` python
def print_registry():
    x = PrettyTable(["ID", "Scientific Name", "Common Name"])
    for animal_data in animal_dic:
        x.add_row([animal_data, animal_dic[animal_data]["scientific_name"],
                   animal_dic[animal_data]["common_name"]])

    print(x.get_string(title="Animal registry"))
```


Pay attention to what happens here:
- To give names to the column headers we write them in **list** format, and we pass it as parameter of *PrettyTable* and store it in the variable "*x*".
- Before we talk about the *for* cycle, we must remember and be aware that we are trying to access a dictionary **within a dictionary*. Just to lining things up, let's compare a common* dictionary, to a *nested* one.

#### Dictionary:
Where we have a collection of items stored in **key**/**value** pairs. 
```py
  {'name': 'Adam'}
 ```
'*name*' is the **key** and '*Adam*' is the **value**.
```py
  {'name': 'Adam', 'last': 'Smith'}
  ```
We can have **more** than one pair of **key**/**value** in just **ONE** dictionary. '*name*' and '*last*' are *both* **keys**.

#### Nested dictionary:
The values to the outside of the dictionary, are also dictionaries.
```py
  {0: {'book': 'Wealth of Nations', 'author': 'Adam Smith'},
   1: {'book': 'Economic Sophisms',' author': 'Frédéric Bastiat'}}
   ```
**0** is the **key** of the *external* dictionary, and '**book**' and '**author** are **keys** of the *internal* dictionary.

With that in mind, let's go back to the *previous* code. 
``` python
for animal_data in animal_dic:
```
What happens here is that we access the **external key**, through *animal_data*.

``` python
x.add_row([animal_data, animal_dic[animal_data]["scientific_name"],
           animal_dic[animal_data]["common_name"]])
```

Since we access the external **key**, we will have access to the internal values. It happens as follows:
- animal_dic = Animal ID
- animal_dic[animal_data]["scientific_name"] = scientific name
- animal_dic[animal_data]["common_name"] = common name 

So, then:
``` python
x.add_row([animal_data, animal_dic[animal_data]["scientific_name"],
           animal_dic[animal_data]["common_name"]])
```
We enter the values of our dictionary into the **row** of PrettyTable as **a list**, by means of the 
method *add_row()*. Look carefully, everything is between braces [].

Finally we give a header to our table:

``` python
print(x.get_string(title="Animal registry"))
```

Now you can print the list by starting the program and pressing *"l" or "L".* Obviously, in this moment our dictionary is empty. We will see later how to enter data into it.

-----------------------------------------------------------------------------------------------------
### Random ID:
We need an ID to identify our rows, this ID will serve as a *binding*, being the external **key** of our nested dictionaries. 
It is necessary, in the future, to be able to update one or both names of the animal, or to delete it definitively.
To make it easier for the user, and to avoid creating duplicates, we will create a function that returns 
a random string. We will do it with the following code:

``` python
def random_id():
    random_string = ''.join(random.choices(string.ascii_uppercase
                                           + string.digits, k=4))
    return random_string
```
Where "*k*" is the length of the string.
>An explanation of how this function works is beyond the scope of this article.

-----------------------------------------------------------------------------------------------------
### Add:
Now let's make the function that will take the values typed by the user and enter them into the dictionary.

``` python
def add_animal():
    animal_id = random_id()
    scientific_name = input("\nPlease enter the scientific name: ").title()
    common_name = input("\nPlease enter the common name: ").title()
    data = {animal_id: {'scientific_name': scientific_name,
                        'common_name': common_name}}
    if not scientific_name and not common_name:
        print("You must write something!")
    else:
        animal_dic.update(data)
```

- The first thing you'll notice is that we call the *random_id* function which returns a string and store it in the *animal_id* variable. As the name implies, it identifies the animal and the row within the record. 
- Using the *input()* method we ask the user to type the common and scientific name. 
and store them in variables. We use the *.title()* method for aesthetics.

Let's pause and look at this part of the code again:

``` python
    data = {animal_id: {'scientific_name': scientific_name,
                        'common_name': common_name}}
```

- We update our nested dictionary and store it in a variable called "*data*". So:
*animal_id* is the *random ID* that our function returns. It is the external **key** of this 
nested dictionary.
'*scientific_name*': which is in quotation marks, is the internal key.
*scientific_name*: which is a variable, is the internal value. Entered by the user.

- The next block of code sets a condition where if the user **does not** enter either a scientific name or a common name, print a message that he/she must do so.

- Finally, in the '*else*': if at least one of the two *input()* have been entered, we will update the "*animal_dic*" with the data entered by the user. 

-----------------------------------------------------------------------------------------------------
### Delete row:
Now let's write the function to delete the row of the user's choice, using the *unique ID*.

``` python
def delete_animal():
    animal_id = input("\nEnter the animal ID you want delete: ").upper()
    if animal_id in animal_dic:
        choice = input("Delete (y/n)").lower()
        if choice == "yes" or choice == "y":
            del animal_dic[animal_id]
            print(f"{animal_id} registry has been deleted!")
    else:
        print("ID not found. Check list pressing 'L'")
```

- We ask the user to enter the ID of the row. We use the * upper () * method so that the user does not have to type in all capital letters.
- We create an '*if*' that checks if the *ID entered* by the user is in the dictionary, then give him the choice to delete it with a yes or no. If user types yes it will be deleted with a 
"**del** animal_dic[animal_id]" which looks for the ID (external **key**) and deletes all its internal **key**/**values**. 
- With the *else* we declare that, if the *ID* has not been found, it prints "ID not found"


-----------------------------------------------------------------------------------------------------
### Update:
Perhaps the user has made a typo, and wants to change the fields in a specific row.
There is a need for a function to update them.

``` python
def update_animal():
    animal_id = input("\nEnter the animal ID you want update: ").upper()
    # If external key in dictionary, if key is equal to ID (animal_id)
    for animal in animal_dic:
        if animal == animal_id:
            choice = input(f"Update registry {animal_id}? (y/n): ").lower()
            if choice == "yes" or choice == "y":
                # Changing names
                scientific_name = input("Write a new scientific name: ").title()
                common_name = input("Write a new common name: ").title()
                if not scientific_name and not common_name:
                    print("You must write something!")
                else:
                # Updating
                    animal_dic[animal]['scientific_name'] = scientific_name
                    animal_dic[animal]['common_name'] = common_name
                    print("registry updated!")
                    print_registry()
        else:
            print("ID not found. Check list pressing 'L'")
```
- The user enters the ID via an *input()* and it is stored in a *variable*.
- If the ID (**external key**) exists in our dictionary it will step through it with a *for loop*,
  otherwise, it will print an "*ID not found*".
- We ask if the user really wants to delete the **row** and store their answer in a 
  *variable. If yes, we proceed. Otherwise the flow stops.
- We ask the user, using an *input()*, to type the new names and we store them in
  *variables*.
- If the user has left **both** fields blank, it will print "*You must write something!*" otherwise we will access the dictionary, its foreign key and store the new values. 

-----------------------------------------------------------------------------------------------------
### Exit the program:
``` python
def exit_program():
    sys.exit("Goodbye!")
```

We use the *exit()* method of the *built-in* **sys** module to exit the program and print a message. 

-----------------------------------------------------------------------------------------------------
## Full code:
https://github.com/RicardoRien/animal_registry