Annotations

Annotations in Python are a way to add metadata to functions, methods, and variables. They provide additional information about the types of arguments, return values, and other aspects of the code. Annotations do not affect the runtime behavior of the code; they are primarily used for documentation and static analysis by tools like linters, type checkers, and IDEs. Here's an overview of how annotations can be used in Python:
  • Function Annotations: Function annotations are specified using colons (:) after the parameter list, followed by the annotation expression. The annotation expression can be any valid Python expression.
  • 
                                def greet(name: str, age: int) -> str:
                                    return f"Hello, {name}! You are {age} years old."
                            
                                # Annotations are optional and not enforced by Python
                                # They are primarily used for documentation and type hinting
                            
  • Type Annotations: Type annotations specify the expected types of function arguments and return values. They are used for static type checking and documentation purposes.
  • 
                                def add(a: int, b: int) -> int:
                                    return a + b
                            
                                # Type annotations can be simple types (int, str, float, etc.)
                                # They can also be complex types (List, Tuple, Dict, etc.) from the typing module
                            
  • Variable Annotations: Variable annotations are similar to function annotations but are used to specify the type of variables. They can be defined inline or separately.
  • 
                                # Inline variable annotation
                                x: int = 10
                                
                                # Separate variable annotation
                                y: str
                                y = "Hello"
                            
  • Annotations for Classes and Methods: Annotations can also be applied to class definitions and methods.
  • 
                                class MyClass:
                                    def __init__(self, x: int, y: str) -> None:
                                        self.x = x
                                        self.y = y
                                
                                    def method(self, z: float) -> None:
                                        pass
                            
  • Accessing Annotations:Annotations can be accessed at runtime using the __annotations__ attribute of functions and classes.
  • 
                                print(greet.__annotations__)  # {'name': , 'age': , 'return': }
                            
  • Type Checking: Annotations can be used with type-checking tools like mypy to perform static type checking on Python code.
  • 
                                # Use mypy to perform static type checking
                                # Install mypy using pip: pip install mypy
                                # Run mypy on a Python file: mypy filename.py
                            
  • Documentation: Annotations serve as documentation for functions, methods, and variables, providing insight into the expected types of arguments and return values.
  • 
                                def calculate_area(radius: float) -> float:
                                    """Calculate the area of a circle given its radius.
                                
                                    Args:
                                        radius: The radius of the circle.
                                
                                    Returns:
                                        The area of the circle.
                                
                                    """
                                    return 3.14 * radius ** 2
                            

Annotations in Python are a powerful tool for improving code readability, facilitating type checking, and enhancing documentation. While they are not enforced by the Python interpreter, they are widely used in the Python community to improve code quality and maintainability.

Decorators

Decorators are a powerful feature in Python that allow you to modify or extend the behavior of functions or methods without changing their source code. Decorators are implemented using functions or classes, and they provide a clean and concise way to add functionality to existing code. Here's an overview of how decorators work in Python:
  • Basic Decorator Syntax: Decorators are defined using the @decorator_name syntax, where decorator_name is the name of the decorator function or class.
  • 
                                def decorator(func):
                                    def wrapper(*args, **kwargs):
                                        print("Before calling the function")
                                        result = func(*args, **kwargs)
                                        print("After calling the function")
                                        return result
                                    return wrapper
                            
                                @decorator
                                def my_function():
                                    print("Inside the function")
                                
                                my_function()
                            
  • Decorator Function: A decorator function is a regular Python function that takes another function as its argument and returns a new function. The new function typically adds some behavior before or after calling the original function.
  • 
                                def log_time(func):
                                    def wrapper(*args, **kwargs):
                                        import time
                                        start_time = time.time()
                                        result = func(*args, **kwargs)
                                        end_time = time.time()
                                        print(f"Execution time: {end_time - start_time} seconds")
                                        return result
                                    return wrapper
                            
  • Applying a Decorator: To apply a decorator to a function or method, simply place the @decorator_name line immediately before the function or method definition.
  • 
                                @log_time
                                def calculate_sum(n):
                                    return sum(range(n+1))
                                
                                calculate_sum(10000)
                            
  • Chaining Decorators: You can apply multiple decorators to a single function by stacking them on top of each other using multiple @decorator_name lines.
  • 
                                @decorator1
                                @decorator2
                                def my_function():
                                    pass
                            

    Example:

    
                                def uppercase_decorator(func):
                                    def wrapper(*args, **kwargs):
                                        result = func(*args, **kwargs)
                                        return result.upper()
                                    return wrapper
                            
                                def bold_decorator(func):
                                    def wrapper(*args, **kwargs):
                                        result = func(*args, **kwargs)
                                        return f"{result}"
                                    return wrapper
                            
                                @bold_decorator
                                @uppercase_decorator
                                def greet(name):
                                    return f"Hello, {name}!"
                                
                                print(greet("John"))
                            
    Output:HELLO JOHN
  • Class Decorators: In addition to functions, decorators can also be implemented using classes. To create a class decorator, the class must implement the __call__ method.
  • 
                                class DecoratorClass:
                                    def __init__(self, func):
                                        self.func = func
                                
                                    def __call__(self, *args, **kwargs):
                                        print("Before calling the function")
                                        result = self.func(*args, **kwargs)
                                        print("After calling the function")
                                        return result
                            

Use cases for Decorators:

  • Logging and profiling
  • Authentication and authorization
  • Caching
  • Rate limiting
  • Error handling
Decorators are a versatile tool in Python that allow you to add cross-cutting concerns to your code in a modular and reusable way. By using decorators, you can keep your codebase clean and maintainable while adding functionality such as logging, caching, and error handling with minimal effort.

References

  1. Checkout my Jupyter notebook on these topics
  2. For object oriented Programming in Python

Some other interesting things to know: