Blog

  • Python – Logging

    Logging in Python

    Logging is the process of recording messages during the execution of a program to provide runtime information that can be useful for monitoring, debugging, and auditing.

    In Python, logging is achieved through the built-in logging module, which provides a flexible framework for generating log messages.

    Benefits of Logging

    Following are the benefits of using logging in Python −

    • Debugging − Helps identify and diagnose issues by capturing relevant information during program execution.
    • Monitoring − Provides insights into the application’s behavior and performance.
    • Auditing − Keeps a record of important events and actions for security purposes.
    • Troubleshooting − Facilitates tracking of program flow and variable values to understand unexpected behavior.

    Components of Python Logging

    Python logging consists of several key components that work together to manage and output log messages effectively −

    • Logger − It is the main entry point that you use to emit log messages. Each logger instance is named and can be configured independently.
    • Handler − It determines where log messages are sent. Handlers send log messages to different destinations such as the console, files, sockets, etc.
    • Formatter − It specifies the layout of log messages. Formatters define the structure of log records by specifying which information to include (e.g., timestamp, log level, message).
    • Logger Level − It defines the severity level of log messages. Messages below this level are ignored. Common levels include DEBUG, INFO, WARNING, ERROR, and CRITICAL.
    • Filter − It is the optional components that provide finer control over which log records are processed and emitted by a handler.

    Logging Levels

    Logging levels in Python define the severity of log messages, allowing developers to categorize and filter messages based on their importance. Each logging level has a specific purpose and helps in understanding the significance of the logged information −

    • DEBUG − Detailed information, typically useful only for debugging purposes. These messages are used to trace the flow of the program and are usually not seen in production environments.
    • INFO − Confirmation that things are working as expected. These messages provide general information about the progress of the application.
    • WARNING − Indicates potential issues that do not prevent the program from running but might require attention. These messages can be used to alert developers about unexpected situations.
    • ERROR − Indicates a more serious problem that prevents a specific function or operation from completing successfully. These messages highlight errors that need immediate attention but do not necessarily terminate the application.
    • CRITICAL − The most severe level, indicating a critical error that may lead to the termination of the program. These messages are reserved for critical failures that require immediate intervention.

    Usage

    Following are the usage scenarios for each logging level in Python applications −

    Choosing the Right Level − Selecting the appropriate logging level ensures that log messages provide relevant information without cluttering the logs.

    Setting Levels − Loggers, handlers, and specific log messages can be configured with different levels to control which messages are recorded and where they are outputted.

    Hierarchy − Logging levels are hierarchical, meaning that setting a level on a logger also affects the handlers and log messages associated with it.

    Basic Logging Example

    Following is a basic logging example in Python to demonstrate its usage and functionality −

    import logging
    
    # Configure logging
    logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(levelname)s - %(message)s')# Example usagedefcalculate_sum(a, b):
       logging.debug(f"Calculating sum of {a} and {b}")
       result = a + b
       logging.info(f"Sum calculated successfully: {result}")return result
    
    # Main programif __name__ =="__main__":
       logging.info("Starting the program")
       result = calculate_sum(10,20)
       logging.info("Program completed")

    Output

    Following is the output of the above code −

    2024-06-19 09:00:06,774 - INFO - Starting the program
    2024-06-19 09:00:06,774 - DEBUG - Calculating sum of 10 and 20
    2024-06-19 09:00:06,774 - INFO - Sum calculated successfully: 30
    2024-06-19 09:00:06,775 - INFO - Program completed
    

    Configuring Logging

    Configuring logging in Python refers to setting up various components such as loggers, handlers, and formatters to control how and where log messages are stored and displayed. This configuration allows developers to customize logging behavior according to their application’s requirements and deployment environment.

    Example

    In the following example, the getLogger() function retrieves or creates a named logger. Loggers are organized hierarchically based on their names. Then, handlers like “StreamHandler” (console handler) are created to define where log messages go. They can be configured with specific log levels and formatters.

    The formatters specify the layout of log records, determining how log messages appear when printed or stored −

    import logging
    
    # Create logger
    logger = logging.getLogger('my_app')
    logger.setLevel(logging.DEBUG)# Set global log level# Create console handler and set level to debug
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG)# Create formatter
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    console_handler.setFormatter(formatter)# Add console handler to logger
    logger.addHandler(console_handler)# Example usage
    logger.debug('This is a debug message')
    logger.info('This is an info message')
    logger.warning('This is a warning message')
    logger.error('This is an error message')
    logger.critical('This is a critical message')

    The result produced is as shown below −

    2024-06-19 09:05:20,852 - my_app - DEBUG - This is a debug message
    2024-06-19 09:05:20,852 - my_app - INFO - This is an info message
    2024-06-19 09:05:20,852 - my_app - WARNING - This is a warning message
    2024-06-19 09:05:20,852 - my_app - ERROR - This is an error message
    2024-06-19 09:05:20,852 - my_app - CRITICAL - This is a critical message
    

    Logging Handlers

    Logging handlers in Python determine where and how log messages are processed and outputted. They play an important role in directing log messages to specific destinations such as the console, files, email, databases, or even remote servers.

    Each handler can be configured independently to control the format, log level, and other properties of the messages it processes.

    Types of Logging Handlers

    Following are the various types of logging handlers in Python −

    • StreamHandler − Sends log messages to streams such as sys.stdout or sys.stderr. Useful for displaying log messages in the console or command line interface.
    • FileHandler − Writes log messages to a specified file on the file system. Useful for persistent logging and archiving of log data.
    • RotatingFileHandler − Similar to FileHandler but automatically rotates log files based on size or time intervals. Helps manage log file sizes and prevent them from growing too large.
    • SMTPHandler − Sends log messages as emails to designated recipients via SMTP. Useful for alerting administrators or developers about critical issues.
    • SysLogHandler − Sends log messages to the system log on Unix-like systems (e.g., syslog). Allows integration with system-wide logging facilities.
    • MemoryHandler − Buffers log messages in memory and sends them to a target handler after reaching a certain buffer size or timeout. Useful for batching and managing bursts of log messages.
    • HTTPHandler − Sends log messages to a web server via HTTP or HTTPS. Enables logging messages to a remote server or logging service.
  • Python – User-Defined Exceptions

    User-Defined Exceptions in Python

    User-defined exceptions in Python are custom error classes that you create to handle specific error conditions in your code. They are derived from the built-in Exception class or any of its sub classes.

    User-defined exceptions provide more precise control over error handling in your application −

    • Clarity − They provide specific error messages that make it clear what went wrong.
    • Granularity − They allow you to handle different error conditions separately.
    • Maintainability − They centralize error handling logic, making your code easier to maintain.

    How to Create a User-Defined Exception

    To create a user-defined exception, follow these steps −

    Step 1 − Define the Exception Class

    Create a new class that inherits from the built-in “Exception” class or any other appropriate base class. This new class will serve as your custom exception.

    classMyCustomError(Exception):pass

    Explanation

    • Inheritance − By inheriting from “Exception”, your custom exception will have the same behaviour and attributes as the built-in exceptions.
    • Class Definition − The class is defined using the standard Python class syntax. For simple custom exceptions, you can define an empty class body using the “pass” statement.

    Step 2 − Initialize the Exception

    Implement the “__init__” method to initialize any attributes or provide custom error messages. This allows you to pass specific information about the error when raising the exception.

    classInvalidAgeError(Exception):def__init__(self, age, message="Age must be between 18 and 100"):
          self.age = age
          self.message = message
          super().__init__(self.message)

    Explanation

    • Attributes − Define attributes such as “age” and “message” to store information about the error.
    • Initialization − The “__init__” method initializes these attributes. The “super().__init__(self.message)” call ensures that the base “Exception” class is properly initialized with the error message.
    • Default Message − A default message is provided, but you can override it when raising the exception.

    Step 3 − Optionally Override “__str__” or “__repr__”

    Override the “__str__” or “__repr__” method to provide a custom string representation of the exception. This is useful for printing or logging the exception.

    classInvalidAgeError(Exception):def__init__(self, age, message="Age must be between 18 and 100"):
          self.age = age
          self.message = message
          super().__init__(self.message)def__str__(self):returnf"{self.message}. Provided age: {self.age}"

    Explanation

    • __str__ Method − The “__str__” method returns a string representation of the exception. This is what will be displayed when the exception is printed.
    • Custom Message − Customize the message to include relevant information, such as the provided age in this example.

    Raising User-Defined Exceptions

    Once you have defined a custom exception, you can raise it in your code to signify specific error conditions. Raising user-defined exceptions involves using the raise statement, which can be done with or without custom messages and attributes.

    Syntax

    Following is the basic syntax for raising an exception −

    raise ExceptionType(args)

    Example

    In this example, the “set_age” function raises an “InvalidAgeError” if the age is outside the valid range −

    defset_age(age):if age <18or age >100:raise InvalidAgeError(age)print(f"Age is set to {age}")

    Handling User-Defined Exceptions

    Handling user-defined exceptions in Python refers to using “try-except” blocks to catch and respond to the specific conditions that your custom exceptions represent. This allows your program to handle errors gracefully and continue running or to take specific actions based on the type of exception raised.

    Syntax

    Following is the basic syntax for handling exceptions −

    try:# Code that may raise an exceptionexcept ExceptionType as e:# Code to handle the exception

    Example

    In the below example, the “try” block calls “set_age” with an invalid age. The “except” block catches the “InvalidAgeError” and prints the custom error message −

    try:
       set_age(150)except InvalidAgeError as e:print(f"Invalid age: {e.age}. {e.message}")

    Complete Example

    Combining all the steps, here is a complete example of creating and using a user-defined exception −

    classInvalidAgeError(Exception):def__init__(self, age, message="Age must be between 18 and 100"):
          self.age = age
          self.message = message
          super().__init__(self.message)def__str__(self):returnf"{self.message}. Provided age: {self.age}"defset_age(age):if age <18or age >100:raise InvalidAgeError(age)print(f"Age is set to {age}")try:
       set_age(150)except InvalidAgeError as e:print(f"Invalid age: {e.age}. {e.message}")

    Following is the output of the above code −

    Invalid age: 150. Age must be between 18 and 100
  • Python – Nested try Block

    Nested try Block in Python

    In a Python program, if there is another try-except construct either inside either a try block or inside its except block, it is known as a nested-try block. This is needed when different blocks like outer and inner may cause different errors. To handle them, we need nested try blocks.

    We start with an example having a single “try − except − finally” construct. If the statements inside try encounter exception, it is handled by except block. With or without exception occurred, the finally block is always executed.

    Example 1

    Here, the try block has “division by 0” situation, hence the except block comes into play. It is equipped to handle the generic exception with Exception class.

    a=10
    b=0try:print(a/b)except Exception:print("General Exception")finally:print("inside outer finally block")

    It will produce the following output −

    General Exception
    inside outer finally block
    

    Example 2

    Let us now see how to nest the try constructs. We put another “try − except − finally” blocks inside the existing try block. The except keyword for inner try now handles generic Exception, while we ask the except block of outer try to handle ZeroDivisionError.

    Since exception doesn’t occur in the inner try block, its corresponding generic Except isn’t called. The division by 0 situation is handled by outer except clause.

    a=10
    b=0try:print(a/b)try:print("This is inner try block")except Exception:print("General exception")finally:print("inside inner finally block")except ZeroDivisionError:print("Division by 0")finally:print("inside outer finally block")

    It will produce the following output −

    Division by 0
    inside outer finally block
    

    Example 3

    Now we reverse the situation. Out of the nested try blocks, the outer one doesn’t have any exception raised, but the statement causing division by 0 is inside inner try, and hence the exception handled by inner except block. Obviously, the except part corresponding to outer try: will not be called upon.

    a=10
    b=0try:print("This is outer try block")try:print(a/b)except ZeroDivisionError:print("Division by 0")finally:print("inside inner finally block")except Exception:print("General Exception")finally:print("inside outer finally block")

    It will produce the following output −

    This is outer try block
    Division by 0
    inside inner finally block
    inside outer finally block
    

    In the end, let us discuss another situation which may occur in case of nested blocks. While there isn’t any exception in the outer try:, there isn’t a suitable except block to handle the one inside the inner try: block.

    Example 4

    In the following example, the inner try: faces “division by 0”, but its corresponding except: is looking for KeyError instead of ZeroDivisionError. Hence, the exception object is passed on to the except: block of the subsequent except statement matching with outer try: statement. There, the zeroDivisionError exception is trapped and handled.

    a=10
    b=0try:print("This is outer try block")try:print(a/b)except KeyError:print("Key Error")finally:print("inside inner finally block")except ZeroDivisionError:print("Division by 0")finally:print("inside outer finally block")

    It will produce the following output −

    This is outer try block
    inside inner finally block
    Division by 0
    inside outer finally block
  • Python – Exception Chaining

    Exception Chaining

    Exception chaining is a technique of handling exceptions by re-throwing a caught exception after wrapping it inside a new exception. The original exception is saved as a property (such as cause) of the new exception.

    During the handling of one exception ‘A’, it is possible that another exception ‘B’ may occur. It is useful to know about both exceptions in order to debug the problem. Sometimes it is useful for an exception handler to deliberately re-raise an exception, either to provide extra information or to translate an exception to another type.

    In Python 3.x, it is possible to implement exception chaining. If there is any unhandled exception inside an except section, it will have the exception being handled attached to it and included in the error message.

    Example

    In the following code snippet, trying to open a non-existent file raises FileNotFoundError. It is detected by the except block. While handling another exception is raised.

    try:open("nofile.txt")except OSError:raise RuntimeError("unable to handle error")

    It will produce the following output −

    Traceback (most recent call last):
      File "/home/cg/root/64afcad39c651/main.py", line 2, in <module>
    open("nofile.txt")
    FileNotFoundError: [Errno 2] No such file or directory: 'nofile.txt'
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/home/cg/root/64afcad39c651/main.py", line 4, in <module>
        raise RuntimeError("unable to handle error")
    RuntimeError: unable to handle error
    

    The raise . . from Statement

    If you use an optional from clause in the raise statement, it indicates that an exception is a direct consequence of another. This can be useful when you are transforming exceptions. The token after from keyword should be the exception object.

    try:open("nofile.txt")except OSError as exc:raise RuntimeError from exc
    

    It will produce the following output −

    Traceback (most recent call last):
      File "/home/cg/root/64afcad39c651/main.py", line 2, in <module>
        open("nofile.txt")
    FileNotFoundError: [Errno 2] No such file or directory: 'nofile.txt'
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/home/cg/root/64afcad39c651/main.py", line 4, in <module>
        raise RuntimeError from exc
    RuntimeError
    

    The raise . . from None Statement

    If we use None in from clause instead of exception object, the automatic exception chaining that was found in the earlier example is disabled.

    try:open("nofile.txt")except OSError as exc:raise RuntimeError fromNone

    It will produce the following output −

    Traceback (most recent call last):
     File "C:\Python311\hello.py", line 4, in <module>
      raise RuntimeError from None
    RuntimeError
    

    The __context__ and __cause__ Expression

    Raising an exception in the except block will automatically add the captured exception to the __context__ attribute of the new exception. Similarly, you can also add __cause__ to any exception using the expression raise … from syntax.

    try:try:raise ValueError("ValueError")except ValueError as e1:raise TypeError("TypeError")from e1
    except TypeError as e2:print("The exception was",repr(e2))print("Its __context__ was",repr(e2.__context__))print("Its __cause__ was",repr(e2.__cause__))

    It will produce the following output −

    The exception was TypeError('TypeError')
    Its __context__ was ValueError('ValueError')
    Its __cause__ was ValueError('ValueError')
  • Python – Raising Exceptions

    Raising Exceptions in Python

    In Python, you can raise exceptions explicitly using the raise statement. Raising exceptions allows you to indicate that an error has occurred and to control the flow of your program by handling these exceptions appropriately.

    Raising an exception refers to explicitly trigger an error condition in your program. This can be useful for handling situations where the normal flow of your program cannot continue due to an error or an unexpected condition.

    In Python, you can raise built-in exceptions like ValueError or TypeError to indicate common error conditions. Additionally, you can create and raise custom exceptions.

    Raising Built-in Exceptions

    You can raise any built-in exception by creating an instance of the exception class and using the raise statement. Following is the syntax −

    raise Exception("This is a general exception")

    Example

    Here is an example where we raise a ValueError when a function receives an invalid argument −

    defdivide(a, b):if b ==0:raise ValueError("Cannot divide by zero")return a / b
    
    try:
       result = divide(10,0)except ValueError as e:print(e)

    Following is the output of the above code −

    Cannot divide by zero
    

    Raising Custom Exceptions

    In addition to built-in exceptions, you can define and raise your own custom exceptions by creating a new exception class that inherits from the base Exception class or any of its subclasses −

    classMyCustomError(Exception):passdefrisky_function():raise MyCustomError("Something went wrong in risky_function")try:
       risky_function()except MyCustomError as e:print(e)

    Output of the above code is as shown below −

    Something went wrong in risky_function
    

    Creating Custom Exceptions

    Custom exceptions is useful for handling specific error conditions that are unique to your application, providing more precise error reporting and control.

    To create a custom exception in Python, you define a new class that inherits from the built-in Exception class or any other appropriate built-in exception class. This custom exception class can have additional attributes and methods to provide more detailed context about the error condition.

    Example

    In this example −

    • We define a custom exception class “InvalidAgeError” that inherits from “Exception”.
    • The __init__() method initializes the exception with the invalid age and a default error message.
    • The set_age() function raises “InvalidAgeError” if the provided age is outside the valid range.
    classInvalidAgeError(Exception):def__init__(self, age, message="Age must be between 18 and 100"):
          self.age = age
          self.message = message
          super().__init__(self.message)defset_age(age):if age <18or age >100:raise InvalidAgeError(age)print(f"Age is set to {age}")try:
       set_age(150)except InvalidAgeError as e:print(f"Invalid age: {e.age}. {e.message}")

    The result obtained is as shown below −

    Invalid age: 150. Age must be between 18 and 100
    

    Re-Raising Exceptions

    Sometimes, you may need to catch an exception, perform specific actions (such as logging, cleanup, or providing additional context), and then re-raise the same exception to be handled further up the call stack

    This is useful when you want to ensure certain actions are taken when an exception occurs, but still allow the exception to propagate for higher-level handling.

    To re-raise an exception in Python, you use the “raise” statement without specifying an exception, which will re-raise the last exception that was active in the current scope.

    Example

    In the following example −

    • The process_file() function attempts to open and read a file.
    • If the file is not found, it prints an error message and re-raises the “FileNotFoundError” exception.
    • The exception is then caught and handled at a higher level in the call stack.
    defprocess_file(filename):try:withopen(filename,"r")asfile:
             data =file.read()# Process dataexcept FileNotFoundError as e:print(f"File not found: {filename}")# Re-raise the exceptionraisetry:
       process_file("nonexistentfile.txt")except FileNotFoundError as e:print("Handling the exception at a higher level")

    After executing the above code, we get the following output −

    File not found: nonexistentfile.txt
    Handling the exception at a higher level
  • Python – The try-finally Block

    Python Try-Finally Block

    In Python, the try-finally block is used to ensure that certain code executes, regardless of whether an exception is raised or not. Unlike the try-except block, which handles exceptions, the try-finally block focuses on cleanup operations that must occur, ensuring resources are properly released and critical tasks are completed.

    Syntax

    The syntax of the try-finally statement is as follows −

    try:# Code that might raise exceptions
       risky_code()finally:# Code that always runs, regardless of exceptions
       cleanup_code()

    In Python, when using exception handling with try blocks, you have the option to include either except clauses to catch specific exceptions or a finally clause to ensure certain cleanup operations are executed, but not both together.

    Example

    Let us consider an example where we want to open a file in write mode (“w”), writes some content to it, and ensures the file is closed regardless of success or failure using a finally block −

    try:
       fh =open("testfile","w")
       fh.write("This is my test file for exception handling!!")finally:print("Error: can\'t find file or read data")
       fh.close()

    If you do not have permission to open the file in writing mode, then it will produce the following output −

    Error: can't find file or read data
    

    The same example can be written more cleanly as follows −

    try:
       fh =open("testfile","w")try:
          fh.write("This is my test file for exception handling!!")finally:print("Going to close the file")
          fh.close()except IOError:print("Error: can\'t find file or read data")

    When an exception is thrown in the try block, the execution immediately passes to the finally block. After all the statements in the finally block are executed, the exception is raised again and is handled in the except statements if present in the next higher layer of the try-except statement.

    Exception with Arguments

    An exception can have an argument, which is a value that gives additional information about the problem. The contents of the argument vary by exception. You capture an exception’s argument by supplying a variable in the except clause as follows −

    try:
       You do your operations here
       ......................except ExceptionType as Argument:
       You can print value of Argument here...

    If you write the code to handle a single exception, you can have a variable follow the name of the exception in the except statement. If you are trapping multiple exceptions, you can have a variable follow the tuple of the exception.

    This variable receives the value of the exception mostly containing the cause of the exception. The variable can receive a single value or multiple values in the form of a tuple. This tuple usually contains the error string, the error number, and an error location.

    Example

    Following is an example for a single exception −

    # Define a function here.deftemp_convert(var):try:returnint(var)except ValueError as Argument:print("The argument does not contain numbers\n",Argument)# Call above function here.
    temp_convert("xyz")

    It will produce the following output −

    The argument does not contain numbers
    invalid literal for int() with base 10: 'xyz'
  • Python – The try-except Block

    Python Try-Except Block

    In Python, the try-except block is used to handle exceptions and errors gracefully, ensuring that your program can continue running even when something goes wrong. This tutorial will cover the basics of using the try-except block, its syntax, and best practices.

    Exception handling allows you to manage errors in your code by capturing exceptions and taking appropriate actions instead of letting the program crash. An exception is an error that occurs during the execution of a program, and handling these exceptions ensures your program can respond to unexpected situations.

    The try-except block in Python is used to catch and handle exceptions. The code that might cause an exception is placed inside the try block, and the code to handle the exception is placed inside the except block.

    Syntax

    Following is the basic syntax of the try-except block in Python −

    try:# Code that might cause an exception
       risky_code()except SomeException as e:# Code that runs if an exception occurs
       handle_exception(e)

    Example

    In this example, if you enter a non-numeric value, a ValueError will be raised. If you enter zero, a ZeroDivisionError will be raised. The except blocks handle these exceptions and prints appropriate error messages −

    try:
       number =int(input("Enter a number: "))
       result =10/ number
       print(f"Result: {result}")except ZeroDivisionError as e:print("Error: Cannot divide by zero.")except ValueError as e:print("Error: Invalid input. Please enter a valid number.")

    Handling Multiple Exceptions

    In Python, you can handle multiple types of exceptions using multiple except blocks within a single try-except statement. This allows your code to respond differently to different types of errors that may occur during execution.

    Syntax

    Following is the basic syntax for handling multiple exceptions in Python −

    try:# Code that might raise exceptions
       risky_code()except FirstExceptionType:# Handle the first type of exception
       handle_first_exception()except SecondExceptionType:# Handle the second type of exception
       handle_second_exception()# Add more except blocks as needed for other exception types

    Example

    In the following example −

    • If you enter zero as the divisor, a “ZeroDivisionError” will be raised, and the corresponding except ZeroDivisionError block will handle it by printing an error message.
    • If you enter a non-numeric input for either the dividend or the divisor, a “ValueError” will be raised, and the except ValueError block will handle it by printing a different error message.
    try:
       dividend =int(input("Enter the dividend: "))
       divisor =int(input("Enter the divisor: "))
       result = dividend / divisor
       print(f"Result of division: {result}")except ZeroDivisionError:print("Error: Cannot divide by zero.")except ValueError:print("Error: Invalid input. Please enter valid integers.")

    Using Else Clause with Try-Except Block

    In Python, the else clause can be used in conjunction with the try-except block to specify code that should run only if no exceptions occur in the try block. This provides a way to differentiate between the main code that may raise exceptions and additional code that should only execute under normal conditions.

    Syntax

    Following is the basic syntax of the else clause in Python −

    try:# Code that might raise exceptions
       risky_code()except SomeExceptionType:# Handle the exception
       handle_exception()else:# Code that runs if no exceptions occurred
       no_exceptions_code()

    Example

    In the following example −

    • If you enter a non-integer input, a ValueError will be raised, and the corresponding except ValueError block will handle it.
    • If you enter zero as the denominator, a ZeroDivisionError will be raised, and the corresponding except ZeroDivisionError block will handle it.
    • If the division is successful (i.e., no exceptions are raised), the else block will execute and print the result of the division.
    try:
       numerator =int(input("Enter the numerator: "))
       denominator =int(input("Enter the denominator: "))
       result = numerator / denominator
    except ValueError:print("Error: Invalid input. Please enter valid integers.")except ZeroDivisionError:print("Error: Cannot divide by zero.")else:print(f"Result of division: {result}")

    The Finally Clause

    The finally clause provides a mechanism to guarantee that specific code will be executed, regardless of whether an exception is raised or not. This is useful for performing cleanup actions such as closing files or network connections, releasing locks, or freeing up resources.

    Syntax

    Following is the basic syntax of the finally clause in Python −

    try:# Code that might raise exceptions
       risky_code()except SomeExceptionType:# Handle the exception
       handle_exception()else:# Code that runs if no exceptions occurred
       no_exceptions_code()finally:# Code that always runs, regardless of exceptions
       cleanup_code()

    Example

    In this example −

    • If the file “example.txt” exists, its content is read and printed, and the else block confirms the successful operation.
    • If the file is not found (FileNotFoundError), an appropriate error message is printed in the except block.
    • The finally block ensures that the file is closed (file.close()) regardless of whether the file operation succeeds or an exception occurs.
    try:file=open("example.txt","r")
       content =file.read()print(content)except FileNotFoundError:print("Error: The file was not found.")else:print("File read operation successful.")finally:if'file'inlocals():file.close()print("File operation is complete.")

  • Python – Exceptions Handling

    Exception Handling in Python

    Exception handling in Python refers to managing runtime errors that may occur during the execution of a program. In Python, exceptions are raised when errors or unexpected situations arise during program execution, such as division by zero, trying to access a file that does not exist, or attempting to perform an operation on incompatible data types.

    Python provides two very important features to handle any unexpected error in your Python programs and to add debugging capabilities in them −

    • Exception Handling − This would be covered in this tutorial. Here is a list of standard Exceptions available in Python: Standard Exceptions.
    • Assertions − This would be covered in Assertions in Python tutorial.

    Assertions in Python

    An assertion is a sanity-check that you can turn on or turn off when you are done with your testing of the program.

    The easiest way to think of an assertion is to liken it to a raise-if statement (or to be more accurate, a raise-if-not statement). An expression is tested, and if the result comes up false, an exception is raised.

    Assertions are carried out by the assert statement, the newest keyword to Python, introduced in version 1.5.

    Programmers often place assertions at the start of a function to check for valid input, and after a function call to check for valid output.

    The assert Statement

    When it encounters an assert statement, Python evaluates the accompanying expression, which is hopefully true. If the expression is false, Python raises an AssertionError exception.

    The syntax for assert is −

    assert Expression[, Arguments]

    If the assertion fails, Python uses ArgumentExpression as the argument for the AssertionError. AssertionError exceptions can be caught and handled like any other exception using the try-except statement, but if not handled, they will terminate the program and produce a trace back.

    Example

    Here is a function that converts a temperature from degrees Kelvin to degrees Fahrenheit. Since zero degrees Kelvin is as cold as it gets, the function bails out if it sees a negative temperature −

    defKelvinToFahrenheit(Temperature):assert(Temperature >=0),"Colder than absolute zero!"return((Temperature-273)*1.8)+32print(KelvinToFahrenheit(273))print(int(KelvinToFahrenheit(505.78)))print(KelvinToFahrenheit(-5))

    When the above code is executed, it produces the following result −

    32.0
    451
    Traceback (most recent call last):
    File "test.py", line 9, in <module>
    print (KelvinToFahrenheit(-5))
    File "test.py", line 4, in KelvinToFahrenheit
    assert (Temperature >= 0),"Colder than absolute zero!"
    AssertionError: Colder than absolute zero!
    

    What is Exception?

    An exception is an event, which occurs during the execution of a program that disrupts the normal flow of the program’s instructions. In general, when a Python script encounters a situation that it cannot cope with, it raises an exception. 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.

    Handling an Exception in Python

    If you have some suspicious code that may raise an exception, you can defend your program by placing the suspicious code in a try: block. After the try: block, include an except: statement, followed by a block of code which handles the problem as elegantly as possible.

    • The try: block contains statements which are susceptible for exception
    • If exception occurs, the program jumps to the except: block.
    • If no exception in the try: block, the except: block is skipped.

    Syntax

    Here is the simple syntax of try…except…else blocks −

    try:
       You do your operations here
       ......................except ExceptionI:
       If there is ExceptionI, then execute this block.except ExceptionII:
       If there is ExceptionII, then execute this block.......................else:
       If there is no exception then execute this block.

    Here are few important points about the above-mentioned syntax −

    • A single try statement can have multiple except statements. This is useful when the try block contains statements that may throw different types of exceptions.
    • You can also provide a generic except clause, which handles any exception.
    • After the except clause(s), you can include an else clause. The code in the else block executes if the code in the try: block does not raise an exception.
    • The else block is a good place for code that does not need the try: block’s protection.

    Example

    This example opens a file, writes content in the file and comes out gracefully because there is no problem at all.

    try:
       fh =open("testfile","w")
       fh.write("This is my test file for exception handling!!")except IOError:print("Error: can\'t find file or read data")else:print("Written content in the file successfully")
       fh.close()

    It will produce the following output −

    Written content in the file successfully
    

    However, change the mode parameter in open() function to “w”. If the testfile is not already present, the program encounters IOError in except block, and prints following error message −

    Error: can't find file or read data
    

    Example

    This example tries to open a file where you do not have write permission, so it raises an exception −

    try:
       fh =open("testfile","r")
       fh.write("This is my test file for exception handling!!")except IOError:print("Error: can\'t find file or read data")else:print("Written content in the file successfully")

    This produces the following result −

    Error: can't find file or read data
    

    The except Clause with No Exceptions

    You can also use the except statement with no exceptions defined as follows −

    try:
       You do your operations here;......................except:
       If there isany exception, then execute this block.......................else:
       If there is no exception then execute this block.

    This kind of a try-except statement catches all the exceptions that occur. Using this kind of try-except statement is not considered a good programming practice though, because it catches all exceptions but does not make the programmer identify the root cause of the problem that may occur.

    The except Clause with Multiple Exceptions

    You can also use the same except statement to handle multiple exceptions as follows −

    try:
       You do your operations here;......................except(Exception1[, Exception2[,...ExceptionN]]]):
       If there isany exception from the given exception list, 
       then execute this block.......................else:
       If there is no exception then execute this block.

    The try-finally Clause

    You can use a finally: block along with a try: block. The finally block is a place to put any code that must execute, whether the try-block raised an exception or not. The syntax of the try-finally statement is this −

    try:
       You do your operations here;......................
       Due to any exception, this may be skipped.finally:
       This would always be executed.......................

    You cannot use else clause as well along with a finally clause.

    Example

    try:
       fh =open("testfile","w")
       fh.write("This is my test file for exception handling!!")finally:print("Error: can\'t find file or read data")

    If you do not have permission to open the file in writing mode, then this will produce the following result −

    Error: can't find file or read data
    

    Same example can be written more cleanly as follows −

    try:
       fh =open("testfile","w")try:
          fh.write("This is my test file for exception handling!!")finally:print("Going to close the file")
          fh.close()except IOError:print("Error: can\'t find file or read data")

    When an exception is thrown in the try block, the execution immediately passes to the finally block. After all the statements in the finally block are executed, the exception is raised again and is handled in the except statements if present in the next higher layer of the try-except statement.

    Argument of an Exception

    An exception can have an argument, which is a value that gives additional information about the problem. The contents of the argument vary by exception. You capture an exception’s argument by supplying a variable in the except clause as follows −

    try:
       You do your operations here;......................except ExceptionType, Argument:
       You can print value of Argument here...

    If you write the code to handle a single exception, you can have a variable follow the name of the exception in the except statement. If you are trapping multiple exceptions, you can have a variable follow the tuple of the exception.

    This variable receives the value of the exception mostly containing the cause of the exception. The variable can receive a single value or multiple values in the form of a tuple. This tuple usually contains the error string, the error number, and an error location.

    Example

    Following is an example for a single exception −

    # Define a function here.deftemp_convert(var):try:returnint(var)except ValueError as Argument:print("The argument does not contain numbers\n", Argument)# Call above function here.
    temp_convert("xyz")

    This produces the following result −

    The argument does not contain numbers
    invalid literal for int() with base 10: 'xyz'
    

    Raising an Exceptions

    You can raise exceptions in several ways by using the raise statement. The general syntax for the raise statement is as follows.

    Syntax

    raise [Exception [, args [, traceback]]]
    

    Here, Exception is the type of exception (for example, NameError) and argument is a value for the exception argument. The argument is optional; if not supplied, the exception argument is None.

    The final argument, trace back, is also optional (and rarely used in practice), and if present, is the traceback object used for the exception.

    Example

    An exception can be a string, a class or an object. Most of the exceptions that the Python core raises are classes, with an argument that is an instance of the class. Defining new exceptions is quite easy and can be done as follows −

    deffunctionName( level ):if level <1:raise"Invalid level!", level
          # The code below to this would not be executed# if we raise the exception

    Note: In order to catch an exception, an “except” clause must refer to the same exception thrown either class object or simple string. For example, to capture above exception, we must write the except clause as follows −

    try:
       Business Logic here...except"Invalid level!":
       Exception handling here...else:
       Rest of the code here...

    User-Defined Exceptions

    Python also allows you to create your own exceptions by deriving classes from the standard built-in exceptions.

    Here is an example related to RuntimeError. Here, a class is created that is subclassed from RuntimeError. This is useful when you need to display more specific information when an exception is caught.

    In the try block, the user-defined exception is raised and caught in the except block. The variable e is used to create an instance of the class Networkerror.

    classNetworkerror(RuntimeError):def__init__(self, arg):
          self.args = arg
    

    So once you defined above class, you can raise the exception as follows −

    try:raise Networkerror("Bad hostname")except Networkerror,e:print(e.args)

    Standard Exceptions

    Here is a list of Standard Exceptions available in Python −

    Sr.No.Exception Name & Description
    1ExceptionBase class for all exceptions
    2StopIterationRaised when the next() method of an iterator does not point to any object.
    3SystemExitRaised by the sys.exit() function.
    4StandardErrorBase class for all built-in exceptions except StopIteration and SystemExit.
    5ArithmeticErrorBase class for all errors that occur for numeric calculation.
    6OverflowErrorRaised when a calculation exceeds maximum limit for a numeric type.
    7FloatingPointErrorRaised when a floating point calculation fails.
    8ZeroDivisionErrorRaised when division or modulo by zero takes place for all numeric types.
    9AssertionErrorRaised in case of failure of the Assert statement.
    10AttributeErrorRaised in case of failure of attribute reference or assignment.
    11EOFErrorRaised when there is no input from either the raw_input() or input() function and the end of file is reached.
    12ImportErrorRaised when an import statement fails.
    13KeyboardInterruptRaised when the user interrupts program execution, usually by pressing Ctrl+c.
    14LookupErrorBase class for all lookup errors.
    15IndexErrorRaised when an index is not found in a sequence.
    16KeyErrorRaised when the specified key is not found in the dictionary.
    17NameErrorRaised when an identifier is not found in the local or global namespace.
    18UnboundLocalErrorRaised when trying to access a local variable in a function or method but no value has been assigned to it.
    19EnvironmentErrorBase class for all exceptions that occur outside the Python environment.
    20IOErrorRaised 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.
    21IOErrorRaised for operating system-related errors.
    22SyntaxErrorRaised when there is an error in Python syntax.
    23IndentationErrorRaised when indentation is not specified properly.
    24SystemErrorRaised when the interpreter finds an internal problem, but when this error is encountered the Python interpreter does not exit.
    25SystemExitRaised when Python interpreter is quit by using the sys.exit() function. If not handled in the code, causes the interpreter to exit.
    26TypeErrorRaised when an operation or function is attempted that is invalid for the specified data type.
    27ValueErrorRaised when the built-in function for a data type has the valid type of arguments, but the arguments have invalid values specified.
    28RuntimeErrorRaised when a generated error does not fall into any category.
    29NotImplementedErrorRaised when an abstract method that needs to be implemented in an inherited class is not actually implemented.
  • Python – Syntax Errors

    Python Syntax Errors

    In Python, syntax errors are among the most common errors encountered by programmers, especially those who are new to the language. This tutorial will help you understand what syntax errors are, how to identify them, and how to fix them.

    What is a Syntax Error?

    A syntax error in Python (or any programming language) is an error that occurs when the code does not follow the syntax rules of the language. Syntax errors are detected by the interpreter or compiler at the time of parsing the code, and they prevent the code from being executed.

    These errors occur because the written code does not conform to the grammatical rules of Python, making it impossible for the interpreter to understand and execute the commands.

    Common Causes of Syntax Errors

    Following are the common causes of syntax errors −Missing colons (:) after control flow statements (e.g., if, for, while) − Colons are used to define the beginning of an indented block, such as in functions, loops, and conditionals.

    # Error: Missing colon (:) after the if statementifTrueprint("This will cause a syntax error")

    Incorrect indentation − Python uses indentation to define the structure of code blocks. Incorrect indentation can lead to syntax errors.

    # Error: The print statement is not correctly indenteddefexample_function():print("This will cause a syntax error")

    Misspelled keywords or incorrect use of keywords.

    # Error: 'print' is misspelled as 'prnt'
    prnt("Hello, World!")

    Unmatched parentheses, brackets, or braces − Python requires that all opening parentheses (, square brackets [, and curly braces { have corresponding closing characters ), ], and }.

    # Error: The closing parenthesis is missing.print("This will cause a syntax error"

    How to Identify Syntax Errors

    Identifying syntax errors in Python can sometimes be easy, especially when you get a clear error message from the interpreter. However, other times, it can be a bit tricky. Here are several ways to help you identify and resolve syntax errors effectively −

    Reading Error Messages

    When you run a Python script, the interpreter will stop execution and display an error message if it encounters a syntax error. Understanding how to read these error messages is very important.

    Example Error Message

    File "script.py", line 1print("Hello, World!"^
    SyntaxError: EOL while scanning string literal
    

    This error message can be broken down into parts −

    • File “script.py”: Indicates the file where the error occurred.
    • line 1: Indicates the line number in the file where the interpreter detected the error.
    • print(“Hello, World!”: Shows the line of code with the error.
    • ^: Points to the location in the line where the error was detected.

    Using an Integrated Development Environment (IDE)

    IDEs are helpful in identifying syntax errors as they often provide real-time feedback. Here are some features of IDEs that helps in identifying syntax errors −

    • Syntax Highlighting: IDEs highlight code syntax in different colors. If a part of the code is incorrectly colored, it may indicate a syntax error.
    • Linting: Tools like pylint or flake8 check your code for errors and stylistic issues.
    • Error Underlining: Many IDEs underline syntax errors with a red squiggly line.
    • Tooltips and Error Messages: Hovering over the underlined code often provides a tooltip with a description of the error.

    Popular IDEs with these features include PyCharm, Visual Studio Code, and Jupyter Notebook.

    Running Code in Small Chunks

    If you have a large script, it can be useful to run the code in smaller chunks. This can help isolate the part of the code causing the syntax error.

    For example, if you have a script with multiple functions and you get a syntax error, try running each function independently to narrow down where the error might be.

    Using Version Control

    Version control systems like Git can help you track changes to your code. If you encounter a syntax error, you can compare the current version of the code with previous versions to see what changes might have introduced the error.

    Fixing Syntax Errors

    Fixing syntax errors in Python involves understanding the error message provided by the interpreter, identifying the exact issue in the code, and then making the necessary corrections. Here is a detailed guide on how to systematically approach and fix syntax errors −

    Read the Error Message Carefully

    Pythons error messages are quite informative. They indicate the file name, line number, and the type of syntax error −

    Example Error Message

    Assume we have written a print statement as shown below −

    print("Hello, World!"

    The following message indicates that there is a syntax error on line 1, showing that somewhere in the code, a parenthesis was left unclosed, which leads to a syntax error.

    File "/home/cg/root/66634a37734ad/main.py", line 1print("Hello, World!"^
    SyntaxError:'(' was never closed
    

    To fix this error, you need to ensure that every opening parenthesis has a corresponding closing parenthesis. Here is the corrected code −

    print("Hello, World!")

    Locate the Error

    To locate the error, you need to go to the line number mentioned in the error message. Additionally, check not only the indicated line but also the lines around it, as sometimes the issue might stem from previous lines.

    Understand the Nature of the Error

    To understand the nature of the error, you need to identify what type of syntax error it is (e.g., missing parenthesis, incorrect indentation, missing colon, etc.). Also, refer to common syntax errors and their patterns.

    Correct the Syntax

    Based on the error type, fix the code.

  • Python – Dataclass

    The dataclass is a feature in Python that helps for automatically adding special methods to user-defined classes. For example, a dataclass can be used to automatically generate a constructor method for your class, so you can easily create instances of the class. In this chapter, we will explain all the features of the dataclass module with the help of examples.

    What is a Dataclass?

    dataclass is a Python class denoted with the @dataclass decorator from the dataclasses module. It is used to automatically generate special methods like constructor method __init__(), string representation method __repr__(), equality method __eq__(), and others based on the class attributes defined in the class body. This simply means that using dataclasses can reduce boilerplate code in your class definitions.

    Syntax of Dataclass

    The syntax to define a dataclass is as follows −

    @dataclass(init=True,repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

    Each argument is taking a boolean value, which indicates whether corresponding special methods should be automatically generated.

    The @dataclass decorator can take these options:

    • init (default: True) − Automatically creates an __init__() method for initializing class instances.
    • repr (default: True) − Automatically creates a __repr__() method to provide a readable string representation of the object.
    • eq (default: True) − Generates an __eq__() method for comparing objects using the == operator.
    • order (default: False) − If set to True, comparison methods (like < and >) are generated for sorting objects.
    • unsafe_hash (default: False) − If false, it produces a default __hash__() method based on how equality and mutability are defined.
    • frozen (default: False) − Creates immutable instances (they can’t be changed after creation).

    Creating a Dataclass

    In the code below, you can see that we have defined a simple dataclass named Student with three attributes: name, age, and percent. Even though we are not using constructor or other special methods, we can still create instances of the Student class and use its attributes. This is because the dataclass decorator automatically generates these methods for us.

    from dataclasses import dataclass  
    
    @dataclassclassStudent:  
        name:str  
        age:int  
        percent:float
    
    s1 = Student("Alice",20,90.0)
    s2 = Student("Bob",22,85.5)print(s1)print(s1 == s2)

    The output of the above code will be −

    Student(name='Alice', age=20, percent=90.0)
    False
    

    Example without Dataclass

    The above code is equivalent to the following code in traditional class definition without using dataclass −

    classStudent:def__init__(self, name:str, age:int, percent:float):  
            self.name = name  
            self.age = age  
            self.percent = percent  
        def__repr__(self):returnf"Student(name={self.name}, age={self.age}, percent={self.percent})"def__eq__(self, other):ifnotisinstance(other, Student):return NotImplemented  
            return(self.name == other.name and self.age == other.age and self.percent == other.percent)
    s1 = Student("Alice",20,90.0)
    s2 = Student("Bob",22,85.5)print(s1)print(s1 == s2)

    The output of this code will same as above −

    Student(name='Alice', age=20, percent=90.0)
    False
    

    Default Values in Dataclass

    You can also provide default values for the attributes in a dataclass. If a value is not provided during the creation of an instance, then the default value will be used. In the code below, we have provided a default value of 0.0 for the percent attribute.

    from dataclasses import dataclass
    
    @dataclassclassStudent:
        name:str
        age:int
        percent:float=0.0# Default value for percent
    
    s1 = Student("Alice",20)
    s2 = Student("Bob",22,85.5)print(s1)print(s2)

    The output of the above code will be −

    Student(name='Alice', age=20, percent=0.0)
    Student(name='Bob', age=22, percent=85.5)
    

    Dataclass with Mutable Default Values

    Mutable default values refers to default values that can be modified after the instance is created, such as lists or dictionaries. When using mutable default values in a dataclass, it is recommended to use the field() function from the dataclasses module with the default_factory parameter. Because if you use a mutable object as a default value directly, it will be shared across all instances of the dataclass. This can lead security issues and unexpected behavior.

    In the code below, we have defined a dataclass named Course with a mutable default value for the students attribute.

    from dataclasses import dataclass, field
    from typing import List
    @dataclassclassCourse:
        name:str
        students: List[str]= field(default_factory=list)# Mutable default value
    
    course1 = Course("Math")
    course2 = Course("Science",["Alice","Bob"])
    course1.students.append("Charlie")print(course1)print(course2)

    The output of the above code will be −

    Course(name='Math', students=['Charlie'])
    Course(name='Science', students=['Alice', 'Bob'])
    

    Explantion: If you used students: List[str] = [] directly, all the instances of Course will get the same list, because default values are evaluated only once, i.e., during creation of class. By using field(default_factory=list), Python ensures that every Course instance gets its own separate list and avoids security vulnerabilities.

    Immuatable/Frozen Dataclasses

    An immutable or frozen dataclass indicates that the instances of the dataclass cannot be modified after they are created. This can be achieved by setting the frozen parameter to True in the @dataclass decorator. When a dataclass is frozen, any attempt to modify its attributes will create a FrozenInstanceError.

    The frozen dataclasses are often used to secure applications by preventing unauthorized access or modification of sensitive data. In the code below, we have defined a frozen dataclass named Point with two attributes: x and y. We will catch the FrozenInstanceError when trying to modify the x attribute.

    from dataclasses import dataclass, FrozenInstanceError
    @dataclass(frozen=True)classPoint:
        x:int
        y:int
    p = Point(1,2)print(p)try:
        p.x =10# This will raise an errorexcept FrozenInstanceError as e:print(e)

    The output of the above code will be −

    Point(x=1, y=2)
    cannot assign to field 'x'
    

    Setting Up Post-Initialization

    The post-Initialization refer to the additional initialization logic that can be added to a dataclass after the automatic __init__() method has been called. This can be done by defining a special method named __post_init__() in the dataclass. The __post_init__() method is called automatically after the instance is created and all the attributes have been initialized.

    In the code below, we have defined a dataclass named Rectangle with default value for the area as 0.0. The area is then calculated in the __post_init__() method based on the recived width and height values.

    from dataclasses import dataclass
    @dataclassclassRectangle:
        width:float
        height:float
        area:float=0.0# This will be calculated in __post_init__def__post_init__(self):
            self.area = self.width * self.height  # Calculate area after initialization
    r = Rectangle(5.0,10.0)print(r)print(f"Area of the rectangle: {r.area}")

    The output of the above code will be −

    Rectangle(width=5.0, height=10.0, area=50.0)
    Area of the rectangle: 50.0
    

    Convert Dataclass to Dictionary

    You can convert a dataclass instance to a dictionary using the asdict() function from the dataclasses module. This function recursively converts the dataclass and its fields into a dictionary. This is useful when you want to serialize the dataclass instance or work with its data in a dictionary format.

    from dataclasses import dataclass, asdict
    from typing import List 
    
    @dataclassclassStudent:
        name:str
        age:int
        grades: List[float]  
    
    student = Student("Alice",20,[88.5,92.0,79.5])
    student_dict = asdict(student)print(student_dict)

    The output of the above code will be −

    {'name': 'Alice', 'age': 20, 'grades': [88.5, 92.0, 79.5]}
    

    Conclusion

    In this chapter, we have learned that dataclass is feature introduced in Python to reduce boilerplate code in class definitions. We have seen how to create a dataclass, provide default values, handle mutable default values, etc. One thing to note is that dataclasses are available in Python 3.7 and later versions. If you are using an earlier version of Python, you will need to use traditional class definitions without the dataclass decorator.