Repository: pdyba/lambdalizator (language: python)

Issue:
Start verifying the code with the usage of Python 3.9 and Python 3.10
The main goal for this pull request was to prepare the repository for more versions of Python than just 3.8 🙂 

_I recommend reading changes through commits because they tell you more directly why something was done._


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 658e89d..447bed8 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -12,7 +12,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.8", "3.9", "3.10"]  # TODO: Support newer versions
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
 
     steps:
       - uses: actions/checkout@v4
@@ -34,7 +34,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.8", "3.9", "3.10"]  # TODO: Support newer versions
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
 
     steps:
       - uses: actions/checkout@v4
diff --git a/.github/workflows/pypi_package.yml b/.github/workflows/pypi_package.yml
index e5b167b..933c664 100644
--- a/.github/workflows/pypi_package.yml
+++ b/.github/workflows/pypi_package.yml
@@ -15,10 +15,10 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
         with:
-          python-version: "3.8"
+          python-version: "3.9"
           cache: "pip"
-      - run: make build-nocache
       - run: pip install -r requirements-deploy.txt
+      - run: python setup.py sdist bdist_wheel
       - run: python -m twine upload --verbose dist/*
         env:
           TWINE_USERNAME: __token__
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d7798f..dc48aee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -321,11 +321,19 @@ Released 2023-09-27
 ### Version 0.5.19
 Released 2024-01-02
 
-- Start verifying the code with the usage of Python 3.9 and Python 3.10
-- Adjust the GitHub Workflow files to make the pipelines more descriptive
-- Improve the rest of the repository for upcoming changes
+- Starts verifying the code with the usage of Python 3.9 and Python 3.10
+- Adjusts the GitHub Workflow files to make the pipelines more descriptive
+- Improves the rest of the repository for upcoming changes
+
+## 0.6.x
 
 ### Version 0.6.0
+Released 2024-01-05
+
+- Adjusts the code to the latest standards set by Python 3.12
+- Disables support for Python 3.8 due to deprecated typing aliases
+
+### Version 0.7.0
 Release ETA 2024-02-31 ;)
 
 - Moves REST (API Gateway) related modules into one package
diff --git a/README.md b/README.md
index 2626f09..f7e622e 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,8 @@
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black/)
 [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1.svg)](https://pycqa.github.io/isort/)
 [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org)
-[![Downloads](https://pepy.tech/badge/lbz)](https://pepy.tech/project/lbz/)
+[![Downloads](https://pepy.tech/badge/lbz)](https://pepy.tech/project/lbz/)  
+_If you want to work with Lambdalizator in Python 3.8, consider using version [0.5.19](https://pypi.org/project/lbz/0.5.19/)._
 
 AWS Lambda Toolbox inspired by Flask. Currently supporting:
 - REST API
diff --git a/examples/event/event_api_and_broker.py b/examples/event/event_api_and_broker.py
index 0d08495..5cb736e 100644
--- a/examples/event/event_api_and_broker.py
+++ b/examples/event/event_api_and_broker.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from __future__ import annotations
 
 from examples.event.event_aware_resource import HelloWorldExample
 from examples.event.event_broker import event_to_handler_map
@@ -8,7 +8,7 @@
 from lbz.type_defs import LambdaContext
 
 
-def handle(event: dict, context: LambdaContext) -> Optional[dict]:
+def handle(event: dict, context: LambdaContext) -> dict | None:
     try:
         if LambdaSource.is_from(event, LambdaSource.API_GW):
             return HelloWorldExample(event)().to_dict()
diff --git a/examples/event/event_broker.py b/examples/event/event_broker.py
index 97dbd9a..3994ebc 100644
--- a/examples/event/event_broker.py
+++ b/examples/event/event_broker.py
@@ -1,4 +1,4 @@
-from typing import Callable, Dict, List
+from collections.abc import Callable
 
 from lbz.events import Event, EventBroker
 from lbz.type_defs import LambdaContext
@@ -9,7 +9,7 @@ def my_event_handler(event: Event) -> None:
     print(f"DATA: {event.data}")
 
 
-event_to_handler_map: Dict[str, List[Callable[[Event], None]]] = {"SOME_TYPE": [my_event_handler]}
+event_to_handler_map: dict[str, list[Callable[[Event], None]]] = {"SOME_TYPE": [my_event_handler]}
 
 
 def handle(event: dict, context: LambdaContext) -> None:
diff --git a/examples/rest/auth_example.py b/examples/rest/auth_example.py
index 2f36e4e..6c251e3 100755
--- a/examples/rest/auth_example.py
+++ b/examples/rest/auth_example.py
@@ -1,6 +1,4 @@
-"""
-Simple Lambda Handler with authorization
-"""
+"""Simple Lambda Handler with authorization"""
 from lbz.authz.decorators import authorization
 from lbz.dev.server import MyDevServer
 from lbz.exceptions import ServerError
diff --git a/examples/rest/simple_resource.py b/examples/rest/simple_resource.py
index ab35616..5f288e5 100755
--- a/examples/rest/simple_resource.py
+++ b/examples/rest/simple_resource.py
@@ -1,8 +1,6 @@
-#!/usr/bin/env python3.8
+#!/usr/bin/env python3.9
 # coding=utf-8
-"""
-Simple Lambda Handler
-"""
+"""Simple Lambda Handler"""
 from lbz.dev.server import MyDevServer
 from lbz.dev.test import Client
 from lbz.exceptions import LambdaFWException
diff --git a/lbz/_cfg.py b/lbz/_cfg.py
index 36da998..20c554f 100644
--- a/lbz/_cfg.py
+++ b/lbz/_cfg.py
@@ -1,19 +1,17 @@
-from typing import List
-
 from lbz.configuration import ConfigParser, EnvValue
 
 # LBZ configuration
 LBZ_DEBUG_MODE = EnvValue("LBZ_DEBUG_MODE", default=False, parser=ConfigParser.cast_to_bool)
 LOGGING_LEVEL = EnvValue("LOGGING_LEVEL", default="INFO")
-CORS_HEADERS = EnvValue[List[str]]("CORS_HEADERS", default=[], parser=ConfigParser.split_by_comma)
-CORS_ORIGIN = EnvValue[List[str]]("CORS_ORIGIN", default=[], parser=ConfigParser.split_by_comma)
+CORS_HEADERS = EnvValue[list[str]]("CORS_HEADERS", default=[], parser=ConfigParser.split_by_comma)
+CORS_ORIGIN = EnvValue[list[str]]("CORS_ORIGIN", default=[], parser=ConfigParser.split_by_comma)
 
 # AWS related configuration
 AWS_LAMBDA_FUNCTION_NAME = EnvValue[str]("AWS_LAMBDA_FUNCTION_NAME")
 EVENTS_BUS_NAME = EnvValue[str]("EVENTS_BUS_NAME")
 
 # Authorization configuration
-ALLOWED_PUBLIC_KEYS = EnvValue[List[dict]](
+ALLOWED_PUBLIC_KEYS = EnvValue[list[dict]](
     "ALLOWED_PUBLIC_KEYS", default=[], parser=ConfigParser.load_jwt_keys
 )
 ALLOWED_AUDIENCES = EnvValue("ALLOWED_AUDIENCES", parser=ConfigParser.split_by_comma)
diff --git a/lbz/authentication.py b/lbz/authentication.py
index ef410d1..3de9a78 100644
--- a/lbz/authentication.py
+++ b/lbz/authentication.py
@@ -1,6 +1,4 @@
-"""
-JWT based Authentication module.
-"""
+"""JWT based Authentication module."""
 from lbz._cfg import AUTH_REMOVE_PREFIXES
 from lbz.jwt_utils import decode_jwt
 
@@ -8,9 +6,7 @@
 
 
 def remove_prefix(text: str) -> str:
-    """
-    Removes prefix of a text based on : sign.
-    """
+    """Removes prefix of a text based on : sign."""
     return text.split(":", maxsplit=1)[1] if ":" in text else text
 
 
@@ -29,9 +25,7 @@ def __repr__(self) -> str:
         return "User"
 
     def get_user_details_from_auth_token(self) -> dict:
-        """
-        Parses auth token for user details.
-        """
+        """Parses auth token for user details."""
         parsed_user = {}
         attributes = decode_jwt(self._token)
         self._validate_attributes(attributes)
diff --git a/lbz/authz/authorizer.py b/lbz/authz/authorizer.py
index c1b7ce4..c845ffd 100644
--- a/lbz/authz/authorizer.py
+++ b/lbz/authz/authorizer.py
@@ -1,4 +1,4 @@
-from typing import Dict, Optional, Union
+from __future__ import annotations
 
 from jose import jwt
 
@@ -17,23 +17,21 @@
 
 
 class Authorizer:
-    """
-    Authorizer class responsible for Authorization.
-    """
+    """Authorizer class responsible for Authorization."""
 
     def __init__(
         self,
-        auth_jwt: Optional[str],
+        auth_jwt: str | None,
         resource_name: str,
         permission_name: str,
-        base_permission_policy: dict = None,
+        base_permission_policy: dict | None = None,
     ):
         self.outcome = DENY
-        self.allowed_resource: Union[str, dict, None] = None
-        self.denied_resource: Union[str, dict, None] = None
+        self.allowed_resource: str | dict | None = None
+        self.denied_resource: str | dict | None = None
         self.resource = resource_name
         self.permission = permission_name
-        self.refs: Dict[str, dict] = {}
+        self.refs: dict[str, dict] = {}
         self.allow: dict = {}
         self.deny: dict = {}
         self._set_policy(auth_jwt, base_permission_policy)
@@ -44,7 +42,9 @@ def __repr__(self) -> str:
             f"permission_name='{self.permission}')"
         )
 
-    def _set_policy(self, auth_jwt: str = None, base_permission_policy: dict = None) -> None:
+    def _set_policy(
+        self, auth_jwt: str | None = None, base_permission_policy: dict | None = None
+    ) -> None:
         policy = base_permission_policy or {}
         if auth_jwt is not None:
             deep_update(policy, decode_jwt(auth_jwt))
@@ -60,9 +60,7 @@ def _raise_permission_denied(self) -> None:
         raise PermissionDenied()
 
     def check_access(self) -> None:
-        """
-        Main authorization checking logic.
-        """
+        """Main authorization checking logic."""
         self.outcome = DENY
 
         if self.deny:
@@ -73,7 +71,7 @@ def check_access(self) -> None:
         if self.outcome == DENY:
             self._raise_permission_denied()
 
-    def _deny_if_all(self, permission: Union[dict, str]) -> None:
+    def _deny_if_all(self, permission: dict | str) -> None:
         if permission == ALL:
             self._raise_permission_denied()
 
@@ -85,7 +83,7 @@ def _check_deny(self) -> None:
                 self._check_resource(resource)
                 self.denied_resource = resource
 
-    def _check_resource(self, resource: Union[dict, str]) -> None:
+    def _check_resource(self, resource: dict | str) -> None:
         # TODO: standardize the naming convention (resource != permission)
         self._deny_if_all(resource)
         if isinstance(resource, dict):
@@ -93,7 +91,7 @@ def _check_resource(self, resource: Union[dict, str]) -> None:
                 self._deny_if_all(key)
                 self._deny_if_all(value)
 
-    def _allow_if_allow_all(self, permission: Union[str, dict]) -> bool:
+    def _allow_if_allow_all(self, permission: str | dict) -> bool:
         if permission == ALL:
             self.outcome = ALLOW
             self.allowed_resource = ALL
@@ -127,16 +125,12 @@ def _check_allow_and_set_resources(self) -> None:
 
     @property
     def restrictions(self) -> dict:
-        """
-        Provides restrictions in standardised format.
-        """
+        """Provides restrictions in standardised format."""
         return {"allow": self.allowed_resource, "deny": self.denied_resource}
 
     @staticmethod
     def sign_authz(authz_data: dict, private_key_jwk: dict) -> str:
-        """
-        Signs authorization in JWT format.
-        """
+        """Signs authorization in JWT format."""
         if not isinstance(private_key_jwk, dict):
             raise ValueError("private_key_jwk must be a jwk dict")
         if "kid" not in private_key_jwk:
diff --git a/lbz/authz/decorators.py b/lbz/authz/decorators.py
index a957156..6bc04b9 100644
--- a/lbz/authz/decorators.py
+++ b/lbz/authz/decorators.py
@@ -1,18 +1,16 @@
-"""
-Authorization helper wrappers (decorators).
-"""
+from __future__ import annotations
+
+from collections.abc import Callable
 from functools import wraps
-from typing import Any, Callable
+from typing import Any
 
 from lbz.authz.utils import check_permission
 from lbz.collector import authz_collector
 from lbz.resource import Resource
 
 
-def authorization(permission_name: str = None) -> Callable:
-    """
-    Wrapper for easy adding authorization requirement.
-    """
+def authorization(permission_name: str | None = None) -> Callable:
+    """Wrapper for easy adding authorization requirement."""
 
     def decorator(func: Callable) -> Callable:
         # to consider adding more abstraction with 0.4 and doing it per Resource
@@ -22,7 +20,6 @@ def decorator(func: Callable) -> Callable:
 
         @wraps(func)
         def wrapped(self: Resource, *args: Any, **kwargs: Any) -> Any:
-
             restrictions = check_permission(self, permission_name or func.__name__)
             return func(self, *args, restrictions=restrictions, **kwargs)
 
diff --git a/lbz/authz/utils.py b/lbz/authz/utils.py
index 9d05767..12bca86 100644
--- a/lbz/authz/utils.py
+++ b/lbz/authz/utils.py
@@ -1,14 +1,10 @@
-"""
-Authorization helper functions.
-"""
 from lbz.authz.authorizer import Authorizer
 from lbz.exceptions import PermissionDenied, Unauthorized
 from lbz.resource import Resource
 
 
 def check_permission(resource: Resource, permission_name: str) -> dict:
-    """
-    Check if requester has sufficient permissions to do something on specific resource.
+    """Check if requester has sufficient permissions to do something on specific resource.
 
     Raises if not.
     """
@@ -28,8 +24,7 @@ def check_permission(resource: Resource, permission_name: str) -> dict:
 
 
 def has_permission(resource: Resource, permission_name: str) -> bool:
-    """
-    Safe Check if requester has sufficient permissions to do something on specific resource.
+    """Safe Check if requester has sufficient permissions to do something on specific resource.
 
     Does not raise.
     """
diff --git a/lbz/aws_ssm.py b/lbz/aws_ssm.py
index 99cf904..9dd28e6 100644
--- a/lbz/aws_ssm.py
+++ b/lbz/aws_ssm.py
@@ -1,11 +1,11 @@
-from typing import Optional
+from __future__ import annotations
 
 from lbz.aws_boto3 import client
 
 
 class SSM:
     @staticmethod
-    def get_parameter(name: str) -> Optional[str]:
+    def get_parameter(name: str) -> str | None:
         try:
             return client.ssm.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"]
         except (KeyError, client.ssm.exceptions.ParameterNotFound):
diff --git a/lbz/configuration.py b/lbz/configuration.py
index 7238c1e..ebcd88b 100644
--- a/lbz/configuration.py
+++ b/lbz/configuration.py
@@ -1,7 +1,10 @@
+from __future__ import annotations
+
 import json
 from abc import ABCMeta, abstractmethod
+from collections.abc import Callable
 from os import getenv
-from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar
+from typing import Any, Generic, TypeVar
 
 from lbz.aws_ssm import SSM
 from lbz.exceptions import ConfigValueParsingFailed, MissingConfigValue
@@ -11,7 +14,7 @@
 
 class ConfigParser:
     @staticmethod
-    def split_by_comma(value: str) -> List[str]:
+    def split_by_comma(value: str) -> list[str]:
         return value.split(",")
 
     @staticmethod
@@ -21,8 +24,8 @@ def cast_to_bool(value: str) -> bool:
         return False
 
     @staticmethod
-    def load_jwt_keys(value: str) -> List[dict]:
-        deserialized_value: Dict[str, List[dict]] = json.loads(value)
+    def load_jwt_keys(value: str) -> list[dict]:
+        deserialized_value: dict[str, list[dict]] = json.loads(value)
         return deserialized_value["keys"]
 
 
@@ -37,12 +40,12 @@ def __init__(
         self,
         key: str,
         parser: Callable[[Any], T] = str,  # type: ignore
-        default: T = None,
+        default: T | None = None,
     ):
         self._key = key
         self._parser = parser
         self._default = default
-        self._value: Optional[T] = None
+        self._value: T | None = None
 
     @abstractmethod
     def getter(self) -> Any:
@@ -71,10 +74,10 @@ def reset(self) -> None:
 
 
 class EnvValue(ConfigValue[T]):
-    def getter(self) -> Optional[str]:
+    def getter(self) -> str | None:
         return getenv(self._key)
 
 
 class SSMValue(ConfigValue[T]):
-    def getter(self) -> Optional[str]:
+    def getter(self) -> str | None:
         return SSM.get_parameter(self._key)
diff --git a/lbz/dev/misc.py b/lbz/dev/misc.py
index 6b7ee95..49bb41b 100644
--- a/lbz/dev/misc.py
+++ b/lbz/dev/misc.py
@@ -1,6 +1,5 @@
-"""
-Dev misc tools.
-"""
+from __future__ import annotations
+
 import json
 import pathlib
 from uuid import uuid4
@@ -17,10 +16,10 @@ def __init__(
         self,
         resource_path: str,
         method: str,
-        body: dict = None,
-        query_params: dict = None,
-        path_params: dict = None,
-        headers: dict = None,
+        body: dict | None = None,
+        query_params: dict | None = None,
+        path_params: dict | None = None,
+        headers: dict | None = None,
     ) -> None:
         super().__init__(**json.loads(EVENT_TEMPLATE))
 
diff --git a/lbz/dev/server.py b/lbz/dev/server.py
index 2387468..05e717a 100644
--- a/lbz/dev/server.py
+++ b/lbz/dev/server.py
@@ -1,6 +1,5 @@
-"""
-Development Server.
-"""
+from __future__ import annotations
+
 import json
 import logging
 import urllib.parse
@@ -8,7 +7,6 @@
 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
 from os import environ
 from threading import Thread
-from typing import Tuple, Type, Union
 
 from lbz.dev.misc import APIGatewayEvent
 from lbz.resource import Resource
@@ -18,22 +16,18 @@
 
 
 class MyLambdaDevHandler(BaseHTTPRequestHandler, metaclass=ABCMeta):
-    """
-    Mimics AWS Lambda behavior.
-    """
+    """Mimics AWS Lambda behavior."""
 
     done: bool = False  # TODO: if possible move to __init__
 
     @property
     @abstractmethod
-    def cls(self) -> Type[Resource]:
+    def cls(self) -> type[Resource]:
         pass
 
-    def _get_route_params(  # noqa:C901
-        self, org_path: str
-    ) -> Tuple[Union[str, None], Union[dict, None]]:
-        """
-        Parses route and params.
+    def _get_route_params(self, org_path: str) -> tuple[str | None, dict | None]:  # noqa:C901
+        """Parses route and params.
+
         :param org_path:
         :return: standardised route, url params / None
         """
@@ -63,7 +57,7 @@ def _get_route_params(  # noqa:C901
                     return org_route, params
         return None, None
 
-    def _send_json(self, code: int, obj: dict, headers: dict = None) -> None:
+    def _send_json(self, code: int, obj: dict, headers: dict | None = None) -> None:
         # Make sure only one response is sent
         if self.done:
             return
@@ -83,16 +77,13 @@ def _error(self, code: int, message: str) -> None:
         self._send_json(code, {"error": message}, headers={"Content-Type": content_type})
 
     def handle_request(self) -> None:
-        """
-        Main method for handling all incoming requests.
-        """
+        """Main method for handling all incoming requests."""
         try:
             if self.path == "/favicon.ico":
                 return
             self.done = False
 
-            request_size = int(self.headers.get("Content-Length", 0))
-            if request_size:
+            if request_size := int(self.headers.get("Content-Length", 0)):
                 request_body = self.rfile.read(request_size).decode(
                     encoding="utf_8", errors="strict"
                 )
@@ -105,7 +96,7 @@ def handle_request(self) -> None:
             if route is None:
                 self._error(666, "Path not Found")
                 return
-            resource = self.cls(  # pylint: disable=not-callable
+            resource = self.cls(
                 APIGatewayEvent(
                     resource_path=route,
                     method=self.command,
@@ -148,18 +139,14 @@ def do_OPTIONS(self) -> None:  # pylint: disable=invalid-name
 
 
 class MyDevServer(Thread):
-    """
-    Development Server base class.
-    """
-
     def __init__(
         self,
-        acls: Type[Resource],
+        acls: type[Resource],
         address: str = "localhost",
         port: int = 8000,
     ) -> None:
         class MyClassLambdaDevHandler(MyLambdaDevHandler):
-            cls: Type[Resource] = acls
+            cls: type[Resource] = acls
 
         super().__init__()
         self.my_handler = MyClassLambdaDevHandler
@@ -170,9 +157,7 @@ class MyClassLambdaDevHandler(MyLambdaDevHandler):
         print(f"server bound to port: {self.port}")
 
     def run(self) -> None:
-        """
-        Start the server in the foreground.
-        """
+        """Start the server in the foreground."""
         print(f"serving on http://{self.address}:{self.port}")
         self.httpd.serve_forever()
 
@@ -182,7 +167,5 @@ def stop(self) -> None:
         print(f"Server stopped and port {self.port} released")
 
     def start(self) -> None:  # pylint: disable=useless-super-delegation
-        """
-        Start the server in the background
-        """
+        """Start the server in the background"""
         super().start()
diff --git a/lbz/events/api.py b/lbz/events/api.py
index 995dbf6..8acf62d 100644
--- a/lbz/events/api.py
+++ b/lbz/events/api.py
@@ -1,6 +1,7 @@
+from collections.abc import Callable
 from copy import deepcopy
 from functools import wraps
-from typing import TYPE_CHECKING, Any, Callable, List
+from typing import TYPE_CHECKING, Any
 
 from lbz._cfg import AWS_LAMBDA_FUNCTION_NAME, EVENTS_BUS_NAME
 from lbz.aws_boto3 import client
@@ -21,10 +22,10 @@
 class EventAPI(metaclass=Singleton):
     def __init__(self) -> None:
         self._source = AWS_LAMBDA_FUNCTION_NAME.value
-        self._resources: List[str] = []
-        self._pending_events: List[Event] = []
-        self._sent_events: List[Event] = []
-        self._failed_events: List[Event] = []
+        self._resources: list[str] = []
+        self._pending_events: list[Event] = []
+        self._sent_events: list[Event] = []
+        self._failed_events: list[Event] = []
         self._bus_name = EVENTS_BUS_NAME.value
 
     def __repr__(self) -> str:
@@ -36,7 +37,7 @@ def __repr__(self) -> str:
     def set_source(self, source: str) -> None:
         self._source = source
 
-    def set_resources(self, resources: List[str]) -> None:
+    def set_resources(self, resources: list[str]) -> None:
         self._resources = resources
 
     def set_bus_name(self, bus_name: str) -> None:
@@ -47,15 +48,15 @@ def bus_name(self) -> str:
         return self._bus_name
 
     @property
-    def sent_events(self) -> List[Event]:
+    def sent_events(self) -> list[Event]:
         return deepcopy(self._sent_events)
 
     @property
-    def pending_events(self) -> List[Event]:
+    def pending_events(self) -> list[Event]:
         return deepcopy(self._pending_events)
 
     @property
-    def failed_events(self) -> List[Event]:
+    def failed_events(self) -> list[Event]:
         return deepcopy(self._failed_events)
 
     def register(self, new_event: Event) -> None:
diff --git a/lbz/events/broker.py b/lbz/events/broker.py
index 9d25f26..38cc346 100644
--- a/lbz/events/broker.py
+++ b/lbz/events/broker.py
@@ -1,5 +1,5 @@
+from collections.abc import Callable, Mapping
 from copy import deepcopy
-from typing import Callable, List, Mapping
 
 from lbz.events.event import Event
 from lbz.handlers import BaseHandler
@@ -12,7 +12,7 @@
 class BaseEventBroker(BaseHandler[None]):
     def __init__(
         self,
-        mapper: Mapping[str, List[Callable[[Event], None]]],
+        mapper: Mapping[str, list[Callable[[Event], None]]],
         event: dict,
         context: LambdaContext,
         *,
@@ -34,7 +34,7 @@ def handle(self) -> None:
 
         self.post_handle()
 
-    def _get_handlers(self) -> List[Callable]:
+    def _get_handlers(self) -> list[Callable]:
         try:
             return self.mapper[self.event.type]
         except KeyError as err:
@@ -44,7 +44,7 @@ def _get_handlers(self) -> List[Callable]:
 class EventBroker(BaseEventBroker):
     def __init__(
         self,
-        mapper: Mapping[str, List[Callable[[Event], None]]],
+        mapper: Mapping[str, list[Callable[[Event], None]]],
         event: dict,
         context: LambdaContext,
     ) -> None:
@@ -54,7 +54,7 @@ def __init__(
 class CognitoEventBroker(BaseEventBroker):
     def __init__(
         self,
-        mapper: Mapping[str, List[Callable[[Event], None]]],
+        mapper: Mapping[str, list[Callable[[Event], None]]],
         event: dict,
         context: LambdaContext,
     ) -> None:
diff --git a/lbz/events/event.py b/lbz/events/event.py
index e21d370..4e0f837 100644
--- a/lbz/events/event.py
+++ b/lbz/events/event.py
@@ -1,11 +1,13 @@
+from __future__ import annotations
+
 import json
-from typing import Optional
 
 
-class Event:
+# TODO: Implement the hash method to stay compatible with pylint requirements
+class Event:  # pylint: disable=eq-without-hash
     type: str
 
-    def __init__(self, data: dict, *, event_type: Optional[str] = None) -> None:
+    def __init__(self, data: dict, *, event_type: str | None = None) -> None:
         self.data = data
         self.type = event_type or self.type
 
diff --git a/lbz/exceptions.py b/lbz/exceptions.py
index 61bf6da..a246107 100644
--- a/lbz/exceptions.py
+++ b/lbz/exceptions.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
+from collections.abc import Generator
 from http import HTTPStatus
-from typing import Any, Generator, Optional, Type
+from typing import Any
 
 from lbz.response import Response
 
@@ -23,9 +26,9 @@ class LambdaFWException(Exception):
 
     message = HTTPStatus.INTERNAL_SERVER_ERROR.description
     status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
-    error_code: Optional[str] = None
+    error_code: str | None = None
 
-    def __init__(self, message: str = "", error_code: str = None) -> None:
+    def __init__(self, message: str = "", error_code: str | None = None) -> None:
         super().__init__(message)
         if message:
             self.message = message
@@ -344,8 +347,8 @@ class NetworkAuthenticationRequired(LambdaFWServerException):
 
 
 def all_lbz_errors(
-    cls: Type[LambdaFWException] = LambdaFWException,
-) -> Generator[Type[LambdaFWException], None, None]:
+    cls: type[LambdaFWException] = LambdaFWException,
+) -> Generator[type[LambdaFWException], None, None]:
     for subcls in cls.__subclasses__():
         if subcls not in [LambdaFWClientException, LambdaFWServerException]:
             yield subcls
diff --git a/lbz/handlers.py b/lbz/handlers.py
index 2e1980b..495a190 100644
--- a/lbz/handlers.py
+++ b/lbz/handlers.py
@@ -1,5 +1,7 @@
+from __future__ import annotations
+
 from abc import ABCMeta, abstractmethod
-from typing import Generic, Optional, TypeVar
+from typing import Generic, TypeVar
 
 from lbz.misc import deprecated, get_logger
 from lbz.type_defs import LambdaContext
@@ -13,7 +15,7 @@ class BaseHandler(Generic[T], metaclass=ABCMeta):
     def __init__(self, event: dict, context: LambdaContext) -> None:
         self.raw_event = event
         self.context = context
-        self.response: Optional[T] = None
+        self.response: T | None = None
 
     def react(self) -> T:
         self.pre_handle()
@@ -21,7 +23,7 @@ def react(self) -> T:
         self._post_handle()
         return self.response
 
-    @deprecated(message="Please use react() for full request flow", version="0.6.0")
+    @deprecated(message="Please use react() for full request flow", version="0.7.0")
     def __call__(self) -> T:
         return self.react()
 
diff --git a/lbz/jwt_utils.py b/lbz/jwt_utils.py
index 98bbe2a..79e9088 100644
--- a/lbz/jwt_utils.py
+++ b/lbz/jwt_utils.py
@@ -1,6 +1,3 @@
-"""
-JWT helpers module.
-"""
 from jose import jwt
 from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError
 
@@ -12,9 +9,7 @@
 
 
 def get_matching_jwk(auth_jwt_token: str) -> dict:
-    """
-    Checks provided JWT token against allowed tokens.
-    """
+    """Checks provided JWT token against allowed tokens."""
     try:
         kid_from_jwt_header = jwt.get_unverified_header(auth_jwt_token)["kid"]
         for key in ALLOWED_PUBLIC_KEYS.value:
@@ -44,9 +39,7 @@ def validate_jwt_properties(decoded_jwt: dict) -> None:
 
 
 def decode_jwt(auth_jwt_token: str) -> dict:  # noqa:C901
-    """
-    Decodes JWT token.
-    """
+    """Decodes JWT token."""
 
     if not ALLOWED_PUBLIC_KEYS.value:
         raise MissingConfigValue("ALLOWED_PUBLIC_KEYS")
diff --git a/lbz/lambdas/broker.py b/lbz/lambdas/broker.py
index 19d3e0a..f1d93aa 100644
--- a/lbz/lambdas/broker.py
+++ b/lbz/lambdas/broker.py
@@ -1,4 +1,4 @@
-from typing import Callable, Mapping
+from collections.abc import Callable, Mapping
 
 from lbz.exceptions import LambdaFWException
 from lbz.handlers import BaseHandler
diff --git a/lbz/lambdas/client.py b/lbz/lambdas/client.py
index 939e2d8..9846ec3 100644
--- a/lbz/lambdas/client.py
+++ b/lbz/lambdas/client.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import json
-from typing import Any, Iterable, Optional, Type
+from collections.abc import Iterable
+from typing import Any
 
 from lbz.aws_boto3 import client
 from lbz.lambdas.enums import LambdaResult, LambdaSource
@@ -18,16 +21,16 @@ def default(self, o: Any) -> Any:
 
 
 class LambdaClient:
-    json_encoder: Type[json.JSONEncoder] = SetsEncoder
+    json_encoder: type[json.JSONEncoder] = SetsEncoder
 
     @classmethod
     def invoke(
         cls,
         function_name: str,
         op: str,
-        data: Optional[dict] = None,
+        data: dict | None = None,
         *,
-        allowed_error_results: Iterable[str] = None,
+        allowed_error_results: Iterable[str] | None = None,
         raise_if_error_resp: bool = False,
         asynchronous: bool = False,
     ) -> LambdaResponse:
@@ -47,8 +50,9 @@ def invoke(
         try:
             response: LambdaResponse = json.loads(raw_response["Payload"].read().decode("utf-8"))
         except Exception:
-            message = "Invalid response received from %s Lambda (op: %s)"
-            logger.error(message, function_name, op, extra=dict(data=data, response=raw_response))
+            # f-string used directly to keep messages unique from a monitoring/tracking perspective
+            error_message = f"Invalid response received from {function_name} Lambda (op: {op})"
+            logger.error(error_message, extra={"data": data, "response": raw_response})
             raise
 
         lambda_result = response.get("result", LambdaResult.SERVER_ERROR)
@@ -59,7 +63,7 @@ def invoke(
         if raise_if_error_resp:
             raise LambdaError(function_name, op, lambda_result, response)
 
-        # f-string used directly to keep messages unique from the monitoring/tracking perspective
+        # f-string used directly to keep messages unique from a monitoring/tracking perspective
         error_message = f"Error response from {function_name} Lambda (op: {op}): {lambda_result}"
-        logger.error(error_message, extra=dict(data=data, response=response))
+        logger.error(error_message, extra={"data": data, "response": response})
         return response
diff --git a/lbz/lambdas/enums.py b/lbz/lambdas/enums.py
index f0b8d3c..0cd9920 100644
--- a/lbz/lambdas/enums.py
+++ b/lbz/lambdas/enums.py
@@ -1,4 +1,4 @@
-from typing import Iterable
+from collections.abc import Iterable
 
 
 class LambdaResult:
diff --git a/lbz/lambdas/response.py b/lbz/lambdas/response.py
index 7618131..f704819 100644
--- a/lbz/lambdas/response.py
+++ b/lbz/lambdas/response.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import Any, TypedDict
 
 from lbz.lambdas.enums import LambdaResult
@@ -18,7 +20,7 @@ def lambda_ok_response(data: Any = None) -> LambdaResponse:
 
 
 def lambda_error_response(
-    result: str, error_message: str, error_code: str = None
+    result: str, error_message: str, error_code: str | None = None
 ) -> LambdaResponse:
     err_response = LambdaResponse(result=result, message=error_message)
     if error_code is not None:
diff --git a/lbz/misc.py b/lbz/misc.py
index 2164b29..4e3d73f 100644
--- a/lbz/misc.py
+++ b/lbz/misc.py
@@ -1,21 +1,19 @@
-"""
-Misc Helpers of Lambda Framework.
-"""
+"""Misc Helpers of Lambda Framework."""
+from __future__ import annotations
+
 import copy
 import logging
 import logging.handlers
 import warnings
-from collections.abc import MutableMapping
+from collections.abc import Callable, Hashable, Iterable, Iterator, MutableMapping
 from functools import wraps
-from typing import Any, Callable, Hashable, Iterable, Iterator, List, Optional
+from typing import Any
 
 from lbz._cfg import LBZ_DEBUG_MODE, LOGGING_LEVEL
 
 
 class NestedDict(dict):
-    """
-    Endless nested dict.
-    """
+    """Endless nested dict."""
 
     def __getitem__(self, key: str) -> Any:
         if key in self:
@@ -24,7 +22,8 @@ def __getitem__(self, key: str) -> Any:
 
 
 class Singleton(type):
-    """
+    """Metaclass that ensures that an inheriting class has only one instance.
+
     Usage:
         class MyClass(metaclass=Singleton):
             pass
@@ -45,11 +44,9 @@ def _del(a_cls: Any) -> None:
 
 
 class MultiDict(MutableMapping):
-    """
-    Advanced Multi Dictionary.
-    """
+    """Advanced Multi Dictionary."""
 
-    def __init__(self, mapping: Optional[dict]):
+    def __init__(self, mapping: dict | None):
         if mapping is None:
             mapping = {}
 
@@ -80,12 +77,10 @@ def __str__(self) -> str:
         return repr(self)
 
     def getlist(self, k: Hashable) -> list:
-        """
-        Returns a list of all values for specific key.
-        """
+        """Returns a list of all values for specific key."""
         return list(self._dict[k])
 
-    def original_items(self, keys_to_skip: Iterable[Hashable] = None) -> List[tuple]:
+    def original_items(self, keys_to_skip: Iterable[Hashable] | None = None) -> list[tuple]:
         keys_to_skip = keys_to_skip or []
         return [(key, values) for key, values in self._dict.items() if key not in keys_to_skip]
 
@@ -101,9 +96,7 @@ def get_logger(name: str) -> logging.Logger:
 
 
 def error_catcher(function: Callable, default_return: Any = False) -> Callable:
-    """
-    Universal Error Catcher
-    """
+    """Universal Error Catcher"""
 
     @wraps(function)
     def wrapped(*args: Any, **kwargs: Any) -> Any:
@@ -134,8 +127,7 @@ def is_in_debug_mode() -> bool:
 
 
 def deprecated(*, message: str, version: str) -> Callable:
-    """
-    This is a decorator which can be used to mark functions as deprecated.
+    """This is a decorator which can be used to mark functions as deprecated.
 
     It will result in a warning being emitted when the function is used.
     """
diff --git a/lbz/request.py b/lbz/request.py
index eec59e6..713d92b 100644
--- a/lbz/request.py
+++ b/lbz/request.py
@@ -1,9 +1,8 @@
-"""
-Request standardisation module.
-"""
+from __future__ import annotations
+
 import base64
 import json
-from typing import Any, Optional, Union
+from typing import Any
 
 from multidict import CIMultiDict
 
@@ -22,12 +21,12 @@ def __init__(
         headers: CIMultiDict,
         uri_params: dict,
         method: str,
-        body: Union[str, bytes, dict],
+        body: str | bytes | dict,
         context: dict,
         stage_vars: dict,
         is_base64_encoded: bool,
-        query_params: Optional[dict] = None,
-        user: Optional[User] = None,
+        query_params: dict | None = None,
+        user: User | None = None,
     ):
         self.query_params = MultiDict(query_params or {})
         self.headers = headers
@@ -38,20 +37,20 @@ def __init__(
         self.user = user
         self._is_base64_encoded = is_base64_encoded
         self._body = body
-        self._json_body: Optional[dict] = None
-        self._raw_body: Optional[Union[bytes, dict]] = None
+        self._json_body: dict | None = None
+        self._raw_body: bytes | dict | None = None
 
     def __repr__(self) -> str:
         return f"<Request {self.method} >"
 
     @staticmethod
-    def _decode_base64(encoded: Union[str, bytes]) -> bytes:
+    def _decode_base64(encoded: str | bytes) -> bytes:
         if not isinstance(encoded, bytes):
             encoded = encoded.encode("ascii")
         return base64.b64decode(encoded)
 
     @property
-    def raw_body(self) -> Optional[Union[bytes, dict]]:
+    def raw_body(self) -> bytes | dict | None:
         if self._raw_body is None and self._body is not None:
             if self._is_base64_encoded and isinstance(self._body, (bytes, str)):
                 self._raw_body = self._decode_base64(self._body)
@@ -62,17 +61,17 @@ def raw_body(self) -> Optional[Union[bytes, dict]]:
         return self._raw_body
 
     @staticmethod
-    def _safe_json_loads(payload: Union[str, bytes]) -> Optional[Any]:
+    def _safe_json_loads(payload: str | bytes) -> Any:
         try:
             return json.loads(payload)
         except ValueError as error:
             raise BadRequestError(f"Invalid payload.\nPayload body:\n {repr(payload)}") from error
 
     @property
-    def json_body(self) -> Optional[dict]:
+    def json_body(self) -> dict | None:
         if self._json_body is None:
-            content_type = self.headers.get("Content-Type")
-            if content_type is None:
+            content_type: str | None = self.headers.get("Content-Type")
+            if content_type is None:  # pylint: disable=consider-using-assignment-expr
                 return None
             if content_type.startswith("application/json"):
                 if isinstance(self.raw_body, dict) or self.raw_body is None:
diff --git a/lbz/resource.py b/lbz/resource.py
index 193472c..aad5632 100644
--- a/lbz/resource.py
+++ b/lbz/resource.py
@@ -1,7 +1,8 @@
-"""Resource Handler."""
+from __future__ import annotations
+
+from collections.abc import Callable
 from copy import deepcopy
 from http import HTTPStatus
-from typing import Callable, List, Optional, Union
 from urllib.parse import urlencode
 
 from multidict import CIMultiDict
@@ -28,10 +29,6 @@
 
 
 class Resource:
-    """
-    Resource class.
-    """
-
     _name: str = ""
     _router = Router()
     _authz_collector = authz_collector
@@ -87,7 +84,7 @@ def __call__(self) -> Response:
     def __repr__(self) -> str:
         return f"<Resource {self.method} @ {self.urn} >"
 
-    def _get_user(self, headers: CIMultiDict) -> Union[None, User]:
+    def _get_user(self, headers: CIMultiDict) -> User | None:
         authentication = headers.get("Authentication")
         if authentication and ALLOWED_PUBLIC_KEYS.value:
             return User(authentication)
@@ -96,28 +93,21 @@ def _get_user(self, headers: CIMultiDict) -> Union[None, User]:
         return None
 
     def _post_request_hook(self) -> None:
-        """
-        Makes the post_request_hook run-time friendly.
-        """
+        """Makes the post_request_hook run-time friendly."""
         try:
             self.post_request_hook()
         except Exception as err:  # pylint: disable=broad-except
             logger.exception(err)
 
     def pre_request_hook(self) -> None:
-        """
-        Place to configure pre request hooks.
-        """
+        """Place to configure pre request hooks."""
 
     def post_request_hook(self) -> None:
-        """
-        Place to configure post request hooks.
-        """
+        """Place to configure post request hooks."""
 
     @staticmethod
     def get_guest_authorization() -> dict:
-        """
-        Place to configure default authorization.
+        """Place to configure default authorization.
 
         That will be used when Authorization Header is not in place.
         """
@@ -128,9 +118,7 @@ def get_authz_data(self) -> dict:
 
 
 class CORSResource(Resource):
-    """
-    CORS capable resource.
-    """
+    """CORS capable resource."""
 
     _cors_headers = (
         "Content-Type",
@@ -144,9 +132,9 @@ class CORSResource(Resource):
     def __init__(
         self,
         event: dict,
-        methods: List[str],
-        origins: List[str] = None,
-        cors_headers: List[str] = None,
+        methods: list[str],
+        origins: list[str] | None = None,
+        cors_headers: list[str] | None = None,
     ):
         # TODO: adjust the rest of the arguments in the near future too.
         super().__init__(event)
@@ -166,14 +154,12 @@ def __call__(self) -> Response:
             resp.headers.update(self.resp_headers())
         return resp
 
-    def _get_allowed_origins(self, origins: List[str]) -> str:
-        """
-        Checks requests origins against allowed origins.
-        """
+    def _get_allowed_origins(self, origins: list[str]) -> str:
+        """Checks requests origins against allowed origins."""
         if "*" in origins:
             return "*"
-        request_origin: Optional[str] = self.request.headers.get("Origin")
-        if request_origin:
+        request_origin: str | None = self.request.headers.get("Origin")
+        if request_origin:  # pylint: disable=consider-using-assignment-expr
             for allowed_origin in origins:
                 if request_origin == allowed_origin:
                     return request_origin
@@ -184,9 +170,7 @@ def _get_allowed_origins(self, origins: List[str]) -> str:
         return origins[0]
 
     def resp_headers(self, content_type: str = "") -> dict:
-        """
-        Properly formatted headers.
-        """
+        """Properly formatted headers."""
         return (
             {**self._resp_headers, "Content-Type": content_type}
             if content_type
@@ -195,21 +179,15 @@ def resp_headers(self, content_type: str = "") -> dict:
 
     @property
     def resp_headers_json(self) -> dict:
-        """
-        Properly formatted json headers.
-        """
+        """Properly formatted json headers."""
         return self.resp_headers(content_type="application/json")
 
 
 class PaginatedCORSResource(CORSResource):
-    """
-    Resource for standardised pagination.
-    """
+    """Resource for standardised pagination."""
 
     def get_pagination(self, total_items: int, limit: int, offset: int) -> dict:
-        """
-        Responsible for paginating the requests.
-        """
+        """Responsible for paginating the requests."""
         base_link = self._pagination_uri
         links = {
             "current": base_link.format(offset=offset, limit=limit),
@@ -218,7 +196,7 @@ def get_pagination(self, total_items: int, limit: int, offset: int) -> dict:
         if previous_offset := max(offset - limit, 0):
             links["prev"] = base_link.format(offset=previous_offset, limit=limit)
         next_offset = offset + limit if offset + limit < total_items else None
-        if next_offset:
+        if next_offset:  # pylint: disable=consider-using-assignment-expr
             links["next"] = base_link.format(offset=next_offset, limit=limit)
         return {
             "count": total_items,
@@ -230,7 +208,7 @@ def _pagination_uri(self) -> str:
         if query_params := self.request.query_params.original_items(
             keys_to_skip=["offset", "limit"]
         ):
-            encoded_params = urlencode(query_params, doseq=True)  # type: ignore
+            encoded_params = urlencode(query_params, doseq=True)
             return f"{self.urn}?{encoded_params}&offset={{offset}}&limit={{limit}}"
         return f"{self.urn}?offset={{offset}}&limit={{limit}}"
 
diff --git a/lbz/response.py b/lbz/response.py
index 068d876..4d25e1a 100644
--- a/lbz/response.py
+++ b/lbz/response.py
@@ -1,8 +1,6 @@
-"""
-Standardised response module.
-"""
+from __future__ import annotations
+
 import json
-from typing import Union
 
 
 class Response:
@@ -12,9 +10,9 @@ class Response:
 
     def __init__(
         self,
-        body: Union[str, dict],
+        body: str | dict,
         /,
-        headers: dict = None,
+        headers: dict | None = None,
         status_code: int = 200,
         base64_encoded: bool = False,
     ):
@@ -25,9 +23,7 @@ def __init__(
         self.is_base64 = base64_encoded
 
     def get_content_header(self) -> dict:
-        """
-        Adds necessary headers based on content type
-        """
+        """Adds necessary headers based on content type"""
         if self.is_json:
             return {"Content-Type": "application/json"}
         if isinstance(self.body, str):
@@ -35,9 +31,7 @@ def get_content_header(self) -> dict:
         raise RuntimeError("Response body type not supported yet.")
 
     def to_dict(self) -> dict:
-        """
-        Dumps response to AWS Lambda compatible response format.
-        """
+        """Dumps response to AWS Lambda compatible response format."""
         body = (
             json.dumps(self.body, separators=(",", ":"), default=str)
             if self.is_json
diff --git a/lbz/router.py b/lbz/router.py
index d3d8a33..00d7f69 100644
--- a/lbz/router.py
+++ b/lbz/router.py
@@ -1,18 +1,12 @@
-"""
-Router module.
-"""
 import json
+from collections.abc import Callable, Iterator
 from functools import wraps
-from typing import Any, Callable, Iterator
+from typing import Any
 
 from lbz.misc import NestedDict, Singleton
 
 
 class Router(metaclass=Singleton):
-    """
-    Router Class.
-    """
-
     def __init__(self) -> None:
         self._routes = NestedDict()
 
@@ -25,7 +19,7 @@ def __str__(self) -> str:
     def __repr__(self) -> str:
         return self.__str__()
 
-    def __contains__(self, o: Any) -> bool:  # pylint: disable=invalid-name
+    def __contains__(self, o: Any) -> bool:
         return self._routes.__contains__(o)
 
     def __len__(self) -> int:
@@ -35,9 +29,7 @@ def __iter__(self) -> Iterator:
         return self._routes.__iter__()
 
     def add_route(self, route: str, method: str, handler: str) -> None:
-        """
-        Registers handler to route and method.
-        """
+        """Registers handler to route and method."""
         self._routes[route][method] = handler
 
     def clear(self) -> None:
@@ -45,9 +37,7 @@ def clear(self) -> None:
 
 
 def add_route(route: str, method: str = "GET") -> Callable:
-    """
-    Flask-like wrapper for adding routes.
-    """
+    """Flask-like wrapper for adding routes."""
 
     def wrapper(func: Callable) -> Callable:
         router = Router()
diff --git a/lbz/type_defs.py b/lbz/type_defs.py
index 94ed98c..460d5b1 100644
--- a/lbz/type_defs.py
+++ b/lbz/type_defs.py
@@ -2,7 +2,7 @@
 
 https://docs.aws.amazon.com/lambda/latest/dg/python-context.html
 """
-from typing import Any, Dict
+from typing import Any
 
 
 class LambdaClientContextMobileClient:
@@ -15,8 +15,8 @@ class LambdaClientContextMobileClient:
 
 class LambdaClientContext:
     client: LambdaClientContextMobileClient
-    custom: Dict[str, Any]
-    env: Dict[str, Any]
+    custom: dict[str, Any]
+    env: dict[str, Any]
 
 
 class LambdaCognitoIdentity:
@@ -37,4 +37,4 @@ class LambdaContext:
 
     @staticmethod
     def get_remaining_time_in_millis() -> int:
-        ...
+        raise NotImplementedError
diff --git a/requirements-deploy.in b/requirements-deploy.in
index af996cf..8ca0192 100644
--- a/requirements-deploy.in
+++ b/requirements-deploy.in
@@ -1,1 +1,2 @@
 twine
+wheel
diff --git a/requirements-deploy.txt b/requirements-deploy.txt
index c079142..5e1ada5 100644
--- a/requirements-deploy.txt
+++ b/requirements-deploy.txt
@@ -1,5 +1,5 @@
 #
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.9
 # by the following command:
 #
 #    pip-compile requirements-deploy.in
@@ -16,8 +16,6 @@ importlib-metadata==7.0.1
     # via
     #   keyring
     #   twine
-importlib-resources==6.1.1
-    # via keyring
 jaraco-classes==3.3.0
     # via keyring
 keyring==24.3.0
@@ -50,13 +48,11 @@ rich==13.7.0
     # via twine
 twine==4.0.2
     # via -r requirements-deploy.in
-typing-extensions==4.9.0
-    # via rich
 urllib3==2.1.0
     # via
     #   requests
     #   twine
+wheel==0.42.0
+    # via -r requirements-deploy.in
 zipp==3.17.0
-    # via
-    #   importlib-metadata
-    #   importlib-resources
+    # via importlib-metadata
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 98813ca..71d4557 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,101 +1,98 @@
 #
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.9
 # by the following command:
 #
 #    pip-compile requirements-dev.in
 #
-astroid==2.11.7
+astroid==3.0.2
     # via pylint
-attrs==22.1.0
-    # via pytest
-black==22.6.0
+black==23.12.1
     # via -r requirements-dev.in
-boto3-stubs[cognito-idp,dynamodb,events,lambda,s3,sns,sqs,ssm]==1.21.46
+boto3-stubs[cognito-idp,dynamodb,events,lambda,s3,sns,sqs,ssm]==1.34.11
     # via -r requirements-dev.in
-botocore-stubs==1.27.45
+botocore-stubs==1.34.11
     # via boto3-stubs
-build==0.8.0
+build==1.0.3
     # via pip-tools
-click==8.1.3
+click==8.1.7
     # via
     #   black
     #   pip-tools
-coverage[toml]==6.4.2
+coverage[toml]==7.4.0
     # via
     #   -r requirements-dev.in
     #   pytest-cov
-dill==0.3.5.1
+dill==0.3.7
     # via pylint
-flake8==5.0.4
+exceptiongroup==1.2.0
+    # via pytest
+flake8==6.1.0
     # via -r requirements-dev.in
-iniconfig==1.1.1
+importlib-metadata==7.0.1
+    # via build
+iniconfig==2.0.0
     # via pytest
-isort==5.10.1
+isort==5.13.2
     # via
     #   -r requirements-dev.in
     #   pylint
-lazy-object-proxy==1.7.1
-    # via astroid
 mccabe==0.7.0
     # via
     #   flake8
     #   pylint
-mypy==0.971
+mypy==1.8.0
     # via -r requirements-dev.in
-mypy-boto3-cognito-idp==1.21.34
+mypy-boto3-cognito-idp==1.34.3
     # via boto3-stubs
-mypy-boto3-dynamodb==1.21.34
+mypy-boto3-dynamodb==1.34.0
     # via boto3-stubs
-mypy-boto3-events==1.21.36
+mypy-boto3-events==1.34.0
     # via boto3-stubs
-mypy-boto3-lambda==1.21.35
+mypy-boto3-lambda==1.34.0
     # via boto3-stubs
-mypy-boto3-s3==1.21.34
+mypy-boto3-s3==1.34.0
     # via boto3-stubs
-mypy-boto3-sns==1.21.34
+mypy-boto3-sns==1.34.0
     # via boto3-stubs
-mypy-boto3-sqs==1.21.34
+mypy-boto3-sqs==1.34.0
     # via boto3-stubs
-mypy-boto3-ssm==1.21.43
+mypy-boto3-ssm==1.34.0
     # via boto3-stubs
-mypy-extensions==0.4.3
+mypy-extensions==1.0.0
     # via
     #   black
     #   mypy
-packaging==21.3
+packaging==23.2
     # via
+    #   black
     #   build
     #   pytest
-pathspec==0.9.0
+pathspec==0.12.1
     # via black
-pep517==0.13.0
-    # via build
 pip-tools==7.3.0
     # via -r requirements-dev.in
-platformdirs==2.5.2
+platformdirs==4.1.0
     # via
     #   black
     #   pylint
-pluggy==1.0.0
-    # via pytest
-py==1.11.0
+pluggy==1.3.0
     # via pytest
-pycodestyle==2.9.1
+pycodestyle==2.11.1
     # via flake8
-pyflakes==2.5.0
+pyflakes==3.1.0
     # via flake8
-pylint==2.14.5
+pylint==3.0.3
     # via -r requirements-dev.in
-pyparsing==3.0.9
-    # via packaging
-pytest==7.1.2
+pyproject-hooks==1.0.0
+    # via build
+pytest==7.4.4
     # via
     #   -r requirements-dev.in
     #   pytest-cov
     #   pytest-mock
-pytest-cov==3.0.0
+pytest-cov==4.1.0
     # via -r requirements-dev.in
-pytest-mock==3.8.2
+pytest-mock==3.12.0
     # via -r requirements-dev.in
 tomli==2.0.1
     # via
@@ -103,15 +100,17 @@ tomli==2.0.1
     #   build
     #   coverage
     #   mypy
-    #   pep517
     #   pip-tools
     #   pylint
+    #   pyproject-hooks
     #   pytest
-tomlkit==0.11.1
+tomlkit==0.12.3
     # via pylint
-types-awscrt==0.13.14
+types-awscrt==0.20.0
     # via botocore-stubs
-typing-extensions==4.3.0
+types-s3transfer==0.10.0
+    # via boto3-stubs
+typing-extensions==4.9.0
     # via
     #   astroid
     #   black
@@ -126,10 +125,10 @@ typing-extensions==4.3.0
     #   mypy-boto3-sqs
     #   mypy-boto3-ssm
     #   pylint
-wheel==0.37.1
+wheel==0.42.0
     # via pip-tools
-wrapt==1.14.1
-    # via astroid
+zipp==3.17.0
+    # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
 # pip
diff --git a/requirements.txt b/requirements.txt
index 7e1d4dc..37bdb98 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,12 @@
 #
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.9
 # by the following command:
 #
 #    pip-compile
 #
-boto3==1.21.46
+boto3==1.34.11
     # via lbz (setup.py)
-botocore==1.24.46
+botocore==1.34.11
     # via
     #   boto3
     #   s3transfer
@@ -16,9 +16,9 @@ jmespath==1.0.1
     # via
     #   boto3
     #   botocore
-multidict==6.0.2
+multidict==6.0.4
     # via lbz (setup.py)
-pyasn1==0.4.8
+pyasn1==0.5.1
     # via
     #   python-jose
     #   rsa
@@ -28,11 +28,11 @@ python-jose==3.3.0
     # via lbz (setup.py)
 rsa==4.9
     # via python-jose
-s3transfer==0.5.2
+s3transfer==0.10.0
     # via boto3
 six==1.16.0
     # via
     #   ecdsa
     #   python-dateutil
-urllib3==1.26.11
+urllib3==1.26.18
     # via botocore
diff --git a/setup.cfg b/setup.cfg
index 8f09253..0dcc17c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,13 +1,14 @@
 [flake8]
 # https://flake8.pycqa.org/en/latest/user/configuration.html
-ignore = W503
+ignore =
+  W503
+  # black compatibility
+  E203
 max-complexity = 7
 max-line-length = 99
 per-file-ignores =
   # imported but unused
   __init__.py: F401
-  # hardcoded RSA key pair
-  tests/fixtures/rsa_pair.py: E501
 
 [isort]
 # https://pycqa.github.io/isort/docs/configuration/options
@@ -28,7 +29,6 @@ namespace_packages = True
 
 # https://mypy.readthedocs.io/en/latest/config_file.html
 check_untyped_defs = True
-# disallow_any_generics = True  # TODO: start using this option in the future
 disallow_incomplete_defs = True
 disallow_untyped_calls = True
 disallow_untyped_defs = True
@@ -42,30 +42,26 @@ warn_unused_ignores = True
 
 show_column_numbers = True
 show_error_codes = True
-# TODO: start validating the rest of the code by mypy
-exclude = tests/test_request.py|tests/test_resource.py|tests/test_dev_server.py
 
 [pylint.BASIC]
-# https://pylint.pycqa.org/en/latest/technical_reference/features.html#basic-checker
-good-names=i,j,k,ex,op,s3,_,id
+# https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#basic-checker
+good-names=i,j,k,ex,op,s3,_,id,pytest_plugins
 
-[pylint.MASTER]
-jobs = 3
+[pylint.FORMAT]
+# https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#format-checker
 max-line-length = 99
 
-load-plugins =
-  pylint.extensions.bad_builtin,
-  pylint.extensions.broad_try_clause,
-  pylint.extensions.check_elif,
-  pylint.extensions.comparetozero,
-  pylint.extensions.empty_comment,
-  pylint.extensions.emptystring,
-  pylint.extensions.overlapping_exceptions,
-
-[pylint.MESSAGES CONTROL]
-# https://pylint.pycqa.org/en/latest/technical_reference/features.html#messages-control-options
+[pylint.MAIN]
+# https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#main-checker
+enable =
+  useless-suppression,
 disable =
+  # code style
+  consider-using-namedtuple-or-dataclass,
   duplicate-code,
+  too-few-public-methods,
+  too-many-public-methods,
+  use-implicit-booleaness-not-comparison,
   # docstring
   missing-class-docstring,
   missing-function-docstring,
@@ -76,11 +72,25 @@ disable =
   wrong-import-order,
   wrong-import-position,
   # library specific
-  too-few-public-methods,
   too-many-instance-attributes,
   too-many-arguments,
-  too-many-try-statements,
+jobs = 3
+load-plugins =
+  pylint.extensions.bad_builtin,
+  pylint.extensions.check_elif,
+  pylint.extensions.code_style,
+  pylint.extensions.comparison_placement,
+  pylint.extensions.confusing_elif,
+  pylint.extensions.docstyle,
+  pylint.extensions.empty_comment,
+  pylint.extensions.eq_without_hash,
+  pylint.extensions.for_any_all,
+  pylint.extensions.overlapping_exceptions,
+  pylint.extensions.private_import,
+  pylint.extensions.redefined_loop_name,
+  pylint.extensions.redefined_variable_type,
+  pylint.extensions.typing,
 
 [pylint.MISCELLANEOUS]
-# http://pylint.pycqa.org/en/latest/technical_reference/features.html#miscellaneous-checker-options
+# https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#miscellaneous-checker
 notes = FIXME
diff --git a/setup.py b/setup.py
index ac0cf0a..5681299 100644
--- a/setup.py
+++ b/setup.py
@@ -17,9 +17,9 @@
     long_description_content_type="text/markdown",
     long_description=pathlib.Path("README.md").read_text("utf-8"),
     install_requires=[
-        "boto3 >=1.21.0, <1.22.0",
-        "multidict >=6.0.0, <6.1.0",
-        "python-jose >=3.3.0, <3.4.0",
+        "boto3>=1.34.11,<1.35.0",
+        "multidict>=6.0.4,<6.1.0",
+        "python-jose>=3.3.0,<3.4.0",
     ],
     classifiers=[
         "Development Status :: 5 - Production/Stable",
@@ -27,10 +27,11 @@
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
         "Programming Language :: Python",
-        "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
         "Topic :: Software Development :: Libraries :: Python Modules",
     ],
-    python_requires=">=3.8",
+    python_requires=">=3.9",
 )
diff --git a/version b/version
index 5ca9ac6..a918a2a 100644
--- a/version
+++ b/version
@@ -1,1 +1,1 @@
-0.5.19
+0.6.0


Test patch:
diff --git a/lbz/dev/test.py b/lbz/dev/test.py
index cedfc4d..b4ed4c2 100644
--- a/lbz/dev/test.py
+++ b/lbz/dev/test.py
@@ -1,5 +1,4 @@
-"""Helper class for testing."""
-from typing import Optional, Type
+from __future__ import annotations
 
 from lbz.dev.misc import APIGatewayEvent
 from lbz.misc import get_logger
@@ -10,60 +9,58 @@
 
 
 class Client:
-    """
-    Client created for testing purposes.
-    """
+    """Client created for testing purposes."""
 
-    def __init__(self, resource: Type[Resource]):
+    def __init__(self, resource: type[Resource]):
         self.resource = resource
 
     def post(
         self,
         path: str,
-        params: dict = None,
-        query_params: dict = None,
-        body: dict = None,
-        headers: dict = None,
+        params: dict | None = None,
+        query_params: dict | None = None,
+        body: dict | None = None,
+        headers: dict | None = None,
     ) -> Response:
         return self._process(path, "POST", params, query_params, body, headers)
 
     def get(
         self,
         path: str,
-        params: dict = None,
-        query_params: dict = None,
-        body: dict = None,
-        headers: dict = None,
+        params: dict | None = None,
+        query_params: dict | None = None,
+        body: dict | None = None,
+        headers: dict | None = None,
     ) -> Response:
         return self._process(path, "GET", params, query_params, body, headers)
 
     def patch(
         self,
         path: str,
-        params: dict = None,
-        query_params: dict = None,
-        body: dict = None,
-        headers: dict = None,
+        params: dict | None = None,
+        query_params: dict | None = None,
+        body: dict | None = None,
+        headers: dict | None = None,
     ) -> Response:
         return self._process(path, "PATCH", params, query_params, body, headers)
 
     def put(
         self,
         path: str,
-        params: dict = None,
-        query_params: dict = None,
-        body: dict = None,
-        headers: dict = None,
+        params: dict | None = None,
+        query_params: dict | None = None,
+        body: dict | None = None,
+        headers: dict | None = None,
     ) -> Response:
         return self._process(path, "PUT", params, query_params, body, headers)
 
     def delete(
         self,
         path: str,
-        params: dict = None,
-        query_params: dict = None,
-        body: dict = None,
-        headers: dict = None,
+        params: dict | None = None,
+        query_params: dict | None = None,
+        body: dict | None = None,
+        headers: dict | None = None,
     ) -> Response:
         return self._process(path, "DELETE", params, query_params, body, headers)
 
@@ -71,10 +68,10 @@ def _process(
         self,
         path: str,
         method: str,
-        params: Optional[dict],
-        query_params: Optional[dict],
-        body: Optional[dict],
-        headers: Optional[dict],
+        params: dict | None,
+        query_params: dict | None,
+        body: dict | None,
+        headers: dict | None,
     ) -> Response:
         if query_params:
             for key, value in query_params.items():
diff --git a/tests/conftest.py b/tests/conftest.py
index d5f115f..69dbb34 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,7 @@
 import json
+from collections.abc import Iterator
 from datetime import datetime, timedelta
 from os import environ
-from typing import Iterator, List, Type
 from unittest.mock import patch
 from uuid import uuid4
 
@@ -34,12 +34,12 @@
 
 
 @pytest.fixture(scope="session", name="allowed_audiences")
-def allowed_audiences_fixture() -> List[str]:
+def allowed_audiences_fixture() -> list[str]:
     return [str(uuid4()), str(uuid4())]
 
 
 @pytest.fixture(autouse=True)
-def setting_initial_lbz_configuration(allowed_audiences: List[str]) -> Iterator[None]:
+def setting_initial_lbz_configuration(allowed_audiences: list[str]) -> Iterator[None]:
     patched_environ = {
         "AUTH_REMOVE_PREFIXES": "1",
         "ALLOWED_PUBLIC_KEYS": json.dumps({"keys": [SAMPLE_PUBLIC_KEY]}),
@@ -104,7 +104,7 @@ def sample_event() -> APIGatewayEvent:
 
 
 @pytest.fixture(scope="session", name="jwt_partial_payload")
-def jwt_partial_payload_fixture(allowed_audiences: List[str]) -> dict:
+def jwt_partial_payload_fixture(allowed_audiences: list[str]) -> dict:
     return {
         "exp": int((datetime.utcnow() + timedelta(hours=6)).timestamp()),
         "iat": int(datetime.utcnow().timestamp()),
@@ -191,7 +191,7 @@ def sample_request_with_user(user: User) -> Request:
 
 
 @pytest.fixture()
-def sample_resource() -> Type[Resource]:
+def sample_resource() -> type[Resource]:
     """Be careful when doing any changes in this fixture"""
 
     class HelloWorld(Resource):
@@ -207,7 +207,7 @@ def get(self) -> Response:
 
 
 @pytest.fixture()
-def sample_resource_with_authorization() -> Type[Resource]:
+def sample_resource_with_authorization() -> type[Resource]:
     """Be careful when doing any changes in this fixture - especially for Auth Collector"""
 
     class XResource(Resource):
diff --git a/tests/fixtures/rsa_pair.py b/tests/fixtures/rsa_pair.py
index 48704e2..fdb44fd 100644
--- a/tests/fixtures/rsa_pair.py
+++ b/tests/fixtures/rsa_pair.py
@@ -1,3 +1,4 @@
+# flake8: noqa
 EXPECTED_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk0OTRhZDc1LTNmNTQtNDE1NS04NGZhLWMxYTE3ZGEyMmIzNSIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6eyIqIjoiKiJ9LCJkZW55Ijp7fX0.nDqCxO2Q1iXpxzbH7syxuyqw7kCY0sDfi9RX-VSUMTRN5aWTLt1bcPw4oN_jx89-YHBzDwnwBc07RsMgpFuo4zz2LU9PF0ciYxMNX-atTNsaIn05NkXT08au2AYb0DRCDS76MZ4QNi-4mRpLrj1SD4mSCwGtc2WNw9f0J0Vm4ZCYPVW6BqpcHcaFXzcFZ6EIoooaK6GvdTOjy498lWsAXjAen2U6Jles_BwFjqW1lW_ky4WV4J9NnK3v5wWKgR1Pg4R4LpnhIXe0dU_l64JHoJA3YcYxl-qilHfoBduc3La4kRKk7FAQDIqbOv4uN03BIoDXLH5t2uJ1Sm79Pe0ngGd5pSBmfUDKOGsHtx_3_9ZKfp-E2IVS0C7r36p4Ue0gKQzn0pXxa591bxm_puJAQ399SdbmlOJsM2cVFYAtlUQvWgErc57WcUJ0Qe4jEycury7hagNbP2fLn-7Gg4gZHiZ_Ul7L6GukbDfCHnhxSS4P3t3cVtWuslZi16hDhNbOTKD95y7PXvHePvI57ALV2v0RecQ5Blwurt1OuDRSjCYXyO6U4Y9MBHcd1wMtDoVW0jjvjXvqkEhuB52Zajh_yTNnJo0OAHpuK5wldVpECGFVx1rkW1ypKqlukGIgD--m6ElKnl6jw5VWSbdh2TJsZHnzjovbQUeqZOeMxwX6SE8"
 SAMPLE_PRIVATE_KEY = {
     "p": "5Cwk4zIvta07E37iZVlnzqeQX2jV1GaHKSUFtUVeaMVY_FBQ5Yr5ux7bxTKrikSs22QI8z1x6GVuEH3MGeh3qjOyUTfJJyOcS_RVmQxdYwjkxOsN953SWMvPRhkLd-svB8LI4Ylwo1NrlSBJOQqM2xvtoQ7KkEAJquvX_UHkTuMuSCcpuDyp-qUkvgfbSUgvCacmwNf-bXh2kKVM14YKt3el4vYITetyy96jzSLKf6V36AX8PFbSOS7oZbeO3dgfGp0MDcpF0flu8McaSipBXoHbrjtxx0MOJjpU3Fhpy6Q7o7TX3-OC7fKplseHol4dxkRCsiDoyCxjhUzPHShy8Q",
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 891cbdc..646c175 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -50,11 +50,11 @@ def test_decoding_user_raises_unauthorized_when_invalid_public_key(user_token: s
 
 def test_loading_user_parses_user_attributes(user_cognito: dict, user: User) -> None:
     parsed = user_cognito.copy()
-    for key in ["aud", "iss", "exp", "iat"]:
+    for key in ("aud", "iss", "exp", "iat"):
         parsed.pop(key, None)
     for key, expected_value in parsed.items():
-        key = key.replace("cognito:", "").replace("custom:", "")
-        value = getattr(user, key)
+        key_without_prefix = key.replace("cognito:", "").replace("custom:", "")
+        value = getattr(user, key_without_prefix)
         assert value == expected_value
 
 
diff --git a/tests/test_authz_authorizer.py b/tests/test_authz_authorizer.py
index 66c6377..106c201 100644
--- a/tests/test_authz_authorizer.py
+++ b/tests/test_authz_authorizer.py
@@ -46,7 +46,6 @@ def test_validate_one_with_expired(self, full_access_authz_payload: dict) -> Non
             )
 
 
-# pylint: disable=too-many-public-methods
 class TestAuthorizerWithMockedJWT:
     @staticmethod
     def _make_mocked_authorizer(token_payload: dict) -> Authorizer:
diff --git a/tests/test_authz_decorator.py b/tests/test_authz_decorator.py
index b31d82d..5246798 100644
--- a/tests/test_authz_decorator.py
+++ b/tests/test_authz_decorator.py
@@ -1,6 +1,5 @@
 # coding=utf-8
 from http import HTTPStatus
-from typing import Type
 from unittest.mock import MagicMock, patch
 
 import pytest
@@ -13,7 +12,7 @@
 
 
 @pytest.fixture(name="sample_guest_resource")
-def sample_guest_resource_fixture() -> Type[Resource]:
+def sample_guest_resource_fixture() -> type[Resource]:
     class GuestResource(Resource):
         _name = "guest_res"
 
@@ -47,7 +46,7 @@ def handler(self, restrictions: dict) -> None:
         mocked_authz_collector.add_authz.assert_called_once()
 
     def test_root_permissions_success(
-        self, sample_resource_with_authorization: Type[Resource], full_access_auth_header: str
+        self, sample_resource_with_authorization: type[Resource], full_access_auth_header: str
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent("/", "GET", headers={"authorization": full_access_auth_header})
@@ -55,7 +54,7 @@ def test_root_permissions_success(
         assert res_instance().status_code == HTTPStatus.OK
 
     def test_limited_permissions_success(
-        self, limited_access_auth_header: str, sample_resource_with_authorization: Type[Resource]
+        self, limited_access_auth_header: str, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent("/", "GET", headers={"authorization": limited_access_auth_header})
@@ -63,7 +62,7 @@ def test_limited_permissions_success(
         assert res_instance().status_code == HTTPStatus.OK
 
     def test_limited_permissions_failed(
-        self, limited_access_auth_header: str, sample_resource_with_authorization: Type[Resource]
+        self, limited_access_auth_header: str, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent(
@@ -74,18 +73,18 @@ def test_limited_permissions_failed(
 
 
 class TestAuthorizationDecoratorGuestPermissions:
-    def test_get_success(self, sample_guest_resource: Type[Resource]) -> None:
+    def test_get_success(self, sample_guest_resource: type[Resource]) -> None:
         res_instance = sample_guest_resource(APIGatewayEvent("/", "GET"))
         assert res_instance().status_code == HTTPStatus.OK
 
-    def test_limited_permissions_failed(self, sample_guest_resource: Type[Resource]) -> None:
+    def test_limited_permissions_failed(self, sample_guest_resource: type[Resource]) -> None:
         res_instance = sample_guest_resource(APIGatewayEvent("/garbage2", "GET"))
         assert res_instance().status_code == HTTPStatus.FORBIDDEN
 
     def test_inharitance_success(
         self,
         full_access_auth_header: str,
-        sample_guest_resource: Type[Resource],
+        sample_guest_resource: type[Resource],
     ) -> None:
         res_instance = sample_guest_resource(
             APIGatewayEvent("/garbage2", "GET", headers={"authorization": full_access_auth_header})
diff --git a/tests/test_authz_utils.py b/tests/test_authz_utils.py
index a0a3016..e3297c5 100644
--- a/tests/test_authz_utils.py
+++ b/tests/test_authz_utils.py
@@ -1,6 +1,4 @@
 # coding=utf-8
-from typing import Type
-
 import pytest
 
 from lbz.authz.utils import check_permission, has_permission
@@ -11,7 +9,7 @@
 
 class TestAuthorizationUtils:
     def test_check_permission(
-        self, limited_access_auth_header: str, sample_resource_with_authorization: Type[Resource]
+        self, limited_access_auth_header: str, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent("/", "GET", headers={"authorization": limited_access_auth_header})
@@ -19,7 +17,7 @@ def test_check_permission(
         assert check_permission(res_instance, "perm-name") == {"allow": "*", "deny": None}
 
     def test_check_permission_raises(
-        self, limited_access_auth_header: str, sample_resource_with_authorization: Type[Resource]
+        self, limited_access_auth_header: str, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent("/", "GET", headers={"authorization": limited_access_auth_header})
@@ -28,14 +26,14 @@ def test_check_permission_raises(
             check_permission(res_instance, "garbage")
 
     def test_check_permission_unauthorised(
-        self, sample_event: APIGatewayEvent, sample_resource_with_authorization: Type[Resource]
+        self, sample_event: APIGatewayEvent, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(sample_event)
         with pytest.raises(Unauthorized):
             check_permission(res_instance, "perm-name")
 
     def test_has_permission_true(
-        self, limited_access_auth_header: str, sample_resource_with_authorization: Type[Resource]
+        self, limited_access_auth_header: str, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent(
@@ -45,7 +43,7 @@ def test_has_permission_true(
         assert has_permission(res_instance, "perm-name")
 
     def test_has_permission_false(
-        self, limited_access_auth_header: str, sample_resource_with_authorization: Type[Resource]
+        self, limited_access_auth_header: str, sample_resource_with_authorization: type[Resource]
     ) -> None:
         res_instance = sample_resource_with_authorization(
             APIGatewayEvent(
diff --git a/tests/test_collector.py b/tests/test_collector.py
index 1d7cc48..3b8c8fc 100644
--- a/tests/test_collector.py
+++ b/tests/test_collector.py
@@ -11,7 +11,7 @@ def test__singleton_only_one_exists(self) -> None:
     def test__init__(self) -> None:
         azc = AuthzCollector()
         assert azc.possible_permissions == {}
-        assert azc.resource_name == ""  # pylint: disable=compare-to-empty-string
+        assert azc.resource_name == ""
         assert azc.guest_permissions == {}
 
     def test_set_resource(self) -> None:
@@ -67,5 +67,5 @@ def test_clean(self) -> None:
         azc.clean()
 
         assert azc.possible_permissions == {}
-        assert azc.resource_name == ""  # pylint: disable=compare-to-empty-string
+        assert azc.resource_name == ""
         assert azc.guest_permissions == {}
diff --git a/tests/test_dev_server.py b/tests/test_dev_server.py
index 9ae98ef..58cf37f 100644
--- a/tests/test_dev_server.py
+++ b/tests/test_dev_server.py
@@ -3,7 +3,6 @@
 import json
 import socket
 from socketserver import BaseServer
-from typing import Type
 from unittest import mock
 from urllib import request
 
@@ -14,10 +13,10 @@
 class MyClass:
     def __init__(self) -> None:
         self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self.tcp_socket.connect("0.0.0.0", "8888")
+        self.tcp_socket.connect("0.0.0.0", "8888")  # type: ignore
 
 
-def test_my_lambda_dev_handler(sample_resource: Type[Resource]) -> None:
+def test_my_lambda_dev_handler(sample_resource: type[Resource]) -> None:
     with mock.patch("socket.socket") as msocket:
 
         class MyLambdaDevHandlerHelloWorld(MyLambdaDevHandler):
@@ -26,8 +25,12 @@ class MyLambdaDevHandlerHelloWorld(MyLambdaDevHandler):
         msocket.makefile = lambda a, b: io.BytesIO(b"GET / HTTP/1.1\r\n")
         msocket.rfile.close = lambda: 0
         my_class = MyClass()
-        handler = MyLambdaDevHandlerHelloWorld(msocket, ("127.0.0.1", 8888), BaseServer)
-        my_class.tcp_socket.connect.assert_called_with(  # pylint: disable=no-member
+        handler = MyLambdaDevHandlerHelloWorld(
+            msocket,
+            ("127.0.0.1", 8888),
+            BaseServer,  # type: ignore
+        )
+        my_class.tcp_socket.connect.assert_called_with(  # type: ignore # pylint: disable=no-member
             "0.0.0.0", "8888"
         )
     path, params = handler._get_route_params("/")  # pylint: disable=protected-access
@@ -39,7 +42,7 @@ class MyLambdaDevHandlerHelloWorld(MyLambdaDevHandler):
     assert params == {"id": "123"}
 
 
-def test_my_dev_server(sample_resource: Type[Resource]) -> None:
+def test_my_dev_server(sample_resource: type[Resource]) -> None:
     dev_serv = MyDevServer(sample_resource)
     assert dev_serv.server_address == ("localhost", 8000)
     assert dev_serv.port == 8000
@@ -47,7 +50,7 @@ def test_my_dev_server(sample_resource: Type[Resource]) -> None:
     assert issubclass(dev_serv.my_handler, MyLambdaDevHandler)
 
 
-def test_server_can_run_in_background(sample_resource: Type[Resource]) -> None:
+def test_server_can_run_in_background(sample_resource: type[Resource]) -> None:
     dev_serv = MyDevServer(sample_resource, port=9999)
     dev_serv.start()
     url = "http://localhost:9999/"
diff --git a/tests/test_dev_test.py b/tests/test_dev_test.py
index 6e3a1e7..805c9ae 100644
--- a/tests/test_dev_test.py
+++ b/tests/test_dev_test.py
@@ -1,12 +1,10 @@
 # coding=utf-8
-from typing import Type
-
 from lbz.dev.test import Client
 from lbz.resource import Resource
 from lbz.response import Response
 
 
-def test_client(sample_resource: Type[Resource]) -> None:
+def test_client(sample_resource: type[Resource]) -> None:
     client = Client(sample_resource)
     resp = client.get("/")
     assert isinstance(resp, Response)
diff --git a/tests/test_events/test_api.py b/tests/test_events/test_api.py
index 52a7a18..dbd2bbe 100644
--- a/tests/test_events/test_api.py
+++ b/tests/test_events/test_api.py
@@ -1,5 +1,5 @@
 import logging
-from typing import Callable
+from collections.abc import Callable
 from unittest.mock import MagicMock, patch
 
 import pytest
@@ -20,7 +20,6 @@ def setup_method(self) -> None:
         self.event_api = EventAPI()
 
     def teardown_method(self, _test_method: Callable) -> None:
-        # pylint: disable= attribute-defined-outside-init
         self.event_api._del()  # type: ignore # pylint: disable=protected-access
 
     @patch.object(Boto3Client, "eventbridge", MagicMock())
diff --git a/tests/test_events/test_broker.py b/tests/test_events/test_broker.py
index 9dc67bb..516cefb 100644
--- a/tests/test_events/test_broker.py
+++ b/tests/test_events/test_broker.py
@@ -1,6 +1,6 @@
 import logging
+from collections.abc import Callable, Mapping
 from copy import deepcopy
-from typing import Callable, Dict, List, Mapping
 from unittest.mock import MagicMock
 
 import pytest
@@ -15,7 +15,7 @@ def test_broker_works_properly(self) -> None:
         func_1 = MagicMock()
         func_2 = MagicMock()
         expected_event = Event({"y": 1}, event_type="x")
-        mapper: Mapping[str, List[Callable[[Event], None]]] = {"x": [func_1, func_2]}
+        mapper: Mapping[str, list[Callable[[Event], None]]] = {"x": [func_1, func_2]}
         event = {"my-type": "x", "data": {"y": 1}}
 
         BaseEventBroker(
@@ -32,7 +32,7 @@ def test_broker_works_properly(self) -> None:
     def test_broker_raises_not_implemented_when_event_type_is_not_recognized(self) -> None:
         func_1 = MagicMock()
         func_2 = MagicMock()
-        mapper: Mapping[str, List[Callable[[Event], None]]] = {"x": [func_1, func_2]}
+        mapper: Mapping[str, list[Callable[[Event], None]]] = {"x": [func_1, func_2]}
         event = {"my-type": "y", "data": {"y": 1}}
 
         with pytest.raises(NotImplementedError, match="No handlers implemented for y"):
@@ -52,7 +52,7 @@ def test_broker_continues_even_if_one_handler_failed(self, caplog: LogCaptureFix
         func_2 = MagicMock(side_effect=TypeError)
         func_3 = MagicMock()
         expected_event = Event({"y": 1}, event_type="x")
-        mapper: Mapping[str, List[Callable[[Event], None]]] = {"x": [func_1, func_2, func_3]}
+        mapper: Mapping[str, list[Callable[[Event], None]]] = {"x": [func_1, func_2, func_3]}
         event = {"my-type": "x", "data": {"y": 1}}
 
         BaseEventBroker(
@@ -76,14 +76,14 @@ def test_broker_continues_even_if_one_handler_failed(self, caplog: LogCaptureFix
 
     def test_broker_events_are_not_affected_by_specific_event_handler(self) -> None:
         expected_events = [Event({"y": 1}, event_type="x")] * 3
-        passed_events: List[Event] = []
+        passed_events: list[Event] = []
 
         def handler(event: Event) -> None:
             passed_events.append(deepcopy(event))
             event.data["y"] += 5
             event.data["b"] = 50
 
-        mapper: Dict[str, list] = {"x": [handler, handler, handler]}
+        mapper: dict[str, list] = {"x": [handler, handler, handler]}
         event_payload = {"my-type": "x", "data": {"y": 1}}
 
         BaseEventBroker(
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index a91965b..b963061 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,6 +1,4 @@
 # coding=utf-8
-from typing import Type
-
 import pytest
 
 from lbz.exceptions import (
@@ -71,7 +69,7 @@ def test__unsupported_method__builds_message_based_on_method_provided_from_outsi
 
 @pytest.mark.parametrize("custom_exception", set(all_lbz_errors()) - {UnsupportedMethod})
 def test__custom_exception__contains_message_and_status_code_according_to_its_docstring(
-    custom_exception: Type[LambdaFWException],
+    custom_exception: type[LambdaFWException],
 ) -> None:
     exception = custom_exception()
     try:
diff --git a/tests/test_lambdas/test_broker.py b/tests/test_lambdas/test_broker.py
index 6ebdc71..fd02b09 100644
--- a/tests/test_lambdas/test_broker.py
+++ b/tests/test_lambdas/test_broker.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import logging
 
 from pytest import LogCaptureFixture
@@ -7,7 +9,7 @@
 from lbz.type_defs import LambdaContext
 
 
-def simple_func(_data: dict = None) -> LambdaResponse:
+def simple_func(_data: dict | None = None) -> LambdaResponse:
     return lambda_ok_response()
 
 
diff --git a/tests/test_lambdas/test_client.py b/tests/test_lambdas/test_client.py
index 55f91be..b2643c2 100644
--- a/tests/test_lambdas/test_client.py
+++ b/tests/test_lambdas/test_client.py
@@ -1,7 +1,8 @@
+from __future__ import annotations
+
 import json
 import logging
 from io import BytesIO
-from typing import Optional
 from unittest.mock import MagicMock
 
 import pytest
@@ -26,7 +27,7 @@ def lambda_client_fixture(mocker: MockerFixture) -> MagicMock:
     ],
 )
 def test__invoke__returns_response_sending_provided_data_as_payload(
-    lambda_client: MagicMock, data: Optional[dict], expected_data_bytes: bytes
+    lambda_client: MagicMock, data: dict | None, expected_data_bytes: bytes
 ) -> None:
     response_payload = {"result": LambdaResult.OK, "data": "test"}
     lambda_client.invoke.return_value = {
diff --git a/tests/test_misc.py b/tests/test_misc.py
index a8f051a..6644c80 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -1,6 +1,6 @@
 # coding=utf-8
 from collections.abc import MutableMapping
-from typing import Any, Dict
+from typing import Any
 
 import pytest
 from pytest import LogCaptureFixture
@@ -145,7 +145,7 @@ def test__deep_update__does_recursive_updates_based_on_given_data() -> None:
 
 def test__deep_update__creates_copy_of_updated_data() -> None:
     dict_to_update: dict = {}
-    update_data: Dict[str, Any] = {"array": [], "dict": {"nested_dict": {}}}
+    update_data: dict[str, Any] = {"array": [], "dict": {"nested_dict": {}}}
 
     deep_update(dict_to_update, update_data)
     update_data["array"].append("post_update_data")
diff --git a/tests/test_request.py b/tests/test_request.py
index b0f5454..24cda7b 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -48,8 +48,8 @@ def test__decode_base64_str(self) -> None:
         assert output == b"j\xc7Z\xb1\xdb\x1d"
 
     def test_accessing_user_attributes(self, sample_request_with_user: Request) -> None:
-        assert isinstance(sample_request_with_user.user.username, str)
-        assert isinstance(sample_request_with_user.user.email, str)
+        assert isinstance(sample_request_with_user.user.username, str)  # type: ignore
+        assert isinstance(sample_request_with_user.user.email, str)  # type: ignore
         for custom_param in range(1, 5):
             assert isinstance(getattr(sample_request_with_user.user, str(custom_param)), str)
 
@@ -102,17 +102,17 @@ def test_json_body_bad_json(self, sample_request: Request) -> None:
             sample_request.json_body  # pylint: disable=pointless-statement
 
     def test_json_body_bad_content_type(self, sample_request: Request) -> None:
-        sample_request.headers = {"Content-Type": "application/dzejson"}
+        sample_request.headers = CIMultiDict({"Content-Type": "application/dzejson"})
         with pytest.raises(BadRequestError) as err:
             sample_request.json_body  # pylint: disable=pointless-statement
-            assert err.message.startswith("Content-Type header is missing or wrong")
+            assert err.value.message.startswith("Content-Type header is missing or wrong")
 
     def test_json_body_none_when_no_content_type(self, sample_request: Request) -> None:
-        sample_request.headers = {}
+        sample_request.headers = CIMultiDict({})
         assert sample_request.json_body is None
 
     def test_json_body_none_as_body(self, sample_request: Request) -> None:
-        sample_request._body = None  # pylint: disable=protected-access
+        sample_request._body = None  # type: ignore # pylint: disable=protected-access
         assert sample_request.json_body is None
 
 
@@ -125,5 +125,5 @@ def test_to_dict(self, sample_request_with_user: Request) -> None:
             "query_params": {},
             "stage_vars": {},
             "uri_params": {},
-            "user": f"User username={sample_request_with_user.user.username}",
+            "user": f"User username={sample_request_with_user.user.username}",  # type: ignore
         }
diff --git a/tests/test_resource.py b/tests/test_resource.py
index 177f486..c302e93 100644
--- a/tests/test_resource.py
+++ b/tests/test_resource.py
@@ -1,9 +1,12 @@
+from __future__ import annotations
+
 import json
 import logging
 from collections import defaultdict
+from collections.abc import Callable
 from http import HTTPStatus
 from os import environ
-from typing import Any, Callable, Dict, List
+from typing import Any
 from unittest.mock import ANY, MagicMock, patch
 
 from jose import jwt
@@ -45,7 +48,7 @@
 event = APIGatewayEvent(
     resource_path="/",
     method="GET",
-    body=req,  # pylint issue #214
+    body=req.to_dict(),
     headers={},
     path_params={},
     query_params={},
@@ -57,7 +60,7 @@
     headers={},
     path_params={},
     query_params={},
-    body=req,
+    body=req.to_dict(),
 )
 
 
@@ -281,10 +284,14 @@ def teardown_method(self) -> None:
         del environ["CORS_ORIGIN"]
 
     def make_cors_handler(
-        self, origins: List[str] = None, req_origin: str = None, cors_headers: list = None
+        self,
+        origins: list[str] | None = None,
+        req_origin: str | None = None,
+        cors_headers: list | None = None,
     ) -> CORSResource:
-        an_event: Dict[Any, MagicMock] = defaultdict(MagicMock())
-        an_event["headers"] = {"origin": req_origin} if req_origin is not None else {}
+        an_event: dict[Any, MagicMock] = defaultdict(MagicMock())
+        headers = {"origin": req_origin} if req_origin is not None else {}
+        an_event["headers"] = headers  # type: ignore
         cors_handler = CORSResource(
             an_event, ["GET", "POST"], origins=origins, cors_headers=cors_headers
         )
diff --git a/tests/test_router.py b/tests/test_router.py
index 430d556..1e16f05 100644
--- a/tests/test_router.py
+++ b/tests/test_router.py
@@ -54,7 +54,7 @@ def test_add_route(self, sample_router: Router) -> None:
 class TestAddRoute:
     def test_add_route(self) -> None:
         router = Router()
-        assert len(router) == 0  # pylint: disable=compare-to-zero
+        assert len(router) == 0
 
         @add_route("/")
         def random_method() -> None: