<jupyter_start><jupyter_text>SLU11 | String & File Handling***In this notebook we will be covering the following: PART 1 | File Handling:- Text and Binary files- `Open()` and `Close()` files- `Read()` and `Write()` files PART 2 | String manipulation:- The `strip()`, `listrip()`, `rstrip()` functions- String concatenation- The `lower()`, `upper()` and `capitalize()` functions- Substring- String indexing - Index characters- The `replace()` function- The `join()` and the `split()` functions- Loop through strings PART 3 | Testing, Exception raising & Handling:- Error and Exception concept- Common errors and exceptions- Raise and handle errors- The `assert` function***  PART 1 | File HandlingHandling Python files is **a very important concept**. Developers build a lot of programs, data scientists create different models, and all of them need to first **read some data and then save the results**... Can you guess from where they read them? And where they save them? Exactly! On **files!**Imagine that you wanted to check how many files on your computer have the word **`'Kittens'`**. I'm sure you wouldn't want to do it **by hand!**:   1. Opening hundreds and thousands of files;   2. Searching for a word on each file   3. Counting this...**Ah... So boring! Let's make the computer do it!**There are multiple ways to read and save files, and they usually depend on the file format you're dealing with.- Images, - Plain text,- JSON, - CSV (data with delimiters)- There are many and many of them.But from now on, let's concentrate on **two basic types** of files:  TEXT FILESThey are the well-known **`.txt`** files that you can easily create by using any simple text editors such as our old-friend **Notepad**. When you open those files (**we'll check this soon**), you'll see the content as **plain text**. * You can easily edit or delete the contents* They take minimum effort to maintain* They are easily readable* Provide the least security* And take more storage space BINARY FILESThese little guys are mostly the **`.bin`** files on your computer. The main mission is to be a file made for computers (**and not for humans, sorry!**). They store the information in the binary form (**only 0's and 1's**), instead of storing it in **plain text**.* They can hold a higher amount of data* They are not readable easily* Provide better security than text files* And take less storage spaceIn this learning unit, we're going to learn how to handle these files using built-in python functions. Let's start!  `Open()` filesIn order to open a file in Python we can use a built-in **`open()`** function. This function gets two arguments as inputs:    * **file path:** _required parameter. Can be a relative or absolute path_   * **mode:** _optional parameter. By default mode='r'_   Let's detail each one below:  FILE PATH   * The **path** of the file is its address on the device where the file is stored.    * The **directory** is the address of the folder where the file is.      * The **root** or **root folder** or **root directory** is the beginning of the structure where the file is.Now, to make things easy, think about this analogy:_**`Device`** is the world, and **`path`** is the address of the **`file`** in this world._ _**`Directory`** is the address of the **`folder`** where the **`file`** is stored._The **`path`** of a file can be **absolute** starting with the **`Directory`** (_a.k.a **`root`**_) or **relative** to the program’s current working **`directory`**.For **file** **`document.txt`**, on image above:   * **Device** is **`C:`**   * **Directory** is **`C:\home\ds-prep-workspace\week 6\SLU-11`**   * **Absolute path** is **`C:\home\ds-prep-workspace\week 6\SLU-11\document.txt`**   * **Relative path** is **`document.txt`**Pretty easy, right? 😄  MODEThe **`mode`** argument indicates the permission that we want to give for that file if we allow operations like **reading from it** and/or **writing on it**. This parameter is **optional** (which means that it has a default value), but by using it we are protecting the file of operations that we don't want to be executed.The accepted values for **`mode`** argument are: - **`'r'`** - **Read** mode which is used when the file is **only being read** - **`'rb'`** - Same **read** mode, now for **binary files**  - **`'w'`** – **Write** mode which is used to **write information to the file** ⚠️ _If the file exists, its contents will be overwritten. If not, it will be created_ ⚠️- **`'wb'`** - Same **write** mode, now for **binary files**  - **`'a'`** – **Appending** mode which is used to **add new data to the end of the file** ⚠️ _If the file does not exist, it will be created_ ⚠️- **`'ab'`** - Same **appending** mode, now for **binary files**  - **`'r+'`** – Special **read and write** mode, which is used to handle both actions when working with a file- **`'rb+'`** - Same special **read and write** mode, now for **binary files**  - **`'a+'`** – Special **read and append** mode, which is used to handle both actions when working with a file- **`'ab+'`** - Same special **read and append** mode, now for **binary files**  Now, it's practice time!Using **`relative` path**, let's open the file **`document.txt`** on **read mode** that is located on the same folder as this SLU.<jupyter_code>relative_path = "document.txt"
our_file = open(relative_path, mode='r')
our_file<jupyter_output><empty_output><jupyter_text>_Ops! Weird, right?_Actually, **`open()`** doesn't read the file's content. As the name suggests, **it only opens the file, returns a file object and keeps it in memory**. Now that we have access to the **file object**, we can use some functions to read the content of the file or write in it. Let's see how we can do that on **`Read()` files** Section  `Close()` filesIt's important to mention, that the same way as we usually close programs after we use them, we also should close python files, because when we **open a file**, it assumes an **`in use`** status which will block it from being opened by others (programs/processes/people). To avoid this we always need to **`close()`** the file after we finish what we are doing. How can we do this?<jupyter_code>our_file.close()<jupyter_output><empty_output><jupyter_text>Very very very simple! `with open()` statementIf we don't want to close the files each time after handling them, we should use the **`with open()`** statement.<jupyter_code>with open('document.txt') as f: # open the file and store it in a variable called 'f'
    lines = f.readlines() # read all the lines
    for line in lines: # iterate over each separate line
        print(line) # print this line<jupyter_output>maria

ate an apple

pie for breakfast<jupyter_text>As soon as the python compiler reaches the end of the **`with`** statement, **the file will automatically be closed.** Amazing!!  `Read()` filesThere are a few ways to read the file's content. Let's see all of them. Read the whole content of a fileWe're going to read the whole file's content and store it in a variable called **`txt`**:<jupyter_code>f = open("document.txt", mode='r')

txt = f.read()
txt<jupyter_output><empty_output><jupyter_text>Great! We can see it now. All the lines are stored in one variable delimited by **`\n`** symbols (**`\n`** means a *new line*).It doesn't look pretty until we **`print()`** it:<jupyter_code>print(txt)<jupyter_output>maria
ate an apple
pie for breakfast<jupyter_text>Now it looks much better. As we can see, all **`\n`** symbols were removed and the text was printed on *3 separate lines* (the same way as in the original file). You can open this file in a text editor if you don't trust me! Read Line By Line - `readlines()`With **`read()`** method we're storing the whole file in just one string.It is also possible to read each line separately using the **`readlines()`** method. What it does is **read all lines in the file and return a list containing each line as a list element**. We can then loop through this list, and access each line's contents. Let's see how it's done!<jupyter_code>lines = f.readlines()
print(lines)<jupyter_output>[]<jupyter_text>_Wait, but why is it empty?_ _Ok, ok! Don't panic!_ Let's think about what we did... We first opened the file and stored it in a variable called **`f`**. Once we opened a file, a **read cursor** appeared in the very **beginning of this file.** (_the same way as if we'd open it in a text editor_)Next, we called **`read()`** method and stored the result in a variable called **`txt`**. Method **read()** read through the entire file and **left the read cursor at the end of the file.** (_with nothing more to read_)So the **read cursor was at the end of the file** when we tried to read lines separately. This is why the list **`lines`** was empty. Very curious, right?Now let's move the **read cursor** back and do the same thing once again using method **[seek()](https://www.tutorialspoint.com/python/file_seek.htm)**.<jupyter_code>f.seek(0) # moves the cursor to the very beggining of the file
lines = f.readlines()
print(lines)<jupyter_output>['maria\n', 'ate an apple\n', 'pie for breakfast']<jupyter_text>As you can see, the output of **`readlines()`** is a list, with each element being a line. Let's iterate over the list and print its elements:<jupyter_code>for line in lines:
    print(line)<jupyter_output>maria

ate an apple

pie for breakfast<jupyter_text>We can access separate lines simply indexing the list:<jupyter_code>print(lines[0])<jupyter_output>maria<jupyter_text>Read a single line - `readline()`Actually, **we don't have to read the whole file if we don't want to.**Sometimes a file might be large, and we don't want to store large files in the memory. Our computer has a limited amount of memory, so we can't just put terabytes of data in there. So, that is when the single line reading (with **`readline()`** method) comes in!Let's just read the **first line of the file** (_don't forget to move the cursor_)<jupyter_code>f.seek(0)
f.readline()<jupyter_output><empty_output><jupyter_text>And the second line:<jupyter_code>f.readline()<jupyter_output><empty_output><jupyter_text>As you can see, every time we execute the readline method, the cursor moves to the next one. Because of that, the output of **`readline()`** is different every time we call it.So, we already know how to open a file, read the whole file or read a single line.  `Write()` filesThe last thing to mention is the fact, that we can also write information to filesWhat we need to do is:1. Open the file with permissions to write   ❗ by default, when we call **`open()`** function, it opens the file with read-only permissions **`mode='r'`**2. Write lines to the file3. Close the files<jupyter_code># open a new file with write permissions (mode='w')

with open('new_document.txt', mode='w') as f:
    f.write("I just learned how to deal with files in python")<jupyter_output><empty_output><jupyter_text>Now find this file locally! It's going to be stored in the same **directory where this learning notebook is stored**.And now that we wrote on the file let's check what is inside with what we learned so far.<jupyter_code>with open('new_document.txt') as f:
    print(f.readlines())<jupyter_output>['I just learned how to deal with files in python']<jupyter_text>And that is all! 🎉🎉You are able, from now on, to manipulate your files using python! 🐍*** PART 2 | String Manipulation We already learned about different types of variables in python* int, * string, * float,* bool,* and etc.Now it's time to learn about **`string` more thoroughly**. As you already know, strings represent **text** in programming languages.Programmers and data scientists work with strings all the time. Look around, almost everything has text!Furthermore, there is a big field in Data Science called **Natural Language Processing** (a.k.a. **`NLP`**), in which the only thing we work with is ... **text** (striiiiings)!**Text** is so good that mathematicians decided to add some letters to formulas just because it's beautiful, you see? 😏So let's learn how to deal with it!<jupyter_code>f = open('document.txt')
lines = f.readlines()
sentence_1 = lines[0]

print(type(sentence_1)) # print the type of the variable
sentence_1<jupyter_output><class 'str'><jupyter_text>The `strip()`, `listrip()`, `rstrip()` functionsAs you can see, `sentence_1` is a **string** **``** representing the **first line** of the **`document.txt`**It contains **`\n`** symbols, so let's **remove them**. We can use the **`strip()`** method for that. This allows us to remove **any whitespaces** in a string. (_any horizontal/vertical space. So both **`" "`** and **`"\n"`** symbols_)There are also **`lstrip()`**, which removes whitespaces **only to the left** of the string, and **`rstrip()`** methods, which removes whitespaces **only to the right** of the string<jupyter_code>s = '\n\n   word   \n\n'
s.strip()
s.rstrip()
s.lstrip()<jupyter_output><empty_output><jupyter_text>We can also define the symbol that should be removed by **`strip()`** method. So if we want to **keep spaces**, but want to **remove only the `"\n"` symbols**, we can do it the following way:<jupyter_code>s.strip('\n')<jupyter_output><empty_output><jupyter_text>String concatenationNow let's read all the lines from the document, concatenate them (**combine them in one string**) and **remove all the `\n` symbols:**<jupyter_code>lines
# concatenate strings using + and remove whitespaces using strip() function
sentence = lines[0].strip() + lines[1].strip() + lines[2].strip() 
sentence<jupyter_output><empty_output><jupyter_text>Looks like we also need to add spaces between lines. Let's do it:<jupyter_code># concatenate strings with spaces (' ')
sentence = lines[0].strip() + ' ' + lines[1].strip() + ' ' + lines[2].strip() 
sentence<jupyter_output><empty_output><jupyter_text>Feel free to explore and play with strings concatenation, it'll be really fun and you will interiorize more naturally these methods which now sound like a bunch of crazy stuff... The `lower()` and `upper()` functionsNow let's learn about how Python allows us to easily change the whole line to upper or lowercase if we want to:<jupyter_code>sentence = sentence.upper()
print(sentence)
sentence = sentence.lower()
print(sentence)<jupyter_output>maria ate an apple pie for breakfast<jupyter_text>These functions might be really useful in some cases. For example, trying to understand if a movie review is **positive** or **negative.** It's often a good idea to **convert all the words to lowercase** because there is no difference for us if a user wrote **`'bad'`, `'BAD'`, `'Bad'`, `'bAd'` or `'BaD'`** (and all the other possibilities), but all these strings **are different for the machine**, even having the **same meaning for us.** SubstringSometimes, we want to extract new **pieces** of string from our base **string** (_remember when we read all lines in one variable?_) maybe extract a word from an expression. Using our **`sentence`** example, how can we create a new string with only **`apple pie`**?Python provides various methods to create a substring. We will look into the most important operations related to substrings. String indexing - Index charactersThe same way as with **lists**, we can index **strings** using this notation: **`string[index]`**It's a simple way to **get just one string's character**_But first let's remind ourselves about python lists_Do you remember how we access the **first line** in a list of 3 lines from our **`readlines()`** example?**`first_line = lines[0]`**In a way, a string is similar to a list of characters. **We can index them the same way as we do with lists**:<jupyter_code># print the whole string
print(sentence)

# print the first character of the string (index 0)
print(sentence[0])<jupyter_output>maria ate an apple pie for breakfast
m<jupyter_text>String slicing The same way as with lists, we can slice strings. Let's print the **`apple pie`** words of the sentence. We should take indexes from 13 to 22:<jupyter_code>sentence[13:22]<jupyter_output><empty_output><jupyter_text>If now we combine our knowledge of strings concatenating, indexing and slicing, we can build a **whole new string** from the original one.<jupyter_code>sentence[3].upper() + ' like ' + sentence[10:18]<jupyter_output><empty_output><jupyter_text>We'd like to be polite and call **Maria** with a capital letter. Let's use the same technique to change the first letter in the original sentence: > **m**aria -> **M**aria.<jupyter_code># apply upper() to the first letter
# concatenate it with the rest of the sentence (starting from second letter)
sentence = sentence[0].upper() + sentence[1:] 
print(sentence)<jupyter_output>Maria ate an apple pie for breakfast<jupyter_text>The `replace()` functionAnother way to do some changes on the **string** is with **`replace()`** method. It allows us to replace all occurrences of a **substring** with a new string. Remember that this method **doesn't replace the original string**. It only **returns a copy of the original string with the replaced substrings**. If we want to overwrite the original string, we can simply write:> sentence = sentence.replace('m', 'M')Now let's try to replace some words in the function above so we get a sentence **`"Anna ate a big apple pie"`** and save them in a **separate variable**<jupyter_code>new_sentence = sentence.replace('Maria', 'Anna')
new_sentence = new_sentence.replace('an', 'a big')
new_sentence = new_sentence.replace(' for breakfast', '')
print(new_sentence)<jupyter_output>Anna ate a big apple pie<jupyter_text>And replace 'a' with 'the' in this sentence<jupyter_code>new_sentence.replace('a', 'the')<jupyter_output><empty_output><jupyter_text>_Oops, something went wrong..._🚨 **Remember** 🚨I told you this function replaces any substring? That's what it did - replaced all **`'a'`** with **`'the'`** So... Be careful with **`replace()`**.  The `join()` and the `split()` functionsThere is another way to combine strings. The **`join()`** method takes all items in an iterable and joins them into one **string**, `separating each element by some symbol`.Let's first try to use **`spaces`** to separate the words from the list on the outputted string.<jupyter_code>' '.join(['maria', 'ate', 'an', 'apple', 'pie', 'for', 'breakfast'])<jupyter_output><empty_output><jupyter_text>And now with '_' to separate words.<jupyter_code>'_'.join(['maria', 'ate', 'an', 'apple', 'pie', 'for', 'breakfast'])<jupyter_output><empty_output><jupyter_text>The last method that is really useful when we work with strings is **`split()`** which does something opposite to **`join()`** method. **`join()`** merges multiple strings into one separating them by a given `separator` (**`space symbol`** as an example), whereas **`split()`** divides a string into a list of strings using a `separator`. By default, a separator is a **`space symbol`**. It means that any words separated by a **`space symbol`** will appear as separate elements of a list. Let's call **`split()`** on the **last line of our text** > `'pie for breakfast'`and see what happens:<jupyter_code># original line
print(lines[2])

# split the line
lines[2].split()<jupyter_output>pie for breakfast<jupyter_text>How is it useful? - Let's imagine this task: > For each separate word in a sentence, save the ones that have a **length > 5.** 🌟 **Hint:** _we can apply the `len()` function to strings the same way as we did with lists_ 🌟<jupyter_code>text = "A pie is a baked dish which is usually made of a pastry dough casing that contains a filling of various sweet or savoury ingredients"<jupyter_output><empty_output><jupyter_text>- Possible solution for this task:<jupyter_code>long_words = []
for word in text.split():
    if len(word) > 5:
        long_words.append(word)
print(long_words)<jupyter_output>['usually', 'pastry', 'casing', 'contains', 'filling', 'various', 'savoury', 'ingredients']<jupyter_text>Or we can do the same thing in a list comprehension in just one line:<jupyter_code>long_words = [word for word in text.split() if len(word) > 5]
print(long_words)<jupyter_output>['usually', 'pastry', 'casing', 'contains', 'filling', 'various', 'savoury', 'ingredients']<jupyter_text>Combining what we learned...Now let's try to use **everything we learned in this learning unit** for a much more complicated task. We'll also use some things that **we learned in the previous lessons** (like dictionaries)* **Task Description:**> 1. Create a function **`preprocess_text()`** that gets a **string as an input**> 2. Returns a **frequency dictionary**, whose `keys` refer to all **unique words** and `values` refer to the **number of times the words appear**> 3. Don't forget to apply **lowercase** to the words> 4. **Replace** each numeric value with a word **`'number'`**>> **Example:**>> input: `"I like 123 I print 2.13"`>> output: `{'i' : 2, 'like' : 1, 'number' : 2, 'print' : 1}`* **Possible solution for this task:**We're going to see **2 ways** of doing that.For the **first way** let's create a few **helper functions**.> We're going to create a function **`helper()`** to create a list of lowercase words from the original text and replace each number with the word 'number'. >> Then we create a function called **`count_words()`** to count the number of times a word appears in the list.And in the end, we call both of them from **`preprocess_text()`** function✨✨ _When you are going through these functions, **don't worry if you might not understand some parts.**_ ✨✨    ✨✨ We have written a recap session below, which will explain everything in detail. ✨✨<jupyter_code># input: string
# output: list of lowercase words. All the numbers replaced with 'number' word

# example: 
# string_input = 'How was your day 123'
# helper(string_input) --> ['how', 'was', 'your', 'day', 'number']

def helper(text):
    """
    create a list of lowercase words from the original text
    replace each number with a word 'number'
    """
    text = text.split()
    result = []
    for word in text:
        if word.isdigit(): # check whether a word is numeric
            result.append('number')
        else:
            result.append(word.lower())
    return result
# input: list of words (list of strings)
# output: dictionary with all unique words and the number of times each word appears in the list

# example: 
# words_list = ['day', 'day', 'evening']
# count_words(words_list) --> {'day' : 2, 'evening' : 1}

def count_words(words_list):
    """
    count each word's appearence
    """
    vocab = {}
    for word in words_list:
        if word in vocab.keys():
            vocab[word] += 1
        else:
            vocab[word] = 1 
    return vocab
# input: string
# output: dictionary with unique lowercase words and numbers, and the number of times they appear in the string

# example: 
# text = ['How was your day day 123 evening']
# preprocess_text(text) -->  {'how' : 1, 'was' : 1, 'your' : 1, 'day' : 2, 'number' : 1, 'evening' : 1}

def preprocess_text(text):
    text = helper(text)
    vocab = count_words(text)
    return vocab
preprocess_text('I like 123 I print 213')<jupyter_output><empty_output><jupyter_text>Now, let's recap what we did step by step: helper(text):- **`split()`** all the words in a **string**- for each `word` in the **split string**, check whether the word is **numeric**- if it's **numeric**, add the word `'number'` to our **final list of words**. If it's **not numeric**, add the `word itself`, lowered, to the **final list of words**  count_words(words_list):- create a dictionary called **`'vocab'`**- for each `word` in the **list of words**, check whether this word is **already in the vocabulary**- if it's **not in the `vocab`**, we understand that we never met this word before, so we **add it to the vocabulary and set the number of occurrences to be equal to 1**- if it's **in the `vocab`**, it means that we already met it before, so the **only thing we need to do is to increase the number of occurrences by 1** preprocess_text(text):- Apply **`helper()`** on the **input string**, receiving a list of `lowercase words`- Apply **`count_words()`** on the list of `lowercase words`, receiving the `number of words occurrences` - Return itAnd... The **second way** is putting all steps in one function and make it shorter using list comprehensions **with no need to use helper functions**<jupyter_code>def preprocess_text(text):
    
    # create a list of lowercase words from the original text
    # (the same thing we did in helper() function)
    words = [word.lower() if not word.isdigit() else 'number' for word in text.split()]
    
    # count words
    # (the same thing we did in count_words() function)
    vocab = {}
    for word in words:
        if word in vocab.keys():
            vocab[word] += 1
        else:
            vocab[word] = 1 
            
    return vocab
preprocess_text('I like 123 I print 213')<jupyter_output><empty_output><jupyter_text>So charming! Didn't you love it? ❤️❤️❤️*** PART 3 | Testing, Exception raising & HandlingUntil now we didn't talk about **errors and exceptions**, but they are an important part of every programming language.We want to be sure that our program works in all the possible scenarios, and that it doesn't crash in the very important moment. More than that, **we might want to expect some types of errors and make our program behavior differently in different cases.**So let's see! Error and Exception conceptWhile doing other exercise notebooks you probably have seen several types of errors (and exceptions). For now, let's understand that an **`error`** is something that went wrong and **cannot be handled**. (in other words - your program will crash out and nothing can be done, other than trying to fix it)Raising an **`exception`** does not eliminate the error but in cases where you can predict which kind of error you may have, it allows you to work around it telling your program how to **handle this scenario**. Common errors and exceptionsMany of these **`errors`** are built-in errors, and they always say **where to look for the error**. Let's see a few examples:>  SYNTAX ERROR>> As the name suggests, **there is some syntax error in your code.** It usually means, that you made a typo somewhere...<jupyter_code># typo in the code: there are no colon after if statement
if True
    print('true')<jupyter_output><empty_output><jupyter_text>>  ZERO DIVISION ERROR>> Means that you're trying to divide by zero (captain obvious). >> Check any places with division and see what are the values of variables there...<jupyter_code># we can't divide by zero
1/0<jupyter_output><empty_output><jupyter_text>>  TYPE ERROR> > means that the operation you're trying to perform is expecting another data type...<jupyter_code># string + int concatenation is not possible
'1' + 1<jupyter_output><empty_output><jupyter_text>>  NAME ERROR>> Python interpreter doesn't recognize the variable named `a`<jupyter_code># call a variable that doesn't exist
print(a)<jupyter_output><empty_output><jupyter_text>🌟 **Important Note:** _Always read the `exception` **name** and **message** so you understand what happened!_ 🌟  Raise ExceptionsThe above errors are **built-in exceptions.** Python also provides us with the possibility to create self-defined exceptions. Sometimes **`we want to stop the program when a condition occurs.`** We can do that by **raising an exception.****We're going to use our exception** (not one of the errors you saw above)Let's **`print()`** a **number only if it's bigger than 5.** If the condition is not met, `raise an exception`<jupyter_code>def test(num):
    if num > 5:
        print(num)
    else:
        raise Exception('The number is less than 5')
test(6)
test(3)<jupyter_output><empty_output><jupyter_text>Or we can also raise exceptions outside functions if we want:<jupyter_code>print('This line will be printed')
raise Exception('The number is less than 5')
print('This line will not be printed')<jupyter_output>This line will be printed<jupyter_text>As you can see, the program will stop working once an exception is raised.Due to this reason, the **print line after the Exception being raised is not executed** Handling ExceptionsGreat. But sometimes (**and even often**) **`we don't want to stop the program if something wrong happens.`** **Why?** Well, because we might foreseen that an error might occur and want to come up with ways to deal with it.We use **`try/except`** statement for that. The syntax is the following:>**`try`**` something:>    if there is no exception, do things`>**`except`**` :>    if there is this type of exception, do other things`**`exception_type`s** are the same things you saw in **Common errors and exceptions**There is a link to more exception types at the **end of this notebook**🌟 _If we don't specify the exception type, every exception will be caught_ 🌟For example, let's iterate over a list of numbers and **add 1 to each of them.** If there is any non-numeric element in the list, **let's catch this element and say that we can't add 1 to a non-numeric value.**<jupyter_code>def test_list_values(array):
    for element in array:
        try:
            print(element + 1)
        except TypeError:
            print(element, ' is not a number. We cannnot add 1 to not a number')
test_list_values([1, 2, 3, 'a', 4, 5])<jupyter_output>2
3
4
a  is not a number. We cannnot add 1 to not a number
5
6<jupyter_text>As you could see, the program didn't stop because we just defined **`what to do`** when something goes wrong with the expected instructions.  The `assert` functionWe have another way to **raise exception**. Instead of writing: `if condition:    raise Exception()`We might use an **`assert`** method:<jupyter_code>def test(num):
    assert num > 5
    print(num)<jupyter_output><empty_output><jupyter_text>**Assertions** are simply **`boolean expressions`** that check if the `condition` returns **true** or **false**.If it is **true**, the program **will continue to run** and move to the next line of code. On the other hand, if it's **false**, the program **stops and throws an AssertionError.**<jupyter_code>test(6)
test(3)<jupyter_output><empty_output><jupyter_text>We can also add a **`message`** so that the **assertion error is easier to understand**<jupyter_code>def test(num):
    assert num > 5, 'The number has to be bigger than 5'
    print(num)
test(6)
test(3)
for line in lines:
    assert line != 'pie for breakfast', "Don't eat pies without me!"
    print(line)<jupyter_output>maria

ate an apple
{'fraction_non_alphanumeric': 0.11832711076177375, 'fraction_numerical': 0.004585052728106373, 'mean_word_length': 4.817298533053915, 'pattern_counts': {'":': 0, '<': 156, '<?xml version=': 0, '>': 201, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '43', 'dir': 'jupyter-structured-clean-dedup', 'id': '258868', 'license': 'MIT', 'n_tokens_mistral': 8996, 'n_tokens_neox': 8602, 'n_words': 4241, 'path': 'Week 06/SLU11 - String & File Handling/Learning notebook.ipynb', 'repo_name': 'LDSSA/ds-prep-course-2021', 'size': '30534'}