Unit 3 Python
Unit 3 Python
Unit 3
Exception Handling and Multi-Threading in python
Exception handling: try, except and finally block, handling multiple exceptions, raise an
exception, User defined exception- python multithreading- thread and threading module-
Synchronizing Threads in Python.
Case Study: Development of student performance evaluation report
Exception Handling
An exception is a Python object that represents an error. When a Python script raises an exception, it
must either handle the exception immediately otherwise it terminates and quits.
The try block lets you test a block of code for errors.
The except block lets you handle the error.
The else block lets you execute code when there is no error.
The finally block lets you execute code, regardless of the result of the try- and except blocks.
Exception Handling
When an error occurs, or exception as we call it, Python will normally stop and generate an error
message.
These exceptions can be handled using the try statement:
The try block will generate an exception, because x is not defined:
try:
print(x)
except:
print("An exception occurred")
Since the try block raises an error, the except block will be executed.
Without the try block, the program will crash and raise an error:
This statement will raise an error, because x is not defined:
print(x)
List of Standard Exceptions −
Sr.No. Exception Name & Description
1 Exception
Base class for all exceptions
2 StopIteration
Raised when the next() method of an iterator does not point to any object.
3 SystemExit
Raised by the sys.exit() function.
4 StandardError
Base class for all built-in exceptions except StopIteration and SystemExit.
5 ArithmeticError
Base class for all errors that occur for numeric calculation.
6 OverflowError
Raised when a calculation exceeds maximum limit for a numeric type.
7 FloatingPointError
Raised when a floating point calculation fails.
8 ZeroDivisionError
Raised when division or modulo by zero takes place for all numeric types.
9 AssertionError
Raised in case of failure of the Assert statement.
10 AttributeError
Raised in case of failure of attribute reference or assignment.
11 EOFError
Raised when there is no input from either the raw_input() or input() function and the end of file is
reached.
12 ImportError
Raised when an import statement fails.
13 KeyboardInterrupt
Raised when the user interrupts program execution, usually by pressing Ctrl+c.
14 LookupError
Base class for all lookup errors.
15 IndexError
Raised hen an index is not found in a sequence.
16 KeyError
Raised when the specified key is not found in the dictionary.
17 NameError
Raised when an identifier is not found in the local or global namespace.
18 UnboundLocalError
Raised when trying to access a local variable in a function or method but no value has been assigned
to it.
19 EnvironmentError
Base class for all exceptions that occur outside the Python environment.
20 IOError
Raised when an input/ output operation fails, such as the print statement or the open() function when
trying to open a file that does not exist.
21 IOError
Raised for operating system-related errors.
22 SyntaxError
Raised when there is an error in Python syntax.
23 IndentationError
Raised when indentation is not specified properly.
24 SystemError
Raised when the interpreter finds an internal problem, but when this error is encountered the Python
interpreter does not exit.
25 SystemExit
Raised when Python interpreter is quit by using the sys.exit() function. If not handled in the code,
causes the interpreter to exit.
26 TypeError
Raised when an operation or function is attempted that is invalid for the specified data type.
27 ValueError
Raised when the built-in function for a data type has the valid type of arguments, but the arguments
have invalid values specified.
28 RuntimeError
Raised when a generated error does not fall into any category.
29 NotImplementedError
Raised when an abstract method that needs to be implemented in an inherited class is not actually
implemented.
Many Exceptions
You can define as many exception blocks as you want, e.g. if you want to execute a special block of
code for a special kind of error:
Print one message if the try block raises a NameError and another for other errors:
try:
print(x)
except NameError:
print("Variable x is not defined")
except:
print("Something else went wrong")
Else
You can use the else keyword to define a block of code to be executed if no errors were raised:
try:
print("Hello")
except:
print("Something went wrong")
else:
print("Nothing went wrong")
Finally
The finally block, if specified, will be executed regardless if the try block raises an error or not.
try:
print(x)
except:
print("Something went wrong")
finally:
print("The 'try except' is finished")
Try to open and write to a file that is not writable:
try:
f = open("demofile.txt")
try:
f.write("Lorum Ipsum")
except:
print("Something went wrong when writing to the file")
finally:
f.close()
except:
print("Something went wrong when opening the file")
The program can continue, without leaving the file object open.
Exception handling in Python is a way to handle runtime errors that occur during the execution of a
program. These errors can be caused by various factors such as invalid input, network failures, file
errors, or programming mistakes. By handling these exceptions, you can prevent your program from
crashing and provide a more informative message to the user.
In Python, you can handle exceptions using a try-except block. Here's an example:
try:
# Code that might raise an exception
x = int(input("Enter a number: "))
y = int(input("Enter another number: "))
result = x / y
print("The result is", result)
except ValueError:
# Exception handling code for invalid input
print("Invalid input. Please enter a number.")
except ZeroDivisionError:
# Exception handling code for division by zero
print("Division by zero is not allowed.")
except:
# Exception handling code for any other exception
print("An error occurred.")
Raising an exception:
Raise an exception
As a Python developer you can choose to throw an exception if a condition occurs.
To throw (or raise) an exception, use the raise keyword.
ExampleGet your own Python Server
Raise an error and stop the program if x is lower than 0:
x = -1
if x < 0:
raise Exception("Sorry, no numbers below zero")
The raise keyword is used to raise an exception.
You can define what kind of error to raise, and the text to print to the user.
ExampleGet your own Python Server
Raise a TypeError if x is not an integer:
x = "hello"
if not type(x) is int:
raise TypeError("Only integers are allowed")
You can raise an exception in Python using the raise keyword followed by an exception type. Here's
an example:
x = -1
if x < 0:
raise ValueError("Invalid input. The value must be non-negative.")
In this program, we check if the value of x is negative. If it is, we raise a ValueError exception with a
custom error message. This will terminate the program and display the error message to the user.
User-defined exception:
You can define your own custom exceptions in Python by creating a new class that inherits from the
built-in Exception class. Here's an example:
class MyException(Exception):
pass
x = 10
if x > 5:
raise MyException("The value of x is too high.")
In this program, we define a new exception class called MyException. We then check if the value of x
is greater than 5. If it is, we raise a MyException exception with a custom error message. This will
terminate the program and display the error message to the user.
Multithreading in Python:
Multithreading is a technique that allows a program to perform multiple tasks concurrently. In Python,
you can create threads using the built-in threading module. Here's an example:
python
Copy code
import threading
def task():
print("Executing task...")
# Do some work here...
t1 = threading.Thread(target=task)
t2 = threading.Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
Python Exceptions
When a Python program meets an error, it stops the execution of the rest of the program. An
error in Python might be either an error in the syntax of an expression or a Python exception
What is an Exception?
An exception in Python is an incident that happens while executing a program that causes the
regular course of the program's commands to be disrupted. When a Python code comes across
a condition it can't handle, it raises an exception. An object in Python that describes an error
is called an exception.
When a Python code throws an exception, it has two options: handle the exception
immediately or stop and quit.
Exceptions versus Syntax Errors
When the interpreter identifies a statement that has an error, syntax errors occur. Consider the
following scenario:
Code
Output:
if (s != o:
^
SyntaxError: invalid syntax
The arrow in the output shows where the interpreter encountered a syntactic error. There was
one unclosed bracket in this case. Close it and rerun the program:
Code
Output:
We encountered an exception error after executing this code. When syntactically valid Python
code produces an error, this is the kind of error that arises. The output's last line specified the
name of the exception error code encountered. Instead of displaying just "exception error",
Python displays information about the sort of exception error that occurred. It was a
NameError in this situation..
In Python, we catch exceptions and handle them using try and except code blocks. The try
clause contains the code that can raise an exception, while the except clause contains the code
lines that handle the exception. Let's see if we can access the index from the array, which is
more than the array's length, and handle the resulting exception.
Code
1. # Python code to catch an exception and handle it using try and except code blocks
2.
3. a = ["Python", "Exceptions", "try and except"]
4. try:
5. #looping through the elements of the array a, choosing a range that goes beyond th
e length of the array
6. for i in range( 4 ):
7. print( "The index and element from the array is", i, a[i] )
8. #if an error occurs in the try block, then except block will be executed by the Python
interpreter
9. except:
10. print ("Index out of range")
Output:
The code blocks that potentially produce an error are inserted inside the try clause in the
preceding example. The value of i greater than 2 attempts to access the list's item beyond its
length, which is not present, resulting in an exception. The except clause then catches this
exception and executes code without stopping it.
If a condition does not meet our criteria but is correct according to the Python interpreter, we
can intentionally raise an exception using the raise keyword. We can use a customized
exception in conjunction with the statement.
Output:
1 num = [3, 4, 5, 7]
2 if len(num) > 3:
----> 3 raise Exception( f"Length of the given list must be less than or equal to 3 but is
{len(num)}" )
Exception: Length of the given list must be less than or equal to 3 but is 4
The implementation stops and shows our exception in the output, providing indications as to
what went incorrect.
Assertions in Python
When we're finished verifying the program, an assertion is a consistency test that we can
switch on or off.
Assertions are made via the assert statement, which was added in Python 1.5 as the latest
keyword.
Assertions are commonly used at the beginning of a function to inspect for valid input and at
the end of calling the function to inspect for valid output.
Python examines the adjacent expression, preferably true when it finds an assert statement.
Python throws an AssertionError exception if the result of the expression is false.
Python uses ArgumentException, if the assertion fails, as the argument for the AssertionError.
We can use the try-except clause to catch and handle AssertionError exceptions, but if they
aren't, the program will stop, and the Python interpreter will generate a traceback.
Code
Output:
Python also supports the else clause, which should come after every except clause, in the try,
and except blocks. Only when the try clause fails to throw an exception the Python interpreter
goes on to the else block.
Code
1. # Python program to show how to use else clause with try and except clauses
2.
3. # Defining a function which returns reciprocal of a number
4. def reciprocal( num1 ):
5. try:
6. reci = 1 / num1
7. except ZeroDivisionError:
8. print( "We cannot divide by zero" )
9. else:
10. print ( reci )
11. # Calling the function and passing values
12. reciprocal( 4 )
13. reciprocal( 0 )
Output:
0.25
We cannot divide by zero
The finally keyword is available in Python, and it is always used after the try-except block.
The finally code block is always executed after the try block has terminated normally or after
the try block has terminated for some other reason.
Code
Output:
Illegal operations can raise exceptions. There are plenty of built-in exceptions in Python that
are raised when corresponding errors occur.
We can view all the built-in exceptions using the built-in local() function as follows:
print(dir(locals()['__builtins__']))
Run Code
RuntimeError Raised when an error does not fall under any other category.
In Python, we can define custom exceptions by creating a new class that is derived from the
built-in Exception class.
Here's the syntax to define custom exceptions,
class CustomError(Exception):
...
pass
try:
...
except CustomError:
...
Here, CustomError is a user-defined error which inherits from the Exception class.
try:
input_num = int(input("Enter a number: "))
if input_num < number:
raise InvalidAgeException
else:
print("Eligible to Vote")
except InvalidAgeException:
print("Exception occurred: Invalid Age")
Run Code
Output
If the user input input_num is greater than 18,
Enter a number: 45
Eligible to Vote
Enter a number: 14
Exception occurred: Invalid Age
In Python, try-except blocks can be used to catch and respond to one or multiple exceptions.
In cases where a process raises more than one possible exception, they can all be handled
using a single except clause.
With this approach, the same code block is executed if any of the listed exceptions occurs.
Here's an example:
try:
name = 'Bob'
name += 5
except (NameError, TypeError) as error:
print(error)
In the above example, the code in the except block will be executed if any of the listed
exceptions occurs. Running the above code raises a TypeError, which is handled by the code,
producing the following output:
try:
name = 'Bob'
name += 5
except NameError as ne:
# Code to handle NameError
print(ne)
In the above example, NameError and TypeError are two possible exceptions in the code,
which are handled differently in their own except blocks.
Multithreading in Python
What is a Process?
A program in execution is known as a process. When you start any app or program on your
computer, such as the internet browser, the operating system treats it as a process.
A process may consist of several threads of execution that may execute concurrently. In other
words, we can say a process facilitates multithreading.
What is a Thread?
To facilitate multithreading in Python, we can make use of the following modules offered by
Python –
Thread Module
Threading Module
Python multithreading facilitates sharing data space and resources of multiple threads with the
main thread. It allows efficient and easy communication between the threads.
What is Multiprocessing?
Multiprocessing breaks down processes into smaller routines that run independently. The
more tasks a single processor is burdened with, the more difficult it becomes for the processor
to keep track of them.
It evidently gives rise to the need for multiprocessing. Multiprocessing tries to ensure that
every processor gets its own processor/processor core and that execution is hassle-free.
Note-In the case of multicore processor systems like Intel i3, a processor core is allotted to a
process.
The main differences to note between Multithreading and Multiprocessing are as follows –
Multithreading Multiprocessing
It is a technique where a process spawns It is the technique where multiple processes run across
multiple threads simultaneously. multiple processors/processor cores simultaneously.
Python multithreading implements Python multiprocessing implements parallelism in its
concurrency. truest form.
It gives the illusion that they are running It is parallel in the sense that the multiprocessing module
parallelly, but they work in a concurrent facilitates the running of independent processes
manner. parallelly by using subprocesses.
In multithreading, the GIL or Global
In multiprocessing, each process has its own Python
Interpreter Lock prevents the threads from
Interpreter performing the execution.
running simultaneously.
If you wish to break down your tasks and applications into multiple sub-tasks and then execute
them simultaneously, then multithreading in Python is your best bet.
All important aspects such as performance, rendering, speed and time consumption will
drastically be improved by using proper Python multithreading.
In Python multithreading, there are two ways in which you can start a new thread-
Let's take a look at the code using which we can create a new thread using the Threading
Module –
Output:
The way to create a new thread using the Thread module is as follows –
import _thread
def MyThread1():
print("This is thread1")
def MyThread2():
print("This is thread2")
_thread.start_new_thread(MyThread1, ())
_thread.start_new_thread(MyThread2, ())
Output:
This is thread1
This is thread2
Note: The output of the above code snippet can differ for different runs. It happens because
multithreading in Python using the _thread module is unstable.
No one can tell which thread is going to get executed first. The _thread module treats threads
as functions while the Threading module is implemented in an object-oriented way, which
means every thread corresponds to an object.
The Threading module is preferred as its intuitive APIs help us synchronize thread executions,
thus making it predictable and highly reliable.
The easiest way to work with multiple threads is by using the ThreadPoolExecutor, part of the
standard Python library. It falls under the concurrent.features library.
Using the with statement, you can create a context manager. It would enable you to create and
delete a pool efficiently. We can also import the ThreadPoolExecutor directly from the
concurrent.features library.
executor = ThreadPoolExecutor(max_workers=””)
The max_worker parameter can take any integer value based on the scenario.
Let's take a look at the following code to see how to achieve this –
def task():
print("Executing the given task")
result = 0
i=0
for i in range(10):
result = result + i
print("I: {}".format(result))
print("The task is executed {}".format(threading.current_thread()))
def main():
executor = ThreadPoolExecutor(max_workers=3)
task1 = executor.submit(task)
task2 = executor.submit(task)
if __name__ == '__main__':
main()
Output –
Along with the methods of the Thread module, the Threading module offers additional
methods such as –
Race Conditions
Race conditions are one of the primary issues you will face while working with multithreading
in Python.
Most commonly, race conditions happen when two or more threads access the shared piece of
data and resource. In real-time multithreading in Python, it can occur when threads overlap.
The solution to this is synchronizing threads which we will see further.
Synchronizing Threads
In Python, you can implement the locking mechanism to enable you to synchronize the
threads.
The low-level synchronization primitive lock is implemented through the _thread module.
Locked
Unlocked
The class that is used to implement primitive locks is known as Lock. Lock objects are created
to make the threads run synchronously. Only one thread at a time can have a lock.
1. acquire()– This method changes the state of an unlocked lock. But if it is already
locked, the acquire() method is blocked.
2. release()– This method is used to free up the locks when no longer required. It can be
called by any state, irrespective of their state.
The parameter that we pass through the Lock class' acquire() method is known as the blocking
parameter.
Let's assume we have a thread object T to understand how all these work together. As
mentioned before, if we wish to call the acquire() method for T, we will pass the blocking
parameter through it.
The blocking parameter can have only two possible values- True or False. It gives rise to two
scenarios.
Scenario 1: When the blocking parameter is set to True, and we call the acquire
method as T.acquire(blocking=True), the thread object will acquire the lock T. This
can happen only when T has no existing locks. On the flip side, if the thread object T
is already locked, the acquire() call is suspended, and it waits until T releases the lock.
The moment thread T frees up, the calling thread immediately re-locks it. The calling
thread then acquires the released lock.
Scenario 2: When the blocking parameter is set to False, and T is unlocked, it acquires
a lock and returns True. Whereas if T is already locked and the blocking parameter is
set to False, the acquire method does not affect T. It simply returns False.
A new queue object can be created using the Queue module. It can hold a specific number of
items.
Threading Objects
1. Semaphore
2. Timer
3. Barrier
1. Semaphore
2. Time
The object used to schedule a function to be called after a certain amount of time has
passed is the threading.Timer.
The Timer is started by using the .start() method.
The Timer is stopped by using the .cancel() method.
3. Barrier
The object used to keep multiple threads in sync is the threading.Barrier() object.
You need to specify the number of threads to be synchronized while creating the
Barrier.
The .wait() method is called by each thread on a Barrier.
Multithreading in Python has several advantages, making it a popular approach. Let's take
a look at some of them –
Python multithreading enables efficient utilization of the resources as the threads share
the data space and memory.
Multithreading in Python allows the concurrent and parallel occurrence of various
tasks.
It causes a reduction in time consumption or response time, thereby increasing the
performance.