Blog

  • Immutable Data Structures

    The Python Immutable data structures are the data structures that once created, cannot be changed. This means that any attempt to modify the data structure will result in a new instance being created rather than altering the original. Immutable data structures are useful for ensuring that data remains constant throughout the execution of a program which can help prevent bugs and make code easier to understand and maintain.

    Before proceeding deep into this topic let’s have a quick recall of what is datastructure? The Data structures are specialized formats for organizing, processing, retrieving and storing data. They define how data is arranged in memory and how operations such as accessing, inserting, deleting and updating can be performed efficiently.

    Different Immutable Data Structures in Python

    Immutable data structures are essential in Python for their stability, thread-safety and ease of use. Here are the different immutable data structures in Python −

    • Tuples: These are the ordered collections of items that cannot be changed after their creation. They can contain mixed data types and are useful for representing fixed collections of related items.
    • Strings: These Data structures are sequences of characters and are immutable. Any operation that modifies a string will create a new string.
    • Frozensets: These are immutable versions of sets. Unlike regular sets, frozensets do not allow modification after creation.
    • Named Tuples: These are a subclass of tuples with named fields which provide more readable and self-documenting code. They are immutable like regular tuples.

    Now, let’s proceed about the each Immutable data structures in detail.

    Tuples

    Tuples in Python are immutable sequences of elements which means once created, they cannot be modified. They are defined using parentheses ‘()’ and can hold a collection of items such as numbers, strings and even other tuples.

    Creating Tuples

    Tuples are created using parentheses ‘()’ and elements separated by commas ‘,’. Even tuples with a single element require a trailing comma to distinguish them from grouped expressions.

    Following is the example of creating a tuple by assigning parentheses ‘()’ to a variable −

    empty_tuple =()
    single_element_tuple =(5,)# Note the comma after the single elementprint("Single element tuple:", single_element_tuple)
    multi_element_tuple =(1,2,'Tutorialspoint',3.14)print("Multi elements tuple:", multi_element_tuple)
    nested_tuple =(1,(2,3),'Learning')print("Nested tuple:", nested_tuple)

    On executing the above code we will get the following output −

    Single element tuple: (5,)
    Multi elements tuple: (1, 2, 'Tutorialspoint', 3.14)
    Nested tuple: (1, (2, 3), 'Learning')
    

    Understanding Tuple Immutability in Python

    Here we are going understand the immutability of the tuples in python. Below is the example −

    # Define a tuple
    my_tuple =(1,2,3,'hello')# Attempt to modify an element (which is not possible with tuples)# This will raise a TypeErrortry:
       my_tuple[0]=10except TypeError as e:print(f"Error: {e}")# Even trying to append or extend a tuple will result in an errortry:
       my_tuple.append(4)except AttributeError as e:print(f"Error: {e}")# Trying to reassign the entire tuple to a new value is also not allowedtry:
       my_tuple =(4,5,6)except TypeError as e:print(f"Error: {e}")print("Original tuple:", my_tuple)

    On executing the above code we will get the following output −

    Error: 'tuple' object does not support item assignment
    Error: 'tuple' object has no attribute 'append'
    Original tuple: (4, 5, 6)
    

    Strings

    Strings in Python are sequences of characters which are used to represent and manipulate textual data. They are enclosed within either single quotes  or double quotes  with the option to use triple quotes “”” for multi-line strings.

    Key characteristics include immutability which means once created those strings cannot be changed, ordered indexing where characters are accessed by position and support for various operations such as concatenation, slicing and iteration.

    Strings are fundamental in Python for tasks such as text processing, input/output operations and data representation in applications offering a versatile toolset with built-in methods for efficient manipulation and formatting of textual information.

    Creating Strings

    Each type of string creation method i.e. ‘, “, “”” has its own use case depending on whether we need to include quotes within the string, handle multi-line text or other specific formatting requirements in our Python code.

    Following is the example of creating the string with the help od three types of quotes ‘, “, “”” −

    # Single line string
    single_quoted_string ='Hello, Welcome to Tutorialspoint'# Double quoted string
    double_quoted_string ="Python Programming"# Triple quoted string for multi-line strings
    multi_line_string ="""This is a 
    multi-line 
    string"""print(single_quoted_string)print(double_quoted_string)print(multi_line_string)

    On executing the above code we will get the following output −

    Hello, Welcome to Tutorialspoint
    Python Programming
    This is a
    multi-line
    string
    

    Understanding String Immutability in Python

    With the help of following example we are going to understand the immutability of the strings in python.

    # Example demonstrating string immutability
    my_string ="Hello"# Attempting to modify a string will create a new string instead of modifying the original
    modified_string = my_string +" Learners"print(modified_string)# Output: Hello Learners# Original string remains unchangedprint(my_string)# Output: Hello# Trying to modify the string directly will raise an errortry:
       my_string[0]='h'# TypeError: 'str' object does not support item assignmentexcept TypeError as e:print(f"Error: {e}")

    On executing the above code we will get the following output −

    Hello Learners
    Hello
    Error: 'str' object does not support item assignment
    

    Frozen Sets

    A frozen set in Python is an immutable version of a set. Once created its elements cannot be changed, added or removed. Frozen sets are particularly useful in situations where we need a set that remains constant throughout the execution of a program especially when we want to use it as a key in a dictionary or as an element in another set.

    Creating Frozen Sets

    We can create a frozen set using the frozenset() constructor by passing an iterable such as a list or another set as an argument. Following is the example of creating the Frozen set −

    # Creating a frozen set
    fset =frozenset([1,2,3,4])# Printing the frozen setprint(fset)

    On executing the above code we will get the following output −

    frozenset({1, 2, 3, 4})
    

    Understanding Frozen Sets Immutability in Python

    Here’s an example shows how frozensets being immutable and do not allow modifications after creation.

    # Creating a frozenset
    frozen_set =frozenset([1,2,3,4])# Attempting to add an element to the frozenset will raise an errortry:
       frozen_set.add(5)except AttributeError as e:print(f"Error: {e}")# Attempting to remove an element from the frozenset will also raise an errortry:
       frozen_set.remove(2)except AttributeError as e:print(f"Error: {e}")# The original frozenset remains unchangedprint("Original frozenset:", frozen_set)

    On executing the above code we will get the following output −

    Error: 'frozenset' object has no attribute 'add'
    Error: 'frozenset' object has no attribute 'remove'
    Original frozenset: frozenset({1, 2, 3, 4})
    

    Named Tuples

    A Named tuple in Python is a lightweight data structure available in the collections module that behaves same as a tuple but allows us to access its elements using named attributes as well as indices.

    It combines the advantages of tuples such as immutable, memory-efficient with the ability to refer to elements by name, enhancing readability and maintainability of code.

    Creating Named Tuples

    we can define a named tuple using the namedtuple() factory function from the collections module. It takes two arguments such as a name for the named tuple type and a sequence i.e. string of field names or iterable of strings which specifies the names of its fields.

    from collections import namedtuple
    
    # Define a named tuple type 'Point' with fields 'x' and 'y'
    Point = namedtuple('Point',['x','y'])# Create an instance of Point
    p1 = Point(1,2)# Access elements by index (like a tuple)print(p1[0])# Access elements by nameprint(p1.x)print(p1.y)

    On executing the above code we will get the following output −

    1
    1
    2
    

    Understanding Named Tuples Immutability in Python

    The Named tuples in Python are provided by the collections.namedtuple factory functions are indeed immutable. They behave similarly to regular tuples but have named fields by making them more readable and self-documenting.

    from collections import namedtuple
    
    # Define a named tuple called Point with fields 'x' and 'y'
    Point = namedtuple('Point',['x','y'])# Create an instance of Point
    p = Point(x=1, y=2)print(p)# Attempt to modify the named tuple# This will raise an AttributeError since named tuples are immutabletry:
       p.x =10except AttributeError as e:print(f"Error occurred: {e}")# Accessing elements in a named tuple is similar to accessing elements in a regular tupleprint(p.x)print(p.y)

    On executing the above code we will get the following output −

    Point(x=1, y=2)
    Error occurred: can't set attribute
    1
    2
  • Diagnosing and Fixing Memory Leaks

    Memory leaks occur when a program incorrectly manages memory allocations which resulting in reduced available memory and potentially causing the program to slow down or crash.

    In Python, memory management is generally handled by the interpreter but memory leaks can still happen especially in long-running applications. Diagnosing and fixing memory leaks in Python involves understanding how memory is allocated, identifying problematic areas and applying appropriate solutions.

    Causes of Memory Leaks in Python

    Memory leaks in Python can arise from several causes, primarily revolving around how objects are referenced and managed. Here are some common causes of memory leaks in Python −

    1. Unreleased References

    When objects are no longer needed but still referenced somewhere in the code then they are not de-allocated which leads to memory leaks. Here is the example of it −

    defcreate_list():
       my_list =[1]*(10**6)return my_list
    
    my_list = create_list()# If my_list is not cleared or reassigned, it continues to consume memory.print(my_list)

    Output

    [1, 1, 1, 1,
    ............
    ............
    1, 1, 1, 1]
    

    2. Circular References

    Circular references in Python can lead to memory leaks if not managed properly but Python’s cyclic garbage collector can handle many cases automatically.

    For understanding how to detect and break circular references we can use the tools such as the gc and weakref modules. These tools are crucial for efficient memory management in complex Python applications. Following is the example of circular references −

    classNode:def__init__(self, value):
    
      self.value = value
      self.next=None
    a = Node(1) b = Node(2) a.next= b b.next= a # 'a' and 'b' reference each other, creating a circular reference.

    3. Global Variables

    Variables declared at the global scope persist for the lifetime of the program which potentially causing memory leaks if not managed properly. Below is the example of it −

    large_data =[1]*(10**6)defprocess_data():global large_data
       # Use large_datapass# large_data remains in memory as long as the program runs.

    4. Long-Lived Objects

    Objects that persist for the lifetime of the application can cause memory issues if they accumulate over time. Here is the example −

    cache ={}defcache_data(key, value):
       cache[key]= value
    
    # Cached data remains in memory until explicitly cleared.

    5. Improper Use of Closures

    Closures that capture and retain references to large objects can inadvertently cause memory leaks. Below is the example of it −

    defcreate_closure():
       large_object =[1]*(10**6)defclosure():return large_object
       return closure
    
    my_closure = create_closure()# The large_object is retained by the closure, causing a memory leak.

    Tools for Diagnosing Memory Leaks

    Diagnosing memory leaks in Python can be challenging but there are several tools and techniques available to help identify and resolve these issues. Here are some of the most effective tools and methods for diagnosing memory leaks in Python −

    1. Using the “gc” Module

    The gc module can help in identifying objects that are not being collected by the garbage collector. Following is the example of diagnosing the memory leaks using the gc module −

    import gc
    
    # Enable automatic garbage collection
    gc.enable()# Collect garbage and return unreachable objects
    unreachable_objects = gc.collect()print(f"Unreachable objects: {unreachable_objects}")# Get a list of all objects tracked by the garbage collector
    all_objects = gc.get_objects()print(f"Number of tracked objects: {len(all_objects)}")

    Output

    Unreachable objects: 51
    Number of tracked objects: 6117
    

    2. Using “tracemalloc”

    The tracemalloc module is used to trace memory allocations in Python. It is helpful for tracking memory usage and identifying where memory is being allocated. Following is the example of diagnosing the memory leaks using the tracemalloc module −

    import tracemalloc
    
    # Start tracing memory allocations
    tracemalloc.start()# our code here
    a =10
    b =20
    c = a+b
    # Take a snapshot of current memory usage
    snapshot = tracemalloc.take_snapshot()# Display the top 10 memory-consuming lines
    top_stats = snapshot.statistics('lineno')for stat in top_stats[:10]:print(stat)

    Output

    C:\Users\Niharikaa\Desktop\sample.py:7: size=400 B, count=1, average=400 B
    

    3. Using “memory_profiler”

    The memory_profiler is a module for monitoring memory usage of a Python program. It provides a decorator to profile functions and a command-line tool for line-by-line memory usage analysis. In the below example we are diagnosing the memory leaks using the memory_profiler module −

    from memory_profiler import profile
    
    @profiledefmy_function():# our code here
       a =10
       b =20
       c = a+b
    
    if __name__ =="__main__":
    my_function()</pre>

    Output

    Line #      Mem   usage    Increment  Occurrences   Line 
    ======================================================================
    
     3     49.1   MiB      49.1 MiB         1       @profile
     4                                              def my_function():
     5                                              # Your code here
     6     49.1   MiB      0.0 MiB          1       a = 10
     7     49.1   MiB      0.0 MiB          1       b = 20
     8     49.1   MiB      0.0 MiB          1       c = a+b

    Fixing Memory Leaks

    Once a memory leak is identified we can fix the memory leaks,, which involves locating and eliminating unnecessary references to objects.

    • Eliminate Global Variables: Avoid using global variables unless and untill absolutely necessary. Instead we can use local variables or pass objects as arguments to functions.
    • Break Circular References: Use weak references to break cycles where possible. The weakref module allows us to create weak references that do not prevent garbage collection.
    • Manual Cleanup: Explicitly delete objects or remove references when they are no longer needed.
    • Use Context Managers: Ensure resources that are properly cleaned up using context managers i.e. with statement.
    • Optimize Data Structures Use appropriate data structures that do not unnecessarily hold onto references.

    Finally we can conclude Diagnosing and fixing memory leaks in Python involves identifying lingering references by using tools like gc, memory_profiler and tracemalloc etc to track memory usage and implementing fixes such as removing unnecessary references and breaking circular references.

    By following these steps, we can ensure our Python programs use memory efficiently and avoid memory leaks.

  • Descriptors

    Python Descriptors

    Python Descriptors are a way to customize the access, assignment and deletion of object attributes. They provide a powerful mechanism for managing the behavior of attributes by defining methods that get, set and delete their values. Descriptors are often used to implement properties, methods and attribute validation.

    descriptor is any object that implements at least one of the methods such as __get__, __set__ and __delete__. These methods control how an attribute’s value is accessed and modified.

    How Python Descriptors Work?

    When an attribute is accessed on an instance then Python looks up the attribute in the instance’s class. If the attribute is found and it is a descriptor then Python invokes the appropriate descriptor method instead of simply returning the attribute’s value. This allows the descriptor to control what happens during attribute access.

    The descriptor protocol is a low-level mechanism that is used by many high-level features in Python such as properties, methods, static methods and class methods. Descriptors can be used to implement patterns like lazy loading, type checking and computed properties.

    Descriptor Methods

    Python Descriptors involve three main methods namely __get__(), __set__() and __delete__(). As we already discussed above these methods control the behavior of attribute access, assignment and deletion, respectively.

    1. The __get__() Method

    The __get__() method in descriptors is a key part of the descriptor protocol in Python. It is called to retrieve the value of an attribute from an instance or from the class. Understanding how the __get__() method works is crucial for creating custom descriptors that can manage attribute access in sophisticated ways.

    Syntax

    The following is the syntax of Python Descriptor __get__ method −

    def__get__(self, instance, owner):"""
       instance: the instance that the attribute is accessed through, or None when accessed through the owner class.
       owner: the owner class where the descriptor is defined.
       """

    Parameters

    Below are the parameters of this method −

    • self: The descriptor instance.
    • instance: The instance of the class where the attribute is accessed. It is None when the attribute is accessed through the class rather than an instance.
    • owner: The class that owns the descriptor.

    Example

    Following is the basic example of __get__() method in which it returns the stored value _value when obj.attr is accessed −

    classDescriptor:def__get__(self, instance, owner):if instance isNone:return self
    
      return instance._value
    classMyClass: attr = Descriptor()def__init__(self, value):
      self._value = value
    obj = MyClass(42)print(obj.attr)

    Output

    42
    

    2. The __set__() Method

    The __set__() method is part of the descriptor protocol in Python and is used to control the behavior of setting an attribute’s value. When an attribute managed by a descriptor is assigned a new value then the __set__() method is called by allowing the user to customize or enforce rules for the assignment.

    Syntax

    The following is the syntax of Python Descriptor __set__() method −

    def__set__(self, instance, value):"""
    
    instance: the instance of the class where the attribute is being set.
    value: the value to assign to the attribute.
    """</pre>

    Parameters

    Below are the parameters of this method −

    • self: The descriptor instance.
    • instance: The instance of the class where the attribute is being set.
    • value: The value being assigned to the attribute.

    Example

    Following is the basic example of __set__() method in which ensures that the value assigned to attr is an integer −

    classDescriptor:def__set__(self, instance, value):ifnotisinstance(value,int):raise TypeError("Value must be an integer")
    
        instance._value = value
    classMyClass:
    attr = Descriptor()def__init__(self, value):
        self.attr = value
    obj = MyClass(42)print(obj.attr) obj.attr =100print(obj.attr)

    Output

    <__main__.Descriptor object at 0x000001E5423ED3D0>
    <__main__.Descriptor object at 0x000001E5423ED3D0>
    

    3. The __delete__() Method

    The __delete__() method in the descriptor protocol allows us to control what happens when an attribute is deleted from an instance. This can be useful for managing resources, cleaning up or enforcing constraints when an attribute is removed.

    Syntax

    The following is the syntax of Python Descriptor __delete__() method −

    def__delete__(self, instance):"""
    
    instance: the instance of the class from which the attribute is being deleted.
    """</pre>

    Parameters

    Below are the parameters of this method −

    • self: The descriptor instance.
    • instance: The instance of the class where the attribute is being deleted.

    Example

    Following is the basic example of __set__() method in which ensures that the value assigned to attr is an integer −

    classLoggedDescriptor:def__init__(self, name):
    
      self.name = name
    def__get__(self, instance, owner):return instance.__dict__.get(self.name)def__set__(self, instance, value):
      instance.__dict__[self.name]= value
    def__delete__(self, instance):if self.name in instance.__dict__:print(f"Deleting {self.name} from {instance}")del instance.__dict__[self.name]else:raise AttributeError(f"{self.name} not found")classPerson: name = LoggedDescriptor("name") age = LoggedDescriptor("age")def__init__(self, name, age):
      self.name = name
      self.age = age
    # Example usage p = Person("Tutorialspoint",30)print(p.name)print(p.age)del p.name print(p.name)del p.age print(p.age)

    Output

    Tutorialspoint
    30
    Deleting name from <__main__.Person object at 0x0000021A1A67E2D0>
    None
    Deleting age from <__main__.Person object at 0x0000021A1A67E2D0>
    None
    

    Types of Python Descriptors

    In Python descriptors can be broadly categorized into two types based on the methods they implement. They are −

    • Data Descriptors
    • Non-data Descriptors

    Let's see about the two types of python descriptors in detail for our better understanding.

    1. Data Descriptors

    Data descriptors are a type of descriptor in Python that define both __get__()and __set__() methods. These descriptors have precedence over instance attributes which meand that the descriptors __get__()and __set__() methods are always called, even if an instance attribute with the same name exists.

    Example

    Below is the example of a data descriptor that ensures an attribute is always an integer and logs access and modification operations −

    classInteger:def__get__(self, instance, owner):print("Getting value")return instance._value
    
       def__set__(self, instance, value):print("Setting value")ifnotisinstance(value,int):raise TypeError("Value must be an integer")
    
      instance._value = value
    def__delete__(self, instance):print("Deleting value")del instance._value classMyClass: attr = Integer()# Usage obj = MyClass() obj.attr =42print(obj.attr) obj.attr =100print(obj.attr)del obj.attr

    Output

    Setting value
    Getting value
    42
    Setting value
    Getting value
    100
    Deleting value
    

    2. Non-data Descriptors

    Non-data descriptors are a type of descriptor in Python that define only the __get__() method. Unlike data descriptors, non-data descriptors can be overridden by instance attributes. This means that if an instance attribute with the same name exists then it will take precedence over the non-data descriptor.

    Example

    Following is an example of a non-data descriptor that provides a default value if the attribute is not set on the instance −

    classDefault:def__init__(self, default):
    
      self.default = default
    def__get__(self, instance, owner):returngetattr(instance,'_value', self.default)classMyClass: attr = Default("default_value")# Usage obj = MyClass()print(obj.attr) obj._value ="Tutorialspoint"print(obj.attr)

    Output

    default_value
    Tutorialspoint
    

    Data Descriptors Vs. Non-data Descriptors

    Understanding the differences between Data Descriptors and Non-data Descriptors of python Descriptors is crucial for leveraging their capabilities effectively.

    CriteriaData DescriptorsNon-Data Descriptors
    DefinitionImplements both __get__(), __set__() methods, and the __delete__() method optionally.Implements only __get__() method.
    Methods__get__(self, instance, owner)
    __set__(self, instance, value)
    __delete__(self, instance) (optional)
    __get__(self, instance, owner)
    PrecedenceTakes precedence over instance attributes.Overridden by instance attributes.
    Use CasesAttribute validation and enforcement,
    Managed attributes (e.g., properties),
    Logging attribute access and modification,
    Enforcing read-only attributes.
    Method binding,
    Caching and,
    Providing default values..

    Finally we can say Descriptors in Python provide a powerful mechanism for managing attribute access and modification. Understanding the differences between data descriptors and non-data descriptors as well as their appropriate use cases is essential for creating robust and maintainable Python code.

    By leveraging the descriptor protocol developers can implement advanced behaviors such as type checking, caching and read-only properties.

  • Coroutines

    Python Coroutines are a fundamental concept in programming that extend the capabilities of traditional functions. They are particularly useful for asynchronous programming and complex data processing pipelines.

    Coroutines are an extension of the concept of functions and generators. They are designed to perform cooperative multitasking and manage asynchronous operations.

    In traditional functions i.e. subroutines which have a single entry and exit point where as coroutines can pause and resume their execution at various points by making them highly flexible.

    Key Characteristics of Coroutines

    Following are the key characteristics of Coroutines in python −

    • Multiple Entry Points: Coroutines are not constrained to a single entry point like traditional functions. They can pause their execution at certain points, when they hit a yield statement and resume later. This allows coroutines to handle complex workflows that involve waiting for or processing asynchronous data.
    • No Central Coordinator: When we see traditional functions i.e subroutines, which are often coordinated by a main function but coroutines operate more independently. They can interact with each other in a pipeline fashion where data flows through a series of coroutines, each performing a different task.
    • Cooperative Multitasking: Coroutines enable cooperative multitasking. This means that instead of relying on the operating system or runtime to switch between tasks the programmer controls when coroutines yield and resume by allowing for more fine-grained control over execution flow.

    Subroutines Vs. Coroutines

    Subroutines are the traditional functions with a single entry point and no inherent mechanism for pausing or resuming execution. They are called in a defined sequence and handle tasks with straightforward control flow.

    Coroutines are the advanced functions with multiple entry points that can pause and resume their execution. They are useful for tasks that require asynchronous execution, complex control flows and data pipelines. They support cooperative multitasking by allowing the programmer to control when execution switches between tasks.

    The following table helps in understanding the key differences and similarities between subroutines and coroutines by making us easier to grasp their respective roles and functionalities in programming.

    CriteriaSubroutinesCoroutines
    DefinitionA sequence of instructions performing a task.A generalization of subroutines that can pause and resume execution.
    Entry PointsSingle entry point.Multiple entry points; can pause and resume execution.
    Execution ControlCalled by a main function or control structure.These can suspend execution and be resumed later and programmer controls switching.
    PurposePerform a specific task or computation.Manage asynchronous operations, cooperative multitasking and complex workflows.
    Calling MechanismTypically called by a main function or other subroutines.Invoked and controlled using ‘next()’, ‘send()’, and ‘close()’ methods.
    Data HandlingNo built-in mechanism for handling data exchanges; typically uses parameters and return values.Can receive and process data using ‘yield’ with ‘send()’.
    State ManagementNo inherent mechanism to maintain state between calls.Maintains execution state between suspensions and can resume from where it left off.
    UsageThese are used for modularizing code into manageable chunks.These are used for asynchronous programming, managing data pipelines and cooperative multitasking.
    ConcurrencyNot inherently designed for concurrent execution; typically used in sequential programming.Supports cooperative multitasking and can work with asynchronous tasks.
    Example UsageHelper functions, utility functions.Data pipelines, asynchronous tasks, cooperative multitasking.
    Control FlowExecution follows a linear path through the code.Execution can jump back and forth between coroutines based on yield points.

    Execution of Coroutines

    Coroutines are initiated with the __next__() method which starts the coroutine and advances execution to the first yield statement. The coroutine then waits for a value to be sent to it. The send() method is used to send values to the coroutine which can then process these values and potentially yield results.

    Example of Basic Coroutine

    A coroutine uses the yield statement which can both send and receive values. Unlike a generator which yields values for iteration where as a coroutine typically uses yield to receive input and perform actions based on that input. Following is the basic example of the Python coroutine −

    defprint_name(prefix):print(f"Searching prefix: {prefix}")whileTrue:
    
        name =(yield)if prefix in name:print(name)# Instantiate the coroutine
    corou = print_name("Welcome to")# Start the coroutine corou.__next__()# Send values to the coroutine corou.send("Tutorialspoint") corou.send("Welcome to Tutorialspoint")

    Output

    Searching prefix: Welcome to
    Welcome to Tutorialspoint
    

    Closing a Coroutine

    Coroutines can run indefinitely so it’s important to close them properly when they are no longer needed. The close() method terminates the coroutine and handles cleanup. If we attempt to send data to a closed coroutine it will raise a StopIteration exception.

    Example

    Following is the example of closing a coroutine in python −

    defprint_name(prefix):print(f"Searching prefix: {prefix}")try:whileTrue:
    
            name =(yield)if prefix in name:print(name)except GeneratorExit:print("Closing coroutine!!")# Instantiate and start the coroutine
    corou = print_name("Come") corou.__next__()# Send values to the coroutine corou.send("Come back Thank You") corou.send("Thank you")# Close the coroutine corou.close()

    Output

    Searching prefix: Come
    Come back Thank You
    Closing coroutine!!
    

    Chaining Coroutines for Pipelines

    Coroutines can be chained together to form a processing pipeline which allows data to flow through a series of stages. This is particularly useful for processing sequences of data in stages where each stage performs a specific task.

    Example

    Below is the example which shows chaining coroutines for pipelines −

    defproducer(sentence, next_coroutine):'''
       Splits the input sentence into tokens and sends them to the next coroutine.
       '''
       tokens = sentence.split(" ")for token in tokens:
    
      next_coroutine.send(token)
    next_coroutine.close()defpattern_filter(pattern="ing", next_coroutine=None):''' Filters tokens based on the specified pattern and sends matching tokens to the next coroutine. '''print(f"Searching for {pattern}")try:whileTrue:
         token =(yield)if pattern in token:
            next_coroutine.send(token)except GeneratorExit:print("Done with filtering!!")
      next_coroutine.close()defprint_token():'''
    Receives tokens and prints them. '''print("I'm the sink, I'll print tokens")try:whileTrue:
         token =(yield)print(token)except GeneratorExit:print("Done with printing!")# Setting up the pipeline
    pt = print_token() pt.__next__() pf = pattern_filter(next_coroutine=pt) pf.__next__() sentence ="Tutorialspoint is welcoming you to learn and succeed in Career!!!" producer(sentence, pf)

    Output

    I'm the sink, I'll print tokens
    Searching for ing
    welcoming
    Done with filtering!!
    Done with printing!
    
  •  Context Managers

    Context managers in Python provide a powerful way to manage resources efficiently and safely. A context manager in Python is an object that defines a runtime context for use with the with statement. It ensures that setup and cleanup operations are performed automatically.

    For instance, when working with file operations, context managers handle the opening and closing of files, ensuring that resources are managed correctly.

    How Context Managers Work?

    Python context managers work by implementing the __enter__() and __exit__() methods (or their asynchronous equivalents for async operations). These methods ensure that resources are correctly acquired and released. Also, Python’s contextlib module further simplifies the creation of custom context managers.

    Example

    Here’s a simple example demonstrating how a context manager works with file operations in Python.

    withopen('example.txt','w')asfile:file.write('Hello, Tutorialspoint!')

    In this example, a file is opened in the write mode, and then automatically closed when the block inside the with statement is exited.

    Python Context Manager Types

    Python supports both synchronous and asynchronous context managers. Each type has specific methods that need to be implemented to manage the life cycle of the context.

    Synchronous Context Managers

    A synchronous context managers are implemented using the __enter__() and __exit__() methods.

    1. The __enter__() Method

    The __enter__(self) method is called when execution enters the context of the with statement. This method should return the resource to be used within the with block.

    Example

    Here is a simple example of creating our own context manager using the __enter__() and __exit__() methods.

    classMyContextManager:def__enter__(self):print("Entering the context")return self
    
       def__exit__(self, exc_type, exc_value, traceback):print("Exiting the context")with MyContextManager():print("body")

    On executing the above code you will get the following output −

    Entering the context
    body
    Exiting the context
    

    2. The __exit__() Method

    The __exit__(self, exc_type, exc_value, traceback) method is called when execution leaves the context of the with statement. It can handle exceptions if any occur, and it returns a Boolean flag indicating if the exception should be suppressed.

    This example demonstrates creating the our own context manager and how the __exit__() methods handle exceptions.

    classMyContextManager:def__enter__(self):print("Entering the context")return self
    
       def__exit__(self, exc_type, exc_value, traceback):print("Exiting the context")if exc_type:print("An exception occurred")returnTrue# Suppress exceptionwith MyContextManager():print("body")
       name ="Python"/3#to raise an exception

    While executing the above code you will get the following output −

    Entering the context
    body
    Exiting the context
    An exception occurred
    

    Asynchronous Context Managers

    Similar to the synchronous context managers, Asynchronous context managers are also implemented using the two methods which are __aenter__() and __aexit__(). These are used within async with statements.

    The __aenter__(self) Method − It must return an awaitable that will be awaited when entering the context.

    __aexit__(self, exc_type, exc_value, traceback) Method − It must return an awaitable that will be awaited when exiting the context.

    Example

    Following is the example of creating an asynchronous context manager class −

    import asyncio
    classAsyncContextManager:asyncdef__aenter__(self):print("Entering the async context class")return self
    
       asyncdef__aexit__(self, exc_type, exc_value, traceback):print("Exiting the async context class")if exc_type:print("Exception occurred")returnTrueasyncdefmain():asyncwith AsyncContextManager():print("Inside the async context")
    
      name ="Python"/3#to raise an exception
    asyncio.run(main())

    On executing the above code you will get the following output −

    Entering the async context class
    Inside the async context
    Exiting the async context class
    Exception occurred
    

    Creating Custom Context Managers

    The contextlib module from the Python standard library provides the utilities to create context managers more easily.

    Using the contextlib.contextmanager() Function

    The contextlib.contextmanager() function is a decorator allows you to create factory functions for with statement context managers. It eliminates the need to define a separate class or implement the __enter__() and __exit__() methods individually.

    Example

    Here’s an example using the contextlib.contextmanager to create a context manager function.

    from contextlib import contextmanager
    
    @contextmanagerdefmy_context_manager():print("Entering the context manager method")try:yieldfinally:print("Exiting the context manager method")with my_context_manager():print("Inside the context")

    On executing the above code you will get the following output −

    Entering the context manager method
    Inside the context
    Exiting the context manager method
    

    Using the contextlib.asynccontextmanager() Function

    The contextlib module also provides asynccontextmanager, specifically designed for creating asynchronous context managers. It is similar to contextmanager and eliminates the need to define a separate class or implement the __aenter__() and __aexit__() methods individually.

    Example

    Here’s an example demonstrating the usage of contextlib.asynccontextmanager() to create an asynchronous context manager function.

    import asyncio
    from contextlib import asynccontextmanager
    
    @asynccontextmanagerasyncdefasync_context_manager():try:print("Entering the async context")# Perform async setup tasks if neededyieldfinally:# Perform async cleanup tasks if neededprint("Exiting the async context")asyncdefmain():asyncwith async_context_manager():print("Inside the async context")await asyncio.sleep(1)# Simulating an async operation# Run the asyncio event loop
    asyncio.run(main())

    On executing the above code you will get the following output −

    Entering the async context
    Inside the async context
    Exiting the async context
  •  Humanize Package

    The Humanize Package in Python is a library which is specifically designed to convert numerical values, dates, times and file sizes into formats that are more easily understood by humans.

    • This package is essential for creating user-friendly interfaces and readable reports where data interpretation needs to be quick and intuitive.
    • The primary goal of the Humanize Package is to bridge the gap between raw data and human understanding.

    Although computers and databases excel at processing raw numerical data but these formats can be challenging for humans to quickly grasp. The Humanize Package tackles this issue by converting these data points into more intuitive and user-friendly formats.

    Installation of Humanize Package

    To install the humanize package in Python we can use pip which is the standard package manager for Python. Following code has to be run in the command line or terminal to install Humanize Package −

    pip install humanize
    

    After installation we can verify if humanize is installed correctly by running a Python interpreter and importing the Humanize package by using the below code −

    import humanize
    

    Different Utilities in Humanize Package

    The Humanize package in Python provides a wide range of utilities that transform data into human-readable formats by enhancing usability and comprehension. Let’s explore the different utilities offered by humanize package in detail −

    Number Utilities

    The Humanize package in Python provides several number of utilities that enhance the readability and comprehension of numerical data. These utilities convert numbers into formats that are more natural and understandable for humans.

    Integer Formatting

    The Integer Formatting utility converts large integers into strings with commas for improved readability. Following is the example of applying the integer formatting utility −

    import humanize
    print(humanize.intcomma(123456))

    Output

    123,456
    

    Integer Word Representation

    The Integer Word Representation converts large integers into their word representation for easier understanding, especially for very large numbers. Below is the example of it −

    import humanize
    print(humanize.intword(12345678908545944))

    Output

    12.3 quadrillion
    

    Ordinal Numbers

    The Ordinal numbers converts integers into their ordinal form. For example 1 will be given as 1st and 2 as 2nd. Below is the example converting 3 as 3rd −

    import humanize
    print(humanize.ordinal(3))

    Output

    3rd
    

    AP Numbers

    These converts the integers into their corresponding words. Here is the example of it −

    import humanize
    print(humanize.apnumber(9))

    Output

    nine
    

    Fractional Units

    This converts decimal numbers into fractions for more intuitive representation. Following is the example of it −

    import humanize
    print(humanize.fractional(0.5))

    Output

    1/2
    

    File Size Utilities

    As we already know that the humanize package in Python provides several utilities among them File Size Utilities is one which is specifically designed to convert raw byte values into human-readable file sizes.

    These utilities help to make file sizes more understandable by converting them into formats that are easier to read and interpret. Here is a detailed overview of the file size utilities available in the humanize package −

    File Size Formatting using naturalsize()

    The naturalsize() function is the primary utility for converting file sizes into human-readable formats. It automatically selects the appropriate units such as bytes, KB, MB, GB based on the size provided.

    Syntax

    Following is the syntax of the naturalsize() function of the File Size Utilities of Python Humanize package −

    humanize.naturalsize(value,binary,gnu,format)

    Parameter

    Below are the parameters of the naturalsize() function of the python humanize package −

    • value: The file size in bytes.
    • binary: A boolean flag to indicate whether to use binary units. The default value is False.
    • gnu: A boolean flag to indicate whether to use GNU-style output and the default value is False.
    • format: A string to specify the output format. The default value is “%.1f”.

    Example

    Following is the example of using the naturalsize() of humanize package in python −

    import humanize
    
    # Default usage with decimal units
    file_size =123456789print(f"File size: {humanize.naturalsize(file_size)}")# Using binary unitsprint(f"File size (binary): {humanize.naturalsize(file_size, binary=True)}")# Using GNU-style prefixesprint(f"File size (GNU): {humanize.naturalsize(file_size, gnu=True)}")# Custom formatprint(f"File size (custom format): {humanize.naturalsize(file_size, format='%.2f')}")

    Following is the output −

    File size: 123.5 MB
    File size (binary): 117.7 MiB
    File size (GNU): 117.7M
    File size (custom format): 123.46 MB
    

    Date Time Utilities

    The Humanize package in Python provides several utilities for making dates and times more readable. These utilities transform date-time objects into formats that are easier for humans to understand such as relative times, natural dates and more. Following is the detailed overview of the date and time utilities offered by the humanize package −

    Natural Time

    The Natural Time converts date-time objects into human-readable relative times such as 2 days ago, 3 hours ago. Following is the example of natural time −

    import humanize
    from datetime import datetime, timedelta
    
    past_date = datetime.now()- timedelta(days=2)print(humanize.naturaltime(past_date))

    Output

    2 days ago
    

    Natural Date

    The Natural Date formats specific dates into a readable format like “July 11, 2024”. Here is the example −

    import humanize
    from datetime import datetime
    some_date = datetime(2022,7,8)print(humanize.naturaldate(some_date))

    Output

    Jul 08 2022
    

    Natural Day

    The Natural Day provides a human-readable representation of a date by considering today’s date for contextual relevance, for example “today”, “tomorrow”, “yesterday” etc. Below is the example of it −

    import humanize
    from datetime import datetime, timedelta 
    today = datetime.now()
    tomorrow = today + timedelta(days=1)print(humanize.naturalday(today))print(humanize.naturalday(tomorrow))

    Output

    today
    tomorrow
    

    Precise Delta

    The Precise Delta converts time duration into human-readable strings by breaking down into days, hours, minutes and second. Here is the example of it −

    import humanize
    from datetime import timedelta
    duration = timedelta(days=2, hours=3, minutes=30)print(humanize.precisedelta(duration))

    Output

    2 days, 3 hours and 30 minutes
    

    Duration Utilities

    The humanize package in Python also includes the Duration Utilities for converting duration (time intervals) into human-readable formats. These utilities help to present duration in a way that is understandable and meaningful to users. Here’s an overview of the duration utilities provided by humanize package −

    Duration Formatting using naturaldelta()

    The naturaldelta() function converts time deltas (duration) into human-readable strings that describe the duration in a natural language format such as “2 hours ago”, “3 days from now”, etc.

    Following is the example of using the naturaldelta() function of the Python Humanize Package −

    from datetime import timedelta
    import humanize
    
    Using naturaldelta for time durations
    delta1 = timedelta(days=3, hours=5)print(f"Time duration: {humanize.naturaldelta(delta1)} from now")  
    
    Example of a future duration (delta2)
    delta2 = timedelta(hours=5)print(f"Future duration: in {humanize.naturaldelta(delta2)}") 
    
    Example of a past duration (delta3)
    delta3 = timedelta(days=1, hours=3)print(f"Past duration: {humanize.naturaldelta(delta3)} ago")

    Output

    Time duration: 3 days from now
    Future duration: in 5 hours
    Past duration: a day ago
  • Automation Tutorial

    Automation using Python

    Automation with Python involves using programming techniques to perform tasks automatically, typically without human intervention. Python provides various libraries to make it a powerful tool for automating different types of repetitive tasks including, task scheduling, web scraping, GUI automation and many more.

    Utilizing the Python’s extensive libraries, can create automation solutions to fit specific needs.

    Python Libraries for Automation

    Following are the few of the Python libraries that are commonly used for automation −

    • unittest − Unit testing framework in Python.
    • Selenium − Web automation framework for testing web applications across different browsers.
    • Beautiful Soup − Library for parsing HTML and XML documents, used for web scraping.
    • pandas − Data manipulation library, useful for automating data analysis tasks.
    • requests − HTTP library for sending HTTP requests and handling responses.
    • Selenium − A web automation framework for testing web applications across different browsers.
    • PyAutoGUI − GUI automation library for simulating mouse and keyboard actions.

    In this tutorial you will learn about the various aspects of automation using Python, from scheduling tasks with schedule module, web scraping with BeautifulSoup, and GUI automation with BeautifulSoup.

    Here we are providing practical examples and please ensure that you have the necessary modules installed before executing the provided codes in your local system using the below commands in your command prompt −

    pip install schedule
    pip install beautifulsoup4
    pip install pyautogui
    

    Python Automation with “schedule” Module

    The schedule module in Python allows scheduling tasks to run automatically at specified intervals or times. It’s ideal for automating repetitive tasks such as job scheduling, periodic tasks, or time-based events.

    Example

    This example demonstrates the application of Python for automating routine tasks such as scheduling weekly meetings, setting alarms, and more, using Python’s schedule module.

    import schedule
    import time
    
    # Function definitions for scheduled tasksdefalarm():print("Time to restart the server   ")defjob():print("Starting daily tasks   ")defmeet():print('Weekly standup meeting   ')# Schedule tasks
    schedule.every(5).seconds.do(alarm)# Run every 5 seconds
    schedule.every().day.at("10:30").do(job)# Run daily at 10:30 AM
    schedule.every().monday.at("12:15").do(meet)# Run every Monday at 12:15 PM# Main loop to execute scheduled taskswhileTrue:
       schedule.run_pending()
       time.sleep(0.5)

    Output

    On executing the above code we will get the following output −

    Time to restart the server   
    Time to restart the server   
    Time to restart the server 
    ...
    

    Python Web Scraping

    Web scraping is a straightforward example of automation task. It is a process of automatically extracting information from websites. This technique involves fetching web pages and extracting the required data from the HTML content.

    Web scraping is widely used for data mining, data analysis, and automated tasks where collecting information from the web is necessary.

    Example

    This example demonstrates how to use BeautifulSoup and the urllib.request module to scrape a webpage for URLs containing the keyword “python”.

    from bs4 import BeautifulSoup
    from urllib.request import urlopen
    import re
    
    html = urlopen("https://www.tutorialspoint.com/python/index.htm")
    content = html.read()
    soup = BeautifulSoup(content,"html.parser")for a in soup.findAll('a',href=True):if re.findall('python', a['href']):print("Python URL:", a['href'])

    Output

    On executing the above code we will get the following output −

    Python URL: /machine_learning_with_python/index.htm
    Python URL: /python_pandas/index.htm
    Python URL: /python/index.htm
    Python URL: /python/index.htm
    Python URL: /python_pandas/index.htm
    Python URL: /python_pillow/index.htm
    Python URL: /machine_learning_with_python/index.htm
    Python URL: /python_technologies_tutorials.htm
    Python URL: /python/index.htm
    ....
    

    Automating Mouse Movements with “pyautogui”

    The pyautogui module allows your Python scripts to automate mouse movements, keyboard inputs, and window interactions, useful for testing GUI applications or automating repetitive desktop tasks.

    Example

    This example demonstrates how the Python’s pyautogui module can be used to automate mouse movements in a square pattern.

    import pyautogui
    
    # Move the mouse in a larger square patternfor i inrange(5):
       pyautogui.moveTo(300,100, duration=0.25)   
       pyautogui.move(300,0, duration=0.25)       
       pyautogui.move(0,300, duration=0.25)       
       pyautogui.move(-300,0, duration=0.25)      
       pyautogui.move(0,-300, duration=0.25)

    Output

    On executing the above code we will get the following output −

    Python Automation

    Automating Unit Testing in Python

    The unittest module in Python is used for automated testing. Automated unit testing is the practice of writing tests for individual units of code, such as functions or methods, and running these tests automatically to ensure they behave as expected.

    The unittest module in Python is a built-in library that provides a framework for creating and running automated tests.

    Example

    This example demonstrates how to use the unittest module in Python to perform automated testing on string methods. Each test case method begins with the prefix test_, which is recognized by the unittest framework as a method to run.

    # importing unittest moduleimport unittest
    
    classTestingStringMethods(unittest.TestCase):# string equaldeftest_string_equality(self):# if both arguments are then it's succes
    
      self.assertEqual('ttp'*5,'ttpttpttpttpttp')# comparing the two stringsdeftest_string_case(self):# if both arguments are then it's succes
      self.assertEqual('Tutorialspoint'.upper(),'TUTORIALSPOINT')# checking whether a string is upper or notdeftest_is_string_upper(self):# used to check whether the statement is True or False
      self.assertTrue('TUTORIALSPOINT'.isupper())
      self.assertFalse('TUTORIALSpoint'.isupper())deftest_string_startswith(self):# Used the startswith() to identify the start of the string
       self.assertTrue('tutorialspoint'.startswith('putor'))
       self.assertFalse('tutorialspoint'.startswith('point'))# running the tests
    unittest.main(verbosity=2)

    Output

    On executing the above code we will get the following output −

    test_is_string_upper (__main__.TestingStringMethods) ... ok
    test_string_case (__main__.TestingStringMethods) ... ok
    test_string_equality (__main__.TestingStringMethods) ... ok
    test_string_startswith (__main__.TestingStringMethods) ... FAIL
    
    ======================================================================
    FAIL: test_string_startswith (__main__.TestingStringMethods)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "main.py", line 23, in test_string_startswith
    
    self.assertTrue('tutorialspoint'.startswith('putor'))
    AssertionError: False is not true ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=1)
  • Type Hints

    Python type hints were introduced in PEP 484 to bring the benefits of static typing to a dynamically typed language. Although type hints do not enforce type checking at runtime, they provide a way to specify the expected types of variables, function parameters, and return values, which can be checked by static analysis tools such as mypy. This enhances code readability, facilitates debugging, and improves the overall maintainability of the code.

    Type hints in Python use annotations for function parameters, return values and variable assignments.

    Python’s type hints can be used to specify a wide variety of types such as basic data types, collections, complex types and custom user-defined types. The typing module provides many built-in types to represent these various types −

    Let’s see each one, one after another in detail.

    Basic Data Types

    In Python when using type hints to specify basic types we can simply use the name of the type as the annotation.

    Example

    Following is the example of using the basic data types such as integer, float, string etc −

    Open Compiler

    from typing import Optional
    
    # Integer typedefcalculate_square_area(side_length:int)->int:return side_length **2# Float typedefcalculate_circle_area(radius:float)->float:return3.14* radius * radius
    
    # String typedefgreet(name:str)->str:returnf"Hello, {name}"# Boolean typedefis_adult(age:int)->bool:return age >=18# None typedefno_return_example()->None:print("This function does not return anything")# Optional type (Union of int or None)defsafe_divide(x:int, y: Optional[int])-> Optional[float]:if y isNoneor y ==0:returnNoneelse:return x / y
    
    # Example usageprint(calculate_square_area(5))print(calculate_circle_area(3.0))print(greet("Alice"))print(is_adult(22))                   
    no_return_example()print(safe_divide(10,2))print(safe_divide(10,0))print(safe_divide(10,None))

    On executing the above code we will get the following output −

    25
    28.259999999999998
    Hello, Alice
    True
    This function does not return anything
    5.0
    None
    None
    

    Collections Types

    In Python when dealing with collections such as liststuplesdictionaries, etc. in type hints we typically use the typing module to specify the collection types.

    Example

    Below is the example of the Collections using in type hints −

    Open Compiler

    from typing import List, Tuple, Dict, Set, Iterable, Generator
    
    # List of integersdefprocess_numbers(numbers: List[int])-> List[int]:return[num *2for num in numbers]# Tuple of floatsdefcoordinates()-> Tuple[float,float]:return(3.0,4.0)# Dictionary with string keys and integer valuesdeffrequency_count(items: List[str])-> Dict[str,int]:
       freq ={}for item in items:
    
      freq[item]= freq.get(item,0)+1return freq
    # Set of unique characters in a stringdefunique_characters(word:str)-> Set[str]:returnset(word)# Iterable of integersdefprint_items(items: Iterable[int])->None:for item in items:print(item)# Generator yielding squares of integers up to ndefsquares(n:int)-> Generator[int,None,None]:for i inrange(n):yield i * i # Example usage numbers =[1,2,3,4,5]print(process_numbers(numbers))print(coordinates()) items =["apple","banana","apple","orange"]print(frequency_count(items)) word ="hello"print(unique_characters(word)) print_items(range(5)) gen = squares(5)print(list(gen))

    On executing the above code we will get the following output −

    [2, 4, 6, 8, 10]
    (3.0, 4.0)
    {'apple': 2, 'banana': 1, 'orange': 1}
    {'l', 'e', 'h', 'o'}
    0
    1
    2
    3
    4
    [0, 1, 4, 9, 16]
    

    Optional Types

    In Python, Optional types are used to indicate that a variable can either be of a specified type or None. This is particularly useful when a function may not always return a value or when a parameter can accept a value or be left unspecified.

    Example

    Here is the example of using the optional types in type hints −

    Open Compiler

    from typing import Optional
    
    defdivide(a:float, b:float)-> Optional[float]:if b ==0:returnNoneelse:return a / b
    
    result1: Optional[float]= divide(10.0,2.0)# result1 will be 5.0
    result2: Optional[float]= divide(10.0,0.0)# result2 will be Noneprint(result1)print(result2)

    On executing the above code we will get the following output −

    5.0
    None
    

    Union Types

    Python uses Union types to allow a variable to accept values of different types. This is useful when a function or data structure can work with various types of inputs or produce different types of outputs.

    Example

    Below is the example of this −

    Open Compiler

    from typing import Union
    
    defsquare_root_or_none(number: Union[int,float])-> Union[float,None]:if number >=0:return number **0.5else:returnNone
    
    result1: Union[float,None]= square_root_or_none(50)   
    result2: Union[float,None]= square_root_or_none(-50)print(result1)print(result2)

    On executing the above code we will get the following output −

    7.0710678118654755
    None
    

    Any Type

    In Python, Any type is a special type hint that indicates that a variable can be of any type. It essentially disables type checking for that particular variable or expression. This can be useful in situations where the type of a value is not known beforehand or when dealing with dynamic data.

    Example

    Following is the example of using Any type in Type hint −

    Open Compiler

    from typing import Any
    
    defprint_value(value: Any)->None:print(value)
    
    print_value(10)         
    print_value("hello")    
    print_value(True)       
    print_value([1,2,3])  
    print_value({'key':'value'})

    On executing the above code we will get the following output −

    10
    hello
    True
    [1, 2, 3]
    {'key': 'value'}
    

    Type Aliases

    Type aliases in Python are used to give alternative names to existing types. They can make code easier to read by giving clear names to complicated type annotations or combinations of types. This is especially helpful when working with nested structures or long-type hints.

    Example

    Below is the example of using the Type Aliases in the Type hints −

    Open Compiler

    from typing import List, Tuple
    
    # Define a type alias for a list of integers
    Vector = List[int]# Define a type alias for a tuple of coordinates
    Coordinates = Tuple[float,float]# Function using the type aliasesdefscale_vector(vector: Vector, factor:float)-> Vector:return[int(num * factor)for num in vector]defcalculate_distance(coord1: Coordinates, coord2: Coordinates)->float:
       x1, y1 = coord1
       x2, y2 = coord2
       return((x2 - x1)**2+(y2 - y1)**2)**0.5# Using the type aliases
    v: Vector =[1,2,3,4]
    scaled_v: Vector = scale_vector(v,2.5)print(scaled_v)  
    
    c1: Coordinates =(3.0,4.0)
    c2: Coordinates =(6.0,8.0)
    distance:float= calculate_distance(c1, c2)print(distance)

    On executing the above code we will get the following output −

    [2, 5, 7, 10]
    5.0
    

    Generic Types

    Generic types create functions, classes or data structures that can handle any type while maintaining type safety. The typing module’s TypeVar and Generic constructs make this possible. They are helpful for making reusable components that can work with various types without compromising type checking.

    Example

    Here is the example of it −

    Open Compiler

    from typing import TypeVar, List
    
    # Define a type variable T
    T = TypeVar('T')# Generic function that returns the first element of a listdeffirst_element(items: List[T])-> T:return items[0]# Example usage
    int_list =[1,2,3,4,5]
    str_list =["apple","banana","cherry"]
    
    first_int = first_element(int_list)# first_int will be of type int
    first_str = first_element(str_list)# first_str will be of type strprint(first_int)print(first_str)

    On executing the above code we will get the following output −

    1
    apple
    

    Callable Types

    Python’s Callable type is utilized to show that a type is a function or a callable object. It is found in the typing module and lets you define the types of the arguments and the return type of a function. This is handy for higher-order functions.

    Example

    Following is the example of using Callable type in type hint −

    Open Compiler

    from typing import Callable
    
    # Define a function that takes another function as an argumentdefapply_operation(x:int, y:int, operation: Callable[[int,int],int])->int:return operation(x, y)# Example functions to be passed as argumentsdefadd(a:int, b:int)->int:return a + b
    
    defmultiply(a:int, b:int)->int:return a * b
    
    # Using the apply_operation function with different operations
    result1 = apply_operation(5,3, add)# result1 will be 8
    result2 = apply_operation(5,3, multiply)# result2 will be 15print(result1)print(result2)

    On executing the above code we will get the following output −

    8
    15
    

    Literal Types

    The Literal type is used to specify that a value must be exactly one of a set of predefined values.

    Example

    Below is the example −

    from typing import Literal
    
    defmove(direction: Literal["left","right","up","down"])->None:print(f"Moving {direction}")
    
    move("left")# Valid
    move("up")# Valid                                     

    On executing the above code we will get the following output −

    Moving left
    Moving up
    

    NewType

    NewType is a function in the typing module that allows us to create distinct types derived from existing ones. This can be useful for adding type safety to our code by distinguishing between different uses of the same underlying type. For example we might want to differentiate between user IDs and product IDs even though both are represented as integers.

    Example

    Below is the example −

    from typing import NewType
    
    # Create new types
    UserId = NewType('UserId',int)
    ProductId = NewType('ProductId',int)# Define functions that use the new typesdefget_user_name(user_id: UserId)->str:returnf"User with ID {user_id}"defget_product_name(product_id: ProductId)->str:returnf"Product with ID {product_id}"# Example usage
    user_id = UserId(42)
    product_id = ProductId(101)print(get_user_name(user_id))# Output: User with ID 42print(get_product_name(product_id))# Output: Product with ID 101                                   

    On executing the above code we will get the following output −

    User with ID 42
    Product with ID 101
  •  Signal Handling

    Signal handling in Python allows you to define custom handlers for managing asynchronous events such as interrupts or termination requests from keyboard, alarms, and even system signals. You can control how your program responds to various signals by defining custom handlers. The signal module in Python provides mechanisms to set and manage signal handlers.

    signal handler is a function that gets executed when a specific signal is received. The signal.signal() function allows defining custom handlers for signals. The signal module offers a way to define custom handlers that will be executed when a specific signal is received. Some default handlers are already installed in Python, which are −

    • SIGPIPE is ignored.
    • SIGINT is translated into a KeyboardInterrupt exception.

    Commonly Used Signals

    Python signal handlers are executed in the main Python thread of the main interpreter, even if the signal is received in another thread. Signals can’t be used for inter-thread communication.

    Following are the list of some common signals and their default actions −

    • SIGINT − Interrupt from keyboard (Ctrl+C), which raises a KeyboardInterrupt.
    • SIGTERM − Termination signal.
    • SIGALRM− Timer signal from alarm().
    • SIGCHLD − Child process stopped or terminated.
    • SIGUSR1 and SIGUSR2 − User-defined signals.

    Setting a Signal Handler

    To set a signal handler, we can use the signal.signal() function. It allows you to define custom handlers for signals. A handler remains installed until explicitly reset, except for SIGCHLD.

    Example

    Here is an example of setting a signal handler using the signal.signal() function with the SIGINT handler.

    import signal
    import time
    
    defhandle_signal(signum, frame):print(f"Signal {signum} received")# Setting the handler for SIGINT
    signal.signal(signal.SIGINT, handle_signal)print("Press Ctrl+C to trigger SIGINT")whileTrue:
       time.sleep(1)

    Output

    On executing the above program, you will get the following results −

    Press Ctrl+C to trigger SIGINT
    Signal 2 received
    Signal 2 received
    Signal 2 received
    Signal 2 received
    

    Signal Handling on Windows

    On Windows, the signal.signal() function can only handle a limited set of signals. If you try to use a signal not supported on Windows, a ValueError will be raised. And, an AttributeError will be raised if a signal name is not defined as a SIG* module level constant.

    The supported signals on Windows are follows −

    • SIGABRT
    • SIGFPE
    • SIGILL
    • SIGINT
    • SIGSEGV
    • SIGTERM
    • SIGBREAK

    Handling Timers and Alarms

    Timers and alarms can be used to schedule signal delivery after a certain amount of time.

    Example

    Let’s observe following example of handling alarms.

    import signal
    import time
    
    defhandler(signum, stack):print('Alarm: ', time.ctime())
    
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(2)
    time.sleep(5)for i inrange(5):
       signal.alarm(2)
       time.sleep(5)print("interrupted #%d"% i)

    Output

    On executing the above program, you will get the following results −

    Alarm:  Wed Jul 17 17:30:11 2024
    Alarm:  Wed Jul 17 17:30:16 2024
    interrupted #0
    Alarm:  Wed Jul 17 17:30:21 2024
    interrupted #1
    Alarm:  Wed Jul 17 17:30:26 2024
    interrupted #2
    Alarm:  Wed Jul 17 17:30:31 2024
    interrupted #3
    Alarm:  Wed Jul 17 17:30:36 2024
    interrupted #4
    

    Getting Signal Names from Numbers

    There is no straightforward way of getting signal names from numbers in Python. You can use the signal module to get all its attributes, filter out those that start with SIG, and store them in a dictionary.

    Example

    This example creates a dictionary where the keys are signal numbers and the values are the corresponding signal names. This is useful for dynamically resolving signal names from their numeric values.

    import signal
    
    sig_items =reversed(sorted(signal.__dict__.items()))
    final =dict((k, v)for v, k in sig_items if v.startswith('SIG')andnot v.startswith('SIG_'))print(final)

    Output

    On executing the above program, you will get the following results −

    {<Signals.SIGXFSZ: 25>: 'SIGXFSZ', <Signals.SIGXCPU: 24>: 'SIGXCPU', <Signals.SIGWINCH: 28>: 'SIGWINCH', <Signals.SIGVTALRM: 26>: 'SIGVTALRM', <Signals.SIGUSR2: 12>: 'SIGUSR2', <Signals.SIGUSR1: 10>: 'SIGUSR1', <Signals.SIGURG: 23>: 'SIGURG', <Signals.SIGTTOU: 22>: 'SIGTTOU', <Signals.SIGTTIN: 21>: 'SIGTTIN', <Signals.SIGTSTP: 20>: 'SIGTSTP', <Signals.SIGTRAP: 5>: 'SIGTRAP', <Signals.SIGTERM: 15>: 'SIGTERM', <Signals.SIGSYS: 31>: 'SIGSYS', <Signals.SIGSTOP: 19>: 'SIGSTOP', <Signals.SIGSEGV: 11>: 'SIGSEGV', <Signals.SIGRTMIN: 34>: 'SIGRTMIN', <Signals.SIGRTMAX: 64>: 'SIGRTMAX', <Signals.SIGQUIT: 3>: 'SIGQUIT', <Signals.SIGPWR: 30>: 'SIGPWR', <Signals.SIGPROF: 27>: 'SIGPROF', <Signals.SIGIO: 29>: 'SIGIO', <Signals.SIGPIPE: 13>: 'SIGPIPE', <Signals.SIGKILL: 9>: 'SIGKILL', <Signals.SIGABRT: 6>: 'SIGABRT', <Signals.SIGINT: 2>: 'SIGINT', <Signals.SIGILL: 4>: 'SIGILL', <Signals.SIGHUP: 1>: 'SIGHUP', <Signals.SIGFPE: 8>: 'SIGFPE', <Signals.SIGCONT: 18>: 'SIGCONT', <Signals.SIGCHLD: 17>: 'SIGCHLD', <Signals.SIGBUS: 7>: 'SIGBUS', <Signals.SIGALRM: 14>: 'SIGALRM'}
  • Monkey Patching

    Monkey patching in Python refers to the practice of dynamically modifying or extending code at runtime typically replacing or adding new functionalities to existing modulesclasses or methods without altering their original source code. This technique is often used for quick fixes, debugging or adding temporary features.

    The term “monkey patching” originates from the idea of making changes in a way that is ad-hoc or temporary, akin to how a monkey might patch something up using whatever materials are at hand.

    Steps to Perform Monkey Patching

    Following are the steps that shows how we can perform monkey patching −

    • First to apply a monkey patch we have to import the module or class we want to modify.
    • In the second step we have to define a new function or method with the desired behavior.
    • Replace the original function or method with the new implementation by assigning it to the attribute of the class or module.

    Example of Monkey Patching

    Now let’s understand the Monkey patching with the help of an example −

    Define a Class or Module to Patch

    First we have to define the original class or module that we want to modify. Below is the code −

    # original_module.pyclassMyClass:defsay_hello(self):return"Hello, Welcome to Tutorialspoint!"

    Create a Patching Function or Method

    Next we have to define a function or method that we will use to monkey patch the original class or module. This function will contain the new behavior or functionality we want to add −

    # patch_module.pyfrom original_module import MyClass
    
    # Define a new function to be patcheddefnew_say_hello(self):return"Greetings!"# Monkey patching MyClass with new_say_hello method
    MyClass.say_hello = new_say_hello
    

    Test the Monkey Patch

    Now we can test the patched functionality. Ensure that the patching is done before we create an instance of MyClass with the patched method −

    # test_patch.pyfrom original_module import MyClass
    import patch_module
    
    # Create an instance of MyClass
    obj = MyClass()# Test the patched methodprint(obj.say_hello())# Output: Greetings!

    Drawbacks of Monkey Patching

    Following are the draw backs of monkey patching −

    • Overuse: Excessive monkey patching can lead to code that is hard to understand and maintain. We have to use it judiciously and consider alternative design patterns if possible.
    • Compatibility: Monkey patching may introduce unexpected behavior especially in complex systems or with large code-bases.