### Exception
* If the codes go wrong, python **stop the program and create exception (raising an exception)**
* What happens:
* python expect coders take care of it
* if not, forcibly terminate the program and error message will occur
* if handled, the program continue
* We can **observe exceptions, identify them and handle them**
* exception name:
> ValueError: math domain error
> ZeroDivisionError: division by zero
> IndexError: list index out of range
### Handle Exception
* Use `try` as a keyword
* Idea:
* first, you have to try to do something;
* next, you have to check whether everything went well.
* Another way to handle wrong codes: check first and do it if it is safe.
* But it is **bloated and illegible**
* Example:
```
first_number = int(input("Enter the first number: "))
second_number = int(input("Enter the second number: "))
if second_number != 0:
print(first_number / second_number)
else:
print("This operation cannot be done.")
print("THE END.")
```
* **favorite Python approach**
> try:
:
:
except:
:
:
* Note:
* the try keyword begins a block of the code which may or may not be performing correctly
* the except keyword starts a piece of code which will be executed if anything inside the try block goes wrong

```python
try:
print("1")
x = 1 / 0
print("2")
except:
print("Oh dear, something went wrong...")
print("3")
# Output: 1, Oh dear, something went wrong..., 3
# print 2 bị bỏ qua
```
* disadvantage: if 2 or more exceptions raised, we didn't know which is the reason from the input. Example:
```python
try:
x = int(input("Enter a number: "))
y = 1 / x
except:
print("Oh dear, something went wrong...")
print("THE END.")
#non-integer data entered by the user;
# x=0
# It just outputs "Oh dear, something went wrong..."
```
* **2 ways to solve this issue:**
* build two consecutive try-except blocks, one for each possible exception reason (easy, but will cause unfavorable code growth)
* use a more advanced variant of the instruction.
```python
try:
:
except exc1:
:
except exc2:
:
except:
:
```

* Example:
```python
try:
x = int(input("Enter a number: "))
y = 1 / x
print(y)
except ZeroDivisionError:
print("You cannot divide by zero, sorry.")
except ValueError:
print("You must enter an integer value.")
except:
print("Oh dear, something went wrong...")
print("THE END.")
# if you enter a valid, non-zero integer value (e.g., 5)
# output:0.2 THE END.
#
# if you enter 0
# output: You cannot divide by zero, sorry. THE END.
#
# if you enter any non-integer string
# output: You must enter an integer value. THE END.
#
# if you press Ctrl-C while the program is waiting for the user's input (which causes an exception named KeyboardInterrupt)
# output: Oh dear, something went wrong...THE END.
```

* Nếu ko có branch nào handle dc thì nó vẫn raise error:
```
try:
x = int(input("Enter a number: "))
y = 1 / x
print(y)
except ValueError:
print("You must enter an integer value.")
print("THE END.")
# output: ZeroDivisionError: division by zero
```
### Cấu trúc của exception
* Có 63 built-in exceptions, tạo ra tree-shaped hierarchy
* Built-in exceptions có genral(include mấy exceptions khác) và chỉ 1 mình nó
* **the closer to the root an exception is located, the more general (abstract) it is**
* leaves là những exception cuối của nhánh (giống tree graph)

* example of a branch from ZeroDivsionError:

* Some example
```python
try:
y = 1 / 0
except ZeroDivisionError:
print("Oooppsss...")
print("THE END.")
# if we change ZeroDivisionError to ArithmeticError or Exception or BaseException,
# it still outputs "Oooppsss..."
```

```python
try:
y = 1 / 0
except ZeroDivisionError:
print("Zero Division!")
except ArithmeticError:
print("Arithmetic problem!")
print("THE END.")
# output: Zero division! THE END.
try:
y = 1 / 0
except ArithmeticError:
print("Arithmetic problem!")
except ZeroDivisionError:
print("Zero Division!")
print("THE END.")
# output: Arithmetic problem! THE END.
# The first is better because the message is more concrete.
```

#### handle two or more exceptions
```python
try:
:
except (exc1, exc2):
:
```
* exception is raised inside a function, it can handled inside and outside
```python
def bad_fun(n):
try:
return 1 / n
except ArithmeticError:
print("Arithmetic Problem!")
return None
bad_fun(0)
print("THE END.")
# output: Arithmetic problem! THE END.
def bad_fun(n):
return 1 / n
try:
bad_fun(0)
except ArithmeticError:
print("What happened? An exception was raised!")
print("THE END.")
# What happened? An exception was raised! THE END.
```

#### Raise
```python
def bad_fun(n):
raise ZeroDivisionError
try:
bad_fun(0)
except ArithmeticError:
print("What happened? An error?")
print("THE END.")
# output: What happened? An error? THE END.
```
* The Python statement raise ExceptionName can raise an exception on demand. The same statement, but lacking ExceptionName, can be used inside the except branch only, and raises the same exception which is currently being handled.
#### Assert
> assert expression
* How does it work?
* It evaluates the expression;
* If it is True, it will go through, else, it will raise AssertionError
```python
def divide(x, y):
assert y != 0, "Cannot divide by zero"
return x / y
result = divide(10, 2) # No assertion error
print(result) # Output: 5.0
result = divide(10, 0) # Assertion error is raised
```
* The Python statement assert expression evaluates the expression and raises the AssertError exception when the expression is equal to zero, an empty string, or None. You can use it to protect some critical parts of your code from devastating data.
```python
def foo(x):
assert x
return 1/x
try:
print(foo(0))
except ZeroDivisionError:
print("zero")
except:
print("some")
# The ouput is some because we met the assertion error before ZeroDivisionError.
```
[Link for references (must read)](https://studyglance.in/python/raise-assert.php)
#### Some exceptions
##### Built-in exceptions
* **ArithmeticError**
* Location: BaseException ← Exception ← ArithmeticError
* Description: an abstract exception including all exceptions caused by arithmetic operations like zero division or an argument's invalid domain
* **AssertionError**
* Location: BaseException ← Exception ← AssertionError
* Description: a concrete exception raised by the assert instruction when its argument evaluates to False, None, 0, or an empty string
* **BaseException**
* Location: BaseException
* Description: the most general (abstract) of all Python exceptions - all other exceptions are included in this one; it can be said that the following two except branches are equivalent: except: and except BaseException:.
* **IndexError**
* Location: BaseException ← Exception ← LookupError ← IndexError
* Description: a concrete exception raised when you try to access a non-existent sequence's element (e.g., a list's element)
* **KeyboardInterrupt**
* Location: BaseException ← KeyboardInterrupt
* Description: a concrete exception raised when the user uses a keyboard shortcut designed to terminate a program's execution (Ctrl-C in most OSs); if handling this exception doesn't lead to program termination, the program continues its execution.
* **LookupError**
* Location: BaseException ← Exception ← LookupError
* Description: an abstract exception including all exceptions caused by errors resulting from invalid references to different collections (lists, dictionaries, tuples, etc.)
* **MemoryError**
* Location: BaseException ← Exception ← MemoryError
* Description: a concrete exception raised when an operation cannot be completed due to a lack of free memory.
* **OverflowError**
* Location: BaseException ← Exception ← ArithmeticError ← OverflowError
* Description: a concrete exception raised when an operation produces a number too big to be successfully stored
* **ImportError**
* Location: BaseException ← Exception ← StandardError ← ImportError
* Description: a concrete exception raised when an import operation fails
* **KeyError**
* Location: BaseException ← Exception ← LookupError ← KeyError
* Description: a concrete exception raised when you try to access a collection's non-existent element (e.g., a dictionary's element)
* **Exceptions are in fact objects**