diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 6aa0073bf95b..edee3bc4febb 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,8 +1,7 @@
 # https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/README.md
-ARG VARIANT=3.12-bookworm
+ARG VARIANT=3.13-bookworm
 FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
 COPY requirements.txt /tmp/pip-tmp/
 RUN python3 -m pip install --upgrade pip \
-  && python3 -m pip install --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
-  && pipx install pre-commit ruff \
-  && pre-commit install
+  && python3 -m pip install --no-cache-dir -r /tmp/pip-tmp/requirements.txt \
+  && pipx install pre-commit ruff
diff --git a/.devcontainer/README.md b/.devcontainer/README.md
index ec3cdb61de7a..8056578ad3a8 100644
--- a/.devcontainer/README.md
+++ b/.devcontainer/README.md
@@ -1 +1,42 @@
-https://code.visualstudio.com/docs/devcontainers/tutorial
+# Development Container
+
+This is **Devcontainer** configuration to provide a consistent development environment for all contributors.
+
+## Features
+
+- [x] Pre-configured **Python environment**
+- [x] Automatic installation of **pre-commit hooks**
+- [x] **Ruff** linter ready to check your code
+- [x] **Oh My Zsh** with plugins:
+- `zsh-autosuggestions`
+- `zsh-syntax-highlighting`
+
+## Usage
+
+1. Install [**Docker** ](https://www.docker.com/get-started/) and [**Visual Studio Code**](https://code.visualstudio.com/)
+2. Install the **Remote - Containers** extension in VS Code
+
+    - Do `CTRL+P`, paste this command and press `Enter`
+
+        ```shell
+        ext install ms-vscode-remote.remote-containers
+        ```
+3. Open this repository in VS Code
+4. When prompted, click **"Reopen in Container"**
+5. Wait for the environment to build and initialize
+
+After setup:
+
+- `pre-commit` hooks are installed
+- `ruff` and other tools are available
+- The shell uses Zsh by default
+
+## Tips
+
+To manually run checks on all files:
+
+```bash
+pre-commit run --all-files
+```
+
+> For further information here's [Microsoft tutorial about devcontainers.](https://code.visualstudio.com/docs/devcontainers/tutorial)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index ae1d4fb7494d..4951d5eb268d 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -7,10 +7,12 @@
 			// Update 'VARIANT' to pick a Python version: 3, 3.11, 3.10, 3.9, 3.8
 			// Append -bullseye or -buster to pin to an OS version.
 			// Use -bullseye variants on local on arm64/Apple Silicon.
-			"VARIANT": "3.12-bookworm",
+			"VARIANT": "3.13-bookworm"
 		}
 	},
 
+	"postCreateCommand": "zsh .devcontainer/post_install",
+
 	// Configure tool-specific properties.
 	"customizations": {
 		// Configure properties specific to VS Code.
@@ -20,7 +22,8 @@
 				"python.defaultInterpreterPath": "/usr/local/bin/python",
 				"python.linting.enabled": true,
 				"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
-				"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy"
+				"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
+				"terminal.integrated.defaultProfile.linux": "zsh"
 			},
 
 			// Add the IDs of extensions you want installed when the container is created.
diff --git a/.devcontainer/post_install b/.devcontainer/post_install
new file mode 100755
index 000000000000..589ee361f5cb
--- /dev/null
+++ b/.devcontainer/post_install
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+echo "Begin post-installation steps..."
+
+set -e
+
+echo "Installing pre-commit hooks..."
+pre-commit install
+
+echo "Installing Oh My Zsh plugins..."
+
+# Install zsh-autosuggestions if not present
+if [ ! -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" ]; then
+  echo "Cloning zsh-autosuggestions..."
+  git clone https://github.com/zsh-users/zsh-autosuggestions \
+    "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions"
+fi
+
+# Install zsh-syntax-highlighting if not present
+if [ ! -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" ]; then
+  echo "Cloning zsh-syntax-highlighting..."
+  git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \
+    "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting"
+fi
+
+echo "Configuring plugins in ~/.zshrc..."
+sed -i '/^plugins=/c\plugins=(git zsh-autosuggestions zsh-syntax-highlighting)' ~/.zshrc
+
+echo "Post-installation steps completed successfully. Enjoy!"
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 05cd709a8f62..3cc25d1bae1c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -7,9 +7,7 @@
 
 # Order is important. The last matching pattern has the most precedence.
 
-/.*  @cclauss @dhruvmanila
-
-# /arithmetic_analysis/
+/.*  @cclauss
 
 # /backtracking/
 
@@ -21,15 +19,15 @@
 
 # /cellular_automata/
 
-# /ciphers/  @cclauss  # TODO: Uncomment this line after Hacktoberfest
+# /ciphers/
 
 # /compression/
 
 # /computer_vision/
 
-# /conversions/  @cclauss  # TODO: Uncomment this line after Hacktoberfest
+# /conversions/
 
-# /data_structures/  @cclauss  # TODO: Uncomment this line after Hacktoberfest
+# /data_structures/
 
 # /digital_image_processing/
 
@@ -67,7 +65,7 @@
 
 # /neural_network/
 
-# /other/  @cclauss  # TODO: Uncomment this line after Hacktoberfest
+# /other/
 
 # /project_euler/
 
@@ -81,7 +79,7 @@
 
 # /sorts/
 
-# /strings/  @cclauss  # TODO: Uncomment this line after Hacktoberfest
+# /strings/
 
 # /traversals/
 
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000000..15e494ec867e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+# Keep GitHub Actions up to date with Dependabot...
+# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 60c1d6d119d0..69192db0c4c6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -9,26 +9,30 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v5
+      - uses: astral-sh/setup-uv@v6
         with:
-          python-version: 3.12
-          allow-prereleases: true
-      - uses: actions/cache@v3
+          enable-cache: true
+          cache-dependency-glob: uv.lock
+      - uses: actions/setup-python@v5
         with:
-          path: ~/.cache/pip
-          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip setuptools six wheel
-          python -m pip install pytest-cov -r requirements.txt
+          python-version: 3.x
+          allow-prereleases: true
+      - run: uv sync --group=test
       - name: Run tests
         # TODO: #8818 Re-enable quantum tests
-        run: pytest
-            --ignore=quantum/q_fourier_transform.py
-            --ignore=project_euler/
-            --ignore=scripts/validate_solutions.py
-            --cov-report=term-missing:skip-covered
-            --cov=. .
+        run: uv run pytest
+          --ignore=computer_vision/cnn_classification.py
+          --ignore=docs/conf.py
+          --ignore=dynamic_programming/k_means_clustering_tensorflow.py
+          --ignore=machine_learning/lstm/lstm_prediction.py
+          --ignore=neural_network/input_data.py
+          --ignore=project_euler/
+          --ignore=quantum/q_fourier_transform.py
+          --ignore=scripts/validate_solutions.py
+          --ignore=web_programming/current_stock_price.py
+          --ignore=web_programming/fetch_anime_and_play.py
+          --cov-report=term-missing:skip-covered
+          --cov=. .
       - if: ${{ success() }}
         run: scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md
diff --git a/.github/workflows/devcontainer_ci.yml b/.github/workflows/devcontainer_ci.yml
new file mode 100644
index 000000000000..71623e5e6e69
--- /dev/null
+++ b/.github/workflows/devcontainer_ci.yml
@@ -0,0 +1,19 @@
+name: Test DevContainer Build
+
+on:
+  push:
+    paths:
+      - ".devcontainer/**"
+  pull_request:
+    paths:
+      - ".devcontainer/**"
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v5
+      - uses: devcontainers/ci@v0.3
+        with:
+          push: never
+          runCmd: "true"
diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml
index 702c15f1e29b..f5167f8d1a58 100644
--- a/.github/workflows/directory_writer.yml
+++ b/.github/workflows/directory_writer.yml
@@ -3,20 +3,20 @@
 name: directory_writer
 on: [push]
 jobs:
-  build:
+  directory_writer:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
         with:
           fetch-depth: 0
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: 3.x
       - name: Write DIRECTORY.md
         run: |
           scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md
-          git config --global user.name github-actions
-          git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com'
+          git config --global user.name "$GITHUB_ACTOR"
+          git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com"
           git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY
       - name: Update DIRECTORY.md
         run: |
diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml
index 7bbccf76e192..8b8cb2a1e68f 100644
--- a/.github/workflows/project_euler.yml
+++ b/.github/workflows/project_euler.yml
@@ -14,26 +14,22 @@ jobs:
   project-euler:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v5
+      - uses: astral-sh/setup-uv@v6
+      - uses: actions/setup-python@v5
         with:
           python-version: 3.x
-      - name: Install pytest and pytest-cov
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install --upgrade numpy pytest pytest-cov
-      - run: pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/
+      - run: uv sync --group=euler-validate --group=test
+      - run: uv run pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/
   validate-solutions:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v5
+      - uses: astral-sh/setup-uv@v6
+      - uses: actions/setup-python@v5
         with:
           python-version: 3.x
-      - name: Install pytest and requests
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install --upgrade numpy pytest requests
-      - run: pytest scripts/validate_solutions.py
+      - run: uv sync --group=euler-validate --group=test
+      - run: uv run pytest scripts/validate_solutions.py
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
index 496f1460e074..7bcc2850782f 100644
--- a/.github/workflows/ruff.yml
+++ b/.github/workflows/ruff.yml
@@ -11,6 +11,6 @@ jobs:
   ruff:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
-    - run: pip install --user ruff
-    - run: ruff --output-format=github .
+      - uses: actions/checkout@v5
+      - uses: astral-sh/setup-uv@v6
+      - run: uvx ruff check --output-format=github .
diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml
new file mode 100644
index 000000000000..a5502a586297
--- /dev/null
+++ b/.github/workflows/sphinx.yml
@@ -0,0 +1,50 @@
+name: sphinx
+
+on:
+  # Triggers the workflow on push or pull request events but only for the "master" branch
+  push:
+    branches: ["master"]
+  pull_request:
+    branches: ["master"]
+  # Or manually from the Actions tab
+  workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+  contents: read
+  pages: write
+  id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+  group: "pages"
+  cancel-in-progress: false
+
+jobs:
+  build_docs:
+    runs-on: ubuntu-24.04-arm
+    steps:
+      - uses: actions/checkout@v5
+      - uses: astral-sh/setup-uv@v6
+      - uses: actions/setup-python@v5
+        with:
+          python-version: 3.13
+          allow-prereleases: true
+      - run: uv sync --group=docs
+      - uses: actions/configure-pages@v5
+      - run: uv run sphinx-build -c docs . docs/_build/html
+      - uses: actions/upload-pages-artifact@v3
+        with:
+          path: docs/_build/html
+
+  deploy_docs:
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    if: github.event_name != 'pull_request'
+    needs: build_docs
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/deploy-pages@v4
+        id: deployment
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b3def463ded2..252cfebc53a8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v6.0.0
     hooks:
       - id: check-executables-have-shebangs
       - id: check-toml
@@ -11,29 +11,25 @@ repos:
       - id: requirements-txt-fixer
 
   - repo: https://github.com/MarcoGorelli/auto-walrus
-    rev: v0.2.2
+    rev: 0.3.4
     hooks:
-    -   id: auto-walrus
+      - id: auto-walrus
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.0.292
+    rev: v0.12.9
     hooks:
-      - id: ruff
-
-  - repo: https://github.com/psf/black
-    rev: 23.9.1
-    hooks:
-      - id: black
+      - id: ruff-check
+      - id: ruff-format
 
   - repo: https://github.com/codespell-project/codespell
-    rev: v2.2.6
+    rev: v2.4.1
     hooks:
       - id: codespell
         additional_dependencies:
           - tomli
 
   - repo: https://github.com/tox-dev/pyproject-fmt
-    rev: "1.2.0"
+    rev: v2.6.0
     hooks:
       - id: pyproject-fmt
 
@@ -46,16 +42,22 @@ repos:
         pass_filenames: false
 
   - repo: https://github.com/abravalheri/validate-pyproject
-    rev: v0.15
+    rev: v0.24.1
     hooks:
       - id: validate-pyproject
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.6.0
+    rev: v1.15.0
     hooks:
       - id: mypy
         args:
+          - --explicit-package-bases
           - --ignore-missing-imports
-          - --install-types # See mirrors-mypy README.md
+          - --install-types
           - --non-interactive
-        additional_dependencies: [types-requests]
+
+  - repo: https://github.com/pre-commit/mirrors-prettier
+    rev: v4.0.0-alpha.8
+    hooks:
+      - id: prettier
+        types_or: [toml, yaml]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bf3420185c1a..3df39f95b784 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,20 +2,20 @@
 
 ## Before contributing
 
-Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before sending your pull requests, make sure that you __read the whole guidelines__. If you have any doubt on the contributing guide, please feel free to [state it clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community in [Gitter](https://gitter.im/TheAlgorithms/community).
+Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before submitting your pull requests, please ensure that you __read the whole guidelines__. If you have any doubts about the contributing guide, please feel free to [state it clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community on [Gitter](https://gitter.im/TheAlgorithms/community).
 
 ## Contributing
 
 ### Contributor
 
-We are very happy that you are considering implementing algorithms and data structures for others! This repository is referenced and used by learners from all over the globe. Being one of our contributors, you agree and confirm that:
+We are delighted that you are considering implementing algorithms and data structures for others! This repository is referenced and used by learners from all over the globe. By being one of our contributors, you agree and confirm that:
 
-- You did your work - no plagiarism allowed
+- You did your work - no plagiarism allowed.
   - Any plagiarized work will not be merged.
-- Your work will be distributed under [MIT License](LICENSE.md) once your pull request is merged
-- Your submitted work fulfils or mostly fulfils our styles and standards
+- Your work will be distributed under [MIT License](LICENSE.md) once your pull request is merged.
+- Your submitted work fulfills or mostly fulfills our styles and standards.
 
-__New implementation__ is welcome! For example, new solutions for a problem, different representations for a graph data structure or algorithm designs with different complexity but __identical implementation__ of an existing implementation is not allowed. Please check whether the solution is already implemented or not before submitting your pull request.
+__New implementation__ is welcome! For example, new solutions for a problem, different representations for a graph data structure or algorithm designs with different complexity, but __identical implementation__ of an existing implementation is not allowed. Please check whether the solution is already implemented or not before submitting your pull request.
 
 __Improving comments__ and __writing proper tests__ are also highly welcome.
 
@@ -23,7 +23,7 @@ __Improving comments__ and __writing proper tests__ are also highly welcome.
 
 We appreciate any contribution, from fixing a grammar mistake in a comment to implementing complex algorithms. Please read this section if you are contributing your work.
 
-Your contribution will be tested by our [automated testing on GitHub Actions](https://github.com/TheAlgorithms/Python/actions) to save time and mental energy.  After you have submitted your pull request, you should see the GitHub Actions tests start to run at the bottom of your submission page.  If those tests fail, then click on the ___details___ button try to read through the GitHub Actions output to understand the failure.  If you do not understand, please leave a comment on your submission page and a community member will try to help.
+Your contribution will be tested by our [automated testing on GitHub Actions](https://github.com/TheAlgorithms/Python/actions) to save time and mental energy.  After you have submitted your pull request, you should see the GitHub Actions tests start to run at the bottom of your submission page. If those tests fail, then click on the ___details___ button to read through the GitHub Actions output to understand the failure.  If you do not understand, please leave a comment on your submission page and a community member will try to help.
 
 #### Issues
 
@@ -58,7 +58,7 @@ Algorithms should:
 * contain doctests that test both valid and erroneous input values
 * return all calculation results instead of printing or plotting them
 
-Algorithms in this repo should not be how-to examples for existing Python packages.  Instead, they should perform internal calculations or manipulations to convert input values into different output values.  Those calculations or manipulations can use data types, classes, or functions of existing Python packages but each algorithm in this repo should add unique value.
+Algorithms in this repo should not be how-to examples for existing Python packages. Instead, they should perform internal calculations or manipulations to convert input values into different output values. Those calculations or manipulations can use data types, classes, or functions of existing Python packages but each algorithm in this repo should add unique value.
 
 #### Pre-commit plugin
 Use [pre-commit](https://pre-commit.com/#installation) to automatically format your code to match our coding style:
@@ -77,7 +77,7 @@ pre-commit run --all-files --show-diff-on-failure
 
 We want your work to be readable by others; therefore, we encourage you to note the following:
 
-- Please write in Python 3.12+. For instance:  `print()` is a function in Python 3 so `print "Hello"` will *not* work but `print("Hello")` will.
+- Please write in Python 3.13+. For instance: `print()` is a function in Python 3 so `print "Hello"` will *not* work but `print("Hello")` will.
 - Please focus hard on the naming of functions, classes, and variables.  Help your reader by using __descriptive names__ that can help you to remove redundant comments.
   - Single letter variable names are *old school* so please avoid them unless their life only spans a few lines.
   - Expand acronyms because `gcd()` is hard to understand but `greatest_common_divisor()` is not.
@@ -96,7 +96,7 @@ We want your work to be readable by others; therefore, we encourage you to note
 
   ```bash
   python3 -m pip install ruff  # only required the first time
-  ruff .
+  ruff check
   ```
 
 - Original code submission require docstrings or comments to describe your work.
@@ -145,7 +145,7 @@ We want your work to be readable by others; therefore, we encourage you to note
   python3 -m doctest -v my_submission.py
   ```
 
-  The use of the Python builtin `input()` function is __not__ encouraged:
+  The use of the Python built-in `input()` function is __not__ encouraged:
 
   ```python
   input('Enter your input:')
diff --git a/DIRECTORY.md b/DIRECTORY.md
index 5b7ca856ea15..81d6f4c70864 100644
--- a/DIRECTORY.md
+++ b/DIRECTORY.md
@@ -1,17 +1,4 @@
 
-## Arithmetic Analysis
-  * [Bisection](arithmetic_analysis/bisection.py)
-  * [Gaussian Elimination](arithmetic_analysis/gaussian_elimination.py)
-  * [In Static Equilibrium](arithmetic_analysis/in_static_equilibrium.py)
-  * [Intersection](arithmetic_analysis/intersection.py)
-  * [Jacobi Iteration Method](arithmetic_analysis/jacobi_iteration_method.py)
-  * [Lu Decomposition](arithmetic_analysis/lu_decomposition.py)
-  * [Newton Forward Interpolation](arithmetic_analysis/newton_forward_interpolation.py)
-  * [Newton Method](arithmetic_analysis/newton_method.py)
-  * [Newton Raphson](arithmetic_analysis/newton_raphson.py)
-  * [Newton Raphson New](arithmetic_analysis/newton_raphson_new.py)
-  * [Secant Method](arithmetic_analysis/secant_method.py)
-
 ## Audio Filters
   * [Butterworth Filter](audio_filters/butterworth_filter.py)
   * [Iir Filter](audio_filters/iir_filter.py)
@@ -23,6 +10,8 @@
   * [All Subsequences](backtracking/all_subsequences.py)
   * [Coloring](backtracking/coloring.py)
   * [Combination Sum](backtracking/combination_sum.py)
+  * [Crossword Puzzle Solver](backtracking/crossword_puzzle_solver.py)
+  * [Generate Parentheses](backtracking/generate_parentheses.py)
   * [Hamiltonian Cycle](backtracking/hamiltonian_cycle.py)
   * [Knight Tour](backtracking/knight_tour.py)
   * [Match Word Pattern](backtracking/match_word_pattern.py)
@@ -33,6 +22,8 @@
   * [Rat In Maze](backtracking/rat_in_maze.py)
   * [Sudoku](backtracking/sudoku.py)
   * [Sum Of Subsets](backtracking/sum_of_subsets.py)
+  * [Word Break](backtracking/word_break.py)
+  * [Word Ladder](backtracking/word_ladder.py)
   * [Word Search](backtracking/word_search.py)
 
 ## Bit Manipulation
@@ -47,6 +38,9 @@
   * [Bitwise Addition Recursive](bit_manipulation/bitwise_addition_recursive.py)
   * [Count 1S Brian Kernighan Method](bit_manipulation/count_1s_brian_kernighan_method.py)
   * [Count Number Of One Bits](bit_manipulation/count_number_of_one_bits.py)
+  * [Excess 3 Code](bit_manipulation/excess_3_code.py)
+  * [Find Previous Power Of Two](bit_manipulation/find_previous_power_of_two.py)
+  * [Find Unique Number](bit_manipulation/find_unique_number.py)
   * [Gray Code Sequence](bit_manipulation/gray_code_sequence.py)
   * [Highest Set Bit](bit_manipulation/highest_set_bit.py)
   * [Index Of Rightmost Set Bit](bit_manipulation/index_of_rightmost_set_bit.py)
@@ -66,6 +60,8 @@
 ## Boolean Algebra
   * [And Gate](boolean_algebra/and_gate.py)
   * [Imply Gate](boolean_algebra/imply_gate.py)
+  * [Karnaugh Map Simplification](boolean_algebra/karnaugh_map_simplification.py)
+  * [Multiplexer](boolean_algebra/multiplexer.py)
   * [Nand Gate](boolean_algebra/nand_gate.py)
   * [Nimply Gate](boolean_algebra/nimply_gate.py)
   * [Nor Gate](boolean_algebra/nor_gate.py)
@@ -91,7 +87,7 @@
   * [Baconian Cipher](ciphers/baconian_cipher.py)
   * [Base16](ciphers/base16.py)
   * [Base32](ciphers/base32.py)
-  * [Base64](ciphers/base64.py)
+  * [Base64 Cipher](ciphers/base64_cipher.py)
   * [Base85](ciphers/base85.py)
   * [Beaufort Cipher](ciphers/beaufort_cipher.py)
   * [Bifid](ciphers/bifid.py)
@@ -105,6 +101,7 @@
   * [Elgamal Key Generator](ciphers/elgamal_key_generator.py)
   * [Enigma Machine2](ciphers/enigma_machine2.py)
   * [Fractionated Morse Cipher](ciphers/fractionated_morse_cipher.py)
+  * [Gronsfeld Cipher](ciphers/gronsfeld_cipher.py)
   * [Hill Cipher](ciphers/hill_cipher.py)
   * [Mixed Keyword Cypher](ciphers/mixed_keyword_cypher.py)
   * [Mono Alphabetic Ciphers](ciphers/mono_alphabetic_ciphers.py)
@@ -120,6 +117,7 @@
   * [Rsa Cipher](ciphers/rsa_cipher.py)
   * [Rsa Factorization](ciphers/rsa_factorization.py)
   * [Rsa Key Generator](ciphers/rsa_key_generator.py)
+  * [Running Key Cipher](ciphers/running_key_cipher.py)
   * [Shuffled Shift Cipher](ciphers/shuffled_shift_cipher.py)
   * [Simple Keyword Cypher](ciphers/simple_keyword_cypher.py)
   * [Simple Substitution Cipher](ciphers/simple_substitution_cipher.py)
@@ -130,20 +128,13 @@
   * [Vigenere Cipher](ciphers/vigenere_cipher.py)
   * [Xor Cipher](ciphers/xor_cipher.py)
 
-## Compression
-  * [Burrows Wheeler](compression/burrows_wheeler.py)
-  * [Huffman](compression/huffman.py)
-  * [Lempel Ziv](compression/lempel_ziv.py)
-  * [Lempel Ziv Decompress](compression/lempel_ziv_decompress.py)
-  * [Lz77](compression/lz77.py)
-  * [Peak Signal To Noise Ratio](compression/peak_signal_to_noise_ratio.py)
-  * [Run Length Encoding](compression/run_length_encoding.py)
-
 ## Computer Vision
+  * [Cnn Classification](computer_vision/cnn_classification.py)
   * [Flip Augmentation](computer_vision/flip_augmentation.py)
   * [Haralick Descriptors](computer_vision/haralick_descriptors.py)
   * [Harris Corner](computer_vision/harris_corner.py)
   * [Horn Schunck](computer_vision/horn_schunck.py)
+  * [Intensity Based Segmentation](computer_vision/intensity_based_segmentation.py)
   * [Mean Threshold](computer_vision/mean_threshold.py)
   * [Mosaic Augmentation](computer_vision/mosaic_augmentation.py)
   * [Pooling Functions](computer_vision/pooling_functions.py)
@@ -162,6 +153,7 @@
   * [Excel Title To Column](conversions/excel_title_to_column.py)
   * [Hex To Bin](conversions/hex_to_bin.py)
   * [Hexadecimal To Decimal](conversions/hexadecimal_to_decimal.py)
+  * [Ipv4 Conversion](conversions/ipv4_conversion.py)
   * [Length Conversion](conversions/length_conversion.py)
   * [Molecular Chemistry](conversions/molecular_chemistry.py)
   * [Octal To Binary](conversions/octal_to_binary.py)
@@ -170,18 +162,33 @@
   * [Prefix Conversions](conversions/prefix_conversions.py)
   * [Prefix Conversions String](conversions/prefix_conversions_string.py)
   * [Pressure Conversions](conversions/pressure_conversions.py)
+  * [Rectangular To Polar](conversions/rectangular_to_polar.py)
+  * [Rgb Cmyk Conversion](conversions/rgb_cmyk_conversion.py)
   * [Rgb Hsv Conversion](conversions/rgb_hsv_conversion.py)
   * [Roman Numerals](conversions/roman_numerals.py)
   * [Speed Conversions](conversions/speed_conversions.py)
   * [Temperature Conversions](conversions/temperature_conversions.py)
+  * [Time Conversions](conversions/time_conversions.py)
   * [Volume Conversions](conversions/volume_conversions.py)
   * [Weight Conversion](conversions/weight_conversion.py)
 
+## Data Compression
+  * [Burrows Wheeler](data_compression/burrows_wheeler.py)
+  * [Huffman](data_compression/huffman.py)
+  * [Lempel Ziv](data_compression/lempel_ziv.py)
+  * [Lempel Ziv Decompress](data_compression/lempel_ziv_decompress.py)
+  * [Lz77](data_compression/lz77.py)
+  * [Peak Signal To Noise Ratio](data_compression/peak_signal_to_noise_ratio.py)
+  * [Run Length Encoding](data_compression/run_length_encoding.py)
+
 ## Data Structures
   * Arrays
     * [Equilibrium Index In Array](data_structures/arrays/equilibrium_index_in_array.py)
     * [Find Triplets With 0 Sum](data_structures/arrays/find_triplets_with_0_sum.py)
+    * [Index 2D Array In 1D](data_structures/arrays/index_2d_array_in_1d.py)
+    * [Kth Largest Element](data_structures/arrays/kth_largest_element.py)
     * [Median Two Array](data_structures/arrays/median_two_array.py)
+    * [Monotonic Array](data_structures/arrays/monotonic_array.py)
     * [Pairs With Given Sum](data_structures/arrays/pairs_with_given_sum.py)
     * [Permutations](data_structures/arrays/permutations.py)
     * [Prefix Sum](data_structures/arrays/prefix_sum.py)
@@ -209,12 +216,15 @@
     * [Lazy Segment Tree](data_structures/binary_tree/lazy_segment_tree.py)
     * [Lowest Common Ancestor](data_structures/binary_tree/lowest_common_ancestor.py)
     * [Maximum Fenwick Tree](data_structures/binary_tree/maximum_fenwick_tree.py)
+    * [Maximum Sum Bst](data_structures/binary_tree/maximum_sum_bst.py)
     * [Merge Two Binary Trees](data_structures/binary_tree/merge_two_binary_trees.py)
+    * [Mirror Binary Tree](data_structures/binary_tree/mirror_binary_tree.py)
     * [Non Recursive Segment Tree](data_structures/binary_tree/non_recursive_segment_tree.py)
     * [Number Of Possible Binary Trees](data_structures/binary_tree/number_of_possible_binary_trees.py)
     * [Red Black Tree](data_structures/binary_tree/red_black_tree.py)
     * [Segment Tree](data_structures/binary_tree/segment_tree.py)
     * [Segment Tree Other](data_structures/binary_tree/segment_tree_other.py)
+    * [Serialize Deserialize Binary Tree](data_structures/binary_tree/serialize_deserialize_binary_tree.py)
     * [Symmetric Tree](data_structures/binary_tree/symmetric_tree.py)
     * [Treap](data_structures/binary_tree/treap.py)
     * [Wavelet Tree](data_structures/binary_tree/wavelet_tree.py)
@@ -240,11 +250,21 @@
     * [Min Heap](data_structures/heap/min_heap.py)
     * [Randomized Heap](data_structures/heap/randomized_heap.py)
     * [Skew Heap](data_structures/heap/skew_heap.py)
+  * Kd Tree
+    * [Build Kdtree](data_structures/kd_tree/build_kdtree.py)
+    * Example
+      * [Example Usage](data_structures/kd_tree/example/example_usage.py)
+      * [Hypercube Points](data_structures/kd_tree/example/hypercube_points.py)
+    * [Kd Node](data_structures/kd_tree/kd_node.py)
+    * [Nearest Neighbour Search](data_structures/kd_tree/nearest_neighbour_search.py)
+    * Tests
+      * [Test Kdtree](data_structures/kd_tree/tests/test_kdtree.py)
   * Linked List
     * [Circular Linked List](data_structures/linked_list/circular_linked_list.py)
     * [Deque Doubly](data_structures/linked_list/deque_doubly.py)
     * [Doubly Linked List](data_structures/linked_list/doubly_linked_list.py)
     * [Doubly Linked List Two](data_structures/linked_list/doubly_linked_list_two.py)
+    * [Floyds Cycle Detection](data_structures/linked_list/floyds_cycle_detection.py)
     * [From Sequence](data_structures/linked_list/from_sequence.py)
     * [Has Loop](data_structures/linked_list/has_loop.py)
     * [Is Palindrome](data_structures/linked_list/is_palindrome.py)
@@ -256,20 +276,22 @@
     * [Singly Linked List](data_structures/linked_list/singly_linked_list.py)
     * [Skip List](data_structures/linked_list/skip_list.py)
     * [Swap Nodes](data_structures/linked_list/swap_nodes.py)
-  * Queue
-    * [Circular Queue](data_structures/queue/circular_queue.py)
-    * [Circular Queue Linked List](data_structures/queue/circular_queue_linked_list.py)
-    * [Double Ended Queue](data_structures/queue/double_ended_queue.py)
-    * [Linked Queue](data_structures/queue/linked_queue.py)
-    * [Priority Queue Using List](data_structures/queue/priority_queue_using_list.py)
-    * [Queue By List](data_structures/queue/queue_by_list.py)
-    * [Queue By Two Stacks](data_structures/queue/queue_by_two_stacks.py)
-    * [Queue On Pseudo Stack](data_structures/queue/queue_on_pseudo_stack.py)
+  * Queues
+    * [Circular Queue](data_structures/queues/circular_queue.py)
+    * [Circular Queue Linked List](data_structures/queues/circular_queue_linked_list.py)
+    * [Double Ended Queue](data_structures/queues/double_ended_queue.py)
+    * [Linked Queue](data_structures/queues/linked_queue.py)
+    * [Priority Queue Using List](data_structures/queues/priority_queue_using_list.py)
+    * [Queue By List](data_structures/queues/queue_by_list.py)
+    * [Queue By Two Stacks](data_structures/queues/queue_by_two_stacks.py)
+    * [Queue On Pseudo Stack](data_structures/queues/queue_on_pseudo_stack.py)
   * Stacks
     * [Balanced Parentheses](data_structures/stacks/balanced_parentheses.py)
     * [Dijkstras Two Stack Algorithm](data_structures/stacks/dijkstras_two_stack_algorithm.py)
     * [Infix To Postfix Conversion](data_structures/stacks/infix_to_postfix_conversion.py)
     * [Infix To Prefix Conversion](data_structures/stacks/infix_to_prefix_conversion.py)
+    * [Largest Rectangle Histogram](data_structures/stacks/largest_rectangle_histogram.py)
+    * [Lexicographical Numbers](data_structures/stacks/lexicographical_numbers.py)
     * [Next Greater Element](data_structures/stacks/next_greater_element.py)
     * [Postfix Evaluation](data_structures/stacks/postfix_evaluation.py)
     * [Prefix Evaluation](data_structures/stacks/prefix_evaluation.py)
@@ -278,6 +300,13 @@
     * [Stack With Doubly Linked List](data_structures/stacks/stack_with_doubly_linked_list.py)
     * [Stack With Singly Linked List](data_structures/stacks/stack_with_singly_linked_list.py)
     * [Stock Span Problem](data_structures/stacks/stock_span_problem.py)
+  * Suffix Tree
+    * Example
+      * [Example Usage](data_structures/suffix_tree/example/example_usage.py)
+    * [Suffix Tree](data_structures/suffix_tree/suffix_tree.py)
+    * [Suffix Tree Node](data_structures/suffix_tree/suffix_tree_node.py)
+    * Tests
+      * [Test Suffix Tree](data_structures/suffix_tree/tests/test_suffix_tree.py)
   * Trie
     * [Radix Tree](data_structures/trie/radix_tree.py)
     * [Trie](data_structures/trie/trie.py)
@@ -326,6 +355,9 @@
   * [Power](divide_and_conquer/power.py)
   * [Strassen Matrix Multiplication](divide_and_conquer/strassen_matrix_multiplication.py)
 
+## Docs
+  * [Conf](docs/conf.py)
+
 ## Dynamic Programming
   * [Abbreviation](dynamic_programming/abbreviation.py)
   * [All Construct](dynamic_programming/all_construct.py)
@@ -341,12 +373,14 @@
   * [Floyd Warshall](dynamic_programming/floyd_warshall.py)
   * [Integer Partition](dynamic_programming/integer_partition.py)
   * [Iterating Through Submasks](dynamic_programming/iterating_through_submasks.py)
+  * [K Means Clustering Tensorflow](dynamic_programming/k_means_clustering_tensorflow.py)
   * [Knapsack](dynamic_programming/knapsack.py)
   * [Largest Divisible Subset](dynamic_programming/largest_divisible_subset.py)
   * [Longest Common Subsequence](dynamic_programming/longest_common_subsequence.py)
   * [Longest Common Substring](dynamic_programming/longest_common_substring.py)
   * [Longest Increasing Subsequence](dynamic_programming/longest_increasing_subsequence.py)
-  * [Longest Increasing Subsequence O(Nlogn)](dynamic_programming/longest_increasing_subsequence_o(nlogn).py)
+  * [Longest Increasing Subsequence Iterative](dynamic_programming/longest_increasing_subsequence_iterative.py)
+  * [Longest Increasing Subsequence O Nlogn](dynamic_programming/longest_increasing_subsequence_o_nlogn.py)
   * [Longest Palindromic Subsequence](dynamic_programming/longest_palindromic_subsequence.py)
   * [Matrix Chain Multiplication](dynamic_programming/matrix_chain_multiplication.py)
   * [Matrix Chain Order](dynamic_programming/matrix_chain_order.py)
@@ -363,6 +397,7 @@
   * [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py)
   * [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py)
   * [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py)
+  * [Range Sum Query](dynamic_programming/range_sum_query.py)
   * [Regex Match](dynamic_programming/regex_match.py)
   * [Rod Cutting](dynamic_programming/rod_cutting.py)
   * [Smith Waterman](dynamic_programming/smith_waterman.py)
@@ -377,6 +412,7 @@
 ## Electronics
   * [Apparent Power](electronics/apparent_power.py)
   * [Builtin Voltage](electronics/builtin_voltage.py)
+  * [Capacitor Equivalence](electronics/capacitor_equivalence.py)
   * [Carrier Concentration](electronics/carrier_concentration.py)
   * [Charging Capacitor](electronics/charging_capacitor.py)
   * [Charging Inductor](electronics/charging_inductor.py)
@@ -406,12 +442,19 @@
   * [Interest](financial/interest.py)
   * [Present Value](financial/present_value.py)
   * [Price Plus Tax](financial/price_plus_tax.py)
+  * [Simple Moving Average](financial/simple_moving_average.py)
+  * [Straight Line Depreciation](financial/straight_line_depreciation.py)
+  * [Time And Half Pay](financial/time_and_half_pay.py)
 
 ## Fractals
   * [Julia Sets](fractals/julia_sets.py)
   * [Koch Snowflake](fractals/koch_snowflake.py)
   * [Mandelbrot](fractals/mandelbrot.py)
   * [Sierpinski Triangle](fractals/sierpinski_triangle.py)
+  * [Vicsek](fractals/vicsek.py)
+
+## Fuzzy Logic
+  * [Fuzzy Operations](fuzzy_logic/fuzzy_operations.py)
 
 ## Genetic Algorithm
   * [Basic String](genetic_algorithm/basic_string.py)
@@ -420,18 +463,25 @@
   * [Haversine Distance](geodesy/haversine_distance.py)
   * [Lamberts Ellipsoidal Distance](geodesy/lamberts_ellipsoidal_distance.py)
 
+## Geometry
+  * [Geometry](geometry/geometry.py)
+
 ## Graphics
   * [Bezier Curve](graphics/bezier_curve.py)
+  * [Butterfly Pattern](graphics/butterfly_pattern.py)
+  * [Digital Differential Analyzer Line](graphics/digital_differential_analyzer_line.py)
   * [Vector3 For 2D Rendering](graphics/vector3_for_2d_rendering.py)
 
 ## Graphs
   * [A Star](graphs/a_star.py)
+  * [Ant Colony Optimization Algorithms](graphs/ant_colony_optimization_algorithms.py)
   * [Articulation Points](graphs/articulation_points.py)
   * [Basic Graphs](graphs/basic_graphs.py)
   * [Bellman Ford](graphs/bellman_ford.py)
   * [Bi Directional Dijkstra](graphs/bi_directional_dijkstra.py)
   * [Bidirectional A Star](graphs/bidirectional_a_star.py)
   * [Bidirectional Breadth First Search](graphs/bidirectional_breadth_first_search.py)
+  * [Bidirectional Search](graphs/bidirectional_search.py)
   * [Boruvka](graphs/boruvka.py)
   * [Breadth First Search](graphs/breadth_first_search.py)
   * [Breadth First Search 2](graphs/breadth_first_search_2.py)
@@ -450,7 +500,7 @@
   * [Dijkstra Alternate](graphs/dijkstra_alternate.py)
   * [Dijkstra Binary Grid](graphs/dijkstra_binary_grid.py)
   * [Dinic](graphs/dinic.py)
-  * [Directed And Undirected (Weighted) Graph](graphs/directed_and_undirected_(weighted)_graph.py)
+  * [Directed And Undirected Weighted Graph](graphs/directed_and_undirected_weighted_graph.py)
   * [Edmonds Karp Multiple Source And Sink](graphs/edmonds_karp_multiple_source_and_sink.py)
   * [Eulerian Path And Circuit For Undirected Graph](graphs/eulerian_path_and_circuit_for_undirected_graph.py)
   * [Even Tree](graphs/even_tree.py)
@@ -467,6 +517,7 @@
   * [Kahns Algorithm Long](graphs/kahns_algorithm_long.py)
   * [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py)
   * [Karger](graphs/karger.py)
+  * [Lanczos Eigenvectors](graphs/lanczos_eigenvectors.py)
   * [Markov Chain](graphs/markov_chain.py)
   * [Matching Min Vertex Cover](graphs/matching_min_vertex_cover.py)
   * [Minimum Path Sum](graphs/minimum_path_sum.py)
@@ -495,6 +546,7 @@
   * [Minimum Coin Change](greedy_methods/minimum_coin_change.py)
   * [Minimum Waiting Time](greedy_methods/minimum_waiting_time.py)
   * [Optimal Merge Pattern](greedy_methods/optimal_merge_pattern.py)
+  * [Smallest Range](greedy_methods/smallest_range.py)
 
 ## Hashes
   * [Adler32](hashes/adler32.py)
@@ -519,8 +571,13 @@
     * [Test Knapsack](knapsack/tests/test_knapsack.py)
 
 ## Linear Algebra
+  * [Gaussian Elimination](linear_algebra/gaussian_elimination.py)
+  * [Jacobi Iteration Method](linear_algebra/jacobi_iteration_method.py)
+  * [Lu Decomposition](linear_algebra/lu_decomposition.py)
+  * [Matrix Inversion](linear_algebra/matrix_inversion.py)
   * Src
     * [Conjugate Gradient](linear_algebra/src/conjugate_gradient.py)
+    * [Gaussian Elimination Pivoting](linear_algebra/src/gaussian_elimination_pivoting.py)
     * [Lib](linear_algebra/src/lib.py)
     * [Polynom For Points](linear_algebra/src/polynom_for_points.py)
     * [Power Iteration](linear_algebra/src/power_iteration.py)
@@ -536,11 +593,14 @@
 ## Machine Learning
   * [Apriori Algorithm](machine_learning/apriori_algorithm.py)
   * [Astar](machine_learning/astar.py)
+  * [Automatic Differentiation](machine_learning/automatic_differentiation.py)
   * [Data Transformations](machine_learning/data_transformations.py)
   * [Decision Tree](machine_learning/decision_tree.py)
   * [Dimensionality Reduction](machine_learning/dimensionality_reduction.py)
   * Forecasting
     * [Run](machine_learning/forecasting/run.py)
+  * [Frequent Pattern Growth](machine_learning/frequent_pattern_growth.py)
+  * [Gradient Boosting Classifier](machine_learning/gradient_boosting_classifier.py)
   * [Gradient Descent](machine_learning/gradient_descent.py)
   * [K Means Clust](machine_learning/k_means_clust.py)
   * [K Nearest Neighbours](machine_learning/k_nearest_neighbours.py)
@@ -549,16 +609,13 @@
   * Local Weighted Learning
     * [Local Weighted Learning](machine_learning/local_weighted_learning/local_weighted_learning.py)
   * [Logistic Regression](machine_learning/logistic_regression.py)
-  * Loss Functions
-    * [Binary Cross Entropy](machine_learning/loss_functions/binary_cross_entropy.py)
-    * [Categorical Cross Entropy](machine_learning/loss_functions/categorical_cross_entropy.py)
-    * [Hinge Loss](machine_learning/loss_functions/hinge_loss.py)
-    * [Huber Loss](machine_learning/loss_functions/huber_loss.py)
-    * [Mean Squared Error](machine_learning/loss_functions/mean_squared_error.py)
-    * [Mean Squared Logarithmic Error](machine_learning/loss_functions/mean_squared_logarithmic_error.py)
+  * [Loss Functions](machine_learning/loss_functions.py)
+  * Lstm
+    * [Lstm Prediction](machine_learning/lstm/lstm_prediction.py)
   * [Mfcc](machine_learning/mfcc.py)
   * [Multilayer Perceptron Classifier](machine_learning/multilayer_perceptron_classifier.py)
   * [Polynomial Regression](machine_learning/polynomial_regression.py)
+  * [Principle Component Analysis](machine_learning/principle_component_analysis.py)
   * [Scoring Functions](machine_learning/scoring_functions.py)
   * [Self Organizing Map](machine_learning/self_organizing_map.py)
   * [Sequential Minimum Optimization](machine_learning/sequential_minimum_optimization.py)
@@ -583,13 +640,10 @@
   * [Bailey Borwein Plouffe](maths/bailey_borwein_plouffe.py)
   * [Base Neg2 Conversion](maths/base_neg2_conversion.py)
   * [Basic Maths](maths/basic_maths.py)
-  * [Binary Exp Mod](maths/binary_exp_mod.py)
   * [Binary Exponentiation](maths/binary_exponentiation.py)
-  * [Binary Exponentiation 2](maths/binary_exponentiation_2.py)
   * [Binary Multiplication](maths/binary_multiplication.py)
   * [Binomial Coefficient](maths/binomial_coefficient.py)
   * [Binomial Distribution](maths/binomial_distribution.py)
-  * [Bisection](maths/bisection.py)
   * [Ceil](maths/ceil.py)
   * [Chebyshev Distance](maths/chebyshev_distance.py)
   * [Check Polygon](maths/check_polygon.py)
@@ -611,6 +665,7 @@
   * [Extended Euclidean Algorithm](maths/extended_euclidean_algorithm.py)
   * [Factorial](maths/factorial.py)
   * [Factors](maths/factors.py)
+  * [Fast Inverse Sqrt](maths/fast_inverse_sqrt.py)
   * [Fermat Little Theorem](maths/fermat_little_theorem.py)
   * [Fibonacci](maths/fibonacci.py)
   * [Find Max](maths/find_max.py)
@@ -618,18 +673,19 @@
   * [Floor](maths/floor.py)
   * [Gamma](maths/gamma.py)
   * [Gaussian](maths/gaussian.py)
-  * [Gaussian Error Linear Unit](maths/gaussian_error_linear_unit.py)
   * [Gcd Of N Numbers](maths/gcd_of_n_numbers.py)
+  * [Geometric Mean](maths/geometric_mean.py)
   * [Germain Primes](maths/germain_primes.py)
   * [Greatest Common Divisor](maths/greatest_common_divisor.py)
   * [Hardy Ramanujanalgo](maths/hardy_ramanujanalgo.py)
-  * [Integration By Simpson Approx](maths/integration_by_simpson_approx.py)
+  * [Integer Square Root](maths/integer_square_root.py)
   * [Interquartile Range](maths/interquartile_range.py)
   * [Is Int Palindrome](maths/is_int_palindrome.py)
   * [Is Ip V4 Address Valid](maths/is_ip_v4_address_valid.py)
   * [Is Square Free](maths/is_square_free.py)
   * [Jaccard Similarity](maths/jaccard_similarity.py)
   * [Joint Probability Distribution](maths/joint_probability_distribution.py)
+  * [Josephus Problem](maths/josephus_problem.py)
   * [Juggler Sequence](maths/juggler_sequence.py)
   * [Karatsuba](maths/karatsuba.py)
   * [Kth Lexicographic Permutation](maths/kth_lexicographic_permutation.py)
@@ -643,19 +699,33 @@
   * [Manhattan Distance](maths/manhattan_distance.py)
   * [Matrix Exponentiation](maths/matrix_exponentiation.py)
   * [Max Sum Sliding Window](maths/max_sum_sliding_window.py)
-  * [Median Of Two Arrays](maths/median_of_two_arrays.py)
   * [Minkowski Distance](maths/minkowski_distance.py)
   * [Mobius Function](maths/mobius_function.py)
   * [Modular Division](maths/modular_division.py)
   * [Modular Exponential](maths/modular_exponential.py)
   * [Monte Carlo](maths/monte_carlo.py)
   * [Monte Carlo Dice](maths/monte_carlo_dice.py)
-  * [Nevilles Method](maths/nevilles_method.py)
-  * [Newton Raphson](maths/newton_raphson.py)
   * [Number Of Digits](maths/number_of_digits.py)
-  * [Numerical Integration](maths/numerical_integration.py)
+  * Numerical Analysis
+    * [Adams Bashforth](maths/numerical_analysis/adams_bashforth.py)
+    * [Bisection](maths/numerical_analysis/bisection.py)
+    * [Bisection 2](maths/numerical_analysis/bisection_2.py)
+    * [Integration By Simpson Approx](maths/numerical_analysis/integration_by_simpson_approx.py)
+    * [Intersection](maths/numerical_analysis/intersection.py)
+    * [Nevilles Method](maths/numerical_analysis/nevilles_method.py)
+    * [Newton Forward Interpolation](maths/numerical_analysis/newton_forward_interpolation.py)
+    * [Newton Raphson](maths/numerical_analysis/newton_raphson.py)
+    * [Numerical Integration](maths/numerical_analysis/numerical_integration.py)
+    * [Proper Fractions](maths/numerical_analysis/proper_fractions.py)
+    * [Runge Kutta](maths/numerical_analysis/runge_kutta.py)
+    * [Runge Kutta Fehlberg 45](maths/numerical_analysis/runge_kutta_fehlberg_45.py)
+    * [Runge Kutta Gills](maths/numerical_analysis/runge_kutta_gills.py)
+    * [Secant Method](maths/numerical_analysis/secant_method.py)
+    * [Simpson Rule](maths/numerical_analysis/simpson_rule.py)
+    * [Square Root](maths/numerical_analysis/square_root.py)
   * [Odd Sieve](maths/odd_sieve.py)
   * [Perfect Cube](maths/perfect_cube.py)
+  * [Perfect Number](maths/perfect_number.py)
   * [Perfect Square](maths/perfect_square.py)
   * [Persistence](maths/persistence.py)
   * [Pi Generator](maths/pi_generator.py)
@@ -678,8 +748,6 @@
   * [Radians](maths/radians.py)
   * [Radix2 Fft](maths/radix2_fft.py)
   * [Remove Digit](maths/remove_digit.py)
-  * [Runge Kutta](maths/runge_kutta.py)
-  * [Runge Kutta Fehlberg 45](maths/runge_kutta_fehlberg_45.py)
   * [Segmented Sieve](maths/segmented_sieve.py)
   * Series
     * [Arithmetic](maths/series/arithmetic.py)
@@ -692,12 +760,12 @@
   * [Sieve Of Eratosthenes](maths/sieve_of_eratosthenes.py)
   * [Sigmoid](maths/sigmoid.py)
   * [Signum](maths/signum.py)
-  * [Simpson Rule](maths/simpson_rule.py)
   * [Simultaneous Linear Equation Solver](maths/simultaneous_linear_equation_solver.py)
   * [Sin](maths/sin.py)
   * [Sock Merchant](maths/sock_merchant.py)
   * [Softmax](maths/softmax.py)
   * [Solovay Strassen Primality Test](maths/solovay_strassen_primality_test.py)
+  * [Spearman Rank Correlation Coefficient](maths/spearman_rank_correlation_coefficient.py)
   * Special Numbers
     * [Armstrong Numbers](maths/special_numbers/armstrong_numbers.py)
     * [Automorphic Number](maths/special_numbers/automorphic_number.py)
@@ -705,6 +773,7 @@
     * [Carmichael Number](maths/special_numbers/carmichael_number.py)
     * [Catalan Number](maths/special_numbers/catalan_number.py)
     * [Hamming Numbers](maths/special_numbers/hamming_numbers.py)
+    * [Happy Number](maths/special_numbers/happy_number.py)
     * [Harshad Numbers](maths/special_numbers/harshad_numbers.py)
     * [Hexagonal Number](maths/special_numbers/hexagonal_number.py)
     * [Krishnamurthy Number](maths/special_numbers/krishnamurthy_number.py)
@@ -712,9 +781,9 @@
     * [Polygonal Numbers](maths/special_numbers/polygonal_numbers.py)
     * [Pronic Number](maths/special_numbers/pronic_number.py)
     * [Proth Number](maths/special_numbers/proth_number.py)
+    * [Triangular Numbers](maths/special_numbers/triangular_numbers.py)
     * [Ugly Numbers](maths/special_numbers/ugly_numbers.py)
     * [Weird Number](maths/special_numbers/weird_number.py)
-  * [Square Root](maths/square_root.py)
   * [Sum Of Arithmetic Series](maths/sum_of_arithmetic_series.py)
   * [Sum Of Digits](maths/sum_of_digits.py)
   * [Sum Of Geometric Progression](maths/sum_of_geometric_progression.py)
@@ -722,6 +791,7 @@
   * [Sumset](maths/sumset.py)
   * [Sylvester Sequence](maths/sylvester_sequence.py)
   * [Tanh](maths/tanh.py)
+  * [Test Factorial](maths/test_factorial.py)
   * [Test Prime Check](maths/test_prime_check.py)
   * [Three Sum](maths/three_sum.py)
   * [Trapezoidal Rule](maths/trapezoidal_rule.py)
@@ -740,7 +810,9 @@
   * [Cramers Rule 2X2](matrix/cramers_rule_2x2.py)
   * [Inverse Of Matrix](matrix/inverse_of_matrix.py)
   * [Largest Square Area In Matrix](matrix/largest_square_area_in_matrix.py)
+  * [Matrix Based Game](matrix/matrix_based_game.py)
   * [Matrix Class](matrix/matrix_class.py)
+  * [Matrix Equalization](matrix/matrix_equalization.py)
   * [Matrix Multiplication Recursion](matrix/matrix_multiplication_recursion.py)
   * [Matrix Operation](matrix/matrix_operation.py)
   * [Max Area Of Island](matrix/max_area_of_island.py)
@@ -760,10 +832,10 @@
   * [Minimum Cut](networking_flow/minimum_cut.py)
 
 ## Neural Network
-  * [2 Hidden Layers Neural Network](neural_network/2_hidden_layers_neural_network.py)
   * Activation Functions
     * [Binary Step](neural_network/activation_functions/binary_step.py)
     * [Exponential Linear Unit](neural_network/activation_functions/exponential_linear_unit.py)
+    * [Gaussian Error Linear Unit](neural_network/activation_functions/gaussian_error_linear_unit.py)
     * [Leaky Rectified Linear Unit](neural_network/activation_functions/leaky_rectified_linear_unit.py)
     * [Mish](neural_network/activation_functions/mish.py)
     * [Rectified Linear Unit](neural_network/activation_functions/rectified_linear_unit.py)
@@ -774,14 +846,15 @@
     * [Swish](neural_network/activation_functions/swish.py)
   * [Back Propagation Neural Network](neural_network/back_propagation_neural_network.py)
   * [Convolution Neural Network](neural_network/convolution_neural_network.py)
-  * [Perceptron](neural_network/perceptron.py)
+  * [Input Data](neural_network/input_data.py)
   * [Simple Neural Network](neural_network/simple_neural_network.py)
+  * [Two Hidden Layers Neural Network](neural_network/two_hidden_layers_neural_network.py)
 
 ## Other
   * [Activity Selection](other/activity_selection.py)
   * [Alternative List Arrange](other/alternative_list_arrange.py)
+  * [Bankers Algorithm](other/bankers_algorithm.py)
   * [Davis Putnam Logemann Loveland](other/davis_putnam_logemann_loveland.py)
-  * [Dijkstra Bankers Algorithm](other/dijkstra_bankers_algorithm.py)
   * [Doomsday](other/doomsday.py)
   * [Fischer Yates Shuffle](other/fischer_yates_shuffle.py)
   * [Gauss Easter](other/gauss_easter.py)
@@ -810,26 +883,36 @@
   * [Archimedes Principle Of Buoyant Force](physics/archimedes_principle_of_buoyant_force.py)
   * [Basic Orbital Capture](physics/basic_orbital_capture.py)
   * [Casimir Effect](physics/casimir_effect.py)
+  * [Center Of Mass](physics/center_of_mass.py)
   * [Centripetal Force](physics/centripetal_force.py)
   * [Coulombs Law](physics/coulombs_law.py)
+  * [Doppler Frequency](physics/doppler_frequency.py)
+  * [Escape Velocity](physics/escape_velocity.py)
   * [Grahams Law](physics/grahams_law.py)
   * [Horizontal Projectile Motion](physics/horizontal_projectile_motion.py)
   * [Hubble Parameter](physics/hubble_parameter.py)
   * [Ideal Gas Law](physics/ideal_gas_law.py)
+  * [In Static Equilibrium](physics/in_static_equilibrium.py)
   * [Kinetic Energy](physics/kinetic_energy.py)
+  * [Lens Formulae](physics/lens_formulae.py)
   * [Lorentz Transformation Four Vector](physics/lorentz_transformation_four_vector.py)
   * [Malus Law](physics/malus_law.py)
+  * [Mass Energy Equivalence](physics/mass_energy_equivalence.py)
   * [Mirror Formulae](physics/mirror_formulae.py)
   * [N Body Simulation](physics/n_body_simulation.py)
   * [Newtons Law Of Gravitation](physics/newtons_law_of_gravitation.py)
   * [Newtons Second Law Of Motion](physics/newtons_second_law_of_motion.py)
+  * [Orbital Transfer Work](physics/orbital_transfer_work.py)
+  * [Period Of Pendulum](physics/period_of_pendulum.py)
   * [Photoelectric Effect](physics/photoelectric_effect.py)
   * [Potential Energy](physics/potential_energy.py)
+  * [Rainfall Intensity](physics/rainfall_intensity.py)
   * [Reynolds Number](physics/reynolds_number.py)
   * [Rms Speed Of Molecule](physics/rms_speed_of_molecule.py)
   * [Shear Stress](physics/shear_stress.py)
   * [Speed Of Sound](physics/speed_of_sound.py)
   * [Speeds Of Gas Molecules](physics/speeds_of_gas_molecules.py)
+  * [Terminal Velocity](physics/terminal_velocity.py)
 
 ## Project Euler
   * Problem 001
@@ -1044,6 +1127,8 @@
     * [Sol1](project_euler/problem_092/sol1.py)
   * Problem 094
     * [Sol1](project_euler/problem_094/sol1.py)
+  * Problem 095
+    * [Sol1](project_euler/problem_095/sol1.py)
   * Problem 097
     * [Sol1](project_euler/problem_097/sol1.py)
   * Problem 099
@@ -1078,6 +1163,8 @@
     * [Sol1](project_euler/problem_120/sol1.py)
   * Problem 121
     * [Sol1](project_euler/problem_121/sol1.py)
+  * Problem 122
+    * [Sol1](project_euler/problem_122/sol1.py)
   * Problem 123
     * [Sol1](project_euler/problem_123/sol1.py)
   * Problem 125
@@ -1088,10 +1175,14 @@
     * [Sol1](project_euler/problem_131/sol1.py)
   * Problem 135
     * [Sol1](project_euler/problem_135/sol1.py)
+  * Problem 136
+    * [Sol1](project_euler/problem_136/sol1.py)
   * Problem 144
     * [Sol1](project_euler/problem_144/sol1.py)
   * Problem 145
     * [Sol1](project_euler/problem_145/sol1.py)
+  * Problem 164
+    * [Sol1](project_euler/problem_164/sol1.py)
   * Problem 173
     * [Sol1](project_euler/problem_173/sol1.py)
   * Problem 174
@@ -1102,6 +1193,8 @@
     * [Sol1](project_euler/problem_187/sol1.py)
   * Problem 188
     * [Sol1](project_euler/problem_188/sol1.py)
+  * Problem 190
+    * [Sol1](project_euler/problem_190/sol1.py)
   * Problem 191
     * [Sol1](project_euler/problem_191/sol1.py)
   * Problem 203
@@ -1116,6 +1209,8 @@
     * [Sol1](project_euler/problem_234/sol1.py)
   * Problem 301
     * [Sol1](project_euler/problem_301/sol1.py)
+  * Problem 345
+    * [Sol1](project_euler/problem_345/sol1.py)
   * Problem 493
     * [Sol1](project_euler/problem_493/sol1.py)
   * Problem 551
@@ -1145,6 +1240,7 @@
   * [Binary Tree Traversal](searches/binary_tree_traversal.py)
   * [Double Linear Search](searches/double_linear_search.py)
   * [Double Linear Search Recursion](searches/double_linear_search_recursion.py)
+  * [Exponential Search](searches/exponential_search.py)
   * [Fibonacci Search](searches/fibonacci_search.py)
   * [Hill Climbing](searches/hill_climbing.py)
   * [Interpolation Search](searches/interpolation_search.py)
@@ -1214,11 +1310,13 @@
   * [Anagrams](strings/anagrams.py)
   * [Autocomplete Using Trie](strings/autocomplete_using_trie.py)
   * [Barcode Validator](strings/barcode_validator.py)
+  * [Bitap String Match](strings/bitap_string_match.py)
   * [Boyer Moore Search](strings/boyer_moore_search.py)
   * [Camel Case To Snake Case](strings/camel_case_to_snake_case.py)
   * [Can String Be Rearranged As Palindrome](strings/can_string_be_rearranged_as_palindrome.py)
   * [Capitalize](strings/capitalize.py)
   * [Check Anagrams](strings/check_anagrams.py)
+  * [Count Vowels](strings/count_vowels.py)
   * [Credit Card Validator](strings/credit_card_validator.py)
   * [Damerau Levenshtein Distance](strings/damerau_levenshtein_distance.py)
   * [Detecting English Programmatically](strings/detecting_english_programmatically.py)
@@ -1258,7 +1356,7 @@
   * [Title](strings/title.py)
   * [Top K Frequent Words](strings/top_k_frequent_words.py)
   * [Upper](strings/upper.py)
-  * [Wave](strings/wave.py)
+  * [Wave String](strings/wave_string.py)
   * [Wildcard Pattern Matching](strings/wildcard_pattern_matching.py)
   * [Word Occurrence](strings/word_occurrence.py)
   * [Word Patterns](strings/word_patterns.py)
@@ -1283,10 +1381,9 @@
   * [Fetch Well Rx Price](web_programming/fetch_well_rx_price.py)
   * [Get Amazon Product Data](web_programming/get_amazon_product_data.py)
   * [Get Imdb Top 250 Movies Csv](web_programming/get_imdb_top_250_movies_csv.py)
-  * [Get Imdbtop](web_programming/get_imdbtop.py)
+  * [Get Ip Geolocation](web_programming/get_ip_geolocation.py)
   * [Get Top Billionaires](web_programming/get_top_billionaires.py)
   * [Get Top Hn Posts](web_programming/get_top_hn_posts.py)
-  * [Get User Tweets](web_programming/get_user_tweets.py)
   * [Giphy](web_programming/giphy.py)
   * [Instagram Crawler](web_programming/instagram_crawler.py)
   * [Instagram Pic](web_programming/instagram_pic.py)
diff --git a/LICENSE.md b/LICENSE.md
index 2897d02e2a01..de631c3ef333 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-MIT License
+## MIT License
 
 Copyright (c) 2016-2022 TheAlgorithms and contributors
 
diff --git a/arithmetic_analysis/README.md b/arithmetic_analysis/README.md
deleted file mode 100644
index 45cf321eb6ad..000000000000
--- a/arithmetic_analysis/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Arithmetic analysis
-
-Arithmetic analysis is a branch of mathematics that deals with solving linear equations.
-
-* <https://en.wikipedia.org/wiki/System_of_linear_equations>
-* <https://en.wikipedia.org/wiki/Gaussian_elimination>
-* <https://en.wikipedia.org/wiki/Root-finding_algorithms>
diff --git a/arithmetic_analysis/newton_method.py b/arithmetic_analysis/newton_method.py
deleted file mode 100644
index 5127bfcafd9a..000000000000
--- a/arithmetic_analysis/newton_method.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""Newton's Method."""
-
-# Newton's Method - https://en.wikipedia.org/wiki/Newton%27s_method
-from collections.abc import Callable
-
-RealFunc = Callable[[float], float]  # type alias for a real -> real function
-
-
-# function is the f(x) and derivative is the f'(x)
-def newton(
-    function: RealFunc,
-    derivative: RealFunc,
-    starting_int: int,
-) -> float:
-    """
-    >>> newton(lambda x: x ** 3 - 2 * x - 5, lambda x: 3 * x ** 2 - 2, 3)
-    2.0945514815423474
-    >>> newton(lambda x: x ** 3 - 1, lambda x: 3 * x ** 2, -2)
-    1.0
-    >>> newton(lambda x: x ** 3 - 1, lambda x: 3 * x ** 2, -4)
-    1.0000000000000102
-    >>> import math
-    >>> newton(math.sin, math.cos, 1)
-    0.0
-    >>> newton(math.sin, math.cos, 2)
-    3.141592653589793
-    >>> newton(math.cos, lambda x: -math.sin(x), 2)
-    1.5707963267948966
-    >>> newton(math.cos, lambda x: -math.sin(x), 0)
-    Traceback (most recent call last):
-        ...
-    ZeroDivisionError: Could not find root
-    """
-    prev_guess = float(starting_int)
-    while True:
-        try:
-            next_guess = prev_guess - function(prev_guess) / derivative(prev_guess)
-        except ZeroDivisionError:
-            raise ZeroDivisionError("Could not find root") from None
-        if abs(prev_guess - next_guess) < 10**-5:
-            return next_guess
-        prev_guess = next_guess
-
-
-def f(x: float) -> float:
-    return (x**3) - (2 * x) - 5
-
-
-def f1(x: float) -> float:
-    return 3 * (x**2) - 2
-
-
-if __name__ == "__main__":
-    print(newton(f, f1, 3))
diff --git a/arithmetic_analysis/newton_raphson.py b/arithmetic_analysis/newton_raphson.py
deleted file mode 100644
index 1b90ad4177f6..000000000000
--- a/arithmetic_analysis/newton_raphson.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Implementing Newton Raphson method in Python
-# Author: Syed Haseeb Shah (github.com/QuantumNovice)
-# The Newton-Raphson method (also known as Newton's method) is a way to
-# quickly find a good approximation for the root of a real-valued function
-from __future__ import annotations
-
-from decimal import Decimal
-from math import *  # noqa: F403
-
-from sympy import diff
-
-
-def newton_raphson(
-    func: str, a: float | Decimal, precision: float = 10**-10
-) -> float:
-    """Finds root from the point 'a' onwards by Newton-Raphson method
-    >>> newton_raphson("sin(x)", 2)
-    3.1415926536808043
-    >>> newton_raphson("x**2 - 5*x +2", 0.4)
-    0.4384471871911695
-    >>> newton_raphson("x**2 - 5", 0.1)
-    2.23606797749979
-    >>> newton_raphson("log(x)- 1", 2)
-    2.718281828458938
-    """
-    x = a
-    while True:
-        x = Decimal(x) - (
-            Decimal(eval(func)) / Decimal(eval(str(diff(func))))  # noqa: S307
-        )
-        # This number dictates the accuracy of the answer
-        if abs(eval(func)) < precision:  # noqa: S307
-            return float(x)
-
-
-# Let's Execute
-if __name__ == "__main__":
-    # Find root of trigonometric function
-    # Find value of pi
-    print(f"The root of sin(x) = 0 is {newton_raphson('sin(x)', 2)}")
-    # Find root of polynomial
-    print(f"The root of x**2 - 5*x + 2 = 0 is {newton_raphson('x**2 - 5*x + 2', 0.4)}")
-    # Find Square Root of 5
-    print(f"The root of log(x) - 1 = 0 is {newton_raphson('log(x) - 1', 2)}")
-    # Exponential Roots
-    print(f"The root of exp(x) - 1 = 0 is {newton_raphson('exp(x) - 1', 0)}")
diff --git a/arithmetic_analysis/newton_raphson_new.py b/arithmetic_analysis/newton_raphson_new.py
deleted file mode 100644
index f61841e2eb84..000000000000
--- a/arithmetic_analysis/newton_raphson_new.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Implementing Newton Raphson method in Python
-# Author: Saksham Gupta
-#
-# The Newton-Raphson method (also known as Newton's method) is a way to
-# quickly find a good approximation for the root of a functreal-valued ion
-# The method can also be extended to complex functions
-#
-# Newton's Method - https://en.wikipedia.org/wiki/Newton's_method
-
-from sympy import diff, lambdify, symbols
-from sympy.functions import *  # noqa: F403
-
-
-def newton_raphson(
-    function: str,
-    starting_point: complex,
-    variable: str = "x",
-    precision: float = 10**-10,
-    multiplicity: int = 1,
-) -> complex:
-    """Finds root from the 'starting_point' onwards by Newton-Raphson method
-    Refer to https://docs.sympy.org/latest/modules/functions/index.html
-    for usable mathematical functions
-
-    >>> newton_raphson("sin(x)", 2)
-    3.141592653589793
-    >>> newton_raphson("x**4 -5", 0.4 + 5j)
-    (-7.52316384526264e-37+1.4953487812212207j)
-    >>> newton_raphson('log(y) - 1', 2, variable='y')
-    2.7182818284590455
-    >>> newton_raphson('exp(x) - 1', 10, precision=0.005)
-    1.2186556186174883e-10
-    >>> newton_raphson('cos(x)', 0)
-    Traceback (most recent call last):
-        ...
-    ZeroDivisionError: Could not find root
-    """
-
-    x = symbols(variable)
-    func = lambdify(x, function)
-    diff_function = lambdify(x, diff(function, x))
-
-    prev_guess = starting_point
-
-    while True:
-        if diff_function(prev_guess) != 0:
-            next_guess = prev_guess - multiplicity * func(prev_guess) / diff_function(
-                prev_guess
-            )
-        else:
-            raise ZeroDivisionError("Could not find root") from None
-
-        # Precision is checked by comparing the difference of consecutive guesses
-        if abs(next_guess - prev_guess) < precision:
-            return next_guess
-
-        prev_guess = next_guess
-
-
-# Let's Execute
-if __name__ == "__main__":
-    # Find root of trigonometric function
-    # Find value of pi
-    print(f"The root of sin(x) = 0 is {newton_raphson('sin(x)', 2)}")
-
-    # Find root of polynomial
-    # Find fourth Root of 5
-    print(f"The root of x**4 - 5 = 0 is {newton_raphson('x**4 -5', 0.4 +5j)}")
-
-    # Find value of e
-    print(
-        "The root of log(y) - 1 = 0 is ",
-        f"{newton_raphson('log(y) - 1', 2, variable='y')}",
-    )
-
-    # Exponential Roots
-    print(
-        "The root of exp(x) - 1 = 0 is",
-        f"{newton_raphson('exp(x) - 1', 10, precision=0.005)}",
-    )
-
-    # Find root of cos(x)
-    print(f"The root of cos(x) = 0 is {newton_raphson('cos(x)', 0)}")
diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py
index cffedb7a68fd..4e6ea1b18fb4 100644
--- a/audio_filters/butterworth_filter.py
+++ b/audio_filters/butterworth_filter.py
@@ -11,7 +11,9 @@
 
 
 def make_lowpass(
-    frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)  # noqa: B008
+    frequency: int,
+    samplerate: int,
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates a low-pass filter
@@ -39,7 +41,9 @@ def make_lowpass(
 
 
 def make_highpass(
-    frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)  # noqa: B008
+    frequency: int,
+    samplerate: int,
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates a high-pass filter
@@ -67,7 +71,9 @@ def make_highpass(
 
 
 def make_bandpass(
-    frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)  # noqa: B008
+    frequency: int,
+    samplerate: int,
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates a band-pass filter
@@ -96,7 +102,9 @@ def make_bandpass(
 
 
 def make_allpass(
-    frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)  # noqa: B008
+    frequency: int,
+    samplerate: int,
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates an all-pass filter
@@ -124,7 +132,7 @@ def make_peak(
     frequency: int,
     samplerate: int,
     gain_db: float,
-    q_factor: float = 1 / sqrt(2),  # noqa: B008
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates a peak filter
@@ -156,7 +164,7 @@ def make_lowshelf(
     frequency: int,
     samplerate: int,
     gain_db: float,
-    q_factor: float = 1 / sqrt(2),  # noqa: B008
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates a low-shelf filter
@@ -193,7 +201,7 @@ def make_highshelf(
     frequency: int,
     samplerate: int,
     gain_db: float,
-    q_factor: float = 1 / sqrt(2),  # noqa: B008
+    q_factor: float = 1 / sqrt(2),
 ) -> IIRFilter:
     """
     Creates a high-shelf filter
diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py
index f3c1ad43b001..fa3e6c54b33f 100644
--- a/audio_filters/iir_filter.py
+++ b/audio_filters/iir_filter.py
@@ -10,13 +10,17 @@ class IIRFilter:
 
     Implementation details:
     Based on the 2nd-order function from
-     https://en.wikipedia.org/wiki/Digital_biquad_filter,
+    https://en.wikipedia.org/wiki/Digital_biquad_filter,
     this generalized N-order function was made.
 
     Using the following transfer function
-    H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}}{a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}}
+        .. math:: H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}}
+                  {a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}}
+
     we can rewrite this to
-    y[n]={\frac{1}{a_{0}}}\left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-\left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right)
+        .. math:: y[n]={\frac{1}{a_{0}}}
+                  \left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-
+                  \left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right)
     """
 
     def __init__(self, order: int) -> None:
@@ -34,17 +38,19 @@ def __init__(self, order: int) -> None:
 
     def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None:
         """
-        Set the coefficients for the IIR filter. These should both be of size order + 1.
-        a_0 may be left out, and it will use 1.0 as default value.
+        Set the coefficients for the IIR filter.
+        These should both be of size `order` + 1.
+        :math:`a_0` may be left out, and it will use 1.0 as default value.
 
         This method works well with scipy's filter design functions
-            >>> # Make a 2nd-order 1000Hz butterworth lowpass filter
-            >>> import scipy.signal
-            >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000,
-            ...                                          btype='lowpass',
-            ...                                          fs=48000)
-            >>> filt = IIRFilter(2)
-            >>> filt.set_coefficients(a_coeffs, b_coeffs)
+
+        >>> # Make a 2nd-order 1000Hz butterworth lowpass filter
+        >>> import scipy.signal
+        >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000,
+        ...                                          btype='lowpass',
+        ...                                          fs=48000)
+        >>> filt = IIRFilter(2)
+        >>> filt.set_coefficients(a_coeffs, b_coeffs)
         """
         if len(a_coeffs) < self.order:
             a_coeffs = [1.0, *a_coeffs]
@@ -68,7 +74,7 @@ def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None
 
     def process(self, sample: float) -> float:
         """
-        Calculate y[n]
+        Calculate :math:`y[n]`
 
         >>> filt = IIRFilter(2)
         >>> filt.process(0)
diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py
index 097b8152b4e6..f9c9537c047c 100644
--- a/audio_filters/show_response.py
+++ b/audio_filters/show_response.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+from abc import abstractmethod
 from math import pi
 from typing import Protocol
 
@@ -8,6 +9,7 @@
 
 
 class FilterType(Protocol):
+    @abstractmethod
     def process(self, sample: float) -> float:
         """
         Calculate y[n]
@@ -15,7 +17,6 @@ def process(self, sample: float) -> float:
         >>> issubclass(FilterType, Protocol)
         True
         """
-        return 0.0
 
 
 def get_bounds(
diff --git a/backtracking/all_combinations.py b/backtracking/all_combinations.py
index ecbcc5882ec1..1d15c6263e14 100644
--- a/backtracking/all_combinations.py
+++ b/backtracking/all_combinations.py
@@ -1,9 +1,10 @@
 """
-        In this problem, we want to determine all possible combinations of k
-        numbers out of 1 ... n. We use backtracking to solve this problem.
+In this problem, we want to determine all possible combinations of k
+numbers out of 1 ... n. We use backtracking to solve this problem.
 
-        Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))),
+Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))),
 """
+
 from __future__ import annotations
 
 from itertools import combinations
@@ -11,6 +12,8 @@
 
 def combination_lists(n: int, k: int) -> list[list[int]]:
     """
+    Generates all possible combinations of k numbers out of 1 ... n using itertools.
+
     >>> combination_lists(n=4, k=2)
     [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
     """
@@ -19,6 +22,8 @@ def combination_lists(n: int, k: int) -> list[list[int]]:
 
 def generate_all_combinations(n: int, k: int) -> list[list[int]]:
     """
+    Generates all possible combinations of k numbers out of 1 ... n using backtracking.
+
     >>> generate_all_combinations(n=4, k=2)
     [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
     >>> generate_all_combinations(n=0, k=0)
@@ -26,16 +31,30 @@ def generate_all_combinations(n: int, k: int) -> list[list[int]]:
     >>> generate_all_combinations(n=10, k=-1)
     Traceback (most recent call last):
         ...
-    RecursionError: maximum recursion depth exceeded
+    ValueError: k must not be negative
     >>> generate_all_combinations(n=-1, k=10)
-    []
+    Traceback (most recent call last):
+        ...
+    ValueError: n must not be negative
     >>> generate_all_combinations(n=5, k=4)
     [[1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 4, 5], [1, 3, 4, 5], [2, 3, 4, 5]]
+    >>> generate_all_combinations(n=3, k=3)
+    [[1, 2, 3]]
+    >>> generate_all_combinations(n=3, k=1)
+    [[1], [2], [3]]
+    >>> generate_all_combinations(n=1, k=0)
+    [[]]
+    >>> generate_all_combinations(n=1, k=1)
+    [[1]]
     >>> from itertools import combinations
     >>> all(generate_all_combinations(n, k) == combination_lists(n, k)
     ...     for n in range(1, 6) for k in range(1, 6))
     True
     """
+    if k < 0:
+        raise ValueError("k must not be negative")
+    if n < 0:
+        raise ValueError("n must not be negative")
 
     result: list[list[int]] = []
     create_all_state(1, n, k, [], result)
@@ -49,6 +68,28 @@ def create_all_state(
     current_list: list[int],
     total_list: list[list[int]],
 ) -> None:
+    """
+    Helper function to recursively build all combinations.
+
+    >>> create_all_state(1, 4, 2, [], result := [])
+    >>> result
+    [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
+    >>> create_all_state(1, 3, 3, [], result := [])
+    >>> result
+    [[1, 2, 3]]
+    >>> create_all_state(2, 2, 1, [1], result := [])
+    >>> result
+    [[1, 2]]
+    >>> create_all_state(1, 0, 0, [], result := [])
+    >>> result
+    [[]]
+    >>> create_all_state(1, 4, 0, [1, 2], result := [])
+    >>> result
+    [[1, 2]]
+    >>> create_all_state(5, 4, 2, [1, 2], result := [])
+    >>> result
+    []
+    """
     if level == 0:
         total_list.append(current_list[:])
         return
diff --git a/backtracking/all_permutations.py b/backtracking/all_permutations.py
index ff8a53e0dd0e..f376e6fa0945 100644
--- a/backtracking/all_permutations.py
+++ b/backtracking/all_permutations.py
@@ -1,10 +1,11 @@
 """
-        In this problem, we want to determine all possible permutations
-        of the given sequence. We use backtracking to solve this problem.
+In this problem, we want to determine all possible permutations
+of the given sequence. We use backtracking to solve this problem.
 
-        Time complexity: O(n! * n),
-        where n denotes the length of the given sequence.
+Time complexity: O(n! * n),
+where n denotes the length of the given sequence.
 """
+
 from __future__ import annotations
 
 
@@ -22,6 +23,42 @@ def create_state_space_tree(
     Creates a state space tree to iterate through each branch using DFS.
     We know that each state has exactly len(sequence) - index children.
     It terminates when it reaches the end of the given sequence.
+
+    :param sequence: The input sequence for which permutations are generated.
+    :param current_sequence: The current permutation being built.
+    :param index: The current index in the sequence.
+    :param index_used: list to track which elements are used in permutation.
+
+    Example 1:
+    >>> sequence = [1, 2, 3]
+    >>> current_sequence = []
+    >>> index_used = [False, False, False]
+    >>> create_state_space_tree(sequence, current_sequence, 0, index_used)
+    [1, 2, 3]
+    [1, 3, 2]
+    [2, 1, 3]
+    [2, 3, 1]
+    [3, 1, 2]
+    [3, 2, 1]
+
+    Example 2:
+    >>> sequence = ["A", "B", "C"]
+    >>> current_sequence = []
+    >>> index_used = [False, False, False]
+    >>> create_state_space_tree(sequence, current_sequence, 0, index_used)
+    ['A', 'B', 'C']
+    ['A', 'C', 'B']
+    ['B', 'A', 'C']
+    ['B', 'C', 'A']
+    ['C', 'A', 'B']
+    ['C', 'B', 'A']
+
+    Example 3:
+    >>> sequence = [1]
+    >>> current_sequence = []
+    >>> index_used = [False]
+    >>> create_state_space_tree(sequence, current_sequence, 0, index_used)
+    [1]
     """
 
     if index == len(sequence):
diff --git a/backtracking/all_subsequences.py b/backtracking/all_subsequences.py
index c465fc542407..18696054eb7e 100644
--- a/backtracking/all_subsequences.py
+++ b/backtracking/all_subsequences.py
@@ -5,6 +5,7 @@
 Time complexity: O(2^n),
 where n denotes the length of the given sequence.
 """
+
 from __future__ import annotations
 
 from typing import Any
@@ -21,6 +22,56 @@ def create_state_space_tree(
     Creates a state space tree to iterate through each branch using DFS.
     We know that each state has exactly two children.
     It terminates when it reaches the end of the given sequence.
+
+    :param sequence: The input sequence for which subsequences are generated.
+    :param current_subsequence: The current subsequence being built.
+    :param index: The current index in the sequence.
+
+    Example:
+    >>> sequence = [3, 2, 1]
+    >>> current_subsequence = []
+    >>> create_state_space_tree(sequence, current_subsequence, 0)
+    []
+    [1]
+    [2]
+    [2, 1]
+    [3]
+    [3, 1]
+    [3, 2]
+    [3, 2, 1]
+
+    >>> sequence = ["A", "B"]
+    >>> current_subsequence = []
+    >>> create_state_space_tree(sequence, current_subsequence, 0)
+    []
+    ['B']
+    ['A']
+    ['A', 'B']
+
+    >>> sequence = []
+    >>> current_subsequence = []
+    >>> create_state_space_tree(sequence, current_subsequence, 0)
+    []
+
+    >>> sequence = [1, 2, 3, 4]
+    >>> current_subsequence = []
+    >>> create_state_space_tree(sequence, current_subsequence, 0)
+    []
+    [4]
+    [3]
+    [3, 4]
+    [2]
+    [2, 4]
+    [2, 3]
+    [2, 3, 4]
+    [1]
+    [1, 4]
+    [1, 3]
+    [1, 3, 4]
+    [1, 2]
+    [1, 2, 4]
+    [1, 2, 3]
+    [1, 2, 3, 4]
     """
 
     if index == len(sequence):
@@ -34,7 +85,7 @@ def create_state_space_tree(
 
 
 if __name__ == "__main__":
-    seq: list[Any] = [3, 1, 2, 4]
+    seq: list[Any] = [1, 2, 3]
     generate_all_subsequences(seq)
 
     seq.clear()
diff --git a/backtracking/coloring.py b/backtracking/coloring.py
index 9d539de8a3c4..f10cdbcf9d26 100644
--- a/backtracking/coloring.py
+++ b/backtracking/coloring.py
@@ -1,9 +1,9 @@
 """
-    Graph Coloring also called "m coloring problem"
-    consists of coloring a given graph with at most m colors
-    such that no adjacent vertices are assigned the same color
+Graph Coloring also called "m coloring problem"
+consists of coloring a given graph with at most m colors
+such that no adjacent vertices are assigned the same color
 
-    Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring
+Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring
 """
 
 
diff --git a/backtracking/crossword_puzzle_solver.py b/backtracking/crossword_puzzle_solver.py
new file mode 100644
index 000000000000..e702c7e52153
--- /dev/null
+++ b/backtracking/crossword_puzzle_solver.py
@@ -0,0 +1,131 @@
+# https://www.geeksforgeeks.org/solve-crossword-puzzle/
+
+
+def is_valid(
+    puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool
+) -> bool:
+    """
+    Check if a word can be placed at the given position.
+
+    >>> puzzle = [
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', '']
+    ... ]
+    >>> is_valid(puzzle, 'word', 0, 0, True)
+    True
+    >>> puzzle = [
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', '']
+    ... ]
+    >>> is_valid(puzzle, 'word', 0, 0, False)
+    True
+    """
+    for i in range(len(word)):
+        if vertical:
+            if row + i >= len(puzzle) or puzzle[row + i][col] != "":
+                return False
+        elif col + i >= len(puzzle[0]) or puzzle[row][col + i] != "":
+            return False
+    return True
+
+
+def place_word(
+    puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool
+) -> None:
+    """
+    Place a word at the given position.
+
+    >>> puzzle = [
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', '']
+    ... ]
+    >>> place_word(puzzle, 'word', 0, 0, True)
+    >>> puzzle
+    [['w', '', '', ''], ['o', '', '', ''], ['r', '', '', ''], ['d', '', '', '']]
+    """
+    for i, char in enumerate(word):
+        if vertical:
+            puzzle[row + i][col] = char
+        else:
+            puzzle[row][col + i] = char
+
+
+def remove_word(
+    puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool
+) -> None:
+    """
+    Remove a word from the given position.
+
+    >>> puzzle = [
+    ...     ['w', '', '', ''],
+    ...     ['o', '', '', ''],
+    ...     ['r', '', '', ''],
+    ...     ['d', '', '', '']
+    ... ]
+    >>> remove_word(puzzle, 'word', 0, 0, True)
+    >>> puzzle
+    [['', '', '', ''], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']]
+    """
+    for i in range(len(word)):
+        if vertical:
+            puzzle[row + i][col] = ""
+        else:
+            puzzle[row][col + i] = ""
+
+
+def solve_crossword(puzzle: list[list[str]], words: list[str]) -> bool:
+    """
+    Solve the crossword puzzle using backtracking.
+
+    >>> puzzle = [
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', '']
+    ... ]
+
+    >>> words = ['word', 'four', 'more', 'last']
+    >>> solve_crossword(puzzle, words)
+    True
+    >>> puzzle = [
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', ''],
+    ...     ['', '', '', '']
+    ... ]
+    >>> words = ['word', 'four', 'more', 'paragraphs']
+    >>> solve_crossword(puzzle, words)
+    False
+    """
+    for row in range(len(puzzle)):
+        for col in range(len(puzzle[0])):
+            if puzzle[row][col] == "":
+                for word in words:
+                    for vertical in [True, False]:
+                        if is_valid(puzzle, word, row, col, vertical):
+                            place_word(puzzle, word, row, col, vertical)
+                            words.remove(word)
+                            if solve_crossword(puzzle, words):
+                                return True
+                            words.append(word)
+                            remove_word(puzzle, word, row, col, vertical)
+                return False
+    return True
+
+
+if __name__ == "__main__":
+    PUZZLE = [[""] * 3 for _ in range(3)]
+    WORDS = ["cat", "dog", "car"]
+
+    if solve_crossword(PUZZLE, WORDS):
+        print("Solution found:")
+        for row in PUZZLE:
+            print(" ".join(row))
+    else:
+        print("No solution found:")
diff --git a/backtracking/generate_parentheses.py b/backtracking/generate_parentheses.py
new file mode 100644
index 000000000000..18c21e2a9b51
--- /dev/null
+++ b/backtracking/generate_parentheses.py
@@ -0,0 +1,77 @@
+"""
+author: Aayush Soni
+Given n pairs of parentheses, write a function to generate all
+combinations of well-formed parentheses.
+Input: n = 2
+Output: ["(())","()()"]
+Leetcode link: https://leetcode.com/problems/generate-parentheses/description/
+"""
+
+
+def backtrack(
+    partial: str, open_count: int, close_count: int, n: int, result: list[str]
+) -> None:
+    """
+    Generate valid combinations of balanced parentheses using recursion.
+
+    :param partial: A string representing the current combination.
+    :param open_count: An integer representing the count of open parentheses.
+    :param close_count: An integer representing the count of close parentheses.
+    :param n: An integer representing the total number of pairs.
+    :param result: A list to store valid combinations.
+    :return: None
+
+    This function uses recursion to explore all possible combinations,
+    ensuring that at each step, the parentheses remain balanced.
+
+    Example:
+    >>> result = []
+    >>> backtrack("", 0, 0, 2, result)
+    >>> result
+    ['(())', '()()']
+    """
+    if len(partial) == 2 * n:
+        # When the combination is complete, add it to the result.
+        result.append(partial)
+        return
+
+    if open_count < n:
+        # If we can add an open parenthesis, do so, and recurse.
+        backtrack(partial + "(", open_count + 1, close_count, n, result)
+
+    if close_count < open_count:
+        # If we can add a close parenthesis (it won't make the combination invalid),
+        # do so, and recurse.
+        backtrack(partial + ")", open_count, close_count + 1, n, result)
+
+
+def generate_parenthesis(n: int) -> list[str]:
+    """
+    Generate valid combinations of balanced parentheses for a given n.
+
+    :param n: An integer representing the number of pairs of parentheses.
+    :return: A list of strings with valid combinations.
+
+    This function uses a recursive approach to generate the combinations.
+
+    Time Complexity: O(2^(2n)) - In the worst case, we have 2^(2n) combinations.
+    Space Complexity: O(n) - where 'n' is the number of pairs.
+
+    Example 1:
+    >>> generate_parenthesis(3)
+    ['((()))', '(()())', '(())()', '()(())', '()()()']
+
+    Example 2:
+    >>> generate_parenthesis(1)
+    ['()']
+    """
+
+    result: list[str] = []
+    backtrack("", 0, 0, n, result)
+    return result
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py
index e9916f83f861..f6e4212e47f4 100644
--- a/backtracking/hamiltonian_cycle.py
+++ b/backtracking/hamiltonian_cycle.py
@@ -1,10 +1,10 @@
 """
-    A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle
-    through a graph that visits each node exactly once.
-    Determining whether such paths and cycles exist in graphs
-    is the 'Hamiltonian path problem', which is NP-complete.
+A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle
+through a graph that visits each node exactly once.
+Determining whether such paths and cycles exist in graphs
+is the 'Hamiltonian path problem', which is NP-complete.
 
-    Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path
+Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path
 """
 
 
diff --git a/backtracking/knight_tour.py b/backtracking/knight_tour.py
index cc88307b7fe8..8906aaa1094c 100644
--- a/backtracking/knight_tour.py
+++ b/backtracking/knight_tour.py
@@ -24,10 +24,10 @@ def get_valid_pos(position: tuple[int, int], n: int) -> list[tuple[int, int]]:
     ]
     permissible_positions = []
 
-    for position in positions:
-        y_test, x_test = position
+    for inner_position in positions:
+        y_test, x_test = inner_position
         if 0 <= y_test < n and 0 <= x_test < n:
-            permissible_positions.append(position)
+            permissible_positions.append(inner_position)
 
     return permissible_positions
 
@@ -79,7 +79,7 @@ def open_knight_tour(n: int) -> list[list[int]]:
     >>> open_knight_tour(2)
     Traceback (most recent call last):
         ...
-    ValueError: Open Kight Tour cannot be performed on a board of size 2
+    ValueError: Open Knight Tour cannot be performed on a board of size 2
     """
 
     board = [[0 for i in range(n)] for j in range(n)]
@@ -91,7 +91,7 @@ def open_knight_tour(n: int) -> list[list[int]]:
                 return board
             board[i][j] = 0
 
-    msg = f"Open Kight Tour cannot be performed on a board of size {n}"
+    msg = f"Open Knight Tour cannot be performed on a board of size {n}"
     raise ValueError(msg)
 
 
diff --git a/backtracking/minimax.py b/backtracking/minimax.py
index 6e310131e069..4eef90b75483 100644
--- a/backtracking/minimax.py
+++ b/backtracking/minimax.py
@@ -7,6 +7,7 @@
 leaves of game tree is stored in scores[]
 height is maximum height of Game tree
 """
+
 from __future__ import annotations
 
 import math
@@ -16,6 +17,22 @@ def minimax(
     depth: int, node_index: int, is_max: bool, scores: list[int], height: float
 ) -> int:
     """
+    This function implements the minimax algorithm, which helps achieve the optimal
+    score for a player in a two-player game by checking all possible moves.
+    If the player is the maximizer, then the score is maximized.
+    If the player is the minimizer, then the score is minimized.
+
+    Parameters:
+    - depth: Current depth in the game tree.
+    - node_index: Index of the current node in the scores list.
+    - is_max: A boolean indicating whether the current move
+              is for the maximizer (True) or minimizer (False).
+    - scores: A list containing the scores of the leaves of the game tree.
+    - height: The maximum height of the game tree.
+
+    Returns:
+    - An integer representing the optimal score for the current player.
+
     >>> import math
     >>> scores = [90, 23, 6, 33, 21, 65, 123, 34423]
     >>> height = math.log(len(scores), 2)
@@ -37,19 +54,24 @@ def minimax(
 
     if depth < 0:
         raise ValueError("Depth cannot be less than 0")
-
     if len(scores) == 0:
         raise ValueError("Scores cannot be empty")
 
+    # Base case: If the current depth equals the height of the tree,
+    # return the score of the current node.
     if depth == height:
         return scores[node_index]
 
+    # If it's the maximizer's turn, choose the maximum score
+    # between the two possible moves.
     if is_max:
         return max(
             minimax(depth + 1, node_index * 2, False, scores, height),
             minimax(depth + 1, node_index * 2 + 1, False, scores, height),
         )
 
+    # If it's the minimizer's turn, choose the minimum score
+    # between the two possible moves.
     return min(
         minimax(depth + 1, node_index * 2, True, scores, height),
         minimax(depth + 1, node_index * 2 + 1, True, scores, height),
@@ -57,8 +79,11 @@ def minimax(
 
 
 def main() -> None:
+    # Sample scores and height calculation
     scores = [90, 23, 6, 33, 21, 65, 123, 34423]
     height = math.log(len(scores), 2)
+
+    # Calculate and print the optimal value using the minimax algorithm
     print("Optimal value : ", end="")
     print(minimax(0, 0, True, scores, height))
 
diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py
index bbf0ce44f91c..d10181f319b3 100644
--- a/backtracking/n_queens.py
+++ b/backtracking/n_queens.py
@@ -1,12 +1,13 @@
 """
 
- The nqueens problem is of placing N queens on a N * N
- chess board such that no queen can attack any other queens placed
- on that chess board.
- This means that one queen cannot have any other queen on its horizontal, vertical and
- diagonal lines.
+The nqueens problem is of placing N queens on a N * N
+chess board such that no queen can attack any other queens placed
+on that chess board.
+This means that one queen cannot have any other queen on its horizontal, vertical and
+diagonal lines.
 
 """
+
 from __future__ import annotations
 
 solution = []
@@ -17,40 +18,50 @@ def is_safe(board: list[list[int]], row: int, column: int) -> bool:
     This function returns a boolean value True if it is safe to place a queen there
     considering the current state of the board.
 
-    Parameters :
-    board(2D matrix) : board
-    row ,column : coordinates of the cell on a board
+    Parameters:
+    board (2D matrix): The chessboard
+    row, column: Coordinates of the cell on the board
 
-    Returns :
+    Returns:
     Boolean Value
 
+    >>> is_safe([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1)
+    True
+    >>> is_safe([[0, 1, 0], [0, 0, 0], [0, 0, 0]], 1, 1)
+    False
+    >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1)
+    False
+    >>> is_safe([[0, 0, 1], [0, 0, 0], [0, 0, 0]], 1, 1)
+    False
     """
-    for i in range(len(board)):
-        if board[row][i] == 1:
-            return False
-    for i in range(len(board)):
-        if board[i][column] == 1:
-            return False
-    for i, j in zip(range(row, -1, -1), range(column, -1, -1)):
-        if board[i][j] == 1:
-            return False
-    for i, j in zip(range(row, -1, -1), range(column, len(board))):
-        if board[i][j] == 1:
-            return False
-    return True
+
+    n = len(board)  # Size of the board
+
+    # Check if there is any queen in the same upper column,
+    # left upper diagonal and right upper diagonal
+    return (
+        all(board[i][j] != 1 for i, j in zip(range(row), [column] * row))
+        and all(
+            board[i][j] != 1
+            for i, j in zip(range(row - 1, -1, -1), range(column - 1, -1, -1))
+        )
+        and all(
+            board[i][j] != 1
+            for i, j in zip(range(row - 1, -1, -1), range(column + 1, n))
+        )
+    )
 
 
 def solve(board: list[list[int]], row: int) -> bool:
     """
-    It creates a state space tree and calls the safe function until it receives a
-    False Boolean and terminates that branch and backtracks to the next
+    This function creates a state space tree and calls the safe function until it
+    receives a False Boolean and terminates that branch and backtracks to the next
     possible solution branch.
     """
     if row >= len(board):
         """
-        If the row number exceeds N we have board with a successful combination
+        If the row number exceeds N, we have a board with a successful combination
         and that combination is appended to the solution list and the board is printed.
-
         """
         solution.append(board)
         printboard(board)
@@ -58,9 +69,9 @@ def solve(board: list[list[int]], row: int) -> bool:
         return True
     for i in range(len(board)):
         """
-        For every row it iterates through each column to check if it is feasible to
+        For every row, it iterates through each column to check if it is feasible to
         place a queen there.
-        If all the combinations for that particular branch are successful the board is
+        If all the combinations for that particular branch are successful, the board is
         reinitialized for the next possible combination.
         """
         if is_safe(board, row, i):
@@ -77,14 +88,14 @@ def printboard(board: list[list[int]]) -> None:
     for i in range(len(board)):
         for j in range(len(board)):
             if board[i][j] == 1:
-                print("Q", end=" ")
+                print("Q", end=" ")  # Queen is present
             else:
-                print(".", end=" ")
+                print(".", end=" ")  # Empty cell
         print()
 
 
-# n=int(input("The no. of queens"))
+# Number of queens (e.g., n=8 for an 8x8 board)
 n = 8
 board = [[0 for i in range(n)] for j in range(n)]
 solve(board, 0)
-print("The total no. of solutions are :", len(solution))
+print("The total number of solutions are:", len(solution))
diff --git a/backtracking/n_queens_math.py b/backtracking/n_queens_math.py
index f3b08ab0a05f..287d1f090373 100644
--- a/backtracking/n_queens_math.py
+++ b/backtracking/n_queens_math.py
@@ -75,6 +75,7 @@
 for another one or vice versa.
 
 """
+
 from __future__ import annotations
 
 
diff --git a/backtracking/sudoku.py b/backtracking/sudoku.py
index 6e4e3e8780f2..cabeebb90433 100644
--- a/backtracking/sudoku.py
+++ b/backtracking/sudoku.py
@@ -1,7 +1,7 @@
 """
-Given a partially filled 9×9 2D array, the objective is to fill a 9×9
+Given a partially filled 9x9 2D array, the objective is to fill a 9x9
 square grid with digits numbered 1 to 9, so that every row, column, and
-and each of the nine 3×3 sub-grids contains all of the digits.
+and each of the nine 3x3 sub-grids contains all of the digits.
 
 This can be solved using Backtracking and is similar to n-queens.
 We check to see if a cell is safe or not and recursively call the
@@ -9,6 +9,7 @@
 have solved the puzzle. else, we backtrack and place another number
 in that cell and repeat this process.
 """
+
 from __future__ import annotations
 
 Matrix = list[list[int]]
diff --git a/backtracking/sum_of_subsets.py b/backtracking/sum_of_subsets.py
index c5e23321cb0c..f26f179f8725 100644
--- a/backtracking/sum_of_subsets.py
+++ b/backtracking/sum_of_subsets.py
@@ -1,15 +1,26 @@
 """
-        The sum-of-subsetsproblem states that a set of non-negative integers, and a
-        value M, determine all possible subsets of the given set whose summation sum
-        equal to given M.
+The sum-of-subsets problem states that a set of non-negative integers, and a
+value M, determine all possible subsets of the given set whose summation sum
+equal to given M.
 
-        Summation of the chosen numbers must be equal to given number M and one number
-        can be used only once.
+Summation of the chosen numbers must be equal to given number M and one number
+can be used only once.
 """
-from __future__ import annotations
 
 
-def generate_sum_of_subsets_soln(nums: list[int], max_sum: int) -> list[list[int]]:
+def generate_sum_of_subsets_solutions(nums: list[int], max_sum: int) -> list[list[int]]:
+    """
+    The main function. For list of numbers 'nums' find the subsets with sum
+    equal to 'max_sum'
+
+    >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=9)
+    [[3, 4, 2], [4, 5]]
+    >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=3)
+    [[3]]
+    >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=1)
+    []
+    """
+
     result: list[list[int]] = []
     path: list[int] = []
     num_index = 0
@@ -33,7 +44,21 @@ def create_state_space_tree(
     This algorithm follows depth-fist-search and backtracks when the node is not
     branchable.
 
+    >>> path = []
+    >>> result = []
+    >>> create_state_space_tree(
+    ...     nums=[1],
+    ...     max_sum=1,
+    ...     num_index=0,
+    ...     path=path,
+    ...     result=result,
+    ...     remaining_nums_sum=1)
+    >>> path
+    []
+    >>> result
+    [[1]]
     """
+
     if sum(path) > max_sum or (remaining_nums_sum + sum(path)) < max_sum:
         return
     if sum(path) == max_sum:
@@ -50,16 +75,7 @@ def create_state_space_tree(
         )
 
 
-"""
-remove the comment to take an input from the user
-
-print("Enter the elements")
-nums = list(map(int, input().split()))
-print("Enter max_sum sum")
-max_sum = int(input())
+if __name__ == "__main__":
+    import doctest
 
-"""
-nums = [3, 34, 4, 12, 5, 2]
-max_sum = 9
-result = generate_sum_of_subsets_soln(nums, max_sum)
-print(*result)
+    doctest.testmod()
diff --git a/backtracking/word_break.py b/backtracking/word_break.py
new file mode 100644
index 000000000000..1f2ab073f499
--- /dev/null
+++ b/backtracking/word_break.py
@@ -0,0 +1,71 @@
+"""
+Word Break Problem is a well-known problem in computer science.
+Given a string and a dictionary of words, the task is to determine if
+the string can be segmented into a sequence of one or more dictionary words.
+
+Wikipedia: https://en.wikipedia.org/wiki/Word_break_problem
+"""
+
+
+def backtrack(input_string: str, word_dict: set[str], start: int) -> bool:
+    """
+    Helper function that uses backtracking to determine if a valid
+    word segmentation is possible starting from index 'start'.
+
+    Parameters:
+    input_string (str): The input string to be segmented.
+    word_dict (set[str]): A set of valid dictionary words.
+    start (int): The starting index of the substring to be checked.
+
+    Returns:
+    bool: True if a valid segmentation is possible, otherwise False.
+
+    Example:
+    >>> backtrack("leetcode", {"leet", "code"}, 0)
+    True
+
+    >>> backtrack("applepenapple", {"apple", "pen"}, 0)
+    True
+
+    >>> backtrack("catsandog", {"cats", "dog", "sand", "and", "cat"}, 0)
+    False
+    """
+
+    # Base case: if the starting index has reached the end of the string
+    if start == len(input_string):
+        return True
+
+    # Try every possible substring from 'start' to 'end'
+    for end in range(start + 1, len(input_string) + 1):
+        if input_string[start:end] in word_dict and backtrack(
+            input_string, word_dict, end
+        ):
+            return True
+
+    return False
+
+
+def word_break(input_string: str, word_dict: set[str]) -> bool:
+    """
+    Determines if the input string can be segmented into a sequence of
+    valid dictionary words using backtracking.
+
+    Parameters:
+    input_string (str): The input string to segment.
+    word_dict (set[str]): The set of valid words.
+
+    Returns:
+    bool: True if the string can be segmented into valid words, otherwise False.
+
+    Example:
+    >>> word_break("leetcode", {"leet", "code"})
+    True
+
+    >>> word_break("applepenapple", {"apple", "pen"})
+    True
+
+    >>> word_break("catsandog", {"cats", "dog", "sand", "and", "cat"})
+    False
+    """
+
+    return backtrack(input_string, word_dict, 0)
diff --git a/backtracking/word_ladder.py b/backtracking/word_ladder.py
new file mode 100644
index 000000000000..7d9fd00f6669
--- /dev/null
+++ b/backtracking/word_ladder.py
@@ -0,0 +1,100 @@
+"""
+Word Ladder is a classic problem in computer science.
+The problem is to transform a start word into an end word
+by changing one letter at a time.
+Each intermediate word must be a valid word from a given list of words.
+The goal is to find a transformation sequence
+from the start word to the end word.
+
+Wikipedia: https://en.wikipedia.org/wiki/Word_ladder
+"""
+
+import string
+
+
+def backtrack(
+    current_word: str, path: list[str], end_word: str, word_set: set[str]
+) -> list[str]:
+    """
+    Helper function to perform backtracking to find the transformation
+    from the current_word to the end_word.
+
+    Parameters:
+    current_word (str): The current word in the transformation sequence.
+    path (list[str]): The list of transformations from begin_word to current_word.
+    end_word (str): The target word for transformation.
+    word_set (set[str]): The set of valid words for transformation.
+
+    Returns:
+    list[str]: The list of transformations from begin_word to end_word.
+               Returns an empty list if there is no valid
+                transformation from current_word to end_word.
+
+    Example:
+    >>> backtrack("hit", ["hit"], "cog", {"hot", "dot", "dog", "lot", "log", "cog"})
+    ['hit', 'hot', 'dot', 'lot', 'log', 'cog']
+
+    >>> backtrack("hit", ["hit"], "cog", {"hot", "dot", "dog", "lot", "log"})
+    []
+
+    >>> backtrack("lead", ["lead"], "gold", {"load", "goad", "gold", "lead", "lord"})
+    ['lead', 'lead', 'load', 'goad', 'gold']
+
+    >>> backtrack("game", ["game"], "code", {"came", "cage", "code", "cade", "gave"})
+    ['game', 'came', 'cade', 'code']
+    """
+
+    # Base case: If the current word is the end word, return the path
+    if current_word == end_word:
+        return path
+
+    # Try all possible single-letter transformations
+    for i in range(len(current_word)):
+        for c in string.ascii_lowercase:  # Try changing each letter
+            transformed_word = current_word[:i] + c + current_word[i + 1 :]
+            if transformed_word in word_set:
+                word_set.remove(transformed_word)
+                # Recur with the new word added to the path
+                result = backtrack(
+                    transformed_word, [*path, transformed_word], end_word, word_set
+                )
+                if result:  # valid transformation found
+                    return result
+                word_set.add(transformed_word)  # backtrack
+
+    return []  # No valid transformation found
+
+
+def word_ladder(begin_word: str, end_word: str, word_set: set[str]) -> list[str]:
+    """
+    Solve the Word Ladder problem using Backtracking and return
+    the list of transformations from begin_word to end_word.
+
+    Parameters:
+    begin_word (str): The word from which the transformation starts.
+    end_word (str): The target word for transformation.
+    word_list (list[str]): The list of valid words for transformation.
+
+    Returns:
+    list[str]: The list of transformations from begin_word to end_word.
+               Returns an empty list if there is no valid transformation.
+
+    Example:
+    >>> word_ladder("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"])
+    ['hit', 'hot', 'dot', 'lot', 'log', 'cog']
+
+    >>> word_ladder("hit", "cog", ["hot", "dot", "dog", "lot", "log"])
+    []
+
+    >>> word_ladder("lead", "gold", ["load", "goad", "gold", "lead", "lord"])
+    ['lead', 'lead', 'load', 'goad', 'gold']
+
+    >>> word_ladder("game", "code", ["came", "cage", "code", "cade", "gave"])
+    ['game', 'came', 'cade', 'code']
+    """
+
+    if end_word not in word_set:  # no valid transformation possible
+        return []
+
+    # Perform backtracking starting from the begin_word
+    return backtrack(begin_word, [begin_word], end_word, word_set)
diff --git a/bit_manipulation/binary_and_operator.py b/bit_manipulation/binary_and_operator.py
index 36f6c668d9b3..f33b8b1c0ab4 100644
--- a/bit_manipulation/binary_and_operator.py
+++ b/bit_manipulation/binary_and_operator.py
@@ -26,7 +26,7 @@ def binary_and(a: int, b: int) -> str:
     >>> binary_and(0, 1.1)
     Traceback (most recent call last):
         ...
-    TypeError: 'float' object cannot be interpreted as an integer
+    ValueError: Unknown format code 'b' for object of type 'float'
     >>> binary_and("0", "1")
     Traceback (most recent call last):
         ...
@@ -35,8 +35,8 @@ def binary_and(a: int, b: int) -> str:
     if a < 0 or b < 0:
         raise ValueError("the value of both inputs must be positive")
 
-    a_binary = str(bin(a))[2:]  # remove the leading "0b"
-    b_binary = str(bin(b))[2:]  # remove the leading "0b"
+    a_binary = format(a, "b")
+    b_binary = format(b, "b")
 
     max_len = max(len(a_binary), len(b_binary))
 
diff --git a/bit_manipulation/excess_3_code.py b/bit_manipulation/excess_3_code.py
new file mode 100644
index 000000000000..7beaabd90e8a
--- /dev/null
+++ b/bit_manipulation/excess_3_code.py
@@ -0,0 +1,27 @@
+def excess_3_code(number: int) -> str:
+    """
+    Find excess-3 code of integer base 10.
+    Add 3 to all digits in a decimal number then convert to a binary-coded decimal.
+    https://en.wikipedia.org/wiki/Excess-3
+
+    >>> excess_3_code(0)
+    '0b0011'
+    >>> excess_3_code(3)
+    '0b0110'
+    >>> excess_3_code(2)
+    '0b0101'
+    >>> excess_3_code(20)
+    '0b01010011'
+    >>> excess_3_code(120)
+    '0b010001010011'
+    """
+    num = ""
+    for digit in str(max(0, number)):
+        num += str(bin(int(digit) + 3))[2:].zfill(4)
+    return "0b" + num
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/bit_manipulation/find_previous_power_of_two.py b/bit_manipulation/find_previous_power_of_two.py
new file mode 100644
index 000000000000..8ac74ac98478
--- /dev/null
+++ b/bit_manipulation/find_previous_power_of_two.py
@@ -0,0 +1,30 @@
+def find_previous_power_of_two(number: int) -> int:
+    """
+    Find the largest power of two that is less than or equal to a given integer.
+    https://stackoverflow.com/questions/1322510
+
+    >>> [find_previous_power_of_two(i) for i in range(18)]
+    [0, 1, 2, 2, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8, 16, 16]
+    >>> find_previous_power_of_two(-5)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input must be a non-negative integer
+    >>> find_previous_power_of_two(10.5)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input must be a non-negative integer
+    """
+    if not isinstance(number, int) or number < 0:
+        raise ValueError("Input must be a non-negative integer")
+    if number == 0:
+        return 0
+    power = 1
+    while power <= number:
+        power <<= 1  # Equivalent to multiplying by 2
+    return power >> 1 if number > 1 else 1
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/bit_manipulation/find_unique_number.py b/bit_manipulation/find_unique_number.py
new file mode 100644
index 000000000000..77970b4865d1
--- /dev/null
+++ b/bit_manipulation/find_unique_number.py
@@ -0,0 +1,37 @@
+def find_unique_number(arr: list[int]) -> int:
+    """
+    Given a list of integers where every element appears twice except for one,
+    this function returns the element that appears only once using bitwise XOR.
+
+    >>> find_unique_number([1, 1, 2, 2, 3])
+    3
+    >>> find_unique_number([4, 5, 4, 6, 6])
+    5
+    >>> find_unique_number([7])
+    7
+    >>> find_unique_number([10, 20, 10])
+    20
+    >>> find_unique_number([])
+    Traceback (most recent call last):
+        ...
+    ValueError: input list must not be empty
+    >>> find_unique_number([1, 'a', 1])
+    Traceback (most recent call last):
+        ...
+    TypeError: all elements must be integers
+    """
+    if not arr:
+        raise ValueError("input list must not be empty")
+    if not all(isinstance(x, int) for x in arr):
+        raise TypeError("all elements must be integers")
+
+    result = 0
+    for num in arr:
+        result ^= num
+    return result
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/bit_manipulation/is_even.py b/bit_manipulation/is_even.py
index ba036f35aa1e..6f95a1160797 100644
--- a/bit_manipulation/is_even.py
+++ b/bit_manipulation/is_even.py
@@ -1,7 +1,7 @@
 def is_even(number: int) -> bool:
     """
     return true if the input integer is even
-    Explanation: Lets take a look at the following deicmal to binary conversions
+    Explanation: Lets take a look at the following decimal to binary conversions
     2 => 10
     14 => 1110
     100 => 1100100
diff --git a/bit_manipulation/missing_number.py b/bit_manipulation/missing_number.py
index 32b949daa717..554887b17562 100644
--- a/bit_manipulation/missing_number.py
+++ b/bit_manipulation/missing_number.py
@@ -11,6 +11,12 @@ def find_missing_number(nums: list[int]) -> int:
     Example:
         >>> find_missing_number([0, 1, 3, 4])
         2
+        >>> find_missing_number([4, 3, 1, 0])
+        2
+        >>> find_missing_number([-4, -3, -1, 0])
+        -2
+        >>> find_missing_number([-2, 2, 1, 3, 0])
+        -1
         >>> find_missing_number([1, 3, 4, 5, 6])
         2
         >>> find_missing_number([6, 5, 4, 2, 1])
@@ -26,3 +32,9 @@ def find_missing_number(nums: list[int]) -> int:
         missing_number ^= i ^ nums[i - low]
 
     return missing_number
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/bit_manipulation/single_bit_manipulation_operations.py b/bit_manipulation/single_bit_manipulation_operations.py
index b43ff07b776f..fcbf033ccb24 100644
--- a/bit_manipulation/single_bit_manipulation_operations.py
+++ b/bit_manipulation/single_bit_manipulation_operations.py
@@ -8,8 +8,8 @@ def set_bit(number: int, position: int) -> int:
     Set the bit at position to 1.
 
     Details: perform bitwise or for given number and X.
-    Where X is a number with all the bits – zeroes and bit on given
-    position – one.
+    Where X is a number with all the bits - zeroes and bit on given
+    position - one.
 
     >>> set_bit(0b1101, 1) # 0b1111
     15
@@ -26,8 +26,8 @@ def clear_bit(number: int, position: int) -> int:
     Set the bit at position to 0.
 
     Details: perform bitwise and for given number and X.
-    Where X is a number with all the bits – ones and bit on given
-    position – zero.
+    Where X is a number with all the bits - ones and bit on given
+    position - zero.
 
     >>> clear_bit(0b10010, 1) # 0b10000
     16
@@ -42,8 +42,8 @@ def flip_bit(number: int, position: int) -> int:
     Flip the bit at position.
 
     Details: perform bitwise xor for given number and X.
-    Where X is a number with all the bits – zeroes and bit on given
-    position – one.
+    Where X is a number with all the bits - zeroes and bit on given
+    position - one.
 
     >>> flip_bit(0b101, 1) # 0b111
     7
@@ -79,7 +79,7 @@ def get_bit(number: int, position: int) -> int:
     Get the bit at the given position
 
     Details: perform bitwise and for the given number and X,
-    Where X is a number with all the bits – zeroes and bit on given position – one.
+    Where X is a number with all the bits - zeroes and bit on given position - one.
     If the result is not equal to 0, then the bit on the given position is 1, else 0.
 
     >>> get_bit(0b1010, 0)
diff --git a/boolean_algebra/and_gate.py b/boolean_algebra/and_gate.py
index 6ae66b5b0a77..650017b7ae10 100644
--- a/boolean_algebra/and_gate.py
+++ b/boolean_algebra/and_gate.py
@@ -1,8 +1,8 @@
 """
-An AND Gate is a logic gate in boolean algebra which results to 1 (True) if both the
-inputs are 1, and 0 (False) otherwise.
+An AND Gate is a logic gate in boolean algebra which results to 1 (True) if all the
+inputs are 1 (True), and 0 (False) otherwise.
 
-Following is the truth table of an AND Gate:
+Following is the truth table of a Two Input AND Gate:
     ------------------------------
     | Input 1 | Input 2 | Output |
     ------------------------------
@@ -12,7 +12,7 @@
     |    1    |    1    |    1   |
     ------------------------------
 
-Refer - https://www.geeksforgeeks.org/logic-gates-in-python/
+Refer - https://www.geeksforgeeks.org/logic-gates/
 """
 
 
@@ -32,6 +32,18 @@ def and_gate(input_1: int, input_2: int) -> int:
     return int(input_1 and input_2)
 
 
+def n_input_and_gate(inputs: list[int]) -> int:
+    """
+    Calculate AND of a list of input values
+
+    >>> n_input_and_gate([1, 0, 1, 1, 0])
+    0
+    >>> n_input_and_gate([1, 1, 1, 1, 1])
+    1
+    """
+    return int(all(inputs))
+
+
 if __name__ == "__main__":
     import doctest
 
diff --git a/boolean_algebra/karnaugh_map_simplification.py b/boolean_algebra/karnaugh_map_simplification.py
new file mode 100644
index 000000000000..c7f2d4c6b897
--- /dev/null
+++ b/boolean_algebra/karnaugh_map_simplification.py
@@ -0,0 +1,55 @@
+"""
+https://en.wikipedia.org/wiki/Karnaugh_map
+https://www.allaboutcircuits.com/technical-articles/karnaugh-map-boolean-algebraic-simplification-technique
+"""
+
+
+def simplify_kmap(kmap: list[list[int]]) -> str:
+    """
+    Simplify the Karnaugh map.
+    >>> simplify_kmap(kmap=[[0, 1], [1, 1]])
+    "A'B + AB' + AB"
+    >>> simplify_kmap(kmap=[[0, 0], [0, 0]])
+    ''
+    >>> simplify_kmap(kmap=[[0, 1], [1, -1]])
+    "A'B + AB' + AB"
+    >>> simplify_kmap(kmap=[[0, 1], [1, 2]])
+    "A'B + AB' + AB"
+    >>> simplify_kmap(kmap=[[0, 1], [1, 1.1]])
+    "A'B + AB' + AB"
+    >>> simplify_kmap(kmap=[[0, 1], [1, 'a']])
+    "A'B + AB' + AB"
+    """
+    simplified_f = []
+    for a, row in enumerate(kmap):
+        for b, item in enumerate(row):
+            if item:
+                term = ("A" if a else "A'") + ("B" if b else "B'")
+                simplified_f.append(term)
+    return " + ".join(simplified_f)
+
+
+def main() -> None:
+    """
+    Main function to create and simplify a K-Map.
+
+    >>> main()
+    [0, 1]
+    [1, 1]
+    Simplified Expression:
+    A'B + AB' + AB
+    """
+    kmap = [[0, 1], [1, 1]]
+
+    # Manually generate the product of [0, 1] and [0, 1]
+
+    for row in kmap:
+        print(row)
+
+    print("Simplified Expression:")
+    print(simplify_kmap(kmap))
+
+
+if __name__ == "__main__":
+    main()
+    print(f"{simplify_kmap(kmap=[[0, 1], [1, 1]]) = }")
diff --git a/boolean_algebra/multiplexer.py b/boolean_algebra/multiplexer.py
new file mode 100644
index 000000000000..7e65c785c829
--- /dev/null
+++ b/boolean_algebra/multiplexer.py
@@ -0,0 +1,42 @@
+def mux(input0: int, input1: int, select: int) -> int:
+    """
+    Implement a 2-to-1 Multiplexer.
+
+    :param input0: The first input value (0 or 1).
+    :param input1: The second input value (0 or 1).
+    :param select: The select signal (0 or 1) to choose between input0 and input1.
+    :return: The output based on the select signal.  input1 if select else input0.
+
+    https://www.electrically4u.com/solved-problems-on-multiplexer
+    https://en.wikipedia.org/wiki/Multiplexer
+
+    >>> mux(0, 1, 0)
+    0
+    >>> mux(0, 1, 1)
+    1
+    >>> mux(1, 0, 0)
+    1
+    >>> mux(1, 0, 1)
+    0
+    >>> mux(2, 1, 0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Inputs and select signal must be 0 or 1
+    >>> mux(0, -1, 0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Inputs and select signal must be 0 or 1
+    >>> mux(0, 1, 1.1)
+    Traceback (most recent call last):
+        ...
+    ValueError: Inputs and select signal must be 0 or 1
+    """
+    if all(i in (0, 1) for i in (input0, input1, select)):
+        return input1 if select else input0
+    raise ValueError("Inputs and select signal must be 0 or 1")
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/boolean_algebra/nor_gate.py b/boolean_algebra/nor_gate.py
index 2c27b80afdbe..d4d6f0da23ea 100644
--- a/boolean_algebra/nor_gate.py
+++ b/boolean_algebra/nor_gate.py
@@ -1,16 +1,20 @@
 """
-A NOR Gate is a logic gate in boolean algebra which results to false(0)
-if any of the input is 1, and True(1) if  both the inputs are 0.
+A NOR Gate is a logic gate in boolean algebra which results in false(0) if any of the
+inputs is 1, and True(1) if all inputs are 0.
 Following is the truth table of a NOR Gate:
-   | Input 1 | Input 2 |  Output |
-   |      0      |     0      |      1      |
-   |      0      |     1      |      0      |
-   |      1      |     0      |      0      |
-   |      1      |     1      |      0      |
+    Truth Table of NOR Gate:
+    | Input 1  | Input 2  |  Output  |
+    |    0     |    0     |    1     |
+    |    0     |    1     |    0     |
+    |    1     |    0     |    0     |
+    |    1     |    1     |    0     |
 
-Following is the code implementation of the NOR Gate
+    Code provided by Akshaj Vishwanathan
+https://www.geeksforgeeks.org/logic-gates-in-python
 """
 
+from collections.abc import Callable
+
 
 def nor_gate(input_1: int, input_2: int) -> int:
     """
@@ -30,19 +34,35 @@ def nor_gate(input_1: int, input_2: int) -> int:
     return int(input_1 == input_2 == 0)
 
 
-def main() -> None:
-    print("Truth Table of NOR Gate:")
-    print("|   Input 1   |  Input 2  | Output |")
-    print(f"|      0      |    0      |  {nor_gate(0, 0)}     |")
-    print(f"|      0      |    1      |  {nor_gate(0, 1)}     |")
-    print(f"|      1      |    0      |  {nor_gate(1, 0)}     |")
-    print(f"|      1      |    1      |  {nor_gate(1, 1)}     |")
+def truth_table(func: Callable) -> str:
+    """
+    >>> print(truth_table(nor_gate))
+    Truth Table of NOR Gate:
+    | Input 1  | Input 2  |  Output  |
+    |    0     |    0     |    1     |
+    |    0     |    1     |    0     |
+    |    1     |    0     |    0     |
+    |    1     |    1     |    0     |
+    """
+
+    def make_table_row(items: list | tuple) -> str:
+        """
+        >>> make_table_row(("One", "Two", "Three"))
+        '|   One    |   Two    |  Three   |'
+        """
+        return f"| {' | '.join(f'{item:^8}' for item in items)} |"
+
+    return "\n".join(
+        (
+            "Truth Table of NOR Gate:",
+            make_table_row(("Input 1", "Input 2", "Output")),
+            *[make_table_row((i, j, func(i, j))) for i in (0, 1) for j in (0, 1)],
+        )
+    )
 
 
 if __name__ == "__main__":
     import doctest
 
     doctest.testmod()
-    main()
-"""Code provided by Akshaj Vishwanathan"""
-"""Reference: https://www.geeksforgeeks.org/logic-gates-in-python/"""
+    print(truth_table(nor_gate))
diff --git a/cellular_automata/conways_game_of_life.py b/cellular_automata/conways_game_of_life.py
index 84f4d5be40da..485f0d47bd8b 100644
--- a/cellular_automata/conways_game_of_life.py
+++ b/cellular_automata/conways_game_of_life.py
@@ -2,6 +2,7 @@
 Conway's Game of Life implemented in Python.
 https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
 """
+
 from __future__ import annotations
 
 from PIL import Image
@@ -57,10 +58,8 @@ def new_generation(cells: list[list[int]]) -> list[list[int]]:
             # 3. All other live cells die in the next generation.
             #    Similarly, all other dead cells stay dead.
             alive = cells[i][j] == 1
-            if (
-                (alive and 2 <= neighbour_count <= 3)
-                or not alive
-                and neighbour_count == 3
+            if (alive and 2 <= neighbour_count <= 3) or (
+                not alive and neighbour_count == 3
             ):
                 next_generation_row.append(1)
             else:
diff --git a/cellular_automata/game_of_life.py b/cellular_automata/game_of_life.py
index d691a2b73af0..76276b272d65 100644
--- a/cellular_automata/game_of_life.py
+++ b/cellular_automata/game_of_life.py
@@ -26,7 +26,8 @@
  4.
  Any dead cell with exactly three live neighbours be-
  comes a live cell, as if by reproduction.
- """
+"""
+
 import random
 import sys
 
@@ -100,9 +101,8 @@ def __judge_point(pt: bool, neighbours: list[list[bool]]) -> bool:
             state = True
         elif alive > 3:
             state = False
-    else:
-        if alive == 3:
-            state = True
+    elif alive == 3:
+        state = True
 
     return state
 
diff --git a/cellular_automata/nagel_schrekenberg.py b/cellular_automata/nagel_schrekenberg.py
index 3fd6afca0153..bcdca902afee 100644
--- a/cellular_automata/nagel_schrekenberg.py
+++ b/cellular_automata/nagel_schrekenberg.py
@@ -24,6 +24,7 @@
 >>> simulate(construct_highway(5, 2, -2), 3, 0, 2)
 [[0, -1, 0, -1, 0], [0, -1, 0, -1, -1], [0, -1, -1, 1, -1], [-1, 1, -1, 0, -1]]
 """
+
 from random import randint, random
 
 
diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py
index e423d1595bdb..29f7ea510bfe 100644
--- a/cellular_automata/wa_tor.py
+++ b/cellular_automata/wa_tor.py
@@ -1,9 +1,9 @@
 """
 Wa-Tor algorithm (1984)
 
-@ https://en.wikipedia.org/wiki/Wa-Tor
-@ https://beltoforion.de/en/wator/
-@ https://beltoforion.de/en/wator/images/wator_medium.webm
+| @ https://en.wikipedia.org/wiki/Wa-Tor
+| @ https://beltoforion.de/en/wator/
+| @ https://beltoforion.de/en/wator/images/wator_medium.webm
 
 This solution aims to completely remove any systematic approach
 to the Wa-Tor planet, and utilise fully random methods.
@@ -97,8 +97,8 @@ class WaTor:
 
     :attr time_passed: A function that is called every time
         time passes (a chronon) in order to visually display
-        the new Wa-Tor planet. The time_passed function can block
-        using time.sleep to slow the algorithm progression.
+        the new Wa-Tor planet. The `time_passed` function can block
+        using ``time.sleep`` to slow the algorithm progression.
 
     >>> wt = WaTor(10, 15)
     >>> wt.width
@@ -216,7 +216,7 @@ def get_surrounding_prey(self, entity: Entity) -> list[Entity]:
         """
         Returns all the prey entities around (N, S, E, W) a predator entity.
 
-        Subtly different to the try_to_move_to_unoccupied square.
+        Subtly different to the `move_and_reproduce`.
 
         >>> wt = WaTor(WIDTH, HEIGHT)
         >>> wt.set_planet([
@@ -260,7 +260,7 @@ def move_and_reproduce(
         """
         Attempts to move to an unoccupied neighbouring square
         in either of the four directions (North, South, East, West).
-        If the move was successful and the remaining_reproduction time is
+        If the move was successful and the `remaining_reproduction_time` is
         equal to 0, then a new prey or predator can also be created
         in the previous square.
 
@@ -351,12 +351,12 @@ def perform_prey_actions(
         Performs the actions for a prey entity
 
         For prey the rules are:
-          1. At each chronon, a prey moves randomly to one of the adjacent unoccupied
-            squares. If there are no free squares, no movement takes place.
-          2. Once a prey has survived a certain number of chronons it may reproduce.
-            This is done as it moves to a neighbouring square,
-            leaving behind a new prey in its old position.
-            Its reproduction time is also reset to zero.
+            1. At each chronon, a prey moves randomly to one of the adjacent unoccupied
+               squares. If there are no free squares, no movement takes place.
+            2. Once a prey has survived a certain number of chronons it may reproduce.
+               This is done as it moves to a neighbouring square,
+               leaving behind a new prey in its old position.
+               Its reproduction time is also reset to zero.
 
         >>> wt = WaTor(WIDTH, HEIGHT)
         >>> reproducable_entity = Entity(True, coords=(0, 1))
@@ -382,15 +382,15 @@ def perform_predator_actions(
         :param occupied_by_prey_coords: Move to this location if there is prey there
 
         For predators the rules are:
-          1. At each chronon, a predator moves randomly to an adjacent square occupied
-            by a prey. If there is none, the predator moves to a random adjacent
-            unoccupied square. If there are no free squares, no movement takes place.
-          2. At each chronon, each predator is deprived of a unit of energy.
-          3. Upon reaching zero energy, a predator dies.
-          4. If a predator moves to a square occupied by a prey,
-            it eats the prey and earns a certain amount of energy.
-          5. Once a predator has survived a certain number of chronons
-          it may reproduce in exactly the same way as the prey.
+            1. At each chronon, a predator moves randomly to an adjacent square occupied
+               by a prey. If there is none, the predator moves to a random adjacent
+               unoccupied square. If there are no free squares, no movement takes place.
+            2. At each chronon, each predator is deprived of a unit of energy.
+            3. Upon reaching zero energy, a predator dies.
+            4. If a predator moves to a square occupied by a prey,
+               it eats the prey and earns a certain amount of energy.
+            5. Once a predator has survived a certain number of chronons
+               it may reproduce in exactly the same way as the prey.
 
         >>> wt = WaTor(WIDTH, HEIGHT)
         >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]])
@@ -430,7 +430,7 @@ def perform_predator_actions(
 
     def run(self, *, iteration_count: int) -> None:
         """
-        Emulate time passing by looping iteration_count times
+        Emulate time passing by looping `iteration_count` times
 
         >>> wt = WaTor(WIDTH, HEIGHT)
         >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1)
@@ -484,11 +484,9 @@ def visualise(wt: WaTor, iter_number: int, *, colour: bool = True) -> None:
     an ascii code in terminal to clear and re-print
     the Wa-Tor planet at intervals.
 
-    Uses ascii colour codes to colourfully display
-    the predators and prey.
-
-    (0x60f197) Prey = #
-    (0xfffff) Predator = x
+    Uses ascii colour codes to colourfully display the predators and prey:
+        * (0x60f197) Prey = ``#``
+        * (0xfffff) Predator = ``x``
 
     >>> wt = WaTor(30, 30)
     >>> wt.set_planet([
diff --git a/ciphers/a1z26.py b/ciphers/a1z26.py
index 0f0eb7c5c083..a1377ea6d397 100644
--- a/ciphers/a1z26.py
+++ b/ciphers/a1z26.py
@@ -5,6 +5,7 @@
 https://www.dcode.fr/letter-number-cipher
 http://bestcodes.weebly.com/a1z26.html
 """
+
 from __future__ import annotations
 
 
diff --git a/ciphers/atbash.py b/ciphers/atbash.py
index 0a86a800c51a..4e8f663ed02d 100644
--- a/ciphers/atbash.py
+++ b/ciphers/atbash.py
@@ -1,4 +1,5 @@
-""" https://en.wikipedia.org/wiki/Atbash """
+"""https://en.wikipedia.org/wiki/Atbash"""
+
 import string
 
 
diff --git a/ciphers/autokey.py b/ciphers/autokey.py
index 8683e6d37001..7751a32d7546 100644
--- a/ciphers/autokey.py
+++ b/ciphers/autokey.py
@@ -1,5 +1,6 @@
 """
 https://en.wikipedia.org/wiki/Autokey_cipher
+
 An autokey cipher (also known as the autoclave cipher) is a cipher that
 incorporates the message (the plaintext) into the key.
 The key is generated from the message in some automated fashion,
@@ -10,8 +11,9 @@
 
 def encrypt(plaintext: str, key: str) -> str:
     """
-    Encrypt a given plaintext (string) and key (string), returning the
+    Encrypt a given `plaintext` (string) and `key` (string), returning the
     encrypted ciphertext.
+
     >>> encrypt("hello world", "coffee")
     'jsqqs avvwo'
     >>> encrypt("coffee is good as python", "TheAlgorithms")
@@ -24,6 +26,14 @@ def encrypt(plaintext: str, key: str) -> str:
     Traceback (most recent call last):
         ...
     ValueError: plaintext is empty
+    >>> encrypt("coffee is good as python", "")
+    Traceback (most recent call last):
+        ...
+    ValueError: key is empty
+    >>> encrypt(527.26, "TheAlgorithms")
+    Traceback (most recent call last):
+        ...
+    TypeError: plaintext must be a string
     """
     if not isinstance(plaintext, str):
         raise TypeError("plaintext must be a string")
@@ -66,8 +76,9 @@ def encrypt(plaintext: str, key: str) -> str:
 
 def decrypt(ciphertext: str, key: str) -> str:
     """
-    Decrypt a given ciphertext (string) and key (string), returning the decrypted
+    Decrypt a given `ciphertext` (string) and `key` (string), returning the decrypted
     ciphertext.
+
     >>> decrypt("jsqqs avvwo", "coffee")
     'hello world'
     >>> decrypt("vvjfpk wj ohvp su ddylsv", "TheAlgorithms")
@@ -80,6 +91,14 @@ def decrypt(ciphertext: str, key: str) -> str:
     Traceback (most recent call last):
         ...
     TypeError: ciphertext must be a string
+    >>> decrypt("", "TheAlgorithms")
+    Traceback (most recent call last):
+        ...
+    ValueError: ciphertext is empty
+    >>> decrypt("vvjfpk wj ohvp su ddylsv", 2)
+    Traceback (most recent call last):
+        ...
+    TypeError: key must be a string
     """
     if not isinstance(ciphertext, str):
         raise TypeError("ciphertext must be a string")
diff --git a/ciphers/base32.py b/ciphers/base32.py
index 1924d1e185d7..911afa2452c0 100644
--- a/ciphers/base32.py
+++ b/ciphers/base32.py
@@ -3,6 +3,7 @@
 
 https://en.wikipedia.org/wiki/Base32
 """
+
 B32_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
 
 
diff --git a/ciphers/base64.py b/ciphers/base64_cipher.py
similarity index 94%
rename from ciphers/base64.py
rename to ciphers/base64_cipher.py
index 2b950b1be37d..038d13963d95 100644
--- a/ciphers/base64.py
+++ b/ciphers/base64_cipher.py
@@ -105,13 +105,13 @@ def base64_decode(encoded_data: str) -> bytes:
 
     # Check if the encoded string contains non base64 characters
     if padding:
-        assert all(
-            char in B64_CHARSET for char in encoded_data[:-padding]
-        ), "Invalid base64 character(s) found."
+        assert all(char in B64_CHARSET for char in encoded_data[:-padding]), (
+            "Invalid base64 character(s) found."
+        )
     else:
-        assert all(
-            char in B64_CHARSET for char in encoded_data
-        ), "Invalid base64 character(s) found."
+        assert all(char in B64_CHARSET for char in encoded_data), (
+            "Invalid base64 character(s) found."
+        )
 
     # Check the padding
     assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding"
diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py
index d19b9a337221..1cf4d67cbaed 100644
--- a/ciphers/caesar_cipher.py
+++ b/ciphers/caesar_cipher.py
@@ -7,24 +7,29 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
     """
     encrypt
     =======
+
     Encodes a given string with the caesar cipher and returns the encoded
     message
 
     Parameters:
     -----------
-    *   input_string: the plain-text that needs to be encoded
-    *   key: the number of letters to shift the message by
+
+    *   `input_string`: the plain-text that needs to be encoded
+    *   `key`: the number of letters to shift the message by
 
     Optional:
-    *   alphabet (None): the alphabet used to encode the cipher, if not
+
+    *   `alphabet` (``None``): the alphabet used to encode the cipher, if not
         specified, the standard english alphabet with upper and lowercase
         letters is used
 
     Returns:
+
     *   A string containing the encoded cipher-text
 
     More on the caesar cipher
     =========================
+
     The caesar cipher is named after Julius Caesar who used it when sending
     secret military messages to his troops. This is a simple substitution cipher
     where every character in the plain-text is shifted by a certain number known
@@ -32,26 +37,28 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
 
     Example:
     Say we have the following message:
-    "Hello, captain"
+    ``Hello, captain``
 
     And our alphabet is made up of lower and uppercase letters:
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``
 
-    And our shift is "2"
+    And our shift is ``2``
 
-    We can then encode the message, one letter at a time. "H" would become "J",
-    since "J" is two letters away, and so on. If the shift is ever two large, or
+    We can then encode the message, one letter at a time. ``H`` would become ``J``,
+    since ``J`` is two letters away, and so on. If the shift is ever two large, or
     our letter is at the end of the alphabet, we just start at the beginning
-    ("Z" would shift to "a" then "b" and so on).
+    (``Z`` would shift to ``a`` then ``b`` and so on).
 
-    Our final message would be "Jgnnq, ecrvckp"
+    Our final message would be ``Jgnnq, ecrvckp``
 
     Further reading
     ===============
+
     *   https://en.m.wikipedia.org/wiki/Caesar_cipher
 
     Doctests
     ========
+
     >>> encrypt('The quick brown fox jumps over the lazy dog', 8)
     'bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo'
 
@@ -85,23 +92,28 @@ def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
     """
     decrypt
     =======
+
     Decodes a given string of cipher-text and returns the decoded plain-text
 
     Parameters:
     -----------
-    *   input_string: the cipher-text that needs to be decoded
-    *   key: the number of letters to shift the message backwards by to decode
+
+    *   `input_string`: the cipher-text that needs to be decoded
+    *   `key`: the number of letters to shift the message backwards by to decode
 
     Optional:
-    *   alphabet (None): the alphabet used to decode the cipher, if not
+
+    *   `alphabet` (``None``): the alphabet used to decode the cipher, if not
         specified, the standard english alphabet with upper and lowercase
         letters is used
 
     Returns:
+
     *   A string containing the decoded plain-text
 
     More on the caesar cipher
     =========================
+
     The caesar cipher is named after Julius Caesar who used it when sending
     secret military messages to his troops. This is a simple substitution cipher
     where very character in the plain-text is shifted by a certain number known
@@ -110,27 +122,29 @@ def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
 
     Example:
     Say we have the following cipher-text:
-    "Jgnnq, ecrvckp"
+    ``Jgnnq, ecrvckp``
 
     And our alphabet is made up of lower and uppercase letters:
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``
 
-    And our shift is "2"
+    And our shift is ``2``
 
     To decode the message, we would do the same thing as encoding, but in
-    reverse. The first letter, "J" would become "H" (remember: we are decoding)
-    because "H" is two letters in reverse (to the left) of "J". We would
-    continue doing this. A letter like "a" would shift back to the end of
-    the alphabet, and would become "Z" or "Y" and so on.
+    reverse. The first letter, ``J`` would become ``H`` (remember: we are decoding)
+    because ``H`` is two letters in reverse (to the left) of ``J``. We would
+    continue doing this. A letter like ``a`` would shift back to the end of
+    the alphabet, and would become ``Z`` or ``Y`` and so on.
 
-    Our final message would be "Hello, captain"
+    Our final message would be ``Hello, captain``
 
     Further reading
     ===============
+
     *   https://en.m.wikipedia.org/wiki/Caesar_cipher
 
     Doctests
     ========
+
     >>> decrypt('bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo', 8)
     'The quick brown fox jumps over the lazy dog'
 
@@ -150,41 +164,44 @@ def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str
     """
     brute_force
     ===========
+
     Returns all the possible combinations of keys and the decoded strings in the
     form of a dictionary
 
     Parameters:
     -----------
-    *   input_string: the cipher-text that needs to be used during brute-force
+
+    *   `input_string`: the cipher-text that needs to be used during brute-force
 
     Optional:
-    *   alphabet:  (None): the alphabet used to decode the cipher, if not
+
+    *   `alphabet` (``None``): the alphabet used to decode the cipher, if not
         specified, the standard english alphabet with upper and lowercase
         letters is used
 
     More about brute force
     ======================
+
     Brute force is when a person intercepts a message or password, not knowing
     the key and tries every single combination. This is easy with the caesar
     cipher since there are only all the letters in the alphabet. The more
     complex the cipher, the larger amount of time it will take to do brute force
 
     Ex:
-    Say we have a 5 letter alphabet (abcde), for simplicity and we intercepted the
-    following message:
-
-    "dbc"
-
+    Say we have a ``5`` letter alphabet (``abcde``), for simplicity and we intercepted
+    the following message: ``dbc``,
     we could then just write out every combination:
-    ecd... and so on, until we reach a combination that makes sense:
-    "cab"
+    ``ecd``... and so on, until we reach a combination that makes sense:
+    ``cab``
 
     Further reading
     ===============
+
     *   https://en.wikipedia.org/wiki/Brute_force
 
     Doctests
     ========
+
     >>> brute_force("jFyuMy xIH'N vLONy zILwy Gy!")[20]
     "Please don't brute force me!"
 
@@ -208,7 +225,7 @@ def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str
 
 if __name__ == "__main__":
     while True:
-        print(f'\n{"-" * 10}\n Menu\n{"-" * 10}')
+        print(f"\n{'-' * 10}\n Menu\n{'-' * 10}")
         print(*["1.Encrypt", "2.Decrypt", "3.BruteForce", "4.Quit"], sep="\n")
 
         # get user input
diff --git a/ciphers/decrypt_caesar_with_chi_squared.py b/ciphers/decrypt_caesar_with_chi_squared.py
index 6c36860207cd..fb95c0f90628 100644
--- a/ciphers/decrypt_caesar_with_chi_squared.py
+++ b/ciphers/decrypt_caesar_with_chi_squared.py
@@ -11,33 +11,31 @@ def decrypt_caesar_with_chi_squared(
     """
     Basic Usage
     ===========
+
     Arguments:
-    * ciphertext (str): the text to decode (encoded with the caesar cipher)
+      * `ciphertext` (str): the text to decode (encoded with the caesar cipher)
 
     Optional Arguments:
-    * cipher_alphabet (list): the alphabet used for the cipher (each letter is
-      a string separated by commas)
-    * frequencies_dict (dict): a dictionary of word frequencies where keys are
-      the letters and values are a percentage representation of the frequency as
-      a decimal/float
-    * case_sensitive (bool): a boolean value: True if the case matters during
-      decryption, False if it doesn't
+      * `cipher_alphabet` (list): the alphabet used for the cipher (each letter is
+        a string separated by commas)
+      * `frequencies_dict` (dict): a dictionary of word frequencies where keys are
+        the letters and values are a percentage representation of the frequency as
+        a decimal/float
+      * `case_sensitive` (bool): a boolean value: ``True`` if the case matters during
+        decryption, ``False`` if it doesn't
 
     Returns:
-    * A tuple in the form of:
-      (
-        most_likely_cipher,
-        most_likely_cipher_chi_squared_value,
-        decoded_most_likely_cipher
-      )
+      * A tuple in the form of:
+        (`most_likely_cipher`, `most_likely_cipher_chi_squared_value`,
+        `decoded_most_likely_cipher`)
 
-      where...
-      - most_likely_cipher is an integer representing the shift of the smallest
-        chi-squared statistic (most likely key)
-      - most_likely_cipher_chi_squared_value is a float representing the
-        chi-squared statistic of the most likely shift
-      - decoded_most_likely_cipher is a string with the decoded cipher
-        (decoded by the most_likely_cipher key)
+        where...
+          - `most_likely_cipher` is an integer representing the shift of the smallest
+            chi-squared statistic (most likely key)
+          - `most_likely_cipher_chi_squared_value` is a float representing the
+            chi-squared statistic of the most likely shift
+          - `decoded_most_likely_cipher` is a string with the decoded cipher
+            (decoded by the most_likely_cipher key)
 
 
     The Chi-squared test
@@ -45,52 +43,57 @@ def decrypt_caesar_with_chi_squared(
 
     The caesar cipher
     -----------------
+
     The caesar cipher is a very insecure encryption algorithm, however it has
     been used since Julius Caesar. The cipher is a simple substitution cipher
     where each character in the plain text is replaced by a character in the
     alphabet a certain number of characters after the original character. The
     number of characters away is called the shift or key. For example:
 
-    Plain text: hello
-    Key: 1
-    Cipher text: ifmmp
-    (each letter in hello has been shifted one to the right in the eng. alphabet)
+    | Plain text: ``hello``
+    | Key: ``1``
+    | Cipher text: ``ifmmp``
+    | (each letter in ``hello`` has been shifted one to the right in the eng. alphabet)
 
     As you can imagine, this doesn't provide lots of security. In fact
     decrypting ciphertext by brute-force is extremely easy even by hand. However
-     one way to do that is the chi-squared test.
+    one way to do that is the chi-squared test.
 
     The chi-squared test
-    -------------------
+    --------------------
+
     Each letter in the english alphabet has a frequency, or the amount of times
     it shows up compared to other letters (usually expressed as a decimal
     representing the percentage likelihood). The most common letter in the
-    english language is "e" with a frequency of 0.11162 or 11.162%. The test is
-    completed in the following fashion.
+    english language is ``e`` with a frequency of ``0.11162`` or ``11.162%``.
+    The test is completed in the following fashion.
 
     1. The ciphertext is decoded in a brute force way (every combination of the
-       26 possible combinations)
+       ``26`` possible combinations)
     2. For every combination, for each letter in the combination, the average
        amount of times the letter should appear the message is calculated by
-       multiplying the total number of characters by the frequency of the letter
+       multiplying the total number of characters by the frequency of the letter.
+
+       | For example:
+       | In a message of ``100`` characters, ``e`` should appear around ``11.162``
+         times.
 
-       For example:
-       In a message of 100 characters, e should appear around 11.162 times.
+    3. Then, to calculate the margin of error (the amount of times the letter
+       SHOULD appear with the amount of times the letter DOES appear), we use
+       the chi-squared test. The following formula is used:
 
-     3. Then, to calculate the margin of error (the amount of times the letter
-        SHOULD appear with the amount of times the letter DOES appear), we use
-        the chi-squared test. The following formula is used:
+       Let:
+         - n be the number of times the letter actually appears
+         - p be the predicted value of the number of times the letter should
+           appear (see item ``2``)
+         - let v be the chi-squared test result (referred to here as chi-squared
+           value/statistic)
 
-        Let:
-        - n be the number of times the letter actually appears
-        - p be the predicted value of the number of times the letter should
-          appear (see #2)
-        - let v be the chi-squared test result (referred to here as chi-squared
-          value/statistic)
+         ::
 
-        (n - p)^2
-        --------- = v
-           p
+           (n - p)^2
+           --------- = v
+              p
 
     4. Each chi squared value for each letter is then added up to the total.
        The total is the chi-squared statistic for that encryption key.
@@ -98,16 +101,16 @@ def decrypt_caesar_with_chi_squared(
        to be the decoded answer.
 
     Further Reading
-    ================
+    ===============
 
-    * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared-
-        statistic/
+    * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared-statistic/
     * https://en.wikipedia.org/wiki/Letter_frequency
     * https://en.wikipedia.org/wiki/Chi-squared_test
     * https://en.m.wikipedia.org/wiki/Caesar_cipher
 
     Doctests
     ========
+
     >>> decrypt_caesar_with_chi_squared(
     ...    'dof pz aol jhlzhy jpwoly zv wvwbshy? pa pz avv lhzf av jyhjr!'
     ... )  # doctest: +NORMALIZE_WHITESPACE
@@ -206,20 +209,19 @@ def decrypt_caesar_with_chi_squared(
 
                     # Add the margin of error to the total chi squared statistic
                     chi_squared_statistic += chi_letter_value
-            else:
-                if letter.lower() in frequencies:
-                    # Get the amount of times the letter occurs in the message
-                    occurrences = decrypted_with_shift.count(letter)
+            elif letter.lower() in frequencies:
+                # Get the amount of times the letter occurs in the message
+                occurrences = decrypted_with_shift.count(letter)
 
-                    # Get the excepcted amount of times the letter should appear based
-                    # on letter frequencies
-                    expected = frequencies[letter] * occurrences
+                # Get the excepcted amount of times the letter should appear based
+                # on letter frequencies
+                expected = frequencies[letter] * occurrences
 
-                    # Complete the chi squared statistic formula
-                    chi_letter_value = ((occurrences - expected) ** 2) / expected
+                # Complete the chi squared statistic formula
+                chi_letter_value = ((occurrences - expected) ** 2) / expected
 
-                    # Add the margin of error to the total chi squared statistic
-                    chi_squared_statistic += chi_letter_value
+                # Add the margin of error to the total chi squared statistic
+                chi_squared_statistic += chi_letter_value
 
         # Add the data to the chi_squared_statistic_values dictionary
         chi_squared_statistic_values[shift] = (
diff --git a/ciphers/enigma_machine2.py b/ciphers/enigma_machine2.py
index ec0d44e4a6c6..e42fdd82ed41 100644
--- a/ciphers/enigma_machine2.py
+++ b/ciphers/enigma_machine2.py
@@ -1,19 +1,22 @@
 """
-Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine
-Video explanation: https://youtu.be/QwQVMqfoB2E
-Also check out Numberphile's and Computerphile's videos on this topic
+| Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine
+| Video explanation: https://youtu.be/QwQVMqfoB2E
+| Also check out Numberphile's and Computerphile's videos on this topic
 
-This module contains function 'enigma' which emulates
+This module contains function ``enigma`` which emulates
 the famous Enigma machine from WWII.
+
 Module includes:
-- enigma function
+
+- ``enigma`` function
 - showcase of function usage
-- 9 randomly generated rotors
+- ``9`` randomly generated rotors
 - reflector (aka static rotor)
 - original alphabet
 
 Created by TrapinchO
 """
+
 from __future__ import annotations
 
 RotorPositionT = tuple[int, int, int]
@@ -72,7 +75,7 @@ def _validator(
     rotpos: RotorPositionT, rotsel: RotorSelectionT, pb: str
 ) -> tuple[RotorPositionT, RotorSelectionT, dict[str, str]]:
     """
-    Checks if the values can be used for the 'enigma' function
+    Checks if the values can be used for the ``enigma`` function
 
     >>> _validator((1,1,1), (rotor1, rotor2, rotor3), 'POLAND')
     ((1, 1, 1), ('EGZWVONAHDCLFQMSIPJBYUKXTR', 'FOBHMDKEXQNRAULPGSJVTYICZW', \
@@ -82,7 +85,7 @@ def _validator(
     :param rotpos: rotor_positon
     :param rotsel: rotor_selection
     :param pb: plugb -> validated and transformed
-    :return: (rotpos, rotsel, pb)
+    :return: (`rotpos`, `rotsel`, `pb`)
     """
     # Checks if there are 3 unique rotors
 
@@ -117,9 +120,10 @@ def _plugboard(pbstring: str) -> dict[str, str]:
     >>> _plugboard('POLAND')
     {'P': 'O', 'O': 'P', 'L': 'A', 'A': 'L', 'N': 'D', 'D': 'N'}
 
-    In the code, 'pb' stands for 'plugboard'
+    In the code, ``pb`` stands for ``plugboard``
 
     Pairs can be separated by spaces
+
     :param pbstring: string containing plugboard setting for the Enigma machine
     :return: dictionary containing converted pairs
     """
@@ -167,31 +171,34 @@ def enigma(
     plugb: str = "",
 ) -> str:
     """
-    The only difference with real-world enigma is that I allowed string input.
+    The only difference with real-world enigma is that ``I`` allowed string input.
     All characters are converted to uppercase. (non-letter symbol are ignored)
-    How it works:
-    (for every letter in the message)
+
+    | How it works:
+    | (for every letter in the message)
 
     - Input letter goes into the plugboard.
-    If it is connected to another one, switch it.
+      If it is connected to another one, switch it.
+
+    - Letter goes through ``3`` rotors.
+      Each rotor can be represented as ``2`` sets of symbol, where one is shuffled.
+      Each symbol from the first set has corresponding symbol in
+      the second set and vice versa.
 
-    - Letter goes through 3 rotors.
-    Each rotor can be represented as 2 sets of symbol, where one is shuffled.
-    Each symbol from the first set has corresponding symbol in
-    the second set and vice versa.
+      example::
 
-    example:
-    | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F
-    | VKLEPDBGRNWTFCJOHQAMUZYIXS |
+      | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F
+      | VKLEPDBGRNWTFCJOHQAMUZYIXS |
 
     - Symbol then goes through reflector (static rotor).
-    There it is switched with paired symbol
-    The reflector can be represented as2 sets, each with half of the alphanet.
-    There are usually 10 pairs of letters.
+      There it is switched with paired symbol.
+      The reflector can be represented as ``2`` sets, each with half of the alphanet.
+      There are usually ``10`` pairs of letters.
+
+      Example::
 
-    Example:
-    | ABCDEFGHIJKLM | e.g. E is paired to X
-    | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa
+      | ABCDEFGHIJKLM | e.g. E is paired to X
+      | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa
 
     - Letter then goes through the rotors again
 
@@ -210,9 +217,9 @@ def enigma(
 
 
     :param text: input message
-    :param rotor_position: tuple with 3 values in range 1..26
-    :param rotor_selection: tuple with 3 rotors ()
-    :param plugb: string containing plugboard configuration (default '')
+    :param rotor_position: tuple with ``3`` values in range ``1``.. ``26``
+    :param rotor_selection: tuple with ``3`` rotors
+    :param plugb: string containing plugboard configuration (default ``''``)
     :return: en/decrypted string
     """
 
diff --git a/ciphers/fractionated_morse_cipher.py b/ciphers/fractionated_morse_cipher.py
index c1d5dc6d50aa..6c4c415abac1 100644
--- a/ciphers/fractionated_morse_cipher.py
+++ b/ciphers/fractionated_morse_cipher.py
@@ -8,6 +8,7 @@
 
 http://practicalcryptography.com/ciphers/fractionated-morse-cipher/
 """
+
 import string
 
 MORSE_CODE_DICT = {
diff --git a/ciphers/gronsfeld_cipher.py b/ciphers/gronsfeld_cipher.py
new file mode 100644
index 000000000000..8fbeab4307fc
--- /dev/null
+++ b/ciphers/gronsfeld_cipher.py
@@ -0,0 +1,45 @@
+from string import ascii_uppercase
+
+
+def gronsfeld(text: str, key: str) -> str:
+    """
+    Encrypt plaintext with the Gronsfeld cipher
+
+    >>> gronsfeld('hello', '412')
+    'LFNPP'
+    >>> gronsfeld('hello', '123')
+    'IGOMQ'
+    >>> gronsfeld('', '123')
+    ''
+    >>> gronsfeld('yes, ¥€$ - _!@#%?', '0')
+    'YES, ¥€$ - _!@#%?'
+    >>> gronsfeld('yes, ¥€$ - _!@#%?', '01')
+    'YFS, ¥€$ - _!@#%?'
+    >>> gronsfeld('yes, ¥€$ - _!@#%?', '012')
+    'YFU, ¥€$ - _!@#%?'
+    >>> gronsfeld('yes, ¥€$ - _!@#%?', '')
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: integer modulo by zero
+    """
+    ascii_len = len(ascii_uppercase)
+    key_len = len(key)
+    encrypted_text = ""
+    keys = [int(char) for char in key]
+    upper_case_text = text.upper()
+
+    for i, char in enumerate(upper_case_text):
+        if char in ascii_uppercase:
+            new_position = (ascii_uppercase.index(char) + keys[i % key_len]) % ascii_len
+            shifted_letter = ascii_uppercase[new_position]
+            encrypted_text += shifted_letter
+        else:
+            encrypted_text += char
+
+    return encrypted_text
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
diff --git a/ciphers/hill_cipher.py b/ciphers/hill_cipher.py
index 1201fda901e5..c690d29bd113 100644
--- a/ciphers/hill_cipher.py
+++ b/ciphers/hill_cipher.py
@@ -35,9 +35,10 @@
 https://www.youtube.com/watch?v=4RhLNDqcjpA
 
 """
+
 import string
 
-import numpy
+import numpy as np
 
 from maths.greatest_common_divisor import greatest_common_divisor
 
@@ -48,11 +49,11 @@ class HillCipher:
     # i.e. a total of 36 characters
 
     # take x and return x % len(key_string)
-    modulus = numpy.vectorize(lambda x: x % 36)
+    modulus = np.vectorize(lambda x: x % 36)
 
-    to_int = numpy.vectorize(round)
+    to_int = np.vectorize(round)
 
-    def __init__(self, encrypt_key: numpy.ndarray) -> None:
+    def __init__(self, encrypt_key: np.ndarray) -> None:
         """
         encrypt_key is an NxN numpy array
         """
@@ -62,7 +63,7 @@ def __init__(self, encrypt_key: numpy.ndarray) -> None:
 
     def replace_letters(self, letter: str) -> int:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.replace_letters('T')
         19
         >>> hill_cipher.replace_letters('0')
@@ -72,20 +73,22 @@ def replace_letters(self, letter: str) -> int:
 
     def replace_digits(self, num: int) -> str:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.replace_digits(19)
         'T'
         >>> hill_cipher.replace_digits(26)
         '0'
+        >>> hill_cipher.replace_digits(26.1)
+        '0'
         """
-        return self.key_string[round(num)]
+        return self.key_string[int(num)]
 
     def check_determinant(self) -> None:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.check_determinant()
         """
-        det = round(numpy.linalg.det(self.encrypt_key))
+        det = round(np.linalg.det(self.encrypt_key))
 
         if det < 0:
             det = det % len(self.key_string)
@@ -100,7 +103,7 @@ def check_determinant(self) -> None:
 
     def process_text(self, text: str) -> str:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.process_text('Testing Hill Cipher')
         'TESTINGHILLCIPHERR'
         >>> hill_cipher.process_text('hello')
@@ -116,7 +119,7 @@ def process_text(self, text: str) -> str:
 
     def encrypt(self, text: str) -> str:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.encrypt('testing hill cipher')
         'WHXYJOLM9C6XT085LL'
         >>> hill_cipher.encrypt('hello')
@@ -128,7 +131,7 @@ def encrypt(self, text: str) -> str:
         for i in range(0, len(text) - self.break_key + 1, self.break_key):
             batch = text[i : i + self.break_key]
             vec = [self.replace_letters(char) for char in batch]
-            batch_vec = numpy.array([vec]).T
+            batch_vec = np.array([vec]).T
             batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[
                 0
             ]
@@ -139,14 +142,14 @@ def encrypt(self, text: str) -> str:
 
         return encrypted
 
-    def make_decrypt_key(self) -> numpy.ndarray:
+    def make_decrypt_key(self) -> np.ndarray:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.make_decrypt_key()
         array([[ 6, 25],
                [ 5, 26]])
         """
-        det = round(numpy.linalg.det(self.encrypt_key))
+        det = round(np.linalg.det(self.encrypt_key))
 
         if det < 0:
             det = det % len(self.key_string)
@@ -157,16 +160,14 @@ def make_decrypt_key(self) -> numpy.ndarray:
                 break
 
         inv_key = (
-            det_inv
-            * numpy.linalg.det(self.encrypt_key)
-            * numpy.linalg.inv(self.encrypt_key)
+            det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key)
         )
 
         return self.to_int(self.modulus(inv_key))
 
     def decrypt(self, text: str) -> str:
         """
-        >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]]))
+        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
         >>> hill_cipher.decrypt('WHXYJOLM9C6XT085LL')
         'TESTINGHILLCIPHERR'
         >>> hill_cipher.decrypt('85FF00')
@@ -179,7 +180,7 @@ def decrypt(self, text: str) -> str:
         for i in range(0, len(text) - self.break_key + 1, self.break_key):
             batch = text[i : i + self.break_key]
             vec = [self.replace_letters(char) for char in batch]
-            batch_vec = numpy.array([vec]).T
+            batch_vec = np.array([vec]).T
             batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0]
             decrypted_batch = "".join(
                 self.replace_digits(num) for num in batch_decrypted
@@ -198,7 +199,7 @@ def main() -> None:
         row = [int(x) for x in input().split()]
         hill_matrix.append(row)
 
-    hc = HillCipher(numpy.array(hill_matrix))
+    hc = HillCipher(np.array(hill_matrix))
 
     print("Would you like to encrypt or decrypt some text? (1 or 2)")
     option = input("\n1. Encrypt\n2. Decrypt\n")
diff --git a/ciphers/mixed_keyword_cypher.py b/ciphers/mixed_keyword_cypher.py
index b984808fced6..1b186108a73e 100644
--- a/ciphers/mixed_keyword_cypher.py
+++ b/ciphers/mixed_keyword_cypher.py
@@ -67,7 +67,7 @@ def mixed_keyword(
     if verbose:
         print(mapping)
     # create the encrypted text by mapping the plaintext to the modified alphabet
-    return "".join(mapping[char] if char in mapping else char for char in plaintext)
+    return "".join(mapping.get(char, char) for char in plaintext)
 
 
 if __name__ == "__main__":
diff --git a/ciphers/onepad_cipher.py b/ciphers/onepad_cipher.py
index 4bfe35b7180a..c4fb22e14a06 100644
--- a/ciphers/onepad_cipher.py
+++ b/ciphers/onepad_cipher.py
@@ -4,7 +4,27 @@
 class Onepad:
     @staticmethod
     def encrypt(text: str) -> tuple[list[int], list[int]]:
-        """Function to encrypt text using pseudo-random numbers"""
+        """
+        Function to encrypt text using pseudo-random numbers
+        >>> Onepad().encrypt("")
+        ([], [])
+        >>> Onepad().encrypt([])
+        ([], [])
+        >>> random.seed(1)
+        >>> Onepad().encrypt(" ")
+        ([6969], [69])
+        >>> random.seed(1)
+        >>> Onepad().encrypt("Hello")
+        ([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61])
+        >>> Onepad().encrypt(1)
+        Traceback (most recent call last):
+        ...
+        TypeError: 'int' object is not iterable
+        >>> Onepad().encrypt(1.1)
+        Traceback (most recent call last):
+        ...
+        TypeError: 'float' object is not iterable
+        """
         plain = [ord(i) for i in text]
         key = []
         cipher = []
@@ -17,7 +37,20 @@ def encrypt(text: str) -> tuple[list[int], list[int]]:
 
     @staticmethod
     def decrypt(cipher: list[int], key: list[int]) -> str:
-        """Function to decrypt text using pseudo-random numbers."""
+        """
+        Function to decrypt text using pseudo-random numbers.
+        >>> Onepad().decrypt([], [])
+        ''
+        >>> Onepad().decrypt([35], [])
+        ''
+        >>> Onepad().decrypt([], [35])
+        Traceback (most recent call last):
+        ...
+        IndexError: list index out of range
+        >>> random.seed(1)
+        >>> Onepad().decrypt([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61])
+        'Hello'
+        """
         plain = []
         for i in range(len(key)):
             p = int((cipher[i] - (key[i]) ** 2) / key[i])
diff --git a/ciphers/permutation_cipher.py b/ciphers/permutation_cipher.py
index c3f3fd1f7f94..9e1c64a7b4ea 100644
--- a/ciphers/permutation_cipher.py
+++ b/ciphers/permutation_cipher.py
@@ -7,6 +7,7 @@
 
 For more info: https://www.nku.edu/~christensen/1402%20permutation%20ciphers.pdf
 """
+
 import random
 
 
diff --git a/ciphers/playfair_cipher.py b/ciphers/playfair_cipher.py
index 7279fb23ecb2..d48f113f02e0 100644
--- a/ciphers/playfair_cipher.py
+++ b/ciphers/playfair_cipher.py
@@ -1,9 +1,30 @@
+"""
+https://en.wikipedia.org/wiki/Playfair_cipher#Description
+
+The Playfair cipher was developed by Charles Wheatstone in 1854
+It's use was heavily promotedby Lord Playfair, hence its name
+
+Some features of the Playfair cipher are:
+
+1) It was the first literal diagram substitution cipher
+2) It is a manual symmetric encryption technique
+3) It is a multiple letter encryption cipher
+
+The implementation in the code below encodes alphabets only.
+It removes spaces, special characters and numbers from the
+code.
+
+Playfair is no longer used by military forces because of known
+insecurities and of the advent of automated encryption devices.
+This cipher is regarded as insecure since before World War I.
+"""
+
 import itertools
 import string
 from collections.abc import Generator, Iterable
 
 
-def chunker(seq: Iterable[str], size: int) -> Generator[tuple[str, ...], None, None]:
+def chunker(seq: Iterable[str], size: int) -> Generator[tuple[str, ...]]:
     it = iter(seq)
     while True:
         chunk = tuple(itertools.islice(it, size))
@@ -60,11 +81,26 @@ def generate_table(key: str) -> list[str]:
 
 
 def encode(plaintext: str, key: str) -> str:
+    """
+    Encode the given plaintext using the Playfair cipher.
+    Takes the plaintext and the key as input and returns the encoded string.
+
+    >>> encode("Hello", "MONARCHY")
+    'CFSUPM'
+    >>> encode("attack on the left flank", "EMERGENCY")
+    'DQZSBYFSDZFMFNLOHFDRSG'
+    >>> encode("Sorry!", "SPECIAL")
+    'AVXETX'
+    >>> encode("Number 1", "NUMBER")
+    'UMBENF'
+    >>> encode("Photosynthesis!", "THE SUN")
+    'OEMHQHVCHESUKE'
+    """
+
     table = generate_table(key)
     plaintext = prepare_input(plaintext)
     ciphertext = ""
 
-    # https://en.wikipedia.org/wiki/Playfair_cipher#Description
     for char1, char2 in chunker(plaintext, 2):
         row1, col1 = divmod(table.index(char1), 5)
         row2, col2 = divmod(table.index(char2), 5)
@@ -83,10 +119,20 @@ def encode(plaintext: str, key: str) -> str:
 
 
 def decode(ciphertext: str, key: str) -> str:
+    """
+    Decode the input string using the provided key.
+
+    >>> decode("BMZFAZRZDH", "HAZARD")
+    'FIREHAZARD'
+    >>> decode("HNBWBPQT", "AUTOMOBILE")
+    'DRIVINGX'
+    >>> decode("SLYSSAQS", "CASTLE")
+    'ATXTACKX'
+    """
+
     table = generate_table(key)
     plaintext = ""
 
-    # https://en.wikipedia.org/wiki/Playfair_cipher#Description
     for char1, char2 in chunker(ciphertext, 2):
         row1, col1 = divmod(table.index(char1), 5)
         row2, col2 = divmod(table.index(char2), 5)
@@ -102,3 +148,12 @@ def decode(ciphertext: str, key: str) -> str:
             plaintext += table[row2 * 5 + col1]
 
     return plaintext
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    print("Encoded:", encode("BYE AND THANKS", "GREETING"))
+    print("Decoded:", decode("CXRBANRLBALQ", "GREETING"))
diff --git a/ciphers/rail_fence_cipher.py b/ciphers/rail_fence_cipher.py
index 47ee7db89831..5b2311a115e4 100644
--- a/ciphers/rail_fence_cipher.py
+++ b/ciphers/rail_fence_cipher.py
@@ -1,4 +1,4 @@
-""" https://en.wikipedia.org/wiki/Rail_fence_cipher """
+"""https://en.wikipedia.org/wiki/Rail_fence_cipher"""
 
 
 def encrypt(input_string: str, key: int) -> str:
diff --git a/ciphers/rsa_cipher.py b/ciphers/rsa_cipher.py
index 9c41cdc5d472..ac9782a49fff 100644
--- a/ciphers/rsa_cipher.py
+++ b/ciphers/rsa_cipher.py
@@ -76,11 +76,9 @@ def encrypt_and_write_to_file(
     key_size, n, e = read_key_file(key_filename)
     if key_size < block_size * 8:
         sys.exit(
-            "ERROR: Block size is {} bits and key size is {} bits. The RSA cipher "
-            "requires the block size to be equal to or greater than the key size. "
-            "Either decrease the block size or use different keys.".format(
-                block_size * 8, key_size
-            )
+            f"ERROR: Block size is {block_size * 8} bits and key size is {key_size} "
+            "bits. The RSA cipher requires the block size to be equal to or greater "
+            "than the key size. Either decrease the block size or use different keys."
         )
 
     encrypted_blocks = [str(i) for i in encrypt_message(message, (n, e), block_size)]
@@ -102,11 +100,9 @@ def read_from_file_and_decrypt(message_filename: str, key_filename: str) -> str:
 
     if key_size < block_size * 8:
         sys.exit(
-            "ERROR: Block size is {} bits and key size is {} bits. The RSA cipher "
-            "requires the block size to be equal to or greater than the key size. "
-            "Did you specify the correct key file and encrypted file?".format(
-                block_size * 8, key_size
-            )
+            f"ERROR: Block size is {block_size * 8} bits and key size is {key_size} "
+            "bits. The RSA cipher requires the block size to be equal to or greater "
+            "than the key size. Were the correct key file and encrypted file specified?"
         )
 
     encrypted_blocks = []
diff --git a/ciphers/rsa_factorization.py b/ciphers/rsa_factorization.py
index 9ee52777ed83..585b21fac856 100644
--- a/ciphers/rsa_factorization.py
+++ b/ciphers/rsa_factorization.py
@@ -3,10 +3,13 @@
 
 The program can efficiently factor RSA prime number given the private key d and
 public key e.
-Source: on page 3 of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf
-More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html
+
+| Source: on page ``3`` of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf
+| More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html
+
 large number can take minutes to factor, therefore are not included in doctest.
 """
+
 from __future__ import annotations
 
 import math
@@ -16,13 +19,14 @@
 def rsafactor(d: int, e: int, n: int) -> list[int]:
     """
     This function returns the factors of N, where p*q=N
-      Return: [p, q]
+
+    Return: [p, q]
 
     We call N the RSA modulus, e the encryption exponent, and d the decryption exponent.
     The pair (N, e) is the public key. As its name suggests, it is public and is used to
-        encrypt messages.
+    encrypt messages.
     The pair (N, d) is the secret key or private key and is known only to the recipient
-        of encrypted messages.
+    of encrypted messages.
 
     >>> rsafactor(3, 16971, 25777)
     [149, 173]
diff --git a/ciphers/running_key_cipher.py b/ciphers/running_key_cipher.py
new file mode 100644
index 000000000000..6bda417be898
--- /dev/null
+++ b/ciphers/running_key_cipher.py
@@ -0,0 +1,75 @@
+"""
+https://en.wikipedia.org/wiki/Running_key_cipher
+"""
+
+
+def running_key_encrypt(key: str, plaintext: str) -> str:
+    """
+    Encrypts the plaintext using the Running Key Cipher.
+
+    :param key: The running key (long piece of text).
+    :param plaintext: The plaintext to be encrypted.
+    :return: The ciphertext.
+    """
+    plaintext = plaintext.replace(" ", "").upper()
+    key = key.replace(" ", "").upper()
+    key_length = len(key)
+    ciphertext = []
+    ord_a = ord("A")
+
+    for i, char in enumerate(plaintext):
+        p = ord(char) - ord_a
+        k = ord(key[i % key_length]) - ord_a
+        c = (p + k) % 26
+        ciphertext.append(chr(c + ord_a))
+
+    return "".join(ciphertext)
+
+
+def running_key_decrypt(key: str, ciphertext: str) -> str:
+    """
+    Decrypts the ciphertext using the Running Key Cipher.
+
+    :param key: The running key (long piece of text).
+    :param ciphertext: The ciphertext to be decrypted.
+    :return: The plaintext.
+    """
+    ciphertext = ciphertext.replace(" ", "").upper()
+    key = key.replace(" ", "").upper()
+    key_length = len(key)
+    plaintext = []
+    ord_a = ord("A")
+
+    for i, char in enumerate(ciphertext):
+        c = ord(char) - ord_a
+        k = ord(key[i % key_length]) - ord_a
+        p = (c - k) % 26
+        plaintext.append(chr(p + ord_a))
+
+    return "".join(plaintext)
+
+
+def test_running_key_encrypt() -> None:
+    """
+    >>> key = "How does the duck know that? said Victor"
+    >>> ciphertext = running_key_encrypt(key, "DEFEND THIS")
+    >>> running_key_decrypt(key, ciphertext) == "DEFENDTHIS"
+    True
+    """
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+    test_running_key_encrypt()
+
+    plaintext = input("Enter the plaintext: ").upper()
+    print(f"\n{plaintext = }")
+
+    key = "How does the duck know that? said Victor"
+    encrypted_text = running_key_encrypt(key, plaintext)
+    print(f"{encrypted_text = }")
+
+    decrypted_text = running_key_decrypt(key, encrypted_text)
+    print(f"{decrypted_text = }")
diff --git a/ciphers/simple_keyword_cypher.py b/ciphers/simple_keyword_cypher.py
index 1635471aebd1..bde137d826c3 100644
--- a/ciphers/simple_keyword_cypher.py
+++ b/ciphers/simple_keyword_cypher.py
@@ -1,16 +1,18 @@
 def remove_duplicates(key: str) -> str:
     """
     Removes duplicate alphabetic characters in a keyword (letter is ignored after its
-        first appearance).
+    first appearance).
+
     :param key: Keyword to use
     :return: String with duplicates removed
+
     >>> remove_duplicates('Hello World!!')
     'Helo Wrd'
     """
 
     key_no_dups = ""
     for ch in key:
-        if ch == " " or ch not in key_no_dups and ch.isalpha():
+        if ch == " " or (ch not in key_no_dups and ch.isalpha()):
             key_no_dups += ch
     return key_no_dups
 
@@ -18,6 +20,7 @@ def remove_duplicates(key: str) -> str:
 def create_cipher_map(key: str) -> dict[str, str]:
     """
     Returns a cipher map given a keyword.
+
     :param key: keyword to use
     :return: dictionary cipher map
     """
@@ -43,9 +46,11 @@ def create_cipher_map(key: str) -> dict[str, str]:
 def encipher(message: str, cipher_map: dict[str, str]) -> str:
     """
     Enciphers a message given a cipher map.
+
     :param message: Message to encipher
     :param cipher_map: Cipher map
     :return: enciphered string
+
     >>> encipher('Hello World!!', create_cipher_map('Goodbye!!'))
     'CYJJM VMQJB!!'
     """
@@ -55,9 +60,11 @@ def encipher(message: str, cipher_map: dict[str, str]) -> str:
 def decipher(message: str, cipher_map: dict[str, str]) -> str:
     """
     Deciphers a message given a cipher map
+
     :param message: Message to decipher
     :param cipher_map: Dictionary mapping to use
     :return: Deciphered string
+
     >>> cipher_map = create_cipher_map('Goodbye!!')
     >>> decipher(encipher('Hello World!!', cipher_map), cipher_map)
     'HELLO WORLD!!'
@@ -70,6 +77,7 @@ def decipher(message: str, cipher_map: dict[str, str]) -> str:
 def main() -> None:
     """
     Handles I/O
+
     :return: void
     """
     message = input("Enter message to encode or decode: ").strip()
diff --git a/ciphers/transposition_cipher.py b/ciphers/transposition_cipher.py
index f1f07ddc3f35..76178cb6a1bc 100644
--- a/ciphers/transposition_cipher.py
+++ b/ciphers/transposition_cipher.py
@@ -52,10 +52,8 @@ def decrypt_message(key: int, message: str) -> str:
         plain_text[col] += symbol
         col += 1
 
-        if (
-            (col == num_cols)
-            or (col == num_cols - 1)
-            and (row >= num_rows - num_shaded_boxes)
+        if (col == num_cols) or (
+            (col == num_cols - 1) and (row >= num_rows - num_shaded_boxes)
         ):
             col = 0
             row += 1
diff --git a/ciphers/trifid_cipher.py b/ciphers/trifid_cipher.py
index 8aa2263ca5ac..13a47e9dd03b 100644
--- a/ciphers/trifid_cipher.py
+++ b/ciphers/trifid_cipher.py
@@ -1,15 +1,35 @@
-# https://en.wikipedia.org/wiki/Trifid_cipher
+"""
+The trifid cipher uses a table to fractionate each plaintext letter into a trigram,
+mixes the constituents of the trigrams, and then applies the table in reverse to turn
+these mixed trigrams into ciphertext letters.
+
+https://en.wikipedia.org/wiki/Trifid_cipher
+"""
+
 from __future__ import annotations
 
+# fmt: off
+TEST_CHARACTER_TO_NUMBER = {
+    "A": "111", "B": "112", "C": "113", "D": "121", "E": "122", "F": "123", "G": "131",
+    "H": "132", "I": "133", "J": "211", "K": "212", "L": "213", "M": "221", "N": "222",
+    "O": "223", "P": "231", "Q": "232", "R": "233", "S": "311", "T": "312", "U": "313",
+    "V": "321", "W": "322", "X": "323", "Y": "331", "Z": "332", "+": "333",
+}
+# fmt: off
 
-def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str:
-    one, two, three = "", "", ""
-    tmp = []
+TEST_NUMBER_TO_CHARACTER = {val: key for key, val in TEST_CHARACTER_TO_NUMBER.items()}
 
-    for character in message_part:
-        tmp.append(character_to_number[character])
 
-    for each in tmp:
+def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str:
+    """
+    Arrange the triagram value of each letter of `message_part` vertically and join
+    them horizontally.
+
+    >>> __encrypt_part('ASK', TEST_CHARACTER_TO_NUMBER)
+    '132111112'
+    """
+    one, two, three = "", "", ""
+    for each in (character_to_number[character] for character in message_part):
         one += each[0]
         two += each[1]
         three += each[2]
@@ -20,12 +40,16 @@ def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> st
 def __decrypt_part(
     message_part: str, character_to_number: dict[str, str]
 ) -> tuple[str, str, str]:
-    tmp, this_part = "", ""
+    """
+    Convert each letter of the input string into their respective trigram values, join
+    them and split them into three equal groups of strings which are returned.
+
+    >>> __decrypt_part('ABCDE', TEST_CHARACTER_TO_NUMBER)
+    ('11111', '21131', '21122')
+    """
+    this_part = "".join(character_to_number[character] for character in message_part)
     result = []
-
-    for character in message_part:
-        this_part += character_to_number[character]
-
+    tmp = ""
     for digit in this_part:
         tmp += digit
         if len(tmp) == len(message_part):
@@ -38,6 +62,46 @@ def __decrypt_part(
 def __prepare(
     message: str, alphabet: str
 ) -> tuple[str, str, dict[str, str], dict[str, str]]:
+    """
+    A helper function that generates the triagrams and assigns each letter of the
+    alphabet to its corresponding triagram and stores this in a dictionary
+    (`character_to_number` and `number_to_character`) after confirming if the
+    alphabet's length is ``27``.
+
+    >>> test = __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ+')
+    >>> expected = ('IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ+',
+    ... TEST_CHARACTER_TO_NUMBER, TEST_NUMBER_TO_CHARACTER)
+    >>> test == expected
+    True
+
+    Testing with incomplete alphabet
+
+    >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVw')
+    Traceback (most recent call last):
+        ...
+    KeyError: 'Length of alphabet has to be 27.'
+
+    Testing with extra long alphabets
+
+    >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd')
+    Traceback (most recent call last):
+        ...
+    KeyError: 'Length of alphabet has to be 27.'
+
+    Testing with punctuation not in the given alphabet
+
+    >>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+')
+    Traceback (most recent call last):
+        ...
+    ValueError: Each message character has to be included in alphabet!
+
+    Testing with numbers
+
+    >>> __prepare(500,'abCdeFghijkLmnopqrStuVwxYZ+')
+    Traceback (most recent call last):
+        ...
+    AttributeError: 'int' object has no attribute 'replace'
+    """
     # Validate message and alphabet, set to upper and remove spaces
     alphabet = alphabet.replace(" ", "").upper()
     message = message.replace(" ", "").upper()
@@ -45,45 +109,14 @@ def __prepare(
     # Check length and characters
     if len(alphabet) != 27:
         raise KeyError("Length of alphabet has to be 27.")
-    for each in message:
-        if each not in alphabet:
-            raise ValueError("Each message character has to be included in alphabet!")
+    if any(char not in alphabet for char in message):
+        raise ValueError("Each message character has to be included in alphabet!")
 
     # Generate dictionares
-    numbers = (
-        "111",
-        "112",
-        "113",
-        "121",
-        "122",
-        "123",
-        "131",
-        "132",
-        "133",
-        "211",
-        "212",
-        "213",
-        "221",
-        "222",
-        "223",
-        "231",
-        "232",
-        "233",
-        "311",
-        "312",
-        "313",
-        "321",
-        "322",
-        "323",
-        "331",
-        "332",
-        "333",
-    )
-    character_to_number = {}
-    number_to_character = {}
-    for letter, number in zip(alphabet, numbers):
-        character_to_number[letter] = number
-        number_to_character[number] = letter
+    character_to_number = dict(zip(alphabet, TEST_CHARACTER_TO_NUMBER.values()))
+    number_to_character = {
+        number: letter for letter, number in character_to_number.items()
+    }
 
     return message, alphabet, character_to_number, number_to_character
 
@@ -91,44 +124,91 @@ def __prepare(
 def encrypt_message(
     message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5
 ) -> str:
+    """
+    encrypt_message
+    ===============
+
+    Encrypts a message using the trifid_cipher. Any punctuatuion chars that
+    would be used should be added to the alphabet.
+
+    PARAMETERS
+    ----------
+
+    *   `message`: The message you want to encrypt.
+    *   `alphabet` (optional): The characters to be used for the cipher .
+    *   `period` (optional): The number of characters you want in a group whilst
+        encrypting.
+
+    >>> encrypt_message('I am a boy')
+    'BCDGBQY'
+
+    >>> encrypt_message(' ')
+    ''
+
+    >>> encrypt_message('   aide toi le c  iel      ta id  era    ',
+    ... 'FELIXMARDSTBCGHJKNOPQUVWYZ+',5)
+    'FMJFVOISSUFTFPUFEQQC'
+
+    """
     message, alphabet, character_to_number, number_to_character = __prepare(
         message, alphabet
     )
-    encrypted, encrypted_numeric = "", ""
 
+    encrypted_numeric = ""
     for i in range(0, len(message) + 1, period):
         encrypted_numeric += __encrypt_part(
             message[i : i + period], character_to_number
         )
 
+    encrypted = ""
     for i in range(0, len(encrypted_numeric), 3):
         encrypted += number_to_character[encrypted_numeric[i : i + 3]]
-
     return encrypted
 
 
 def decrypt_message(
     message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5
 ) -> str:
+    """
+    decrypt_message
+    ===============
+
+    Decrypts a trifid_cipher encrypted message.
+
+    PARAMETERS
+    ----------
+
+    *   `message`: The message you want to decrypt.
+    *   `alphabet` (optional): The characters used for the cipher.
+    *   `period` (optional): The number of characters used in grouping when it
+        was encrypted.
+
+    >>> decrypt_message('BCDGBQY')
+    'IAMABOY'
+
+    Decrypting with your own alphabet and period
+
+    >>> decrypt_message('FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ+',5)
+    'AIDETOILECIELTAIDERA'
+    """
     message, alphabet, character_to_number, number_to_character = __prepare(
         message, alphabet
     )
-    decrypted_numeric = []
-    decrypted = ""
 
-    for i in range(0, len(message) + 1, period):
+    decrypted_numeric = []
+    for i in range(0, len(message), period):
         a, b, c = __decrypt_part(message[i : i + period], character_to_number)
 
         for j in range(len(a)):
             decrypted_numeric.append(a[j] + b[j] + c[j])
 
-    for each in decrypted_numeric:
-        decrypted += number_to_character[each]
-
-    return decrypted
+    return "".join(number_to_character[each] for each in decrypted_numeric)
 
 
 if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
     msg = "DEFEND THE EAST WALL OF THE CASTLE."
     encrypted = encrypt_message(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ")
     decrypted = decrypt_message(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ")
diff --git a/ciphers/xor_cipher.py b/ciphers/xor_cipher.py
index 559036d305c5..24d88a0fd588 100644
--- a/ciphers/xor_cipher.py
+++ b/ciphers/xor_cipher.py
@@ -1,21 +1,22 @@
 """
-        author: Christian Bender
-        date: 21.12.2017
-        class: XORCipher
-
-        This class implements the XOR-cipher algorithm and provides
-        some useful methods for encrypting and decrypting strings and
-        files.
-
-        Overview about methods
-
-        - encrypt : list of char
-        - decrypt : list of char
-        - encrypt_string : str
-        - decrypt_string : str
-        - encrypt_file : boolean
-        - decrypt_file : boolean
+author: Christian Bender
+date: 21.12.2017
+class: XORCipher
+
+This class implements the XOR-cipher algorithm and provides
+some useful methods for encrypting and decrypting strings and
+files.
+
+Overview about methods
+
+- encrypt : list of char
+- decrypt : list of char
+- encrypt_string : str
+- decrypt_string : str
+- encrypt_file : boolean
+- decrypt_file : boolean
 """
+
 from __future__ import annotations
 
 
@@ -35,6 +36,22 @@ def encrypt(self, content: str, key: int) -> list[str]:
         output: encrypted string 'content' as a list of chars
         if key not passed the method uses the key by the constructor.
         otherwise key = 1
+
+        Empty list
+        >>> XORCipher().encrypt("", 5)
+        []
+
+        One key
+        >>> XORCipher().encrypt("hallo welt", 1)
+        ['i', '`', 'm', 'm', 'n', '!', 'v', 'd', 'm', 'u']
+
+        Normal key
+        >>> XORCipher().encrypt("HALLO WELT", 32)
+        ['h', 'a', 'l', 'l', 'o', '\\x00', 'w', 'e', 'l', 't']
+
+        Key greater than 255
+        >>> XORCipher().encrypt("hallo welt", 256)
+        ['h', 'a', 'l', 'l', 'o', ' ', 'w', 'e', 'l', 't']
         """
 
         # precondition
@@ -44,7 +61,7 @@ def encrypt(self, content: str, key: int) -> list[str]:
         key = key or self.__key or 1
 
         # make sure key is an appropriate size
-        key %= 255
+        key %= 256
 
         return [chr(ord(ch) ^ key) for ch in content]
 
@@ -54,16 +71,32 @@ def decrypt(self, content: str, key: int) -> list[str]:
         output: decrypted string 'content' as a list of chars
         if key not passed the method uses the key by the constructor.
         otherwise key = 1
+
+        Empty list
+        >>> XORCipher().decrypt("", 5)
+        []
+
+        One key
+        >>> XORCipher().decrypt("hallo welt", 1)
+        ['i', '`', 'm', 'm', 'n', '!', 'v', 'd', 'm', 'u']
+
+        Normal key
+        >>> XORCipher().decrypt("HALLO WELT", 32)
+        ['h', 'a', 'l', 'l', 'o', '\\x00', 'w', 'e', 'l', 't']
+
+        Key greater than 255
+        >>> XORCipher().decrypt("hallo welt", 256)
+        ['h', 'a', 'l', 'l', 'o', ' ', 'w', 'e', 'l', 't']
         """
 
         # precondition
         assert isinstance(key, int)
-        assert isinstance(content, list)
+        assert isinstance(content, str)
 
         key = key or self.__key or 1
 
         # make sure key is an appropriate size
-        key %= 255
+        key %= 256
 
         return [chr(ord(ch) ^ key) for ch in content]
 
@@ -73,6 +106,22 @@ def encrypt_string(self, content: str, key: int = 0) -> str:
         output: encrypted string 'content'
         if key not passed the method uses the key by the constructor.
         otherwise key = 1
+
+        Empty list
+        >>> XORCipher().encrypt_string("", 5)
+        ''
+
+        One key
+        >>> XORCipher().encrypt_string("hallo welt", 1)
+        'i`mmn!vdmu'
+
+        Normal key
+        >>> XORCipher().encrypt_string("HALLO WELT", 32)
+        'hallo\\x00welt'
+
+        Key greater than 255
+        >>> XORCipher().encrypt_string("hallo welt", 256)
+        'hallo welt'
         """
 
         # precondition
@@ -81,9 +130,8 @@ def encrypt_string(self, content: str, key: int = 0) -> str:
 
         key = key or self.__key or 1
 
-        # make sure key can be any size
-        while key > 255:
-            key -= 255
+        # make sure key is an appropriate size
+        key %= 256
 
         # This will be returned
         ans = ""
@@ -99,6 +147,22 @@ def decrypt_string(self, content: str, key: int = 0) -> str:
         output: decrypted string 'content'
         if key not passed the method uses the key by the constructor.
         otherwise key = 1
+
+        Empty list
+        >>> XORCipher().decrypt_string("", 5)
+        ''
+
+        One key
+        >>> XORCipher().decrypt_string("hallo welt", 1)
+        'i`mmn!vdmu'
+
+        Normal key
+        >>> XORCipher().decrypt_string("HALLO WELT", 32)
+        'hallo\\x00welt'
+
+        Key greater than 255
+        >>> XORCipher().decrypt_string("hallo welt", 256)
+        'hallo welt'
         """
 
         # precondition
@@ -107,9 +171,8 @@ def decrypt_string(self, content: str, key: int = 0) -> str:
 
         key = key or self.__key or 1
 
-        # make sure key can be any size
-        while key > 255:
-            key -= 255
+        # make sure key is an appropriate size
+        key %= 256
 
         # This will be returned
         ans = ""
@@ -132,6 +195,9 @@ def encrypt_file(self, file: str, key: int = 0) -> bool:
         assert isinstance(file, str)
         assert isinstance(key, int)
 
+        # make sure key is an appropriate size
+        key %= 256
+
         try:
             with open(file) as fin, open("encrypt.out", "w+") as fout:
                 # actual encrypt-process
@@ -156,6 +222,9 @@ def decrypt_file(self, file: str, key: int) -> bool:
         assert isinstance(file, str)
         assert isinstance(key, int)
 
+        # make sure key is an appropriate size
+        key %= 256
+
         try:
             with open(file) as fin, open("decrypt.out", "w+") as fout:
                 # actual encrypt-process
@@ -168,6 +237,11 @@ def decrypt_file(self, file: str, key: int) -> bool:
         return True
 
 
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+
 # Tests
 # crypt = XORCipher()
 # key = 67
diff --git a/computer_vision/README.md b/computer_vision/README.md
index 8d2f4a130d05..61462567b662 100644
--- a/computer_vision/README.md
+++ b/computer_vision/README.md
@@ -8,4 +8,3 @@ Image processing and computer vision are a little different from each other. Ima
 While computer vision comes from modelling image processing using the techniques of machine learning, computer vision applies machine learning to recognize patterns for interpretation of images (much like the process of visual reasoning of human vision).
 
 * <https://en.wikipedia.org/wiki/Computer_vision>
-* <https://www.algorithmia.com/blog/introduction-to-computer-vision>
diff --git a/computer_vision/cnn_classification.py.DISABLED.txt b/computer_vision/cnn_classification.py
similarity index 98%
rename from computer_vision/cnn_classification.py.DISABLED.txt
rename to computer_vision/cnn_classification.py
index b813b71033f3..115333eba0d1 100644
--- a/computer_vision/cnn_classification.py.DISABLED.txt
+++ b/computer_vision/cnn_classification.py
@@ -25,7 +25,7 @@
 
 # Importing the Keras libraries and packages
 import tensorflow as tf
-from tensorflow.keras import layers, models
+from keras import layers, models
 
 if __name__ == "__main__":
     # Initialising the CNN
diff --git a/computer_vision/flip_augmentation.py b/computer_vision/flip_augmentation.py
index 77a8cbd7b14f..7301424824df 100644
--- a/computer_vision/flip_augmentation.py
+++ b/computer_vision/flip_augmentation.py
@@ -33,7 +33,7 @@ def main() -> None:
         file_name = paths[index].split(os.sep)[-1].rsplit(".", 1)[0]
         file_root = f"{OUTPUT_DIR}/{file_name}_FLIP_{letter_code}"
         cv2.imwrite(f"{file_root}.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85])
-        print(f"Success {index+1}/{len(new_images)} with {file_name}")
+        print(f"Success {index + 1}/{len(new_images)} with {file_name}")
         annos_list = []
         for anno in new_annos[index]:
             obj = f"{anno[0]} {anno[1]} {anno[2]} {anno[3]} {anno[4]}"
diff --git a/computer_vision/haralick_descriptors.py b/computer_vision/haralick_descriptors.py
index 413cea304f6c..54632160dcf2 100644
--- a/computer_vision/haralick_descriptors.py
+++ b/computer_vision/haralick_descriptors.py
@@ -2,6 +2,7 @@
 https://en.wikipedia.org/wiki/Image_texture
 https://en.wikipedia.org/wiki/Co-occurrence_matrix#Application_to_image_analysis
 """
+
 import imageio.v2 as imageio
 import numpy as np
 
@@ -18,7 +19,7 @@ def root_mean_square_error(original: np.ndarray, reference: np.ndarray) -> float
         >>> root_mean_square_error(np.array([1, 2, 3]), np.array([6, 4, 2]))
         3.1622776601683795
     """
-    return np.sqrt(((original - reference) ** 2).mean())
+    return float(np.sqrt(((original - reference) ** 2).mean()))
 
 
 def normalize_image(
@@ -140,7 +141,7 @@ def transform(
 
     center_x, center_y = (x // 2 for x in kernel.shape)
 
-    # Use padded image when applying convolotion
+    # Use padded image when applying convolution
     # to not go out of bounds of the original the image
     transformed = np.zeros(image.shape, dtype=np.uint8)
     padded = np.pad(image, 1, "constant", constant_values=constant)
@@ -253,13 +254,13 @@ def matrix_concurrency(image: np.ndarray, coordinate: tuple[int, int]) -> np.nda
 
 
 def haralick_descriptors(matrix: np.ndarray) -> list[float]:
-    """Calculates all 8 Haralick descriptors based on co-occurence input matrix.
+    """Calculates all 8 Haralick descriptors based on co-occurrence input matrix.
     All descriptors are as follows:
     Maximum probability, Inverse Difference, Homogeneity, Entropy,
     Energy, Dissimilarity, Contrast and Correlation
 
     Args:
-        matrix: Co-occurence matrix to use as base for calculating descriptors.
+        matrix: Co-occurrence matrix to use as base for calculating descriptors.
 
     Returns:
         Reverse ordered list of resulting descriptors
@@ -272,7 +273,7 @@ def haralick_descriptors(matrix: np.ndarray) -> list[float]:
         >>> morphological = opening_filter(binary)
         >>> mask_1 = binary_mask(gray, morphological)[0]
         >>> concurrency = matrix_concurrency(mask_1, (0, 1))
-        >>> haralick_descriptors(concurrency)
+        >>> [float(f) for f in haralick_descriptors(concurrency)]
         [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
     """
     # Function np.indices could be used for bigger input types,
@@ -334,7 +335,7 @@ def get_descriptors(
     return np.concatenate(descriptors, axis=None)
 
 
-def euclidean(point_1: np.ndarray, point_2: np.ndarray) -> np.float32:
+def euclidean(point_1: np.ndarray, point_2: np.ndarray) -> float:
     """
     Simple method for calculating the euclidean distance between two points,
     with type np.ndarray.
@@ -345,7 +346,7 @@ def euclidean(point_1: np.ndarray, point_2: np.ndarray) -> np.float32:
         >>> euclidean(a, b)
         3.3166247903554
     """
-    return np.sqrt(np.sum(np.square(point_1 - point_2)))
+    return float(np.sqrt(np.sum(np.square(point_1 - point_2))))
 
 
 def get_distances(descriptors: np.ndarray, base: int) -> list[tuple[int, float]]:
diff --git a/computer_vision/horn_schunck.py b/computer_vision/horn_schunck.py
index b63e0268294c..f33b5b1c794b 100644
--- a/computer_vision/horn_schunck.py
+++ b/computer_vision/horn_schunck.py
@@ -1,12 +1,12 @@
 """
-    The Horn-Schunck method estimates the optical flow for every single pixel of
-    a sequence of images.
-    It works by assuming brightness constancy between two consecutive frames
-    and smoothness in the optical flow.
-
-    Useful resources:
-    Wikipedia: https://en.wikipedia.org/wiki/Horn%E2%80%93Schunck_method
-    Paper: http://image.diku.dk/imagecanon/material/HornSchunckOptical_Flow.pdf
+The Horn-Schunck method estimates the optical flow for every single pixel of
+a sequence of images.
+It works by assuming brightness constancy between two consecutive frames
+and smoothness in the optical flow.
+
+Useful resources:
+Wikipedia: https://en.wikipedia.org/wiki/Horn%E2%80%93Schunck_method
+Paper: http://image.diku.dk/imagecanon/material/HornSchunckOptical_Flow.pdf
 """
 
 from typing import SupportsIndex
diff --git a/computer_vision/intensity_based_segmentation.py b/computer_vision/intensity_based_segmentation.py
new file mode 100644
index 000000000000..7f2b1141acc4
--- /dev/null
+++ b/computer_vision/intensity_based_segmentation.py
@@ -0,0 +1,62 @@
+# Source: "https://www.ijcse.com/docs/IJCSE11-02-03-117.pdf"
+
+# Importing necessary libraries
+import matplotlib.pyplot as plt
+import numpy as np
+from PIL import Image
+
+
+def segment_image(image: np.ndarray, thresholds: list[int]) -> np.ndarray:
+    """
+    Performs image segmentation based on intensity thresholds.
+
+    Args:
+        image: Input grayscale image as a 2D array.
+        thresholds: Intensity thresholds to define segments.
+
+    Returns:
+        A labeled 2D array where each region corresponds to a threshold range.
+
+    Example:
+        >>> img = np.array([[80, 120, 180], [40, 90, 150], [20, 60, 100]])
+        >>> segment_image(img, [50, 100, 150])
+        array([[1, 2, 3],
+               [0, 1, 2],
+               [0, 1, 1]], dtype=int32)
+    """
+    # Initialize segmented array with zeros
+    segmented = np.zeros_like(image, dtype=np.int32)
+
+    # Assign labels based on thresholds
+    for i, threshold in enumerate(thresholds):
+        segmented[image > threshold] = i + 1
+
+    return segmented
+
+
+if __name__ == "__main__":
+    # Load the image
+    image_path = "path_to_image"  # Replace with your image path
+    original_image = Image.open(image_path).convert("L")
+    image_array = np.array(original_image)
+
+    # Define thresholds
+    thresholds = [50, 100, 150, 200]
+
+    # Perform segmentation
+    segmented_image = segment_image(image_array, thresholds)
+
+    # Display the results
+    plt.figure(figsize=(10, 5))
+
+    plt.subplot(1, 2, 1)
+    plt.title("Original Image")
+    plt.imshow(image_array, cmap="gray")
+    plt.axis("off")
+
+    plt.subplot(1, 2, 2)
+    plt.title("Segmented Image")
+    plt.imshow(segmented_image, cmap="tab20")
+    plt.axis("off")
+
+    plt.show()
diff --git a/computer_vision/mosaic_augmentation.py b/computer_vision/mosaic_augmentation.py
index cd923dfe095f..d881347121ea 100644
--- a/computer_vision/mosaic_augmentation.py
+++ b/computer_vision/mosaic_augmentation.py
@@ -41,7 +41,7 @@ def main() -> None:
         file_name = path.split(os.sep)[-1].rsplit(".", 1)[0]
         file_root = f"{OUTPUT_DIR}/{file_name}_MOSAIC_{letter_code}"
         cv2.imwrite(f"{file_root}.jpg", new_image, [cv2.IMWRITE_JPEG_QUALITY, 85])
-        print(f"Succeeded {index+1}/{NUMBER_IMAGES} with {file_name}")
+        print(f"Succeeded {index + 1}/{NUMBER_IMAGES} with {file_name}")
         annos_list = []
         for anno in new_annos:
             width = anno[3] - anno[1]
diff --git a/conversions/convert_number_to_words.py b/conversions/convert_number_to_words.py
index 0c428928b31d..6aa43738b9fe 100644
--- a/conversions/convert_number_to_words.py
+++ b/conversions/convert_number_to_words.py
@@ -1,5 +1,5 @@
 from enum import Enum
-from typing import ClassVar, Literal
+from typing import Literal
 
 
 class NumberingSystem(Enum):
@@ -41,7 +41,7 @@ def max_value(cls, system: str) -> int:
         >>> NumberingSystem.max_value("indian") == 10**19 - 1
         True
         """
-        match (system_enum := cls[system.upper()]):
+        match system_enum := cls[system.upper()]:
             case cls.SHORT:
                 max_exp = system_enum.value[0][0] + 3
             case cls.LONG:
@@ -54,7 +54,7 @@ def max_value(cls, system: str) -> int:
 
 
 class NumberWords(Enum):
-    ONES: ClassVar[dict[int, str]] = {
+    ONES = {  # noqa: RUF012
         0: "",
         1: "one",
         2: "two",
@@ -67,7 +67,7 @@ class NumberWords(Enum):
         9: "nine",
     }
 
-    TEENS: ClassVar[dict[int, str]] = {
+    TEENS = {  # noqa: RUF012
         0: "ten",
         1: "eleven",
         2: "twelve",
@@ -80,7 +80,7 @@ class NumberWords(Enum):
         9: "nineteen",
     }
 
-    TENS: ClassVar[dict[int, str]] = {
+    TENS = {  # noqa: RUF012
         2: "twenty",
         3: "thirty",
         4: "forty",
diff --git a/conversions/decimal_to_hexadecimal.py b/conversions/decimal_to_hexadecimal.py
index b1fb4f082242..ee79592de5ca 100644
--- a/conversions/decimal_to_hexadecimal.py
+++ b/conversions/decimal_to_hexadecimal.py
@@ -1,4 +1,4 @@
-""" Convert Base 10 (Decimal) Values to Hexadecimal Representations """
+"""Convert Base 10 (Decimal) Values to Hexadecimal Representations"""
 
 # set decimal value for each hexadecimal digit
 values = {
diff --git a/conversions/ipv4_conversion.py b/conversions/ipv4_conversion.py
new file mode 100644
index 000000000000..862309b7251e
--- /dev/null
+++ b/conversions/ipv4_conversion.py
@@ -0,0 +1,85 @@
+# https://www.geeksforgeeks.org/convert-ip-address-to-integer-and-vice-versa/
+
+
+def ipv4_to_decimal(ipv4_address: str) -> int:
+    """
+    Convert an IPv4 address to its decimal representation.
+
+    Args:
+        ip_address: A string representing an IPv4 address (e.g., "192.168.0.1").
+
+    Returns:
+        int: The decimal representation of the IP address.
+
+    >>> ipv4_to_decimal("192.168.0.1")
+    3232235521
+    >>> ipv4_to_decimal("10.0.0.255")
+    167772415
+    >>> ipv4_to_decimal("10.0.255")
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid IPv4 address format
+    >>> ipv4_to_decimal("10.0.0.256")
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid IPv4 octet 256
+    """
+
+    octets = [int(octet) for octet in ipv4_address.split(".")]
+    if len(octets) != 4:
+        raise ValueError("Invalid IPv4 address format")
+
+    decimal_ipv4 = 0
+    for octet in octets:
+        if not 0 <= octet <= 255:
+            raise ValueError(f"Invalid IPv4 octet {octet}")  # noqa: EM102
+        decimal_ipv4 = (decimal_ipv4 << 8) + int(octet)
+
+    return decimal_ipv4
+
+
+def alt_ipv4_to_decimal(ipv4_address: str) -> int:
+    """
+    >>> alt_ipv4_to_decimal("192.168.0.1")
+    3232235521
+    >>> alt_ipv4_to_decimal("10.0.0.255")
+    167772415
+    """
+    return int("0x" + "".join(f"{int(i):02x}" for i in ipv4_address.split(".")), 16)
+
+
+def decimal_to_ipv4(decimal_ipv4: int) -> str:
+    """
+    Convert a decimal representation of an IP address to its IPv4 format.
+
+    Args:
+        decimal_ipv4: An integer representing the decimal IP address.
+
+    Returns:
+        The IPv4 representation of the decimal IP address.
+
+    >>> decimal_to_ipv4(3232235521)
+    '192.168.0.1'
+    >>> decimal_to_ipv4(167772415)
+    '10.0.0.255'
+    >>> decimal_to_ipv4(-1)
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid decimal IPv4 address
+    """
+
+    if not (0 <= decimal_ipv4 <= 4294967295):
+        raise ValueError("Invalid decimal IPv4 address")
+
+    ip_parts = []
+    for _ in range(4):
+        ip_parts.append(str(decimal_ipv4 & 255))
+        decimal_ipv4 >>= 8
+
+    return ".".join(reversed(ip_parts))
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/conversions/prefix_conversions.py b/conversions/prefix_conversions.py
index 06b759e355a7..714677f3b242 100644
--- a/conversions/prefix_conversions.py
+++ b/conversions/prefix_conversions.py
@@ -1,6 +1,7 @@
 """
 Convert International System of Units (SI) and Binary prefixes
 """
+
 from __future__ import annotations
 
 from enum import Enum
diff --git a/conversions/prefix_conversions_string.py b/conversions/prefix_conversions_string.py
index 9344c9672a1f..c5fef49874ca 100644
--- a/conversions/prefix_conversions_string.py
+++ b/conversions/prefix_conversions_string.py
@@ -53,7 +53,7 @@ class SIUnit(Enum):
     yocto = -24
 
     @classmethod
-    def get_positive(cls: type[T]) -> dict:
+    def get_positive(cls) -> dict:
         """
         Returns a dictionary with only the elements of this enum
         that has a positive value
@@ -68,7 +68,7 @@ def get_positive(cls: type[T]) -> dict:
         return {unit.name: unit.value for unit in cls if unit.value > 0}
 
     @classmethod
-    def get_negative(cls: type[T]) -> dict:
+    def get_negative(cls) -> dict:
         """
         Returns a dictionary with only the elements of this enum
         that has a negative value
diff --git a/conversions/rectangular_to_polar.py b/conversions/rectangular_to_polar.py
new file mode 100644
index 000000000000..bed97d7410ec
--- /dev/null
+++ b/conversions/rectangular_to_polar.py
@@ -0,0 +1,32 @@
+import math
+
+
+def rectangular_to_polar(real: float, img: float) -> tuple[float, float]:
+    """
+    https://en.wikipedia.org/wiki/Polar_coordinate_system
+
+    >>> rectangular_to_polar(5,-5)
+    (7.07, -45.0)
+    >>> rectangular_to_polar(-1,1)
+    (1.41, 135.0)
+    >>> rectangular_to_polar(-1,-1)
+    (1.41, -135.0)
+    >>> rectangular_to_polar(1e-10,1e-10)
+    (0.0, 45.0)
+    >>> rectangular_to_polar(-1e-10,1e-10)
+    (0.0, 135.0)
+    >>> rectangular_to_polar(9.75,5.93)
+    (11.41, 31.31)
+    >>> rectangular_to_polar(10000,99999)
+    (100497.76, 84.29)
+    """
+
+    mod = round(math.sqrt((real**2) + (img**2)), 2)
+    ang = round(math.degrees(math.atan2(img, real)), 2)
+    return (mod, ang)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/conversions/rgb_cmyk_conversion.py b/conversions/rgb_cmyk_conversion.py
new file mode 100644
index 000000000000..07d65b704c44
--- /dev/null
+++ b/conversions/rgb_cmyk_conversion.py
@@ -0,0 +1,71 @@
+def rgb_to_cmyk(r_input: int, g_input: int, b_input: int) -> tuple[int, int, int, int]:
+    """
+    Simple RGB to CMYK conversion. Returns percentages of CMYK paint.
+    https://www.programmingalgorithms.com/algorithm/rgb-to-cmyk/
+
+    Note: this is a very popular algorithm that converts colors linearly and gives
+    only approximate results. Actual preparation for printing requires advanced color
+    conversion considering the color profiles and parameters of the target device.
+
+    >>> rgb_to_cmyk(255, 200, "a")
+    Traceback (most recent call last):
+        ...
+    ValueError: Expected int, found (<class 'int'>, <class 'int'>, <class 'str'>)
+
+    >>> rgb_to_cmyk(255, 255, 999)
+    Traceback (most recent call last):
+        ...
+    ValueError: Expected int of the range 0..255
+
+    >>> rgb_to_cmyk(255, 255, 255)  # white
+    (0, 0, 0, 0)
+
+    >>> rgb_to_cmyk(128, 128, 128)  # gray
+    (0, 0, 0, 50)
+
+    >>> rgb_to_cmyk(0, 0, 0)    # black
+    (0, 0, 0, 100)
+
+    >>> rgb_to_cmyk(255, 0, 0)  # red
+    (0, 100, 100, 0)
+
+    >>> rgb_to_cmyk(0, 255, 0)  # green
+    (100, 0, 100, 0)
+
+    >>> rgb_to_cmyk(0, 0, 255)    # blue
+    (100, 100, 0, 0)
+    """
+
+    if (
+        not isinstance(r_input, int)
+        or not isinstance(g_input, int)
+        or not isinstance(b_input, int)
+    ):
+        msg = f"Expected int, found {type(r_input), type(g_input), type(b_input)}"
+        raise ValueError(msg)
+
+    if not 0 <= r_input < 256 or not 0 <= g_input < 256 or not 0 <= b_input < 256:
+        raise ValueError("Expected int of the range 0..255")
+
+    # changing range from 0..255 to 0..1
+    r = r_input / 255
+    g = g_input / 255
+    b = b_input / 255
+
+    k = 1 - max(r, g, b)
+
+    if k == 1:  # pure black
+        return 0, 0, 0, 100
+
+    c = round(100 * (1 - r - k) / (1 - k))
+    m = round(100 * (1 - g - k) / (1 - k))
+    y = round(100 * (1 - b - k) / (1 - k))
+    k = round(100 * k)
+
+    return c, m, y, k
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
diff --git a/conversions/temperature_conversions.py b/conversions/temperature_conversions.py
index f7af6c8f1e2b..dde1d2f0f166 100644
--- a/conversions/temperature_conversions.py
+++ b/conversions/temperature_conversions.py
@@ -1,4 +1,4 @@
-""" Convert between different units of temperature """
+"""Convert between different units of temperature"""
 
 
 def celsius_to_fahrenheit(celsius: float, ndigits: int = 2) -> float:
diff --git a/conversions/time_conversions.py b/conversions/time_conversions.py
new file mode 100644
index 000000000000..8c30f5bc4a45
--- /dev/null
+++ b/conversions/time_conversions.py
@@ -0,0 +1,86 @@
+"""
+A unit of time is any particular time interval, used as a standard way of measuring or
+expressing duration.  The base unit of time in the International System of Units (SI),
+and by extension most of the Western world, is the second, defined as about 9 billion
+oscillations of the caesium atom.
+
+https://en.wikipedia.org/wiki/Unit_of_time
+"""
+
+time_chart: dict[str, float] = {
+    "seconds": 1.0,
+    "minutes": 60.0,  # 1 minute = 60 sec
+    "hours": 3600.0,  # 1 hour = 60 minutes = 3600 seconds
+    "days": 86400.0,  # 1 day = 24 hours = 1440 min = 86400 sec
+    "weeks": 604800.0,  # 1 week=7d=168hr=10080min = 604800 sec
+    "months": 2629800.0,  # Approximate value for a month in seconds
+    "years": 31557600.0,  # Approximate value for a year in seconds
+}
+
+time_chart_inverse: dict[str, float] = {
+    key: 1 / value for key, value in time_chart.items()
+}
+
+
+def convert_time(time_value: float, unit_from: str, unit_to: str) -> float:
+    """
+    Convert time from one unit to another using the time_chart above.
+
+    >>> convert_time(3600, "seconds", "hours")
+    1.0
+    >>> convert_time(3500, "Seconds", "Hours")
+    0.972
+    >>> convert_time(1, "DaYs", "hours")
+    24.0
+    >>> convert_time(120, "minutes", "SeCoNdS")
+    7200.0
+    >>> convert_time(2, "WEEKS", "days")
+    14.0
+    >>> convert_time(0.5, "hours", "MINUTES")
+    30.0
+    >>> convert_time(-3600, "seconds", "hours")
+    Traceback (most recent call last):
+        ...
+    ValueError: 'time_value' must be a non-negative number.
+    >>> convert_time("Hello", "hours", "minutes")
+    Traceback (most recent call last):
+        ...
+    ValueError: 'time_value' must be a non-negative number.
+    >>> convert_time([0, 1, 2], "weeks", "days")
+    Traceback (most recent call last):
+        ...
+    ValueError: 'time_value' must be a non-negative number.
+    >>> convert_time(1, "cool", "century")  # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid unit cool is not in seconds, minutes, hours, days, weeks, ...
+    >>> convert_time(1, "seconds", "hot")  # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid unit hot is not in seconds, minutes, hours, days, weeks, ...
+    """
+    if not isinstance(time_value, (int, float)) or time_value < 0:
+        msg = "'time_value' must be a non-negative number."
+        raise ValueError(msg)
+
+    unit_from = unit_from.lower()
+    unit_to = unit_to.lower()
+    if unit_from not in time_chart or unit_to not in time_chart:
+        invalid_unit = unit_from if unit_from not in time_chart else unit_to
+        msg = f"Invalid unit {invalid_unit} is not in {', '.join(time_chart)}."
+        raise ValueError(msg)
+
+    return round(
+        time_value * time_chart[unit_from] * time_chart_inverse[unit_to],
+        3,
+    )
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+    print(f"{convert_time(3600,'seconds', 'hours') = :,}")
+    print(f"{convert_time(360, 'days', 'months') = :,}")
+    print(f"{convert_time(360, 'months', 'years') = :,}")
+    print(f"{convert_time(1, 'years', 'seconds') = :,}")
diff --git a/conversions/weight_conversion.py b/conversions/weight_conversion.py
index e8326e0b688f..0777aead9f02 100644
--- a/conversions/weight_conversion.py
+++ b/conversions/weight_conversion.py
@@ -297,6 +297,12 @@ def weight_conversion(from_type: str, to_type: str, value: float) -> float:
     1.660540199e-23
     >>> weight_conversion("atomic-mass-unit","atomic-mass-unit",2)
     1.999999998903455
+    >>> weight_conversion("slug", "kilogram", 1)
+    Traceback (most recent call last):
+    ...
+    ValueError: Invalid 'from_type' or 'to_type' value: 'slug', 'kilogram'
+    Supported values are: kilogram, gram, milligram, metric-ton, long-ton, short-ton, \
+pound, stone, ounce, carrat, atomic-mass-unit
     """
     if to_type not in KILOGRAM_CHART or from_type not in WEIGHT_TYPE_CHART:
         msg = (
diff --git a/compression/README.md b/data_compression/README.md
similarity index 100%
rename from compression/README.md
rename to data_compression/README.md
diff --git a/arithmetic_analysis/__init__.py b/data_compression/__init__.py
similarity index 100%
rename from arithmetic_analysis/__init__.py
rename to data_compression/__init__.py
diff --git a/compression/burrows_wheeler.py b/data_compression/burrows_wheeler.py
similarity index 98%
rename from compression/burrows_wheeler.py
rename to data_compression/burrows_wheeler.py
index 52bb045d9398..857d677c904e 100644
--- a/compression/burrows_wheeler.py
+++ b/data_compression/burrows_wheeler.py
@@ -1,7 +1,7 @@
 """
 https://en.wikipedia.org/wiki/Burrows%E2%80%93Wheeler_transform
 
-The Burrows–Wheeler transform (BWT, also called block-sorting compression)
+The Burrows-Wheeler transform (BWT, also called block-sorting compression)
 rearranges a character string into runs of similar characters. This is useful
 for compression, since it tends to be easy to compress a string that has runs
 of repeated characters by techniques such as move-to-front transform and
@@ -10,6 +10,7 @@
 original character. The BWT is thus a "free" method of improving the efficiency
 of text compression algorithms, costing only some extra computation.
 """
+
 from __future__ import annotations
 
 from typing import TypedDict
diff --git a/compression/huffman.py b/data_compression/huffman.py
similarity index 96%
rename from compression/huffman.py
rename to data_compression/huffman.py
index 65e5c2f25385..44eda6c03180 100644
--- a/compression/huffman.py
+++ b/data_compression/huffman.py
@@ -40,7 +40,7 @@ def build_tree(letters: list[Letter]) -> Letter | TreeNode:
     Run through the list of Letters and build the min heap
     for the Huffman Tree.
     """
-    response: list[Letter | TreeNode] = letters  # type: ignore
+    response: list[Letter | TreeNode] = list(letters)
     while len(response) > 1:
         left = response.pop(0)
         right = response.pop(0)
@@ -59,7 +59,7 @@ def traverse_tree(root: Letter | TreeNode, bitstring: str) -> list[Letter]:
     if isinstance(root, Letter):
         root.bitstring[root.letter] = bitstring
         return [root]
-    treenode: TreeNode = root  # type: ignore
+    treenode: TreeNode = root
     letters = []
     letters += traverse_tree(treenode.left, bitstring + "0")
     letters += traverse_tree(treenode.right, bitstring + "1")
diff --git a/compression/image_data/PSNR-example-base.png b/data_compression/image_data/PSNR-example-base.png
similarity index 100%
rename from compression/image_data/PSNR-example-base.png
rename to data_compression/image_data/PSNR-example-base.png
diff --git a/compression/image_data/PSNR-example-comp-10.jpg b/data_compression/image_data/PSNR-example-comp-10.jpg
similarity index 100%
rename from compression/image_data/PSNR-example-comp-10.jpg
rename to data_compression/image_data/PSNR-example-comp-10.jpg
diff --git a/compression/image_data/compressed_image.png b/data_compression/image_data/compressed_image.png
similarity index 100%
rename from compression/image_data/compressed_image.png
rename to data_compression/image_data/compressed_image.png
diff --git a/compression/image_data/example_image.jpg b/data_compression/image_data/example_image.jpg
similarity index 100%
rename from compression/image_data/example_image.jpg
rename to data_compression/image_data/example_image.jpg
diff --git a/compression/image_data/example_wikipedia_image.jpg b/data_compression/image_data/example_wikipedia_image.jpg
similarity index 100%
rename from compression/image_data/example_wikipedia_image.jpg
rename to data_compression/image_data/example_wikipedia_image.jpg
diff --git a/compression/image_data/original_image.png b/data_compression/image_data/original_image.png
similarity index 100%
rename from compression/image_data/original_image.png
rename to data_compression/image_data/original_image.png
diff --git a/compression/lempel_ziv.py b/data_compression/lempel_ziv.py
similarity index 91%
rename from compression/lempel_ziv.py
rename to data_compression/lempel_ziv.py
index ea6f33944a91..648b029471bd 100644
--- a/compression/lempel_ziv.py
+++ b/data_compression/lempel_ziv.py
@@ -1,6 +1,6 @@
 """
-    One of the several implementations of Lempel–Ziv–Welch compression algorithm
-    https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch
+One of the several implementations of Lempel-Ziv-Welch compression algorithm
+https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch
 """
 
 import math
@@ -35,15 +35,15 @@ def add_key_to_lexicon(
     lexicon[curr_string + "0"] = last_match_id
 
     if math.log2(index).is_integer():
-        for curr_key in lexicon:
-            lexicon[curr_key] = "0" + lexicon[curr_key]
+        for curr_key, value in lexicon.items():
+            lexicon[curr_key] = f"0{value}"
 
     lexicon[curr_string + "1"] = bin(index)[2:]
 
 
 def compress_data(data_bits: str) -> str:
     """
-    Compresses given data_bits using Lempel–Ziv–Welch compression algorithm
+    Compresses given data_bits using Lempel-Ziv-Welch compression algorithm
     and returns the result as a string
     """
     lexicon = {"0": "0", "1": "1"}
diff --git a/compression/lempel_ziv_decompress.py b/data_compression/lempel_ziv_decompress.py
similarity index 92%
rename from compression/lempel_ziv_decompress.py
rename to data_compression/lempel_ziv_decompress.py
index ddedc3d6d32a..225e96236c2c 100644
--- a/compression/lempel_ziv_decompress.py
+++ b/data_compression/lempel_ziv_decompress.py
@@ -1,6 +1,6 @@
 """
-    One of the several implementations of Lempel–Ziv–Welch decompression algorithm
-    https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch
+One of the several implementations of Lempel-Ziv-Welch decompression algorithm
+https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch
 """
 
 import math
@@ -26,7 +26,7 @@ def read_file_binary(file_path: str) -> str:
 
 def decompress_data(data_bits: str) -> str:
     """
-    Decompresses given data_bits using Lempel–Ziv–Welch compression algorithm
+    Decompresses given data_bits using Lempel-Ziv-Welch compression algorithm
     and returns the result as a string
     """
     lexicon = {"0": "0", "1": "1"}
diff --git a/compression/lz77.py b/data_compression/lz77.py
similarity index 99%
rename from compression/lz77.py
rename to data_compression/lz77.py
index 1b201c59f186..09b8b021e9d5 100644
--- a/compression/lz77.py
+++ b/data_compression/lz77.py
@@ -28,7 +28,6 @@
 en.wikipedia.org/wiki/LZ77_and_LZ78
 """
 
-
 from dataclasses import dataclass
 
 __version__ = "0.1"
diff --git a/compression/peak_signal_to_noise_ratio.py b/data_compression/peak_signal_to_noise_ratio.py
similarity index 100%
rename from compression/peak_signal_to_noise_ratio.py
rename to data_compression/peak_signal_to_noise_ratio.py
diff --git a/compression/run_length_encoding.py b/data_compression/run_length_encoding.py
similarity index 100%
rename from compression/run_length_encoding.py
rename to data_compression/run_length_encoding.py
diff --git a/arithmetic_analysis/image_data/__init__.py b/data_structures/arrays/__init__.py
similarity index 100%
rename from arithmetic_analysis/image_data/__init__.py
rename to data_structures/arrays/__init__.py
diff --git a/data_structures/arrays/equilibrium_index_in_array.py b/data_structures/arrays/equilibrium_index_in_array.py
index 4099896d226d..0717a45d9f4b 100644
--- a/data_structures/arrays/equilibrium_index_in_array.py
+++ b/data_structures/arrays/equilibrium_index_in_array.py
@@ -2,8 +2,8 @@
 Find the Equilibrium Index of an Array.
 Reference: https://www.geeksforgeeks.org/equilibrium-index-of-an-array/
 
-Python doctests can be run with the following command:
-python -m doctest -v equilibrium_index.py
+Python doctest can be run with the following command:
+python -m doctest -v equilibrium_index_in_array.py
 
 Given a sequence arr[] of size n, this function returns
 an equilibrium index (if any) or -1 if no equilibrium index exists.
@@ -20,35 +20,34 @@
 """
 
 
-def equilibrium_index(arr: list[int], size: int) -> int:
+def equilibrium_index(arr: list[int]) -> int:
     """
     Find the equilibrium index of an array.
 
     Args:
-        arr : The input array of integers.
-        size : The size of the array.
+        arr (list[int]): The input array of integers.
 
     Returns:
         int: The equilibrium index or -1 if no equilibrium index exists.
 
     Examples:
-        >>> equilibrium_index([-7, 1, 5, 2, -4, 3, 0], 7)
+        >>> equilibrium_index([-7, 1, 5, 2, -4, 3, 0])
         3
-        >>> equilibrium_index([1, 2, 3, 4, 5], 5)
+        >>> equilibrium_index([1, 2, 3, 4, 5])
         -1
-        >>> equilibrium_index([1, 1, 1, 1, 1], 5)
+        >>> equilibrium_index([1, 1, 1, 1, 1])
         2
-        >>> equilibrium_index([2, 4, 6, 8, 10, 3], 6)
+        >>> equilibrium_index([2, 4, 6, 8, 10, 3])
         -1
     """
     total_sum = sum(arr)
     left_sum = 0
 
-    for i in range(size):
-        total_sum -= arr[i]
+    for i, value in enumerate(arr):
+        total_sum -= value
         if left_sum == total_sum:
             return i
-        left_sum += arr[i]
+        left_sum += value
 
     return -1
 
diff --git a/data_structures/arrays/find_triplets_with_0_sum.py b/data_structures/arrays/find_triplets_with_0_sum.py
index 8217ff857e3d..52e521906873 100644
--- a/data_structures/arrays/find_triplets_with_0_sum.py
+++ b/data_structures/arrays/find_triplets_with_0_sum.py
@@ -22,3 +22,66 @@ def find_triplets_with_0_sum(nums: list[int]) -> list[list[int]]:
         list(x)
         for x in sorted({abc for abc in combinations(sorted(nums), 3) if not sum(abc)})
     ]
+
+
+def find_triplets_with_0_sum_hashing(arr: list[int]) -> list[list[int]]:
+    """
+    Function for finding the triplets with a given sum in the array using hashing.
+
+    Given a list of integers, return elements a, b, c such that a + b + c = 0.
+
+    Args:
+        nums: list of integers
+    Returns:
+        list of lists of integers where sum(each_list) == 0
+    Examples:
+        >>> find_triplets_with_0_sum_hashing([-1, 0, 1, 2, -1, -4])
+        [[-1, 0, 1], [-1, -1, 2]]
+        >>> find_triplets_with_0_sum_hashing([])
+        []
+        >>> find_triplets_with_0_sum_hashing([0, 0, 0])
+        [[0, 0, 0]]
+        >>> find_triplets_with_0_sum_hashing([1, 2, 3, 0, -1, -2, -3])
+        [[-1, 0, 1], [-3, 1, 2], [-2, 0, 2], [-2, -1, 3], [-3, 0, 3]]
+
+    Time complexity: O(N^2)
+    Auxiliary Space: O(N)
+
+    """
+    target_sum = 0
+
+    # Initialize the final output array with blank.
+    output_arr = []
+
+    # Set the initial element as arr[i].
+    for index, item in enumerate(arr[:-2]):
+        # to store second elements that can complement the final sum.
+        set_initialize = set()
+
+        # current sum needed for reaching the target sum
+        current_sum = target_sum - item
+
+        # Traverse the subarray arr[i+1:].
+        for other_item in arr[index + 1 :]:
+            # required value for the second element
+            required_value = current_sum - other_item
+
+            # Verify if the desired value exists in the set.
+            if required_value in set_initialize:
+                # finding triplet elements combination.
+                combination_array = sorted([item, other_item, required_value])
+                if combination_array not in output_arr:
+                    output_arr.append(combination_array)
+
+            # Include the current element in the set
+            # for subsequent complement verification.
+            set_initialize.add(other_item)
+
+    # Return all the triplet combinations.
+    return output_arr
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
diff --git a/data_structures/arrays/index_2d_array_in_1d.py b/data_structures/arrays/index_2d_array_in_1d.py
new file mode 100644
index 000000000000..27a9fa5f9121
--- /dev/null
+++ b/data_structures/arrays/index_2d_array_in_1d.py
@@ -0,0 +1,105 @@
+"""
+Retrieves the value of an 0-indexed 1D index from a 2D array.
+There are two ways to retrieve value(s):
+
+1. Index2DArrayIterator(matrix) -> Iterator[int]
+This iterator allows you to iterate through a 2D array by passing in the matrix and
+calling next(your_iterator). You can also use the iterator in a loop.
+Examples:
+list(Index2DArrayIterator(matrix))
+set(Index2DArrayIterator(matrix))
+tuple(Index2DArrayIterator(matrix))
+sum(Index2DArrayIterator(matrix))
+-5 in Index2DArrayIterator(matrix)
+
+2. index_2d_array_in_1d(array: list[int], index: int) -> int
+This function allows you to provide a 2D array and a 0-indexed 1D integer index,
+and retrieves the integer value at that index.
+
+Python doctests can be run using this command:
+python3 -m doctest -v index_2d_array_in_1d.py
+"""
+
+from collections.abc import Iterator
+from dataclasses import dataclass
+
+
+@dataclass
+class Index2DArrayIterator:
+    matrix: list[list[int]]
+
+    def __iter__(self) -> Iterator[int]:
+        """
+        >>> tuple(Index2DArrayIterator([[5], [-523], [-1], [34], [0]]))
+        (5, -523, -1, 34, 0)
+        >>> tuple(Index2DArrayIterator([[5, -523, -1], [34, 0]]))
+        (5, -523, -1, 34, 0)
+        >>> tuple(Index2DArrayIterator([[5, -523, -1, 34, 0]]))
+        (5, -523, -1, 34, 0)
+        >>> t = Index2DArrayIterator([[5, 2, 25], [23, 14, 5], [324, -1, 0]])
+        >>> tuple(t)
+        (5, 2, 25, 23, 14, 5, 324, -1, 0)
+        >>> list(t)
+        [5, 2, 25, 23, 14, 5, 324, -1, 0]
+        >>> sorted(t)
+        [-1, 0, 2, 5, 5, 14, 23, 25, 324]
+        >>> tuple(t)[3]
+        23
+        >>> sum(t)
+        397
+        >>> -1 in t
+        True
+        >>> t = iter(Index2DArrayIterator([[5], [-523], [-1], [34], [0]]))
+        >>> next(t)
+        5
+        >>> next(t)
+        -523
+        """
+        for row in self.matrix:
+            yield from row
+
+
+def index_2d_array_in_1d(array: list[list[int]], index: int) -> int:
+    """
+    Retrieves the value of the one-dimensional index from a two-dimensional array.
+
+    Args:
+        array: A 2D array of integers where all rows are the same size and all
+               columns are the same size.
+        index: A 1D index.
+
+    Returns:
+        int: The 0-indexed value of the 1D index in the array.
+
+    Examples:
+    >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], 5)
+    5
+    >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: index out of range
+    >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], 12)
+    Traceback (most recent call last):
+        ...
+    ValueError: index out of range
+    >>> index_2d_array_in_1d([[]], 0)
+    Traceback (most recent call last):
+        ...
+    ValueError: no items in array
+    """
+    rows = len(array)
+    cols = len(array[0])
+
+    if rows == 0 or cols == 0:
+        raise ValueError("no items in array")
+
+    if index < 0 or index >= rows * cols:
+        raise ValueError("index out of range")
+
+    return array[index // cols][index % cols]
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/arrays/kth_largest_element.py b/data_structures/arrays/kth_largest_element.py
new file mode 100644
index 000000000000..f25cc68e9b72
--- /dev/null
+++ b/data_structures/arrays/kth_largest_element.py
@@ -0,0 +1,117 @@
+"""
+Given an array of integers and an integer k, find the kth largest element in the array.
+
+https://stackoverflow.com/questions/251781
+"""
+
+
+def partition(arr: list[int], low: int, high: int) -> int:
+    """
+    Partitions list based on the pivot element.
+
+    This function rearranges the elements in the input list 'elements' such that
+    all elements greater than or equal to the chosen pivot are on the right side
+    of the pivot, and all elements smaller than the pivot are on the left side.
+
+    Args:
+        arr: The list to be partitioned
+        low: The lower index of the list
+        high: The higher index of the list
+
+    Returns:
+        int: The index of pivot element after partitioning
+
+        Examples:
+        >>> partition([3, 1, 4, 5, 9, 2, 6, 5, 3, 5], 0, 9)
+        4
+        >>> partition([7, 1, 4, 5, 9, 2, 6, 5, 8], 0, 8)
+        1
+        >>> partition(['apple', 'cherry', 'date', 'banana'], 0, 3)
+        2
+        >>> partition([3.1, 1.2, 5.6, 4.7], 0, 3)
+        1
+    """
+    pivot = arr[high]
+    i = low - 1
+    for j in range(low, high):
+        if arr[j] >= pivot:
+            i += 1
+            arr[i], arr[j] = arr[j], arr[i]
+    arr[i + 1], arr[high] = arr[high], arr[i + 1]
+    return i + 1
+
+
+def kth_largest_element(arr: list[int], position: int) -> int:
+    """
+    Finds the kth largest element in a list.
+    Should deliver similar results to:
+    ```python
+    def kth_largest_element(arr, position):
+        return sorted(arr)[-position]
+    ```
+
+    Args:
+        nums: The list of numbers.
+        k: The position of the desired kth largest element.
+
+    Returns:
+        int: The kth largest element.
+
+    Examples:
+        >>> kth_largest_element([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5], 3)
+        5
+        >>> kth_largest_element([2, 5, 6, 1, 9, 3, 8, 4, 7, 3, 5], 1)
+        9
+        >>> kth_largest_element([2, 5, 6, 1, 9, 3, 8, 4, 7, 3, 5], -2)
+        Traceback (most recent call last):
+        ...
+        ValueError: Invalid value of 'position'
+        >>> kth_largest_element([9, 1, 3, 6, 7, 9, 8, 4, 2, 4, 9], 110)
+        Traceback (most recent call last):
+        ...
+        ValueError: Invalid value of 'position'
+        >>> kth_largest_element([1, 2, 4, 3, 5, 9, 7, 6, 5, 9, 3], 0)
+        Traceback (most recent call last):
+        ...
+        ValueError: Invalid value of 'position'
+        >>> kth_largest_element(['apple', 'cherry', 'date', 'banana'], 2)
+        'cherry'
+        >>> kth_largest_element([3.1, 1.2, 5.6, 4.7,7.9,5,0], 2)
+        5.6
+        >>> kth_largest_element([-2, -5, -4, -1], 1)
+        -1
+        >>> kth_largest_element([], 1)
+        -1
+        >>> kth_largest_element([3.1, 1.2, 5.6, 4.7, 7.9, 5, 0], 1.5)
+        Traceback (most recent call last):
+        ...
+        ValueError: The position should be an integer
+        >>> kth_largest_element((4, 6, 1, 2), 4)
+        Traceback (most recent call last):
+        ...
+        TypeError: 'tuple' object does not support item assignment
+    """
+    if not arr:
+        return -1
+    if not isinstance(position, int):
+        raise ValueError("The position should be an integer")
+    if not 1 <= position <= len(arr):
+        raise ValueError("Invalid value of 'position'")
+    low, high = 0, len(arr) - 1
+    while low <= high:
+        if low > len(arr) - 1 or high < 0:
+            return -1
+        pivot_index = partition(arr, low, high)
+        if pivot_index == position - 1:
+            return arr[pivot_index]
+        elif pivot_index > position - 1:
+            high = pivot_index - 1
+        else:
+            low = pivot_index + 1
+    return -1
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/arrays/monotonic_array.py b/data_structures/arrays/monotonic_array.py
new file mode 100644
index 000000000000..342d443a9cfc
--- /dev/null
+++ b/data_structures/arrays/monotonic_array.py
@@ -0,0 +1,37 @@
+# https://leetcode.com/problems/monotonic-array/
+def is_monotonic(nums: list[int]) -> bool:
+    """
+    Check if a list is monotonic.
+
+    >>> is_monotonic([1, 2, 2, 3])
+    True
+    >>> is_monotonic([6, 5, 4, 4])
+    True
+    >>> is_monotonic([1, 3, 2])
+    False
+    >>> is_monotonic([1,2,3,4,5,6,5])
+    False
+    >>> is_monotonic([-3,-2,-1])
+    True
+    >>> is_monotonic([-5,-6,-7])
+    True
+    >>> is_monotonic([0,0,0])
+    True
+    >>> is_monotonic([-100,0,100])
+    True
+    """
+    return all(nums[i] <= nums[i + 1] for i in range(len(nums) - 1)) or all(
+        nums[i] >= nums[i + 1] for i in range(len(nums) - 1)
+    )
+
+
+# Test the function with your examples
+if __name__ == "__main__":
+    # Test the function with your examples
+    print(is_monotonic([1, 2, 2, 3]))  # Output: True
+    print(is_monotonic([6, 5, 4, 4]))  # Output: True
+    print(is_monotonic([1, 3, 2]))  # Output: False
+
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/arrays/pairs_with_given_sum.py b/data_structures/arrays/pairs_with_given_sum.py
index c4a5ceeae456..b27bd78e1e0f 100644
--- a/data_structures/arrays/pairs_with_given_sum.py
+++ b/data_structures/arrays/pairs_with_given_sum.py
@@ -6,6 +6,7 @@
 
 https://practice.geeksforgeeks.org/problems/count-pairs-with-given-sum5022/0
 """
+
 from itertools import combinations
 
 
diff --git a/data_structures/arrays/prefix_sum.py b/data_structures/arrays/prefix_sum.py
index 2243a5308937..717b5f9d7e7e 100644
--- a/data_structures/arrays/prefix_sum.py
+++ b/data_structures/arrays/prefix_sum.py
@@ -30,11 +30,29 @@ def get_sum(self, start: int, end: int) -> int:
         5
         >>> PrefixSum([1,2,3]).get_sum(2, 2)
         3
+        >>> PrefixSum([]).get_sum(0, 0)
+        Traceback (most recent call last):
+        ...
+        ValueError: The array is empty.
+        >>> PrefixSum([1,2,3]).get_sum(-1, 2)
+        Traceback (most recent call last):
+        ...
+        ValueError: Invalid range specified.
         >>> PrefixSum([1,2,3]).get_sum(2, 3)
         Traceback (most recent call last):
         ...
-        IndexError: list index out of range
+        ValueError: Invalid range specified.
+        >>> PrefixSum([1,2,3]).get_sum(2, 1)
+        Traceback (most recent call last):
+        ...
+        ValueError: Invalid range specified.
         """
+        if not self.prefix_sum:
+            raise ValueError("The array is empty.")
+
+        if start < 0 or end >= len(self.prefix_sum) or start > end:
+            raise ValueError("Invalid range specified.")
+
         if start == 0:
             return self.prefix_sum[end]
 
diff --git a/data_structures/arrays/sparse_table.py b/data_structures/arrays/sparse_table.py
index a15d5649e712..4606fe908607 100644
--- a/data_structures/arrays/sparse_table.py
+++ b/data_structures/arrays/sparse_table.py
@@ -1,15 +1,16 @@
 """
-    Sparse table is a data structure that allows answering range queries on
-    a static number list, i.e. the elements do not change throughout all the queries.
+Sparse table is a data structure that allows answering range queries on
+a static number list, i.e. the elements do not change throughout all the queries.
 
-    The implementation below will solve the problem of Range Minimum Query:
-    Finding the minimum value of a subset [L..R] of a static number list.
+The implementation below will solve the problem of Range Minimum Query:
+Finding the minimum value of a subset [L..R] of a static number list.
 
-    Overall time complexity: O(nlogn)
-    Overall space complexity: O(nlogn)
+Overall time complexity: O(nlogn)
+Overall space complexity: O(nlogn)
 
-    Wikipedia link: https://en.wikipedia.org/wiki/Range_minimum_query
+Wikipedia link: https://en.wikipedia.org/wiki/Range_minimum_query
 """
+
 from math import log2
 
 
diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py
index 8d38bd7295ea..4c722f12fd6e 100644
--- a/data_structures/arrays/sudoku_solver.py
+++ b/data_structures/arrays/sudoku_solver.py
@@ -3,12 +3,15 @@
 only minimal changes to work with modern versions of Python.  If you have improvements,
 please make them in a separate file.
 """
+
 import random
 import time
 
 
 def cross(items_a, items_b):
-    "Cross product of elements in A and elements in B."
+    """
+    Cross product of elements in A and elements in B.
+    """
     return [a + b for a in items_a for b in items_b]
 
 
@@ -22,11 +25,11 @@ def cross(items_a, items_b):
     + [cross(rs, cs) for rs in ("ABC", "DEF", "GHI") for cs in ("123", "456", "789")]
 )
 units = {s: [u for u in unitlist if s in u] for s in squares}
-peers = {s: set(sum(units[s], [])) - {s} for s in squares}
+peers = {s: {x for u in units[s] for x in u} - {s} for s in squares}
 
 
 def test():
-    "A set of unit tests."
+    """A set of unit tests."""
     assert len(squares) == 81
     assert len(unitlist) == 27
     assert all(len(units[s]) == 3 for s in squares)
@@ -46,10 +49,12 @@ def test():
 
 
 def parse_grid(grid):
-    """Convert grid to a dict of possible values, {square: digits}, or
-    return False if a contradiction is detected."""
+    """
+    Convert grid to a dict of possible values, {square: digits}, or
+    return False if a contradiction is detected.
+    """
     ## To start, every square can be any digit; then assign values from the grid.
-    values = {s: digits for s in squares}
+    values = dict.fromkeys(squares, digits)
     for s, d in grid_values(grid).items():
         if d in digits and not assign(values, s, d):
             return False  ## (Fail if we can't assign d to square s.)
@@ -57,15 +62,19 @@ def parse_grid(grid):
 
 
 def grid_values(grid):
-    "Convert grid into a dict of {square: char} with '0' or '.' for empties."
+    """
+    Convert grid into a dict of {square: char} with '0' or '.' for empties.
+    """
     chars = [c for c in grid if c in digits or c in "0."]
     assert len(chars) == 81
     return dict(zip(squares, chars))
 
 
 def assign(values, s, d):
-    """Eliminate all the other values (except d) from values[s] and propagate.
-    Return values, except return False if a contradiction is detected."""
+    """
+    Eliminate all the other values (except d) from values[s] and propagate.
+    Return values, except return False if a contradiction is detected.
+    """
     other_values = values[s].replace(d, "")
     if all(eliminate(values, s, d2) for d2 in other_values):
         return values
@@ -74,8 +83,10 @@ def assign(values, s, d):
 
 
 def eliminate(values, s, d):
-    """Eliminate d from values[s]; propagate when values or places <= 2.
-    Return values, except return False if a contradiction is detected."""
+    """
+    Eliminate d from values[s]; propagate when values or places <= 2.
+    Return values, except return False if a contradiction is detected.
+    """
     if d not in values[s]:
         return values  ## Already eliminated
     values[s] = values[s].replace(d, "")
@@ -91,15 +102,16 @@ def eliminate(values, s, d):
         dplaces = [s for s in u if d in values[s]]
         if len(dplaces) == 0:
             return False  ## Contradiction: no place for this value
-        elif len(dplaces) == 1:
-            # d can only be in one place in unit; assign it there
-            if not assign(values, dplaces[0], d):
-                return False
+        # d can only be in one place in unit; assign it there
+        elif len(dplaces) == 1 and not assign(values, dplaces[0], d):
+            return False
     return values
 
 
 def display(values):
-    "Display these values as a 2-D grid."
+    """
+    Display these values as a 2-D grid.
+    """
     width = 1 + max(len(values[s]) for s in squares)
     line = "+".join(["-" * (width * 3)] * 3)
     for r in rows:
@@ -114,11 +126,14 @@ def display(values):
 
 
 def solve(grid):
+    """
+    Solve the grid.
+    """
     return search(parse_grid(grid))
 
 
 def some(seq):
-    "Return some element of seq that is true."
+    """Return some element of seq that is true."""
     for e in seq:
         if e:
             return e
@@ -126,7 +141,9 @@ def some(seq):
 
 
 def search(values):
-    "Using depth-first search and propagation, try all possible values."
+    """
+    Using depth-first search and propagation, try all possible values.
+    """
     if values is False:
         return False  ## Failed earlier
     if all(len(values[s]) == 1 for s in squares):
@@ -137,9 +154,11 @@ def search(values):
 
 
 def solve_all(grids, name="", showif=0.0):
-    """Attempt to solve a sequence of grids. Report results.
+    """
+    Attempt to solve a sequence of grids. Report results.
     When showif is a number of seconds, display puzzles that take longer.
-    When showif is None, don't display any puzzles."""
+    When showif is None, don't display any puzzles.
+    """
 
     def time_solve(grid):
         start = time.monotonic()
@@ -150,19 +169,21 @@ def time_solve(grid):
             display(grid_values(grid))
             if values:
                 display(values)
-            print("(%.5f seconds)\n" % t)
+            print(f"({t:.5f} seconds)\n")
         return (t, solved(values))
 
     times, results = zip(*[time_solve(grid) for grid in grids])
     if (n := len(grids)) > 1:
         print(
-            "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)."
+            "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)."  # noqa: UP031
             % (sum(results), n, name, sum(times) / n, n / sum(times), max(times))
         )
 
 
 def solved(values):
-    "A puzzle is solved if each unit is a permutation of the digits 1 to 9."
+    """
+    A puzzle is solved if each unit is a permutation of the digits 1 to 9.
+    """
 
     def unitsolved(unit):
         return {values[s] for s in unit} == set(digits)
@@ -172,14 +193,17 @@ def unitsolved(unit):
 
 def from_file(filename, sep="\n"):
     "Parse a file into a list of strings, separated by sep."
-    return open(filename).read().strip().split(sep)  # noqa: SIM115
+    with open(filename) as file:
+        return file.read().strip().split(sep)
 
 
 def random_puzzle(assignments=17):
-    """Make a random puzzle with N or more assignments. Restart on contradictions.
+    """
+    Make a random puzzle with N or more assignments. Restart on contradictions.
     Note the resulting puzzle is not guaranteed to be solvable, but empirically
-    about 99.8% of them are solvable. Some have multiple solutions."""
-    values = {s: digits for s in squares}
+    about 99.8% of them are solvable. Some have multiple solutions.
+    """
+    values = dict.fromkeys(squares, digits)
     for s in shuffled(squares):
         if not assign(values, s, random.choice(values[s])):
             break
@@ -190,7 +214,9 @@ def random_puzzle(assignments=17):
 
 
 def shuffled(seq):
-    "Return a randomly shuffled copy of the input sequence."
+    """
+    Return a randomly shuffled copy of the input sequence.
+    """
     seq = list(seq)
     random.shuffle(seq)
     return seq
@@ -217,4 +243,4 @@ def shuffled(seq):
         start = time.monotonic()
         solve(puzzle)
         t = time.monotonic() - start
-        print("Solved: %.5f sec" % t)
+        print(f"Solved: {t:.5f} sec")
diff --git a/data_structures/binary_tree/avl_tree.py b/data_structures/binary_tree/avl_tree.py
index 4c1fb17afe86..8558305eefe4 100644
--- a/data_structures/binary_tree/avl_tree.py
+++ b/data_structures/binary_tree/avl_tree.py
@@ -5,6 +5,7 @@
 For testing run:
 python avl_tree.py
 """
+
 from __future__ import annotations
 
 import math
@@ -214,11 +215,15 @@ def del_node(root: MyNode, data: Any) -> MyNode | None:
             return root
         else:
             root.set_left(del_node(left_child, data))
-    else:  # root.get_data() < data
-        if right_child is None:
-            return root
-        else:
-            root.set_right(del_node(right_child, data))
+    # root.get_data() < data
+    elif right_child is None:
+        return root
+    else:
+        root.set_right(del_node(right_child, data))
+
+    # Re-fetch left_child and right_child references
+    left_child = root.get_left()
+    right_child = root.get_right()
 
     if get_height(right_child) - get_height(left_child) == 2:
         assert right_child is not None
diff --git a/data_structures/binary_tree/basic_binary_tree.py b/data_structures/binary_tree/basic_binary_tree.py
index 0439413d95b5..9d4c1bdbb57a 100644
--- a/data_structures/binary_tree/basic_binary_tree.py
+++ b/data_structures/binary_tree/basic_binary_tree.py
@@ -85,7 +85,7 @@ def depth(self) -> int:
         """
         return self._depth(self.root)
 
-    def _depth(self, node: Node | None) -> int:  # noqa: UP007
+    def _depth(self, node: Node | None) -> int:
         if not node:
             return 0
         return 1 + max(self._depth(node.left), self._depth(node.right))
diff --git a/data_structures/binary_tree/binary_search_tree.py b/data_structures/binary_tree/binary_search_tree.py
index a706d21e3bb2..3f214d0113a4 100644
--- a/data_structures/binary_tree/binary_search_tree.py
+++ b/data_structures/binary_tree/binary_search_tree.py
@@ -10,10 +10,19 @@
              / \   /
             4   7 13
 
->>> t = BinarySearchTree()
->>> t.insert(8, 3, 6, 1, 10, 14, 13, 4, 7)
+>>> t = BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7)
 >>> print(" ".join(repr(i.value) for i in t.traversal_tree()))
 8 3 1 6 4 7 10 14 13
+
+>>> tuple(i.value for i in t.traversal_tree(inorder))
+(1, 3, 4, 6, 7, 8, 10, 13, 14)
+>>> tuple(t)
+(1, 3, 4, 6, 7, 8, 10, 13, 14)
+>>> t.find_kth_smallest(3, t.root)
+4
+>>> tuple(t)[3-1]
+4
+
 >>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder)))
 1 4 7 6 3 13 14 10 8
 >>> t.remove(20)
@@ -30,7 +39,16 @@
 >>> testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7)
 >>> t = BinarySearchTree()
 >>> for i in testlist:
-...     t.insert(i)
+...     t.insert(i)  # doctest: +ELLIPSIS
+BinarySearchTree(root=8)
+BinarySearchTree(root={'8': (3, None)})
+BinarySearchTree(root={'8': ({'3': (None, 6)}, None)})
+BinarySearchTree(root={'8': ({'3': (1, 6)}, None)})
+BinarySearchTree(root={'8': ({'3': (1, 6)}, 10)})
+BinarySearchTree(root={'8': ({'3': (1, 6)}, {'10': (None, 14)})})
+BinarySearchTree(root={'8': ({'3': (1, 6)}, {'10': (None, {'14': (13, None)})})})
+BinarySearchTree(root={'8': ({'3': (1, {'6': (4, None)})}, {'10': (None, {'14': ...
+BinarySearchTree(root={'8': ({'3': (1, {'6': (4, 7)})}, {'10': (None, {'14': (13, ...
 
 Prints all the elements of the list in order traversal
 >>> print(t)
@@ -39,8 +57,12 @@
 Test existence
 >>> t.search(6) is not None
 True
+>>> 6 in t
+True
 >>> t.search(-1) is not None
 False
+>>> -1 in t
+False
 
 >>> t.search(6).is_right
 True
@@ -49,26 +71,48 @@
 
 >>> t.get_max().value
 14
+>>> max(t)
+14
 >>> t.get_min().value
 1
+>>> min(t)
+1
 >>> t.empty()
 False
+>>> not t
+False
 >>> for i in testlist:
 ...     t.remove(i)
 >>> t.empty()
 True
+>>> not t
+True
 """
 
-from collections.abc import Iterable
-from typing import Any
+from __future__ import annotations
+
+from collections.abc import Iterable, Iterator
+from dataclasses import dataclass
+from typing import Any, Self
 
 
+@dataclass
 class Node:
-    def __init__(self, value: int | None = None):
-        self.value = value
-        self.parent: Node | None = None  # Added in order to delete a node easier
-        self.left: Node | None = None
-        self.right: Node | None = None
+    value: int
+    left: Node | None = None
+    right: Node | None = None
+    parent: Node | None = None  # Added in order to delete a node easier
+
+    def __iter__(self) -> Iterator[int]:
+        """
+        >>> list(Node(0))
+        [0]
+        >>> list(Node(0, Node(-1), Node(1), None))
+        [-1, 0, 1]
+        """
+        yield from self.left or []
+        yield self.value
+        yield from self.right or []
 
     def __repr__(self) -> str:
         from pprint import pformat
@@ -79,12 +123,18 @@ def __repr__(self) -> str:
 
     @property
     def is_right(self) -> bool:
-        return self.parent is not None and self is self.parent.right
+        return bool(self.parent and self is self.parent.right)
 
 
+@dataclass
 class BinarySearchTree:
-    def __init__(self, root: Node | None = None):
-        self.root = root
+    root: Node | None = None
+
+    def __bool__(self) -> bool:
+        return bool(self.root)
+
+    def __iter__(self) -> Iterator[int]:
+        yield from self.root or []
 
     def __str__(self) -> str:
         """
@@ -104,7 +154,18 @@ def __reassign_nodes(self, node: Node, new_children: Node | None) -> None:
             self.root = new_children
 
     def empty(self) -> bool:
-        return self.root is None
+        """
+        Returns True if the tree does not have any element(s).
+        False if the tree has element(s).
+
+        >>> BinarySearchTree().empty()
+        True
+        >>> BinarySearchTree().insert(1).empty()
+        False
+        >>> BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7).empty()
+        False
+        """
+        return not self.root
 
     def __insert(self, value) -> None:
         """
@@ -124,19 +185,43 @@ def __insert(self, value) -> None:
                         break
                     else:
                         parent_node = parent_node.left
+                elif parent_node.right is None:
+                    parent_node.right = new_node
+                    break
                 else:
-                    if parent_node.right is None:
-                        parent_node.right = new_node
-                        break
-                    else:
-                        parent_node = parent_node.right
+                    parent_node = parent_node.right
             new_node.parent = parent_node
 
-    def insert(self, *values) -> None:
+    def insert(self, *values) -> Self:
         for value in values:
             self.__insert(value)
+        return self
 
     def search(self, value) -> Node | None:
+        """
+        >>> tree = BinarySearchTree().insert(10, 20, 30, 40, 50)
+        >>> tree.search(10)
+        {'10': (None, {'20': (None, {'30': (None, {'40': (None, 50)})})})}
+        >>> tree.search(20)
+        {'20': (None, {'30': (None, {'40': (None, 50)})})}
+        >>> tree.search(30)
+        {'30': (None, {'40': (None, 50)})}
+        >>> tree.search(40)
+        {'40': (None, 50)}
+        >>> tree.search(50)
+        50
+        >>> tree.search(5) is None  # element not present
+        True
+        >>> tree.search(0) is None  # element not present
+        True
+        >>> tree.search(-5) is None  # element not present
+        True
+        >>> BinarySearchTree().search(10)
+        Traceback (most recent call last):
+            ...
+        IndexError: Warning: Tree is empty! please use another.
+        """
+
         if self.empty():
             raise IndexError("Warning: Tree is empty! please use another.")
         else:
@@ -149,6 +234,15 @@ def search(self, value) -> Node | None:
     def get_max(self, node: Node | None = None) -> Node | None:
         """
         We go deep on the right branch
+
+        >>> BinarySearchTree().insert(10, 20, 30, 40, 50).get_max()
+        50
+        >>> BinarySearchTree().insert(-5, -1, 0.1, -0.3, -4.5).get_max()
+        {'0.1': (-0.3, None)}
+        >>> BinarySearchTree().insert(1, 78.3, 30, 74.0, 1).get_max()
+        {'78.3': ({'30': (1, 74.0)}, None)}
+        >>> BinarySearchTree().insert(1, 783, 30, 740, 1).get_max()
+        {'783': ({'30': (1, 740)}, None)}
         """
         if node is None:
             if self.root is None:
@@ -163,6 +257,15 @@ def get_max(self, node: Node | None = None) -> Node | None:
     def get_min(self, node: Node | None = None) -> Node | None:
         """
         We go deep on the left branch
+
+        >>> BinarySearchTree().insert(10, 20, 30, 40, 50).get_min()
+        {'10': (None, {'20': (None, {'30': (None, {'40': (None, 50)})})})}
+        >>> BinarySearchTree().insert(-5, -1, 0, -0.3, -4.5).get_min()
+        {'-5': (None, {'-1': (-4.5, {'0': (-0.3, None)})})}
+        >>> BinarySearchTree().insert(1, 78.3, 30, 74.0, 1).get_min()
+        {'1': (None, {'78.3': ({'30': (1, 74.0)}, None)})}
+        >>> BinarySearchTree().insert(1, 783, 30, 740, 1).get_min()
+        {'1': (None, {'783': ({'30': (1, 740)}, None)})}
         """
         if node is None:
             node = self.root
@@ -191,9 +294,9 @@ def remove(self, value: int) -> None:
             predecessor = self.get_max(
                 node.left
             )  # Gets the max value of the left branch
-            self.remove(predecessor.value)  # type: ignore
+            self.remove(predecessor.value)  # type: ignore[union-attr]
             node.value = (
-                predecessor.value  # type: ignore
+                predecessor.value  # type: ignore[union-attr]
             )  # Assigns the value to the node to delete and keep tree structure
 
     def preorder_traverse(self, node: Node | None) -> Iterable:
@@ -227,6 +330,16 @@ def find_kth_smallest(self, k: int, node: Node) -> int:
         return arr[k - 1]
 
 
+def inorder(curr_node: Node | None) -> list[Node]:
+    """
+    inorder (left, self, right)
+    """
+    node_list = []
+    if curr_node is not None:
+        node_list = [*inorder(curr_node.left), curr_node, *inorder(curr_node.right)]
+    return node_list
+
+
 def postorder(curr_node: Node | None) -> list[Node]:
     """
     postOrder (left, right, self)
diff --git a/data_structures/binary_tree/binary_search_tree_recursive.py b/data_structures/binary_tree/binary_search_tree_recursive.py
index 13b9b392175c..d94ac5253360 100644
--- a/data_structures/binary_tree/binary_search_tree_recursive.py
+++ b/data_structures/binary_tree/binary_search_tree_recursive.py
@@ -7,6 +7,7 @@
 To run an example:
 python binary_search_tree_recursive.py
 """
+
 from __future__ import annotations
 
 import unittest
@@ -73,14 +74,13 @@ def put(self, label: int) -> None:
     def _put(self, node: Node | None, label: int, parent: Node | None = None) -> Node:
         if node is None:
             node = Node(label, parent)
+        elif label < node.label:
+            node.left = self._put(node.left, label, node)
+        elif label > node.label:
+            node.right = self._put(node.right, label, node)
         else:
-            if label < node.label:
-                node.left = self._put(node.left, label, node)
-            elif label > node.label:
-                node.right = self._put(node.right, label, node)
-            else:
-                msg = f"Node with label {label} already exists"
-                raise ValueError(msg)
+            msg = f"Node with label {label} already exists"
+            raise ValueError(msg)
 
         return node
 
@@ -105,11 +105,10 @@ def _search(self, node: Node | None, label: int) -> Node:
         if node is None:
             msg = f"Node with label {label} does not exist"
             raise ValueError(msg)
-        else:
-            if label < node.label:
-                node = self._search(node.left, label)
-            elif label > node.label:
-                node = self._search(node.right, label)
+        elif label < node.label:
+            node = self._search(node.left, label)
+        elif label > node.label:
+            node = self._search(node.right, label)
 
         return node
 
diff --git a/data_structures/binary_tree/binary_tree_node_sum.py b/data_structures/binary_tree/binary_tree_node_sum.py
index 5a13e74e3c9f..066617b616c4 100644
--- a/data_structures/binary_tree/binary_tree_node_sum.py
+++ b/data_structures/binary_tree/binary_tree_node_sum.py
@@ -8,7 +8,6 @@
                             frames that could be in memory is `n`
 """
 
-
 from __future__ import annotations
 
 from collections.abc import Iterator
diff --git a/data_structures/binary_tree/binary_tree_traversals.py b/data_structures/binary_tree/binary_tree_traversals.py
index 2b33cdca4fed..5ba149d0cbc6 100644
--- a/data_structures/binary_tree/binary_tree_traversals.py
+++ b/data_structures/binary_tree/binary_tree_traversals.py
@@ -30,7 +30,7 @@ def make_tree() -> Node | None:
     return tree
 
 
-def preorder(root: Node | None) -> Generator[int, None, None]:
+def preorder(root: Node | None) -> Generator[int]:
     """
     Pre-order traversal visits root node, left subtree, right subtree.
     >>> list(preorder(make_tree()))
@@ -43,7 +43,7 @@ def preorder(root: Node | None) -> Generator[int, None, None]:
     yield from preorder(root.right)
 
 
-def postorder(root: Node | None) -> Generator[int, None, None]:
+def postorder(root: Node | None) -> Generator[int]:
     """
     Post-order traversal visits left subtree, right subtree, root node.
     >>> list(postorder(make_tree()))
@@ -56,7 +56,7 @@ def postorder(root: Node | None) -> Generator[int, None, None]:
     yield root.data
 
 
-def inorder(root: Node | None) -> Generator[int, None, None]:
+def inorder(root: Node | None) -> Generator[int]:
     """
     In-order traversal visits left subtree, root node, right subtree.
     >>> list(inorder(make_tree()))
@@ -69,7 +69,7 @@ def inorder(root: Node | None) -> Generator[int, None, None]:
     yield from inorder(root.right)
 
 
-def reverse_inorder(root: Node | None) -> Generator[int, None, None]:
+def reverse_inorder(root: Node | None) -> Generator[int]:
     """
     Reverse in-order traversal visits right subtree, root node, left subtree.
     >>> list(reverse_inorder(make_tree()))
@@ -93,10 +93,12 @@ def height(root: Node | None) -> int:
     return (max(height(root.left), height(root.right)) + 1) if root else 0
 
 
-def level_order(root: Node | None) -> Generator[int, None, None]:
+def level_order(root: Node | None) -> Generator[int]:
     """
     Returns a list of nodes value from a whole binary tree in Level Order Traverse.
     Level Order traverse: Visit nodes of the tree level-by-level.
+    >>> list(level_order(make_tree()))
+    [1, 2, 3, 4, 5]
     """
 
     if root is None:
@@ -114,15 +116,17 @@ def level_order(root: Node | None) -> Generator[int, None, None]:
             process_queue.append(node.right)
 
 
-def get_nodes_from_left_to_right(
-    root: Node | None, level: int
-) -> Generator[int, None, None]:
+def get_nodes_from_left_to_right(root: Node | None, level: int) -> Generator[int]:
     """
     Returns a list of nodes value from a particular level:
     Left to right direction of the binary tree.
+    >>> list(get_nodes_from_left_to_right(make_tree(), 1))
+    [1]
+    >>> list(get_nodes_from_left_to_right(make_tree(), 2))
+    [2, 3]
     """
 
-    def populate_output(root: Node | None, level: int) -> Generator[int, None, None]:
+    def populate_output(root: Node | None, level: int) -> Generator[int]:
         if not root:
             return
         if level == 1:
@@ -134,16 +138,18 @@ def populate_output(root: Node | None, level: int) -> Generator[int, None, None]
     yield from populate_output(root, level)
 
 
-def get_nodes_from_right_to_left(
-    root: Node | None, level: int
-) -> Generator[int, None, None]:
+def get_nodes_from_right_to_left(root: Node | None, level: int) -> Generator[int]:
     """
     Returns a list of nodes value from a particular level:
     Right to left direction of the binary tree.
+    >>> list(get_nodes_from_right_to_left(make_tree(), 1))
+    [1]
+    >>> list(get_nodes_from_right_to_left(make_tree(), 2))
+    [3, 2]
     """
 
-    def populate_output(root: Node | None, level: int) -> Generator[int, None, None]:
-        if root is None:
+    def populate_output(root: Node | None, level: int) -> Generator[int]:
+        if not root:
             return
         if level == 1:
             yield root.data
@@ -154,10 +160,12 @@ def populate_output(root: Node | None, level: int) -> Generator[int, None, None]
     yield from populate_output(root, level)
 
 
-def zigzag(root: Node | None) -> Generator[int, None, None]:
+def zigzag(root: Node | None) -> Generator[int]:
     """
     ZigZag traverse:
     Returns a list of nodes value from left to right and right to left, alternatively.
+    >>> list(zigzag(make_tree()))
+    [1, 3, 2, 4, 5]
     """
     if root is None:
         return
diff --git a/data_structures/binary_tree/diameter_of_binary_tree.py b/data_structures/binary_tree/diameter_of_binary_tree.py
index bbe70b028d24..75e5e7373323 100644
--- a/data_structures/binary_tree/diameter_of_binary_tree.py
+++ b/data_structures/binary_tree/diameter_of_binary_tree.py
@@ -2,6 +2,7 @@
 The diameter/width of a tree is defined as the number of nodes on the longest path
 between two end nodes.
 """
+
 from __future__ import annotations
 
 from dataclasses import dataclass
diff --git a/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py b/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py
index 8820a509ecba..9b2c7b9af24b 100644
--- a/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py
+++ b/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py
@@ -10,6 +10,7 @@
 Author: Arunkumar A
 Date: 04/09/2023
 """
+
 from __future__ import annotations
 
 
diff --git a/data_structures/binary_tree/floor_and_ceiling.py b/data_structures/binary_tree/floor_and_ceiling.py
index f8a1adbd967b..b464aefad3a2 100644
--- a/data_structures/binary_tree/floor_and_ceiling.py
+++ b/data_structures/binary_tree/floor_and_ceiling.py
@@ -9,6 +9,7 @@
 Author : Arunkumar
 Date : 14th October 2023
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterator
diff --git a/data_structures/binary_tree/is_sorted.py b/data_structures/binary_tree/is_sorted.py
index 5876c5a9c96a..91fc8ca82633 100644
--- a/data_structures/binary_tree/is_sorted.py
+++ b/data_structures/binary_tree/is_sorted.py
@@ -13,6 +13,7 @@
 Runtime: O(n)
 Space: O(1)
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterator
@@ -79,9 +80,9 @@ def is_sorted(self) -> bool:
         """
         if self.left and (self.data < self.left.data or not self.left.is_sorted):
             return False
-        if self.right and (self.data > self.right.data or not self.right.is_sorted):
-            return False
-        return True
+        return not (
+            self.right and (self.data > self.right.data or not self.right.is_sorted)
+        )
 
 
 if __name__ == "__main__":
diff --git a/data_structures/binary_tree/is_sum_tree.py b/data_structures/binary_tree/is_sum_tree.py
index 3f9cf1d560a6..846bea0fe0f2 100644
--- a/data_structures/binary_tree/is_sum_tree.py
+++ b/data_structures/binary_tree/is_sum_tree.py
@@ -3,6 +3,7 @@
 of the values of its left and right subtrees?
 https://www.geeksforgeeks.org/check-if-a-given-binary-tree-is-sumtree
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterator
diff --git a/data_structures/binary_tree/lowest_common_ancestor.py b/data_structures/binary_tree/lowest_common_ancestor.py
index 651037703b95..ea0e31256903 100644
--- a/data_structures/binary_tree/lowest_common_ancestor.py
+++ b/data_structures/binary_tree/lowest_common_ancestor.py
@@ -15,6 +15,8 @@ def swap(a: int, b: int) -> tuple[int, int]:
     (4, 3)
     >>> swap(67, 12)
     (12, 67)
+    >>> swap(3,-4)
+    (-4, 3)
     """
     a ^= b
     b ^= a
@@ -25,6 +27,23 @@ def swap(a: int, b: int) -> tuple[int, int]:
 def create_sparse(max_node: int, parent: list[list[int]]) -> list[list[int]]:
     """
     creating sparse table which saves each nodes 2^i-th parent
+    >>> max_node = 6
+    >>> parent = [[0, 0, 1, 1, 2, 2, 3]] + [[0] * 7 for _ in range(19)]
+    >>> parent = create_sparse(max_node=max_node, parent=parent)
+    >>> parent[0]
+    [0, 0, 1, 1, 2, 2, 3]
+    >>> parent[1]
+    [0, 0, 0, 0, 1, 1, 1]
+    >>> parent[2]
+    [0, 0, 0, 0, 0, 0, 0]
+
+    >>> max_node = 1
+    >>> parent = [[0, 0]] + [[0] * 2 for _ in range(19)]
+    >>> parent = create_sparse(max_node=max_node, parent=parent)
+    >>> parent[0]
+    [0, 0]
+    >>> parent[1]
+    [0, 0]
     """
     j = 1
     while (1 << j) < max_node:
@@ -38,6 +57,21 @@ def create_sparse(max_node: int, parent: list[list[int]]) -> list[list[int]]:
 def lowest_common_ancestor(
     u: int, v: int, level: list[int], parent: list[list[int]]
 ) -> int:
+    """
+    Return the lowest common ancestor between u and v
+
+    >>> level = [-1, 0, 1, 1, 2, 2, 2]
+    >>> parent = [[0, 0, 1, 1, 2, 2, 3],[0, 0, 0, 0, 1, 1, 1]] + \
+                    [[0] * 7 for _ in range(17)]
+    >>> lowest_common_ancestor(u=4, v=5, level=level, parent=parent)
+    2
+    >>> lowest_common_ancestor(u=4, v=6, level=level, parent=parent)
+    1
+    >>> lowest_common_ancestor(u=2, v=3, level=level, parent=parent)
+    1
+    >>> lowest_common_ancestor(u=6, v=6, level=level, parent=parent)
+    6
+    """
     # u must be deeper in the tree than v
     if level[u] < level[v]:
         u, v = swap(u, v)
@@ -68,6 +102,26 @@ def breadth_first_search(
     sets every nodes direct parent
     parent of root node is set to 0
     calculates depth of each node from root node
+    >>> level = [-1] * 7
+    >>> parent = [[0] * 7 for _ in range(20)]
+    >>> graph = {1: [2, 3], 2: [4, 5], 3: [6], 4: [], 5: [], 6: []}
+    >>> level, parent = breadth_first_search(
+    ...     level=level, parent=parent, max_node=6, graph=graph, root=1)
+    >>> level
+    [-1, 0, 1, 1, 2, 2, 2]
+    >>> parent[0]
+    [0, 0, 1, 1, 2, 2, 3]
+
+
+    >>> level = [-1] * 2
+    >>> parent = [[0] * 2 for _ in range(20)]
+    >>> graph = {1: []}
+    >>> level, parent = breadth_first_search(
+    ...     level=level, parent=parent, max_node=1, graph=graph, root=1)
+    >>> level
+    [-1, 0]
+    >>> parent[0]
+    [0, 0]
     """
     level[root] = 0
     q: Queue[int] = Queue(maxsize=max_node)
diff --git a/data_structures/binary_tree/maximum_sum_bst.py b/data_structures/binary_tree/maximum_sum_bst.py
new file mode 100644
index 000000000000..7dadc7b95920
--- /dev/null
+++ b/data_structures/binary_tree/maximum_sum_bst.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import sys
+from dataclasses import dataclass
+
+INT_MIN = -sys.maxsize + 1
+INT_MAX = sys.maxsize - 1
+
+
+@dataclass
+class TreeNode:
+    val: int = 0
+    left: TreeNode | None = None
+    right: TreeNode | None = None
+
+
+def max_sum_bst(root: TreeNode | None) -> int:
+    """
+    The solution traverses a binary tree to find the maximum sum of
+    keys in any subtree that is a Binary Search Tree (BST). It uses
+    recursion to validate BST properties and calculates sums, returning
+    the highest sum found among all valid BST subtrees.
+
+    >>> t1 = TreeNode(4)
+    >>> t1.left = TreeNode(3)
+    >>> t1.left.left = TreeNode(1)
+    >>> t1.left.right = TreeNode(2)
+    >>> print(max_sum_bst(t1))
+    2
+    >>> t2 = TreeNode(-4)
+    >>> t2.left = TreeNode(-2)
+    >>> t2.right = TreeNode(-5)
+    >>> print(max_sum_bst(t2))
+    0
+    >>> t3 = TreeNode(1)
+    >>> t3.left = TreeNode(4)
+    >>> t3.left.left = TreeNode(2)
+    >>> t3.left.right = TreeNode(4)
+    >>> t3.right = TreeNode(3)
+    >>> t3.right.left = TreeNode(2)
+    >>> t3.right.right = TreeNode(5)
+    >>> t3.right.right.left = TreeNode(4)
+    >>> t3.right.right.right = TreeNode(6)
+    >>> print(max_sum_bst(t3))
+    20
+    """
+    ans: int = 0
+
+    def solver(node: TreeNode | None) -> tuple[bool, int, int, int]:
+        """
+        Returns the maximum sum by making recursive calls
+        >>> t1 = TreeNode(1)
+        >>> print(solver(t1))
+        1
+        """
+        nonlocal ans
+
+        if not node:
+            return True, INT_MAX, INT_MIN, 0  # Valid BST, min, max, sum
+
+        is_left_valid, min_left, max_left, sum_left = solver(node.left)
+        is_right_valid, min_right, max_right, sum_right = solver(node.right)
+
+        if is_left_valid and is_right_valid and max_left < node.val < min_right:
+            total_sum = sum_left + sum_right + node.val
+            ans = max(ans, total_sum)
+            return True, min(min_left, node.val), max(max_right, node.val), total_sum
+
+        return False, -1, -1, -1  # Not a valid BST
+
+    solver(root)
+    return ans
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/binary_tree/merge_two_binary_trees.py b/data_structures/binary_tree/merge_two_binary_trees.py
index 3380f8c5fb31..6bbb30428704 100644
--- a/data_structures/binary_tree/merge_two_binary_trees.py
+++ b/data_structures/binary_tree/merge_two_binary_trees.py
@@ -5,6 +5,7 @@
 both nodes to the new value of the merged node. Otherwise, the NOT null node
 will be used as the node of new tree.
 """
+
 from __future__ import annotations
 
 
diff --git a/data_structures/binary_tree/mirror_binary_tree.py b/data_structures/binary_tree/mirror_binary_tree.py
new file mode 100644
index 000000000000..f6611d66d676
--- /dev/null
+++ b/data_structures/binary_tree/mirror_binary_tree.py
@@ -0,0 +1,160 @@
+"""
+Given the root of a binary tree, mirror the tree, and return its root.
+
+Leetcode problem reference: https://leetcode.com/problems/mirror-binary-tree/
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterator
+from dataclasses import dataclass
+
+
+@dataclass
+class Node:
+    """
+    A Node has value variable and pointers to Nodes to its left and right.
+    """
+
+    value: int
+    left: Node | None = None
+    right: Node | None = None
+
+    def __iter__(self) -> Iterator[int]:
+        if self.left:
+            yield from self.left
+        yield self.value
+        if self.right:
+            yield from self.right
+
+    def __len__(self) -> int:
+        return sum(1 for _ in self)
+
+    def mirror(self) -> Node:
+        """
+        Mirror the binary tree rooted at this node by swapping left and right children.
+
+        >>> tree = Node(0)
+        >>> list(tree)
+        [0]
+        >>> list(tree.mirror())
+        [0]
+        >>> tree = Node(1, Node(0), Node(3, Node(2), Node(4, None, Node(5))))
+        >>> tuple(tree)
+        (0, 1, 2, 3, 4, 5)
+        >>> tuple(tree.mirror())
+        (5, 4, 3, 2, 1, 0)
+        """
+        self.left, self.right = self.right, self.left
+        if self.left:
+            self.left.mirror()
+        if self.right:
+            self.right.mirror()
+        return self
+
+
+def make_tree_seven() -> Node:
+    r"""
+    Return a binary tree with 7 nodes that looks like this:
+    ::
+
+           1
+         /   \
+        2     3
+       / \   / \
+      4   5 6   7
+
+    >>> tree_seven = make_tree_seven()
+    >>> len(tree_seven)
+    7
+    >>> list(tree_seven)
+    [4, 2, 5, 1, 6, 3, 7]
+    """
+    tree = Node(1)
+    tree.left = Node(2)
+    tree.right = Node(3)
+    tree.left.left = Node(4)
+    tree.left.right = Node(5)
+    tree.right.left = Node(6)
+    tree.right.right = Node(7)
+    return tree
+
+
+def make_tree_nine() -> Node:
+    r"""
+    Return a binary tree with 9 nodes that looks like this:
+    ::
+
+            1
+           / \
+          2   3
+         / \   \
+        4   5   6
+       / \   \
+      7   8   9
+
+    >>> tree_nine = make_tree_nine()
+    >>> len(tree_nine)
+    9
+    >>> list(tree_nine)
+    [7, 4, 8, 2, 5, 9, 1, 3, 6]
+    """
+    tree = Node(1)
+    tree.left = Node(2)
+    tree.right = Node(3)
+    tree.left.left = Node(4)
+    tree.left.right = Node(5)
+    tree.right.right = Node(6)
+    tree.left.left.left = Node(7)
+    tree.left.left.right = Node(8)
+    tree.left.right.right = Node(9)
+    return tree
+
+
+def main() -> None:
+    r"""
+    Mirror binary trees with the given root and returns the root
+
+    >>> tree = make_tree_nine()
+    >>> tuple(tree)
+    (7, 4, 8, 2, 5, 9, 1, 3, 6)
+    >>> tuple(tree.mirror())
+    (6, 3, 1, 9, 5, 2, 8, 4, 7)
+
+    nine_tree::
+
+            1
+           / \
+          2   3
+         / \   \
+        4   5   6
+       / \   \
+      7   8   9
+
+    The mirrored tree looks like this::
+
+          1
+         / \
+        3   2
+       /   / \
+      6   5   4
+         /   / \
+        9   8   7
+    """
+    trees = {"zero": Node(0), "seven": make_tree_seven(), "nine": make_tree_nine()}
+    for name, tree in trees.items():
+        print(f"      The {name} tree: {tuple(tree)}")
+        # (0,)
+        # (4, 2, 5, 1, 6, 3, 7)
+        # (7, 4, 8, 2, 5, 9, 1, 3, 6)
+        print(f"Mirror of {name} tree: {tuple(tree.mirror())}")
+        # (0,)
+        # (7, 3, 6, 1, 5, 2, 4)
+        # (6, 3, 1, 9, 5, 2, 8, 4, 7)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+    main()
diff --git a/data_structures/binary_tree/non_recursive_segment_tree.py b/data_structures/binary_tree/non_recursive_segment_tree.py
index 04164e5cba4e..7d1c965fab50 100644
--- a/data_structures/binary_tree/non_recursive_segment_tree.py
+++ b/data_structures/binary_tree/non_recursive_segment_tree.py
@@ -35,15 +35,16 @@
 >>> st.query(0, 2)
 [1, 2, 3]
 """
+
 from __future__ import annotations
 
 from collections.abc import Callable
-from typing import Any, Generic, TypeVar
+from typing import Any, TypeVar
 
 T = TypeVar("T")
 
 
-class SegmentTree(Generic[T]):
+class SegmentTree[T]:
     def __init__(self, arr: list[T], fnc: Callable[[T, T], T]) -> None:
         """
         Segment Tree constructor, it works just with commutative combiner.
@@ -86,12 +87,12 @@ def update(self, p: int, v: T) -> None:
             p = p // 2
             self.st[p] = self.fn(self.st[p * 2], self.st[p * 2 + 1])
 
-    def query(self, l: int, r: int) -> T | None:  # noqa: E741
+    def query(self, left: int, right: int) -> T | None:
         """
         Get range query value in log(N) time
-        :param l: left element index
-        :param r: right element index
-        :return: element combined in the range [l, r]
+        :param left: left element index
+        :param right: right element index
+        :return: element combined in the range [left, right]
 
         >>> st = SegmentTree([1, 2, 3, 4], lambda a, b: a + b)
         >>> st.query(0, 2)
@@ -103,15 +104,15 @@ def query(self, l: int, r: int) -> T | None:  # noqa: E741
         >>> st.query(2, 3)
         7
         """
-        l, r = l + self.N, r + self.N
+        left, right = left + self.N, right + self.N
 
         res: T | None = None
-        while l <= r:
-            if l % 2 == 1:
-                res = self.st[l] if res is None else self.fn(res, self.st[l])
-            if r % 2 == 0:
-                res = self.st[r] if res is None else self.fn(res, self.st[r])
-            l, r = (l + 1) // 2, (r - 1) // 2
+        while left <= right:
+            if left % 2 == 1:
+                res = self.st[left] if res is None else self.fn(res, self.st[left])
+            if right % 2 == 0:
+                res = self.st[right] if res is None else self.fn(res, self.st[right])
+            left, right = (left + 1) // 2, (right - 1) // 2
         return res
 
 
diff --git a/data_structures/binary_tree/number_of_possible_binary_trees.py b/data_structures/binary_tree/number_of_possible_binary_trees.py
index 684c518b1eb6..b39cbafd0a61 100644
--- a/data_structures/binary_tree/number_of_possible_binary_trees.py
+++ b/data_structures/binary_tree/number_of_possible_binary_trees.py
@@ -6,6 +6,7 @@
 
 Further details at Wikipedia: https://en.wikipedia.org/wiki/Catalan_number
 """
+
 """
 Our Contribution:
 Basically we Create the 2 function:
@@ -30,8 +31,7 @@ def binomial_coefficient(n: int, k: int) -> int:
     """
     result = 1  # To kept the Calculated Value
     # Since C(n, k) = C(n, n-k)
-    if k > (n - k):
-        k = n - k
+    k = min(k, n - k)
     # Calculate C(n,k)
     for i in range(k):
         result *= n - i
diff --git a/data_structures/binary_tree/red_black_tree.py b/data_structures/binary_tree/red_black_tree.py
index 4ebe0e927ca0..752db1e7026c 100644
--- a/data_structures/binary_tree/red_black_tree.py
+++ b/data_structures/binary_tree/red_black_tree.py
@@ -1,7 +1,3 @@
-"""
-psf/black : true
-ruff : passed
-"""
 from __future__ import annotations
 
 from collections.abc import Iterator
@@ -16,7 +12,7 @@ class RedBlackTree:
     and slower for reading in the average case, though, because they're
     both balanced binary search trees, both will get the same asymptotic
     performance.
-    To read more about them, https://en.wikipedia.org/wiki/Red–black_tree
+    To read more about them, https://en.wikipedia.org/wiki/Red-black_tree
     Unless otherwise specified, all asymptotic runtimes are specified in
     terms of the size of the tree.
     """
@@ -106,12 +102,11 @@ def insert(self, label: int) -> RedBlackTree:
             else:
                 self.left = RedBlackTree(label, 1, self)
                 self.left._insert_repair()
+        elif self.right:
+            self.right.insert(label)
         else:
-            if self.right:
-                self.right.insert(label)
-            else:
-                self.right = RedBlackTree(label, 1, self)
-                self.right._insert_repair()
+            self.right = RedBlackTree(label, 1, self)
+            self.right._insert_repair()
         return self.parent or self
 
     def _insert_repair(self) -> None:
@@ -152,7 +147,7 @@ def _insert_repair(self) -> None:
                     self.grandparent.color = 1
                     self.grandparent._insert_repair()
 
-    def remove(self, label: int) -> RedBlackTree:  # noqa: PLR0912
+    def remove(self, label: int) -> RedBlackTree:
         """Remove label from this tree."""
         if self.label == label:
             if self.left and self.right:
@@ -177,36 +172,34 @@ def remove(self, label: int) -> RedBlackTree:  # noqa: PLR0912
                             self.parent.left = None
                         else:
                             self.parent.right = None
-                else:
-                    # The node is black
-                    if child is None:
-                        # This node and its child are black
-                        if self.parent is None:
-                            # The tree is now empty
-                            return RedBlackTree(None)
-                        else:
-                            self._remove_repair()
-                            if self.is_left():
-                                self.parent.left = None
-                            else:
-                                self.parent.right = None
-                            self.parent = None
+                # The node is black
+                elif child is None:
+                    # This node and its child are black
+                    if self.parent is None:
+                        # The tree is now empty
+                        return RedBlackTree(None)
                     else:
-                        # This node is black and its child is red
-                        # Move the child node here and make it black
-                        self.label = child.label
-                        self.left = child.left
-                        self.right = child.right
-                        if self.left:
-                            self.left.parent = self
-                        if self.right:
-                            self.right.parent = self
+                        self._remove_repair()
+                        if self.is_left():
+                            self.parent.left = None
+                        else:
+                            self.parent.right = None
+                        self.parent = None
+                else:
+                    # This node is black and its child is red
+                    # Move the child node here and make it black
+                    self.label = child.label
+                    self.left = child.left
+                    self.right = child.right
+                    if self.left:
+                        self.left.parent = self
+                    if self.right:
+                        self.right.parent = self
         elif self.label is not None and self.label > label:
             if self.left:
                 self.left.remove(label)
-        else:
-            if self.right:
-                self.right.remove(label)
+        elif self.right:
+            self.right.remove(label)
         return self.parent or self
 
     def _remove_repair(self) -> None:
@@ -323,9 +316,7 @@ def check_coloring(self) -> bool:
             return False
         if self.left and not self.left.check_coloring():
             return False
-        if self.right and not self.right.check_coloring():
-            return False
-        return True
+        return not (self.right and not self.right.check_coloring())
 
     def black_height(self) -> int | None:
         """Returns the number of black nodes from this node to the
@@ -368,11 +359,10 @@ def search(self, label: int) -> RedBlackTree | None:
                 return None
             else:
                 return self.right.search(label)
+        elif self.left is None:
+            return None
         else:
-            if self.left is None:
-                return None
-            else:
-                return self.left.search(label)
+            return self.left.search(label)
 
     def floor(self, label: int) -> int | None:
         """Returns the largest element in this tree which is at most label.
@@ -451,7 +441,7 @@ def is_left(self) -> bool:
         """Returns true iff this node is the left child of its parent."""
         if self.parent is None:
             return False
-        return self.parent.left is self.parent.left is self
+        return self.parent.left is self
 
     def is_right(self) -> bool:
         """Returns true iff this node is the right child of its parent."""
@@ -564,9 +554,7 @@ def test_rotations() -> bool:
     right_rot.right.right = RedBlackTree(10, parent=right_rot.right)
     right_rot.right.right.left = RedBlackTree(5, parent=right_rot.right.right)
     right_rot.right.right.right = RedBlackTree(20, parent=right_rot.right.right)
-    if tree != right_rot:
-        return False
-    return True
+    return tree == right_rot
 
 
 def test_insertion_speed() -> bool:
@@ -609,13 +597,11 @@ def test_insert_and_search() -> bool:
     tree.insert(12)
     tree.insert(10)
     tree.insert(11)
-    if 5 in tree or -6 in tree or -10 in tree or 13 in tree:
+    if any(i in tree for i in (5, -6, -10, 13)):
         # Found something not in there
         return False
-    if not (11 in tree and 12 in tree and -8 in tree and 0 in tree):
-        # Didn't find something in there
-        return False
-    return True
+    # Find all these things in there
+    return all(i in tree for i in (11, 12, -8, 0))
 
 
 def test_insert_delete() -> bool:
@@ -637,9 +623,7 @@ def test_insert_delete() -> bool:
     tree = tree.remove(9)
     if not tree.check_color_properties():
         return False
-    if list(tree.inorder_traverse()) != [-8, 0, 4, 8, 10, 11, 12]:
-        return False
-    return True
+    return list(tree.inorder_traverse()) == [-8, 0, 4, 8, 10, 11, 12]
 
 
 def test_floor_ceil() -> bool:
@@ -667,9 +651,7 @@ def test_min_max() -> bool:
     tree.insert(24)
     tree.insert(20)
     tree.insert(22)
-    if tree.get_max() != 22 or tree.get_min() != -16:
-        return False
-    return True
+    return not (tree.get_max() != 22 or tree.get_min() != -16)
 
 
 def test_tree_traversal() -> bool:
@@ -685,9 +667,7 @@ def test_tree_traversal() -> bool:
         return False
     if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]:
         return False
-    if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]:
-        return False
-    return True
+    return list(tree.postorder_traverse()) == [-16, 8, 20, 24, 22, 16, 0]
 
 
 def test_tree_chaining() -> bool:
@@ -698,9 +678,7 @@ def test_tree_chaining() -> bool:
         return False
     if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]:
         return False
-    if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]:
-        return False
-    return True
+    return list(tree.postorder_traverse()) == [-16, 8, 20, 24, 22, 16, 0]
 
 
 def print_results(msg: str, passes: bool) -> None:
diff --git a/data_structures/binary_tree/segment_tree.py b/data_structures/binary_tree/segment_tree.py
index 3b0b32946f6e..084fcf84955d 100644
--- a/data_structures/binary_tree/segment_tree.py
+++ b/data_structures/binary_tree/segment_tree.py
@@ -35,13 +35,13 @@ def right(self, idx):
         """
         return idx * 2 + 1
 
-    def build(self, idx, l, r):  # noqa: E741
-        if l == r:
-            self.st[idx] = self.A[l]
+    def build(self, idx, left, right):
+        if left == right:
+            self.st[idx] = self.A[left]
         else:
-            mid = (l + r) // 2
-            self.build(self.left(idx), l, mid)
-            self.build(self.right(idx), mid + 1, r)
+            mid = (left + right) // 2
+            self.build(self.left(idx), left, mid)
+            self.build(self.right(idx), mid + 1, right)
             self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)])
 
     def update(self, a, b, val):
@@ -56,18 +56,18 @@ def update(self, a, b, val):
         """
         return self.update_recursive(1, 0, self.N - 1, a - 1, b - 1, val)
 
-    def update_recursive(self, idx, l, r, a, b, val):  # noqa: E741
+    def update_recursive(self, idx, left, right, a, b, val):
         """
         update(1, 1, N, a, b, v) for update val v to [a,b]
         """
-        if r < a or l > b:
+        if right < a or left > b:
             return True
-        if l == r:
+        if left == right:
             self.st[idx] = val
             return True
-        mid = (l + r) // 2
-        self.update_recursive(self.left(idx), l, mid, a, b, val)
-        self.update_recursive(self.right(idx), mid + 1, r, a, b, val)
+        mid = (left + right) // 2
+        self.update_recursive(self.left(idx), left, mid, a, b, val)
+        self.update_recursive(self.right(idx), mid + 1, right, a, b, val)
         self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)])
         return True
 
@@ -83,22 +83,22 @@ def query(self, a, b):
         """
         return self.query_recursive(1, 0, self.N - 1, a - 1, b - 1)
 
-    def query_recursive(self, idx, l, r, a, b):  # noqa: E741
+    def query_recursive(self, idx, left, right, a, b):
         """
         query(1, 1, N, a, b) for query max of [a,b]
         """
-        if r < a or l > b:
+        if right < a or left > b:
             return -math.inf
-        if l >= a and r <= b:
+        if left >= a and right <= b:
             return self.st[idx]
-        mid = (l + r) // 2
-        q1 = self.query_recursive(self.left(idx), l, mid, a, b)
-        q2 = self.query_recursive(self.right(idx), mid + 1, r, a, b)
+        mid = (left + right) // 2
+        q1 = self.query_recursive(self.left(idx), left, mid, a, b)
+        q2 = self.query_recursive(self.right(idx), mid + 1, right, a, b)
         return max(q1, q2)
 
     def show_data(self):
         show_list = []
-        for i in range(1, N + 1):
+        for i in range(1, self.N + 1):
             show_list += [self.query(i, i)]
         print(show_list)
 
diff --git a/data_structures/binary_tree/segment_tree_other.py b/data_structures/binary_tree/segment_tree_other.py
index cc77c4951f1a..95f21ddd4777 100644
--- a/data_structures/binary_tree/segment_tree_other.py
+++ b/data_structures/binary_tree/segment_tree_other.py
@@ -3,6 +3,7 @@
 allowing queries to be done later in log(N) time
 function takes 2 values and returns a same type value
 """
+
 from collections.abc import Sequence
 from queue import Queue
 
diff --git a/data_structures/binary_tree/serialize_deserialize_binary_tree.py b/data_structures/binary_tree/serialize_deserialize_binary_tree.py
new file mode 100644
index 000000000000..7d3e0c61f96d
--- /dev/null
+++ b/data_structures/binary_tree/serialize_deserialize_binary_tree.py
@@ -0,0 +1,140 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+from dataclasses import dataclass
+
+
+@dataclass
+class TreeNode:
+    """
+    A binary tree node has a value, left child, and right child.
+
+    Props:
+        value: The value of the node.
+        left: The left child of the node.
+        right: The right child of the node.
+    """
+
+    value: int = 0
+    left: TreeNode | None = None
+    right: TreeNode | None = None
+
+    def __post_init__(self):
+        if not isinstance(self.value, int):
+            raise TypeError("Value must be an integer.")
+
+    def __iter__(self) -> Iterator[TreeNode]:
+        """
+        Iterate through the tree in preorder.
+
+        Returns:
+            An iterator of the tree nodes.
+
+        >>> list(TreeNode(1))
+        [1,null,null]
+        >>> tuple(TreeNode(1, TreeNode(2), TreeNode(3)))
+        (1,2,null,null,3,null,null, 2,null,null, 3,null,null)
+        """
+        yield self
+        yield from self.left or ()
+        yield from self.right or ()
+
+    def __len__(self) -> int:
+        """
+        Count the number of nodes in the tree.
+
+        Returns:
+            The number of nodes in the tree.
+
+        >>> len(TreeNode(1))
+        1
+        >>> len(TreeNode(1, TreeNode(2), TreeNode(3)))
+        3
+        """
+        return sum(1 for _ in self)
+
+    def __repr__(self) -> str:
+        """
+        Represent the tree as a string.
+
+        Returns:
+            A string representation of the tree.
+
+        >>> repr(TreeNode(1))
+        '1,null,null'
+        >>> repr(TreeNode(1, TreeNode(2), TreeNode(3)))
+        '1,2,null,null,3,null,null'
+        >>> repr(TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5))))
+        '1,2,null,null,3,4,null,null,5,null,null'
+        """
+        return f"{self.value},{self.left!r},{self.right!r}".replace("None", "null")
+
+    @classmethod
+    def five_tree(cls) -> TreeNode:
+        """
+        >>> repr(TreeNode.five_tree())
+        '1,2,null,null,3,4,null,null,5,null,null'
+        """
+        root = TreeNode(1)
+        root.left = TreeNode(2)
+        root.right = TreeNode(3)
+        root.right.left = TreeNode(4)
+        root.right.right = TreeNode(5)
+        return root
+
+
+def deserialize(data: str) -> TreeNode | None:
+    """
+    Deserialize a string to a binary tree.
+
+    Args:
+        data(str): The serialized string.
+
+    Returns:
+        The root of the binary tree.
+
+    >>> root = TreeNode.five_tree()
+    >>> serialzed_data = repr(root)
+    >>> deserialized = deserialize(serialzed_data)
+    >>> root == deserialized
+    True
+    >>> root is deserialized  # two separate trees
+    False
+    >>> root.right.right.value = 6
+    >>> root == deserialized
+    False
+    >>> serialzed_data = repr(root)
+    >>> deserialized = deserialize(serialzed_data)
+    >>> root == deserialized
+    True
+    >>> deserialize("")
+    Traceback (most recent call last):
+        ...
+    ValueError: Data cannot be empty.
+    """
+
+    if not data:
+        raise ValueError("Data cannot be empty.")
+
+    # Split the serialized string by a comma to get node values
+    nodes = data.split(",")
+
+    def build_tree() -> TreeNode | None:
+        # Get the next value from the list
+        value = nodes.pop(0)
+
+        if value == "null":
+            return None
+
+        node = TreeNode(int(value))
+        node.left = build_tree()  # Recursively build left subtree
+        node.right = build_tree()  # Recursively build right subtree
+        return node
+
+    return build_tree()
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/binary_tree/symmetric_tree.py b/data_structures/binary_tree/symmetric_tree.py
index 331a25849c1c..2bfeac98b2c9 100644
--- a/data_structures/binary_tree/symmetric_tree.py
+++ b/data_structures/binary_tree/symmetric_tree.py
@@ -4,6 +4,7 @@
 
 Leetcode reference: https://leetcode.com/problems/symmetric-tree/
 """
+
 from __future__ import annotations
 
 from dataclasses import dataclass
@@ -12,7 +13,21 @@
 @dataclass
 class Node:
     """
-    A Node has data variable and pointers to Nodes to its left and right.
+    A Node represents an element of a binary tree, which contains:
+
+    Attributes:
+    data: The value stored in the node (int).
+    left: Pointer to the left child node (Node or None).
+    right: Pointer to the right child node (Node or None).
+
+    Example:
+    >>> node = Node(1, Node(2), Node(3))
+    >>> node.data
+    1
+    >>> node.left.data
+    2
+    >>> node.right.data
+    3
     """
 
     data: int
@@ -23,12 +38,25 @@ class Node:
 def make_symmetric_tree() -> Node:
     r"""
     Create a symmetric tree for testing.
+
     The tree looks like this:
            1
          /   \
         2     2
       / \    / \
      3   4   4  3
+
+    Returns:
+    Node: Root node of a symmetric tree.
+
+    Example:
+    >>> tree = make_symmetric_tree()
+    >>> tree.data
+    1
+    >>> tree.left.data == tree.right.data
+    True
+    >>> tree.left.left.data == tree.right.right.data
+    True
     """
     root = Node(1)
     root.left = Node(2)
@@ -42,13 +70,26 @@ def make_symmetric_tree() -> Node:
 
 def make_asymmetric_tree() -> Node:
     r"""
-    Create a asymmetric tree for testing.
+    Create an asymmetric tree for testing.
+
     The tree looks like this:
            1
          /   \
         2     2
       / \    / \
      3   4   3  4
+
+    Returns:
+    Node: Root node of an asymmetric tree.
+
+    Example:
+    >>> tree = make_asymmetric_tree()
+    >>> tree.data
+    1
+    >>> tree.left.data == tree.right.data
+    True
+    >>> tree.left.left.data == tree.right.right.data
+    False
     """
     root = Node(1)
     root.left = Node(2)
@@ -62,7 +103,15 @@ def make_asymmetric_tree() -> Node:
 
 def is_symmetric_tree(tree: Node) -> bool:
     """
-    Test cases for is_symmetric_tree function
+    Check if a binary tree is symmetric (i.e., a mirror of itself).
+
+    Parameters:
+    tree: The root node of the binary tree.
+
+    Returns:
+    bool: True if the tree is symmetric, False otherwise.
+
+    Example:
     >>> is_symmetric_tree(make_symmetric_tree())
     True
     >>> is_symmetric_tree(make_asymmetric_tree())
@@ -75,8 +124,17 @@ def is_symmetric_tree(tree: Node) -> bool:
 
 def is_mirror(left: Node | None, right: Node | None) -> bool:
     """
+    Check if two subtrees are mirror images of each other.
+
+    Parameters:
+    left: The root node of the left subtree.
+    right: The root node of the right subtree.
+
+    Returns:
+    bool: True if the two subtrees are mirrors of each other, False otherwise.
+
+    Example:
     >>> tree1 = make_symmetric_tree()
-    >>> tree1.right.right = Node(3)
     >>> is_mirror(tree1.left, tree1.right)
     True
     >>> tree2 = make_asymmetric_tree()
@@ -90,7 +148,7 @@ def is_mirror(left: Node | None, right: Node | None) -> bool:
         # One side is empty while the other is not, which is not symmetric.
         return False
     if left.data == right.data:
-        # The values match, so check the subtree
+        # The values match, so check the subtrees recursively.
         return is_mirror(left.left, right.right) and is_mirror(left.right, right.left)
     return False
 
diff --git a/data_structures/binary_tree/treap.py b/data_structures/binary_tree/treap.py
index a53ac566ed54..3114c6fa1c26 100644
--- a/data_structures/binary_tree/treap.py
+++ b/data_structures/binary_tree/treap.py
@@ -39,26 +39,23 @@ def split(root: Node | None, value: int) -> tuple[Node | None, Node | None]:
     Left tree contains all values less than split value.
     Right tree contains all values greater or equal, than split value
     """
-    if root is None:  # None tree is split into 2 Nones
-        return None, None
-    elif root.value is None:
+    if root is None or root.value is None:  # None tree is split into 2 Nones
         return None, None
+    elif value < root.value:
+        """
+        Right tree's root will be current node.
+        Now we split(with the same value) current node's left son
+        Left tree: left part of that split
+        Right tree's left son: right part of that split
+        """
+        left, root.left = split(root.left, value)
+        return left, root
     else:
-        if value < root.value:
-            """
-            Right tree's root will be current node.
-            Now we split(with the same value) current node's left son
-            Left tree: left part of that split
-            Right tree's left son: right part of that split
-            """
-            left, root.left = split(root.left, value)
-            return left, root
-        else:
-            """
-            Just symmetric to previous case
-            """
-            root.right, right = split(root.right, value)
-            return root, right
+        """
+        Just symmetric to previous case
+        """
+        root.right, right = split(root.right, value)
+        return root, right
 
 
 def merge(left: Node | None, right: Node | None) -> Node | None:
diff --git a/data_structures/binary_tree/wavelet_tree.py b/data_structures/binary_tree/wavelet_tree.py
index 041e140f5b15..2da571e8d326 100644
--- a/data_structures/binary_tree/wavelet_tree.py
+++ b/data_structures/binary_tree/wavelet_tree.py
@@ -7,6 +7,7 @@
 2. https://www.youtube.com/watch?v=4aSv9PcecDw&t=811s
 3. https://www.youtube.com/watch?v=CybAgVF-MMc&t=1178s
 """
+
 from __future__ import annotations
 
 test_array = [2, 1, 4, 5, 6, 0, 8, 9, 1, 2, 0, 6, 4, 2, 0, 6, 5, 3, 2, 7]
diff --git a/data_structures/disjoint_set/disjoint_set.py b/data_structures/disjoint_set/disjoint_set.py
index 12dafb2d935e..edc4736b6132 100644
--- a/data_structures/disjoint_set/disjoint_set.py
+++ b/data_structures/disjoint_set/disjoint_set.py
@@ -1,6 +1,6 @@
 """
-    Disjoint set.
-    Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
+Disjoint set.
+Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
 """
 
 
diff --git a/data_structures/hashing/bloom_filter.py b/data_structures/hashing/bloom_filter.py
index 7fd0985bdc33..eb2cb4b79c46 100644
--- a/data_structures/hashing/bloom_filter.py
+++ b/data_structures/hashing/bloom_filter.py
@@ -58,6 +58,7 @@
 >>> bloom.bitstring
 '01100101'
 """
+
 from hashlib import md5, sha256
 
 HASH_FUNCTIONS = (sha256, md5)
diff --git a/data_structures/hashing/double_hash.py b/data_structures/hashing/double_hash.py
index be21e74cadd0..324282cbfd8d 100644
--- a/data_structures/hashing/double_hash.py
+++ b/data_structures/hashing/double_hash.py
@@ -11,6 +11,7 @@
 
 Reference: https://en.wikipedia.org/wiki/Double_hashing
 """
+
 from .hash_table import HashTable
 from .number_theory.prime_numbers import is_prime, next_prime
 
@@ -35,6 +36,33 @@ def __hash_double_function(self, key, data, increment):
         return (increment * self.__hash_function_2(key, data)) % self.size_table
 
     def _collision_resolution(self, key, data=None):
+        """
+        Examples:
+
+        1. Try to add three data elements when the size is three
+        >>> dh = DoubleHash(3)
+        >>> dh.insert_data(10)
+        >>> dh.insert_data(20)
+        >>> dh.insert_data(30)
+        >>> dh.keys()
+        {1: 10, 2: 20, 0: 30}
+
+        2. Try to add three data elements when the size is two
+        >>> dh = DoubleHash(2)
+        >>> dh.insert_data(10)
+        >>> dh.insert_data(20)
+        >>> dh.insert_data(30)
+        >>> dh.keys()
+        {10: 10, 9: 20, 8: 30}
+
+        3. Try to add three data elements when the size is four
+        >>> dh = DoubleHash(4)
+        >>> dh.insert_data(10)
+        >>> dh.insert_data(20)
+        >>> dh.insert_data(30)
+        >>> dh.keys()
+        {9: 20, 10: 10, 8: 30}
+        """
         i = 1
         new_key = self.hash_function(data)
 
@@ -50,3 +78,9 @@ def _collision_resolution(self, key, data=None):
                 i += 1
 
         return new_key
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/hashing/hash_map.py b/data_structures/hashing/hash_map.py
index 1dfcc8bbf906..0d99e578b73e 100644
--- a/data_structures/hashing/hash_map.py
+++ b/data_structures/hashing/hash_map.py
@@ -7,16 +7,17 @@
 Modern Dictionaries by Raymond Hettinger
 https://www.youtube.com/watch?v=p33CVV29OG8
 """
+
 from collections.abc import Iterator, MutableMapping
 from dataclasses import dataclass
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 KEY = TypeVar("KEY")
 VAL = TypeVar("VAL")
 
 
-@dataclass(frozen=True, slots=True)
-class _Item(Generic[KEY, VAL]):
+@dataclass(slots=True)
+class _Item[KEY, VAL]:
     key: KEY
     val: VAL
 
@@ -54,6 +55,14 @@ def _get_next_ind(self, ind: int) -> int:
         Get next index.
 
         Implements linear open addressing.
+        >>> HashMap(5)._get_next_ind(3)
+        4
+        >>> HashMap(5)._get_next_ind(5)
+        1
+        >>> HashMap(5)._get_next_ind(6)
+        2
+        >>> HashMap(5)._get_next_ind(9)
+        0
         """
         return (ind + 1) % len(self._buckets)
 
@@ -63,16 +72,17 @@ def _try_set(self, ind: int, key: KEY, val: VAL) -> bool:
 
         If bucket is empty or key is the same, does insert and return True.
 
-        If bucket has another key or deleted placeholder,
-        that means that we need to check next bucket.
+        If bucket has another key that means that we need to check next bucket.
         """
         stored = self._buckets[ind]
         if not stored:
+            # A falsy item means that bucket was never used (None)
+            # or was deleted (_deleted).
             self._buckets[ind] = _Item(key, val)
             self._len += 1
             return True
         elif stored.key == key:
-            self._buckets[ind] = _Item(key, val)
+            stored.val = val
             return True
         else:
             return False
@@ -82,6 +92,14 @@ def _is_full(self) -> bool:
         Return true if we have reached safe capacity.
 
         So we need to increase the number of buckets to avoid collisions.
+
+        >>> hm = HashMap(2)
+        >>> hm._add_item(1, 10)
+        >>> hm._add_item(2, 20)
+        >>> hm._is_full()
+        True
+        >>> HashMap(2)._is_full()
+        False
         """
         limit = len(self._buckets) * self._capacity_factor
         return len(self) >= int(limit)
@@ -114,17 +132,125 @@ def _iterate_buckets(self, key: KEY) -> Iterator[int]:
             ind = self._get_next_ind(ind)
 
     def _add_item(self, key: KEY, val: VAL) -> None:
+        """
+        Try to add 3 elements when the size is 5
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm._add_item(2, 20)
+        >>> hm._add_item(3, 30)
+        >>> hm
+        HashMap(1: 10, 2: 20, 3: 30)
+
+        Try to add 3 elements when the size is 5
+        >>> hm = HashMap(5)
+        >>> hm._add_item(-5, 10)
+        >>> hm._add_item(6, 30)
+        >>> hm._add_item(-7, 20)
+        >>> hm
+        HashMap(-5: 10, 6: 30, -7: 20)
+
+        Try to add 3 elements when size is 1
+        >>> hm = HashMap(1)
+        >>> hm._add_item(10, 13.2)
+        >>> hm._add_item(6, 5.26)
+        >>> hm._add_item(7, 5.155)
+        >>> hm
+        HashMap(10: 13.2)
+
+        Trying to add an element with a key that is a floating point value
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1.5, 10)
+        >>> hm
+        HashMap(1.5: 10)
+
+        5. Trying to add an item with the same key
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm._add_item(1, 20)
+        >>> hm
+        HashMap(1: 20)
+        """
         for ind in self._iterate_buckets(key):
             if self._try_set(ind, key, val):
                 break
 
     def __setitem__(self, key: KEY, val: VAL) -> None:
+        """
+        1. Changing value of item whose key is present
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm.__setitem__(1, 20)
+        >>> hm
+        HashMap(1: 20)
+
+        2. Changing value of item whose key is not present
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm.__setitem__(0, 20)
+        >>> hm
+        HashMap(0: 20, 1: 10)
+
+        3. Changing the value of the same item multiple times
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm.__setitem__(1, 20)
+        >>> hm.__setitem__(1, 30)
+        >>> hm
+        HashMap(1: 30)
+        """
         if self._is_full():
             self._size_up()
 
         self._add_item(key, val)
 
     def __delitem__(self, key: KEY) -> None:
+        """
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm._add_item(2, 20)
+        >>> hm._add_item(3, 30)
+        >>> hm.__delitem__(3)
+        >>> hm
+        HashMap(1: 10, 2: 20)
+        >>> hm = HashMap(5)
+        >>> hm._add_item(-5, 10)
+        >>> hm._add_item(6, 30)
+        >>> hm._add_item(-7, 20)
+        >>> hm.__delitem__(-5)
+        >>> hm
+        HashMap(6: 30, -7: 20)
+
+        # Trying to remove a non-existing item
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm._add_item(2, 20)
+        >>> hm._add_item(3, 30)
+        >>> hm.__delitem__(4)
+        Traceback (most recent call last):
+        ...
+        KeyError: 4
+
+        # Test resize down when sparse
+        ## Setup: resize up
+        >>> hm = HashMap(initial_block_size=100, capacity_factor=0.75)
+        >>> len(hm._buckets)
+        100
+        >>> for i in range(75):
+        ...     hm[i] = i
+        >>> len(hm._buckets)
+        100
+        >>> hm[75] = 75
+        >>> len(hm._buckets)
+        200
+
+        ## Resize down
+        >>> del hm[75]
+        >>> len(hm._buckets)
+        200
+        >>> del hm[74]
+        >>> len(hm._buckets)
+        100
+        """
         for ind in self._iterate_buckets(key):
             item = self._buckets[ind]
             if item is None:
@@ -139,6 +265,25 @@ def __delitem__(self, key: KEY) -> None:
             self._size_down()
 
     def __getitem__(self, key: KEY) -> VAL:
+        """
+        Returns the item at the given key
+
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm.__getitem__(1)
+        10
+
+        >>> hm = HashMap(5)
+        >>> hm._add_item(10, -10)
+        >>> hm._add_item(20, -20)
+        >>> hm.__getitem__(20)
+        -20
+
+        >>> hm = HashMap(5)
+        >>> hm._add_item(-1, 10)
+        >>> hm.__getitem__(-1)
+        10
+        """
         for ind in self._iterate_buckets(key):
             item = self._buckets[ind]
             if item is None:
@@ -150,13 +295,33 @@ def __getitem__(self, key: KEY) -> VAL:
         raise KeyError(key)
 
     def __len__(self) -> int:
+        """
+        Returns the number of items present in hashmap
+
+        >>> hm = HashMap(5)
+        >>> hm._add_item(1, 10)
+        >>> hm._add_item(2, 20)
+        >>> hm._add_item(3, 30)
+        >>> hm.__len__()
+        3
+
+        >>> hm = HashMap(5)
+        >>> hm.__len__()
+        0
+        """
         return self._len
 
     def __iter__(self) -> Iterator[KEY]:
         yield from (item.key for item in self._buckets if item)
 
     def __repr__(self) -> str:
-        val_string = " ,".join(
+        val_string = ", ".join(
             f"{item.key}: {item.val}" for item in self._buckets if item
         )
         return f"HashMap({val_string})"
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/hashing/hash_table.py b/data_structures/hashing/hash_table.py
index 7ca2f7c401cf..40fcad9a3dab 100644
--- a/data_structures/hashing/hash_table.py
+++ b/data_structures/hashing/hash_table.py
@@ -1,4 +1,6 @@
 #!/usr/bin/env python3
+from abc import abstractmethod
+
 from .number_theory.prime_numbers import next_prime
 
 
@@ -21,6 +23,29 @@ def __init__(
         self._keys: dict = {}
 
     def keys(self):
+        """
+        The keys function returns a dictionary containing the key value pairs.
+        key being the index number in hash table and value being the data value.
+
+        Examples:
+        1. creating HashTable with size 10 and inserting 3 elements
+        >>> ht = HashTable(10)
+        >>> ht.insert_data(10)
+        >>> ht.insert_data(20)
+        >>> ht.insert_data(30)
+        >>> ht.keys()
+        {0: 10, 1: 20, 2: 30}
+
+        2. creating HashTable with size 5 and inserting 5 elements
+        >>> ht = HashTable(5)
+        >>> ht.insert_data(5)
+        >>> ht.insert_data(4)
+        >>> ht.insert_data(3)
+        >>> ht.insert_data(2)
+        >>> ht.insert_data(1)
+        >>> ht.keys()
+        {0: 5, 4: 4, 3: 3, 2: 2, 1: 1}
+        """
         return self._keys
 
     def balanced_factor(self):
@@ -29,6 +54,30 @@ def balanced_factor(self):
         )
 
     def hash_function(self, key):
+        """
+        Generates hash for the given key value
+
+        Examples:
+
+        Creating HashTable with size 5
+        >>> ht = HashTable(5)
+        >>> ht.hash_function(10)
+        0
+        >>> ht.hash_function(20)
+        0
+        >>> ht.hash_function(4)
+        4
+        >>> ht.hash_function(18)
+        3
+        >>> ht.hash_function(-18)
+        2
+        >>> ht.hash_function(18.5)
+        3.5
+        >>> ht.hash_function(0)
+        0
+        >>> ht.hash_function(-0)
+        0
+        """
         return key % self.size_table
 
     def _step_by_step(self, step_ord):
@@ -37,6 +86,43 @@ def _step_by_step(self, step_ord):
         print(self.values)
 
     def bulk_insert(self, values):
+        """
+        bulk_insert is used for entering more than one element at a time
+        in the HashTable.
+
+        Examples:
+        1.
+        >>> ht = HashTable(5)
+        >>> ht.bulk_insert((10,20,30))
+        step 1
+        [0, 1, 2, 3, 4]
+        [10, None, None, None, None]
+        step 2
+        [0, 1, 2, 3, 4]
+        [10, 20, None, None, None]
+        step 3
+        [0, 1, 2, 3, 4]
+        [10, 20, 30, None, None]
+
+        2.
+        >>> ht = HashTable(5)
+        >>> ht.bulk_insert([5,4,3,2,1])
+        step 1
+        [0, 1, 2, 3, 4]
+        [5, None, None, None, None]
+        step 2
+        [0, 1, 2, 3, 4]
+        [5, None, None, None, 4]
+        step 3
+        [0, 1, 2, 3, 4]
+        [5, None, None, 3, 4]
+        step 4
+        [0, 1, 2, 3, 4]
+        [5, None, 2, 3, 4]
+        step 5
+        [0, 1, 2, 3, 4]
+        [5, 1, 2, 3, 4]
+        """
         i = 1
         self.__aux_list = values
         for value in values:
@@ -45,10 +131,100 @@ def bulk_insert(self, values):
             i += 1
 
     def _set_value(self, key, data):
+        """
+        _set_value functions allows to update value at a particular hash
+
+        Examples:
+        1. _set_value in HashTable of size 5
+        >>> ht = HashTable(5)
+        >>> ht.insert_data(10)
+        >>> ht.insert_data(20)
+        >>> ht.insert_data(30)
+        >>> ht._set_value(0,15)
+        >>> ht.keys()
+        {0: 15, 1: 20, 2: 30}
+
+        2. _set_value in HashTable of size 2
+        >>> ht = HashTable(2)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99)
+        >>> ht._set_value(3,15)
+        >>> ht.keys()
+        {3: 15, 2: 17, 4: 99}
+
+        3. _set_value in HashTable when hash is not present
+        >>> ht = HashTable(2)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99)
+        >>> ht._set_value(0,15)
+        >>> ht.keys()
+        {3: 18, 2: 17, 4: 99, 0: 15}
+
+        4. _set_value in HashTable when multiple hash are not present
+        >>> ht = HashTable(2)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99)
+        >>> ht._set_value(0,15)
+        >>> ht._set_value(1,20)
+        >>> ht.keys()
+        {3: 18, 2: 17, 4: 99, 0: 15, 1: 20}
+        """
         self.values[key] = data
         self._keys[key] = data
 
+    @abstractmethod
     def _collision_resolution(self, key, data=None):
+        """
+        This method is a type of open addressing which is used for handling collision.
+
+        In this implementation the concept of linear probing has been used.
+
+        The hash table is searched sequentially from the original location of the
+        hash, if the new hash/location we get is already occupied we check for the next
+        hash/location.
+
+        references:
+            - https://en.wikipedia.org/wiki/Linear_probing
+
+        Examples:
+        1. The collision will be with keys 18 & 99, so new hash will be created for 99
+        >>> ht = HashTable(3)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99)
+        >>> ht.keys()
+        {2: 17, 0: 18, 1: 99}
+
+        2. The collision will be with keys 17 & 101, so new hash
+        will be created for 101
+        >>> ht = HashTable(4)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99)
+        >>> ht.insert_data(101)
+        >>> ht.keys()
+        {1: 17, 2: 18, 3: 99, 0: 101}
+
+        2. The collision will be with all keys, so new hash will be created for all
+        >>> ht = HashTable(1)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99)
+        >>> ht.keys()
+        {2: 17, 3: 18, 4: 99}
+
+        3. Trying to insert float key in hash
+        >>> ht = HashTable(1)
+        >>> ht.insert_data(17)
+        >>> ht.insert_data(18)
+        >>> ht.insert_data(99.99)
+        Traceback (most recent call last):
+        ...
+        TypeError: list indices must be integers or slices, not float
+        """
         new_key = self.hash_function(key + 1)
 
         while self.values[new_key] is not None and self.values[new_key] != key:
@@ -69,6 +245,21 @@ def rehashing(self):
             self.insert_data(value)
 
     def insert_data(self, data):
+        """
+        insert_data is used for inserting a single element at a time in the HashTable.
+
+        Examples:
+
+        >>> ht = HashTable(3)
+        >>> ht.insert_data(5)
+        >>> ht.keys()
+        {2: 5}
+        >>> ht = HashTable(5)
+        >>> ht.insert_data(30)
+        >>> ht.insert_data(50)
+        >>> ht.keys()
+        {0: 30, 1: 50}
+        """
         key = self.hash_function(data)
 
         if self.values[key] is None:
@@ -84,3 +275,9 @@ def insert_data(self, data):
             else:
                 self.rehashing()
                 self.insert_data(data)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/hashing/number_theory/prime_numbers.py b/data_structures/hashing/number_theory/prime_numbers.py
index 0c25896f9880..82071b5e9f09 100644
--- a/data_structures/hashing/number_theory/prime_numbers.py
+++ b/data_structures/hashing/number_theory/prime_numbers.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-    module to operations with prime numbers
+module to operations with prime numbers
 """
 
 import math
@@ -32,9 +32,9 @@ def is_prime(number: int) -> bool:
     """
 
     # precondition
-    assert isinstance(number, int) and (
-        number >= 0
-    ), "'number' must been an int and positive"
+    assert isinstance(number, int) and (number >= 0), (
+        "'number' must been an int and positive"
+    )
 
     if 1 < number < 4:
         # 2 and 3 are primes
diff --git a/data_structures/hashing/quadratic_probing.py b/data_structures/hashing/quadratic_probing.py
index 0930340a347f..56d4926eee9b 100644
--- a/data_structures/hashing/quadratic_probing.py
+++ b/data_structures/hashing/quadratic_probing.py
@@ -11,7 +11,56 @@ class QuadraticProbing(HashTable):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-    def _collision_resolution(self, key, data=None):
+    def _collision_resolution(self, key, data=None):  # noqa: ARG002
+        """
+        Quadratic probing is an open addressing scheme used for resolving
+        collisions in hash table.
+
+        It works by taking the original hash index and adding successive
+        values of an arbitrary quadratic polynomial until open slot is found.
+
+        Hash + 1², Hash + 2², Hash + 3² .... Hash + n²
+
+        reference:
+            - https://en.wikipedia.org/wiki/Quadratic_probing
+        e.g:
+        1. Create hash table with size 7
+        >>> qp = QuadraticProbing(7)
+        >>> qp.insert_data(90)
+        >>> qp.insert_data(340)
+        >>> qp.insert_data(24)
+        >>> qp.insert_data(45)
+        >>> qp.insert_data(99)
+        >>> qp.insert_data(73)
+        >>> qp.insert_data(7)
+        >>> qp.keys()
+        {11: 45, 14: 99, 7: 24, 0: 340, 5: 73, 6: 90, 8: 7}
+
+        2. Create hash table with size 8
+        >>> qp = QuadraticProbing(8)
+        >>> qp.insert_data(0)
+        >>> qp.insert_data(999)
+        >>> qp.insert_data(111)
+        >>> qp.keys()
+        {0: 0, 7: 999, 3: 111}
+
+        3. Try to add three data elements when the size is two
+        >>> qp =  QuadraticProbing(2)
+        >>> qp.insert_data(0)
+        >>> qp.insert_data(999)
+        >>> qp.insert_data(111)
+        >>> qp.keys()
+        {0: 0, 4: 999, 1: 111}
+
+        4. Try to add three data elements when the size is one
+        >>> qp =  QuadraticProbing(1)
+        >>> qp.insert_data(0)
+        >>> qp.insert_data(999)
+        >>> qp.insert_data(111)
+        >>> qp.keys()
+        {4: 999, 1: 111}
+        """
+
         i = 1
         new_key = self.hash_function(key + i * i)
 
@@ -27,3 +76,9 @@ def _collision_resolution(self, key, data=None):
                 break
 
         return new_key
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/compression/__init__.py b/data_structures/hashing/tests/__init__.py
similarity index 100%
rename from compression/__init__.py
rename to data_structures/hashing/tests/__init__.py
diff --git a/data_structures/heap/binomial_heap.py b/data_structures/heap/binomial_heap.py
index 099bd2871023..9cfdf0c12fe0 100644
--- a/data_structures/heap/binomial_heap.py
+++ b/data_structures/heap/binomial_heap.py
@@ -73,7 +73,7 @@ class BinomialHeap:
     30
 
     Deleting - delete() test
-    >>> [first_heap.delete_min() for _ in range(20)]
+    >>> [int(first_heap.delete_min()) for _ in range(20)]
     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
 
     Create a new Heap
@@ -118,7 +118,7 @@ class BinomialHeap:
     values in merged heap; (merge is inplace)
     >>> results = []
     >>> while not first_heap.is_empty():
-    ...     results.append(first_heap.delete_min())
+    ...     results.append(int(first_heap.delete_min()))
     >>> results
     [17, 20, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34]
     """
@@ -354,7 +354,7 @@ def delete_min(self):
         # Merge heaps
         self.merge_heaps(new_heap)
 
-        return min_value
+        return int(min_value)
 
     def pre_order(self):
         """
diff --git a/data_structures/heap/heap.py b/data_structures/heap/heap.py
index c1004f349479..41ef0ddd1005 100644
--- a/data_structures/heap/heap.py
+++ b/data_structures/heap/heap.py
@@ -2,7 +2,7 @@
 
 from abc import abstractmethod
 from collections.abc import Iterable
-from typing import Generic, Protocol, TypeVar
+from typing import Protocol, TypeVar
 
 
 class Comparable(Protocol):
@@ -22,7 +22,7 @@ def __eq__(self: T, other: object) -> bool:
 T = TypeVar("T", bound=Comparable)
 
 
-class Heap(Generic[T]):
+class Heap[T: Comparable]:
     """A Max Heap Implementation
 
     >>> unsorted = [103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5]
@@ -53,7 +53,37 @@ def __repr__(self) -> str:
         return str(self.h)
 
     def parent_index(self, child_idx: int) -> int | None:
-        """return the parent index of given child"""
+        """
+        returns the parent index based on the given child index
+
+        >>> h = Heap()
+        >>> h.build_max_heap([103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5])
+        >>> h
+        [209, 201, 25, 103, 107, 15, 1, 9, 7, 11, 5]
+
+        >>> h.parent_index(-1)  # returns none if index is <=0
+
+        >>> h.parent_index(0)   # returns none if index is <=0
+
+        >>> h.parent_index(1)
+        0
+        >>> h.parent_index(2)
+        0
+        >>> h.parent_index(3)
+        1
+        >>> h.parent_index(4)
+        1
+        >>> h.parent_index(5)
+        2
+        >>> h.parent_index(10.5)
+        4.0
+        >>> h.parent_index(209.0)
+        104.0
+        >>> h.parent_index("Test")
+        Traceback (most recent call last):
+        ...
+        TypeError: '>' not supported between instances of 'str' and 'int'
+        """
         if child_idx > 0:
             return (child_idx - 1) // 2
         return None
@@ -81,6 +111,9 @@ def right_child_idx(self, parent_idx: int) -> int | None:
     def max_heapify(self, index: int) -> None:
         """
         correct a single violation of the heap property in a subtree's root.
+
+        It is the function that is responsible for restoring the property
+        of Max heap i.e the maximum element is always at top.
         """
         if index < self.heap_size:
             violation: int = index
@@ -99,7 +132,29 @@ def max_heapify(self, index: int) -> None:
                 self.max_heapify(violation)
 
     def build_max_heap(self, collection: Iterable[T]) -> None:
-        """build max heap from an unsorted array"""
+        """
+        build max heap from an unsorted array
+
+        >>> h = Heap()
+        >>> h.build_max_heap([20,40,50,20,10])
+        >>> h
+        [50, 40, 20, 20, 10]
+
+        >>> h = Heap()
+        >>> h.build_max_heap([1,2,3,4,5,6,7,8,9,0])
+        >>> h
+        [9, 8, 7, 4, 5, 6, 3, 2, 1, 0]
+
+        >>> h = Heap()
+        >>> h.build_max_heap([514,5,61,57,8,99,105])
+        >>> h
+        [514, 57, 105, 5, 8, 99, 61]
+
+        >>> h = Heap()
+        >>> h.build_max_heap([514,5,61.6,57,8,9.9,105])
+        >>> h
+        [514, 57, 105, 5, 8, 9.9, 61.6]
+        """
         self.h = list(collection)
         self.heap_size = len(self.h)
         if self.heap_size > 1:
@@ -108,7 +163,24 @@ def build_max_heap(self, collection: Iterable[T]) -> None:
                 self.max_heapify(i)
 
     def extract_max(self) -> T:
-        """get and remove max from heap"""
+        """
+        get and remove max from heap
+
+        >>> h = Heap()
+        >>> h.build_max_heap([20,40,50,20,10])
+        >>> h.extract_max()
+        50
+
+        >>> h = Heap()
+        >>> h.build_max_heap([514,5,61,57,8,99,105])
+        >>> h.extract_max()
+        514
+
+        >>> h = Heap()
+        >>> h.build_max_heap([1,2,3,4,5,6,7,8,9,0])
+        >>> h.extract_max()
+        9
+        """
         if self.heap_size >= 2:
             me = self.h[0]
             self.h[0] = self.h.pop(-1)
@@ -122,7 +194,34 @@ def extract_max(self) -> T:
             raise Exception("Empty heap")
 
     def insert(self, value: T) -> None:
-        """insert a new value into the max heap"""
+        """
+        insert a new value into the max heap
+
+        >>> h = Heap()
+        >>> h.insert(10)
+        >>> h
+        [10]
+
+        >>> h = Heap()
+        >>> h.insert(10)
+        >>> h.insert(10)
+        >>> h
+        [10, 10]
+
+        >>> h = Heap()
+        >>> h.insert(10)
+        >>> h.insert(10.1)
+        >>> h
+        [10.1, 10]
+
+        >>> h = Heap()
+        >>> h.insert(0.1)
+        >>> h.insert(0)
+        >>> h.insert(9)
+        >>> h.insert(5)
+        >>> h
+        [9, 5, 0.1, 0]
+        """
         self.h.append(value)
         idx = (self.heap_size - 1) // 2
         self.heap_size += 1
diff --git a/data_structures/heap/max_heap.py b/data_structures/heap/max_heap.py
index fbc8eed09226..589f2595a8da 100644
--- a/data_structures/heap/max_heap.py
+++ b/data_structures/heap/max_heap.py
@@ -38,13 +38,12 @@ def insert(self, value: int) -> None:
     def __swap_down(self, i: int) -> None:
         """Swap the element down"""
         while self.__size >= 2 * i:
-            if 2 * i + 1 > self.__size:
+            if 2 * i + 1 > self.__size:  # noqa: SIM114
+                bigger_child = 2 * i
+            elif self.__heap[2 * i] > self.__heap[2 * i + 1]:
                 bigger_child = 2 * i
             else:
-                if self.__heap[2 * i] > self.__heap[2 * i + 1]:
-                    bigger_child = 2 * i
-                else:
-                    bigger_child = 2 * i + 1
+                bigger_child = 2 * i + 1
             temporary = self.__heap[i]
             if self.__heap[i] < self.__heap[bigger_child]:
                 self.__heap[i] = self.__heap[bigger_child]
diff --git a/data_structures/heap/min_heap.py b/data_structures/heap/min_heap.py
index ecb1876493b0..577b98d788a1 100644
--- a/data_structures/heap/min_heap.py
+++ b/data_structures/heap/min_heap.py
@@ -66,14 +66,14 @@ def build_heap(self, array):
     # this is min-heapify method
     def sift_down(self, idx, array):
         while True:
-            l = self.get_left_child_idx(idx)  # noqa: E741
-            r = self.get_right_child_idx(idx)
+            left = self.get_left_child_idx(idx)
+            right = self.get_right_child_idx(idx)
 
             smallest = idx
-            if l < len(array) and array[l] < array[idx]:
-                smallest = l
-            if r < len(array) and array[r] < array[smallest]:
-                smallest = r
+            if left < len(array) and array[left] < array[idx]:
+                smallest = left
+            if right < len(array) and array[right] < array[smallest]:
+                smallest = right
 
             if smallest != idx:
                 array[idx], array[smallest] = array[smallest], array[idx]
@@ -124,9 +124,9 @@ def is_empty(self):
         return len(self.heap) == 0
 
     def decrease_key(self, node, new_value):
-        assert (
-            self.heap[self.idx_of_element[node]].val > new_value
-        ), "newValue must be less that current value"
+        assert self.heap[self.idx_of_element[node]].val > new_value, (
+            "newValue must be less that current value"
+        )
         node.val = new_value
         self.heap_dict[node.name] = new_value
         self.sift_up(self.idx_of_element[node])
diff --git a/data_structures/heap/randomized_heap.py b/data_structures/heap/randomized_heap.py
index c0f9888f80c7..9668d3ac2a45 100644
--- a/data_structures/heap/randomized_heap.py
+++ b/data_structures/heap/randomized_heap.py
@@ -4,12 +4,12 @@
 
 import random
 from collections.abc import Iterable
-from typing import Any, Generic, TypeVar
+from typing import Any, TypeVar
 
 T = TypeVar("T", bound=bool)
 
 
-class RandomizedHeapNode(Generic[T]):
+class RandomizedHeapNode[T: bool]:
     """
     One node of the randomized heap. Contains the value and references to
     two children.
@@ -22,14 +22,40 @@ def __init__(self, value: T) -> None:
 
     @property
     def value(self) -> T:
-        """Return the value of the node."""
+        """
+        Return the value of the node.
+
+        >>> rhn = RandomizedHeapNode(10)
+        >>> rhn.value
+        10
+        >>> rhn = RandomizedHeapNode(-10)
+        >>> rhn.value
+        -10
+        """
         return self._value
 
     @staticmethod
     def merge(
         root1: RandomizedHeapNode[T] | None, root2: RandomizedHeapNode[T] | None
     ) -> RandomizedHeapNode[T] | None:
-        """Merge 2 nodes together."""
+        """
+        Merge 2 nodes together.
+
+        >>> rhn1 = RandomizedHeapNode(10)
+        >>> rhn2 = RandomizedHeapNode(20)
+        >>> RandomizedHeapNode.merge(rhn1, rhn2).value
+        10
+
+        >>> rhn1 = RandomizedHeapNode(20)
+        >>> rhn2 = RandomizedHeapNode(10)
+        >>> RandomizedHeapNode.merge(rhn1, rhn2).value
+        10
+
+        >>> rhn1 = RandomizedHeapNode(5)
+        >>> rhn2 = RandomizedHeapNode(0)
+        >>> RandomizedHeapNode.merge(rhn1, rhn2).value
+        0
+        """
         if not root1:
             return root2
 
@@ -47,7 +73,7 @@ def merge(
         return root1
 
 
-class RandomizedHeap(Generic[T]):
+class RandomizedHeap[T: bool]:
     """
     A data structure that allows inserting a new value and to pop the smallest
     values. Both operations take O(logN) time where N is the size of the
diff --git a/data_structures/heap/skew_heap.py b/data_structures/heap/skew_heap.py
index c4c13b08276a..4e82569444fa 100644
--- a/data_structures/heap/skew_heap.py
+++ b/data_structures/heap/skew_heap.py
@@ -3,12 +3,12 @@
 from __future__ import annotations
 
 from collections.abc import Iterable, Iterator
-from typing import Any, Generic, TypeVar
+from typing import Any, TypeVar
 
 T = TypeVar("T", bound=bool)
 
 
-class SkewNode(Generic[T]):
+class SkewNode[T: bool]:
     """
     One node of the skew heap. Contains the value and references to
     two children.
@@ -21,14 +21,55 @@ def __init__(self, value: T) -> None:
 
     @property
     def value(self) -> T:
-        """Return the value of the node."""
+        """
+        Return the value of the node.
+
+        >>> SkewNode(0).value
+        0
+        >>> SkewNode(3.14159).value
+        3.14159
+        >>> SkewNode("hello").value
+        'hello'
+        >>> SkewNode(None).value
+
+        >>> SkewNode(True).value
+        True
+        >>> SkewNode([]).value
+        []
+        >>> SkewNode({}).value
+        {}
+        >>> SkewNode(set()).value
+        set()
+        >>> SkewNode(0.0).value
+        0.0
+        >>> SkewNode(-1e-10).value
+        -1e-10
+        >>> SkewNode(10).value
+        10
+        >>> SkewNode(-10.5).value
+        -10.5
+        >>> SkewNode().value
+        Traceback (most recent call last):
+        ...
+        TypeError: SkewNode.__init__() missing 1 required positional argument: 'value'
+        """
         return self._value
 
     @staticmethod
     def merge(
         root1: SkewNode[T] | None, root2: SkewNode[T] | None
     ) -> SkewNode[T] | None:
-        """Merge 2 nodes together."""
+        """
+        Merge 2 nodes together.
+        >>> SkewNode.merge(SkewNode(10),SkewNode(-10.5)).value
+        -10.5
+        >>> SkewNode.merge(SkewNode(10),SkewNode(10.5)).value
+        10
+        >>> SkewNode.merge(SkewNode(10),SkewNode(10)).value
+        10
+        >>> SkewNode.merge(SkewNode(-100),SkewNode(-10.5)).value
+        -100
+        """
         if not root1:
             return root2
 
@@ -46,7 +87,7 @@ def merge(
         return result
 
 
-class SkewHeap(Generic[T]):
+class SkewHeap[T: bool]:
     """
     A data structure that allows inserting a new value and to pop the smallest
     values. Both operations take O(logN) time where N is the size of the
diff --git a/compression/image_data/__init__.py b/data_structures/kd_tree/__init__.py
similarity index 100%
rename from compression/image_data/__init__.py
rename to data_structures/kd_tree/__init__.py
diff --git a/data_structures/kd_tree/build_kdtree.py b/data_structures/kd_tree/build_kdtree.py
new file mode 100644
index 000000000000..074a5dac4d42
--- /dev/null
+++ b/data_structures/kd_tree/build_kdtree.py
@@ -0,0 +1,43 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11532
+#  https://github.com/TheAlgorithms/Python/pull/11532
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+from data_structures.kd_tree.kd_node import KDNode
+
+
+def build_kdtree(points: list[list[float]], depth: int = 0) -> KDNode | None:
+    """
+    Builds a KD-Tree from a list of points.
+
+    Args:
+        points: The list of points to build the KD-Tree from.
+        depth: The current depth in the tree
+                     (used to determine axis for splitting).
+
+    Returns:
+        The root node of the KD-Tree,
+                       or None if no points are provided.
+    """
+    if not points:
+        return None
+
+    k = len(points[0])  # Dimensionality of the points
+    axis = depth % k
+
+    # Sort point list and choose median as pivot element
+    points.sort(key=lambda point: point[axis])
+    median_idx = len(points) // 2
+
+    # Create node and construct subtrees
+    left_points = points[:median_idx]
+    right_points = points[median_idx + 1 :]
+
+    return KDNode(
+        point=points[median_idx],
+        left=build_kdtree(left_points, depth + 1),
+        right=build_kdtree(right_points, depth + 1),
+    )
diff --git a/data_structures/queue/__init__.py b/data_structures/kd_tree/example/__init__.py
similarity index 100%
rename from data_structures/queue/__init__.py
rename to data_structures/kd_tree/example/__init__.py
diff --git a/data_structures/kd_tree/example/example_usage.py b/data_structures/kd_tree/example/example_usage.py
new file mode 100644
index 000000000000..892c3b8c4a2a
--- /dev/null
+++ b/data_structures/kd_tree/example/example_usage.py
@@ -0,0 +1,46 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11532
+#  https://github.com/TheAlgorithms/Python/pull/11532
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+import numpy as np
+
+from data_structures.kd_tree.build_kdtree import build_kdtree
+from data_structures.kd_tree.example.hypercube_points import hypercube_points
+from data_structures.kd_tree.nearest_neighbour_search import nearest_neighbour_search
+
+
+def main() -> None:
+    """
+    Demonstrates the use of KD-Tree by building it from random points
+    in a 10-dimensional hypercube and performing a nearest neighbor search.
+    """
+    num_points: int = 5000
+    cube_size: float = 10.0  # Size of the hypercube (edge length)
+    num_dimensions: int = 10
+
+    # Generate random points within the hypercube
+    points: np.ndarray = hypercube_points(num_points, cube_size, num_dimensions)
+    hypercube_kdtree = build_kdtree(points.tolist())
+
+    # Generate a random query point within the same space
+    rng = np.random.default_rng()
+    query_point: list[float] = rng.random(num_dimensions).tolist()
+
+    # Perform nearest neighbor search
+    nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search(
+        hypercube_kdtree, query_point
+    )
+
+    # Print the results
+    print(f"Query point: {query_point}")
+    print(f"Nearest point: {nearest_point}")
+    print(f"Distance: {nearest_dist:.4f}")
+    print(f"Nodes visited: {nodes_visited}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/data_structures/kd_tree/example/hypercube_points.py b/data_structures/kd_tree/example/hypercube_points.py
new file mode 100644
index 000000000000..66744856e6d5
--- /dev/null
+++ b/data_structures/kd_tree/example/hypercube_points.py
@@ -0,0 +1,29 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11532
+#  https://github.com/TheAlgorithms/Python/pull/11532
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+import numpy as np
+
+
+def hypercube_points(
+    num_points: int, hypercube_size: float, num_dimensions: int
+) -> np.ndarray:
+    """
+    Generates random points uniformly distributed within an n-dimensional hypercube.
+
+    Args:
+        num_points: Number of points to generate.
+        hypercube_size: Size of the hypercube.
+        num_dimensions: Number of dimensions of the hypercube.
+
+    Returns:
+        An array of shape (num_points, num_dimensions)
+                    with generated points.
+    """
+    rng = np.random.default_rng()
+    shape = (num_points, num_dimensions)
+    return hypercube_size * rng.random(shape)
diff --git a/data_structures/kd_tree/kd_node.py b/data_structures/kd_tree/kd_node.py
new file mode 100644
index 000000000000..5a22ef609077
--- /dev/null
+++ b/data_structures/kd_tree/kd_node.py
@@ -0,0 +1,38 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11532
+#  https://github.com/TheAlgorithms/Python/pull/11532
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+from __future__ import annotations
+
+
+class KDNode:
+    """
+    Represents a node in a KD-Tree.
+
+    Attributes:
+        point: The point stored in this node.
+        left: The left child node.
+        right: The right child node.
+    """
+
+    def __init__(
+        self,
+        point: list[float],
+        left: KDNode | None = None,
+        right: KDNode | None = None,
+    ) -> None:
+        """
+        Initializes a KDNode with the given point and child nodes.
+
+        Args:
+            point (list[float]): The point stored in this node.
+            left (Optional[KDNode]): The left child node.
+            right (Optional[KDNode]): The right child node.
+        """
+        self.point = point
+        self.left = left
+        self.right = right
diff --git a/data_structures/kd_tree/nearest_neighbour_search.py b/data_structures/kd_tree/nearest_neighbour_search.py
new file mode 100644
index 000000000000..8104944c08f0
--- /dev/null
+++ b/data_structures/kd_tree/nearest_neighbour_search.py
@@ -0,0 +1,79 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11532
+#  https://github.com/TheAlgorithms/Python/pull/11532
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+from data_structures.kd_tree.kd_node import KDNode
+
+
+def nearest_neighbour_search(
+    root: KDNode | None, query_point: list[float]
+) -> tuple[list[float] | None, float, int]:
+    """
+    Performs a nearest neighbor search in a KD-Tree for a given query point.
+
+    Args:
+        root (KDNode | None): The root node of the KD-Tree.
+        query_point (list[float]): The point for which the nearest neighbor
+                                    is being searched.
+
+    Returns:
+        tuple[list[float] | None, float, int]:
+            - The nearest point found in the KD-Tree to the query point,
+              or None if no point is found.
+            - The squared distance to the nearest point.
+            - The number of nodes visited during the search.
+    """
+    nearest_point: list[float] | None = None
+    nearest_dist: float = float("inf")
+    nodes_visited: int = 0
+
+    def search(node: KDNode | None, depth: int = 0) -> None:
+        """
+        Recursively searches for the nearest neighbor in the KD-Tree.
+
+        Args:
+            node: The current node in the KD-Tree.
+            depth: The current depth in the KD-Tree.
+        """
+        nonlocal nearest_point, nearest_dist, nodes_visited
+        if node is None:
+            return
+
+        nodes_visited += 1
+
+        # Calculate the current distance (squared distance)
+        current_point = node.point
+        current_dist = sum(
+            (query_coord - point_coord) ** 2
+            for query_coord, point_coord in zip(query_point, current_point)
+        )
+
+        # Update nearest point if the current node is closer
+        if nearest_point is None or current_dist < nearest_dist:
+            nearest_point = current_point
+            nearest_dist = current_dist
+
+        # Determine which subtree to search first (based on axis and query point)
+        k = len(query_point)  # Dimensionality of points
+        axis = depth % k
+
+        if query_point[axis] <= current_point[axis]:
+            nearer_subtree = node.left
+            further_subtree = node.right
+        else:
+            nearer_subtree = node.right
+            further_subtree = node.left
+
+        # Search the nearer subtree first
+        search(nearer_subtree, depth + 1)
+
+        # If the further subtree has a closer point
+        if (query_point[axis] - current_point[axis]) ** 2 < nearest_dist:
+            search(further_subtree, depth + 1)
+
+    search(root, 0)
+    return nearest_point, nearest_dist, nodes_visited
diff --git a/data_structures/kd_tree/tests/__init__.py b/data_structures/kd_tree/tests/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/data_structures/kd_tree/tests/test_kdtree.py b/data_structures/kd_tree/tests/test_kdtree.py
new file mode 100644
index 000000000000..d6a4a66dd24d
--- /dev/null
+++ b/data_structures/kd_tree/tests/test_kdtree.py
@@ -0,0 +1,108 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11532
+#  https://github.com/TheAlgorithms/Python/pull/11532
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+import numpy as np
+import pytest
+
+from data_structures.kd_tree.build_kdtree import build_kdtree
+from data_structures.kd_tree.example.hypercube_points import hypercube_points
+from data_structures.kd_tree.kd_node import KDNode
+from data_structures.kd_tree.nearest_neighbour_search import nearest_neighbour_search
+
+
+@pytest.mark.parametrize(
+    ("num_points", "cube_size", "num_dimensions", "depth", "expected_result"),
+    [
+        (0, 10.0, 2, 0, None),  # Empty points list
+        (10, 10.0, 2, 2, KDNode),  # Depth = 2, 2D points
+        (10, 10.0, 3, -2, KDNode),  # Depth = -2, 3D points
+    ],
+)
+def test_build_kdtree(num_points, cube_size, num_dimensions, depth, expected_result):
+    """
+    Test that KD-Tree is built correctly.
+
+    Cases:
+        - Empty points list.
+        - Positive depth value.
+        - Negative depth value.
+    """
+    points = (
+        hypercube_points(num_points, cube_size, num_dimensions).tolist()
+        if num_points > 0
+        else []
+    )
+
+    kdtree = build_kdtree(points, depth=depth)
+
+    if expected_result is None:
+        # Empty points list case
+        assert kdtree is None, f"Expected None for empty points list, got {kdtree}"
+    else:
+        # Check if root node is not None
+        assert kdtree is not None, "Expected a KDNode, got None"
+
+        # Check if root has correct dimensions
+        assert len(kdtree.point) == num_dimensions, (
+            f"Expected point dimension {num_dimensions}, got {len(kdtree.point)}"
+        )
+
+        # Check that the tree is balanced to some extent (simplistic check)
+        assert isinstance(kdtree, KDNode), (
+            f"Expected KDNode instance, got {type(kdtree)}"
+        )
+
+
+def test_nearest_neighbour_search():
+    """
+    Test the nearest neighbor search function.
+    """
+    num_points = 10
+    cube_size = 10.0
+    num_dimensions = 2
+    points = hypercube_points(num_points, cube_size, num_dimensions)
+    kdtree = build_kdtree(points.tolist())
+
+    rng = np.random.default_rng()
+    query_point = rng.random(num_dimensions).tolist()
+
+    nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search(
+        kdtree, query_point
+    )
+
+    # Check that nearest point is not None
+    assert nearest_point is not None
+
+    # Check that distance is a non-negative number
+    assert nearest_dist >= 0
+
+    # Check that nodes visited is a non-negative integer
+    assert nodes_visited >= 0
+
+
+def test_edge_cases():
+    """
+    Test edge cases such as an empty KD-Tree.
+    """
+    empty_kdtree = build_kdtree([])
+    query_point = [0.0] * 2  # Using a default 2D query point
+
+    nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search(
+        empty_kdtree, query_point
+    )
+
+    # With an empty KD-Tree, nearest_point should be None
+    assert nearest_point is None
+    assert nearest_dist == float("inf")
+    assert nodes_visited == 0
+
+
+if __name__ == "__main__":
+    import pytest
+
+    pytest.main()
diff --git a/data_structures/linked_list/__init__.py b/data_structures/linked_list/__init__.py
index 225113f72cee..00ef337a1211 100644
--- a/data_structures/linked_list/__init__.py
+++ b/data_structures/linked_list/__init__.py
@@ -5,6 +5,7 @@
                  head node gives us access of the complete list
     - Last node: points to null
 """
+
 from __future__ import annotations
 
 from typing import Any
diff --git a/data_structures/linked_list/circular_linked_list.py b/data_structures/linked_list/circular_linked_list.py
index 54343c80a30f..bb64441d4560 100644
--- a/data_structures/linked_list/circular_linked_list.py
+++ b/data_structures/linked_list/circular_linked_list.py
@@ -1,27 +1,20 @@
 from __future__ import annotations
 
 from collections.abc import Iterator
+from dataclasses import dataclass
 from typing import Any
 
 
+@dataclass
 class Node:
-    def __init__(self, data: Any):
-        """
-        Initialize a new Node with the given data.
-        Args:
-            data: The data to be stored in the node.
-        """
-        self.data: Any = data
-        self.next: Node | None = None  # Reference to the next node
+    data: Any
+    next_node: Node | None = None
 
 
+@dataclass
 class CircularLinkedList:
-    def __init__(self) -> None:
-        """
-        Initialize an empty Circular Linked List.
-        """
-        self.head: Node | None = None  # Reference to the head (first node)
-        self.tail: Node | None = None  # Reference to the tail (last node)
+    head: Node | None = None  # Reference to the head (first node)
+    tail: Node | None = None  # Reference to the tail (last node)
 
     def __iter__(self) -> Iterator[Any]:
         """
@@ -32,7 +25,7 @@ def __iter__(self) -> Iterator[Any]:
         node = self.head
         while node:
             yield node.data
-            node = node.next
+            node = node.next_node
             if node == self.head:
                 break
 
@@ -76,20 +69,20 @@ def insert_nth(self, index: int, data: Any) -> None:
             raise IndexError("list index out of range.")
         new_node: Node = Node(data)
         if self.head is None:
-            new_node.next = new_node  # First node points to itself
+            new_node.next_node = new_node  # First node points to itself
             self.tail = self.head = new_node
         elif index == 0:  # Insert at the head
-            new_node.next = self.head
+            new_node.next_node = self.head
             assert self.tail is not None  # List is not empty, tail exists
-            self.head = self.tail.next = new_node
+            self.head = self.tail.next_node = new_node
         else:
             temp: Node | None = self.head
             for _ in range(index - 1):
                 assert temp is not None
-                temp = temp.next
+                temp = temp.next_node
             assert temp is not None
-            new_node.next = temp.next
-            temp.next = new_node
+            new_node.next_node = temp.next_node
+            temp.next_node = new_node
             if index == len(self) - 1:  # Insert at the tail
                 self.tail = new_node
 
@@ -130,18 +123,18 @@ def delete_nth(self, index: int = 0) -> Any:
         if self.head == self.tail:  # Just one node
             self.head = self.tail = None
         elif index == 0:  # Delete head node
-            assert self.tail.next is not None
-            self.tail.next = self.tail.next.next
-            self.head = self.head.next
+            assert self.tail.next_node is not None
+            self.tail.next_node = self.tail.next_node.next_node
+            self.head = self.head.next_node
         else:
             temp: Node | None = self.head
             for _ in range(index - 1):
                 assert temp is not None
-                temp = temp.next
+                temp = temp.next_node
             assert temp is not None
-            assert temp.next is not None
-            delete_node = temp.next
-            temp.next = temp.next.next
+            assert temp.next_node is not None
+            delete_node = temp.next_node
+            temp.next_node = temp.next_node.next_node
             if index == len(self) - 1:  # Delete at tail
                 self.tail = temp
         return delete_node.data
diff --git a/data_structures/linked_list/deque_doubly.py b/data_structures/linked_list/deque_doubly.py
index 2b9d70c223c4..e554ead91c5a 100644
--- a/data_structures/linked_list/deque_doubly.py
+++ b/data_structures/linked_list/deque_doubly.py
@@ -12,7 +12,7 @@ class _DoublyLinkedBase:
     """A Private class (to be inherited)"""
 
     class _Node:
-        __slots__ = "_prev", "_data", "_next"
+        __slots__ = "_data", "_next", "_prev"
 
         def __init__(self, link_p, element, link_n):
             self._prev = link_p
diff --git a/data_structures/linked_list/doubly_linked_list_two.py b/data_structures/linked_list/doubly_linked_list_two.py
index e993cc5a20af..a7f639a6e289 100644
--- a/data_structures/linked_list/doubly_linked_list_two.py
+++ b/data_structures/linked_list/doubly_linked_list_two.py
@@ -9,24 +9,20 @@
      Delete operation is more efficient
 """
 
+from dataclasses import dataclass
+from typing import Self, TypeVar
 
-class Node:
-    def __init__(self, data: int, previous=None, next_node=None):
-        self.data = data
-        self.previous = previous
-        self.next = next_node
+DataType = TypeVar("DataType")
 
-    def __str__(self) -> str:
-        return f"{self.data}"
-
-    def get_data(self) -> int:
-        return self.data
 
-    def get_next(self):
-        return self.next
+@dataclass
+class Node[DataType]:
+    data: DataType
+    previous: Self | None = None
+    next: Self | None = None
 
-    def get_previous(self):
-        return self.previous
+    def __str__(self) -> str:
+        return f"{self.data}"
 
 
 class LinkedListIterator:
@@ -40,30 +36,30 @@ def __next__(self):
         if not self.current:
             raise StopIteration
         else:
-            value = self.current.get_data()
-            self.current = self.current.get_next()
+            value = self.current.data
+            self.current = self.current.next
             return value
 
 
+@dataclass
 class LinkedList:
-    def __init__(self):
-        self.head = None  # First node in list
-        self.tail = None  # Last node in list
+    head: Node | None = None  # First node in list
+    tail: Node | None = None  # Last node in list
 
     def __str__(self):
         current = self.head
         nodes = []
         while current is not None:
-            nodes.append(current.get_data())
-            current = current.get_next()
+            nodes.append(current.data)
+            current = current.next
         return " ".join(str(node) for node in nodes)
 
-    def __contains__(self, value: int):
+    def __contains__(self, value: DataType):
         current = self.head
         while current:
-            if current.get_data() == value:
+            if current.data == value:
                 return True
-            current = current.get_next()
+            current = current.next
         return False
 
     def __iter__(self):
@@ -71,12 +67,12 @@ def __iter__(self):
 
     def get_head_data(self):
         if self.head:
-            return self.head.get_data()
+            return self.head.data
         return None
 
     def get_tail_data(self):
         if self.tail:
-            return self.tail.get_data()
+            return self.tail.data
         return None
 
     def set_head(self, node: Node) -> None:
@@ -87,12 +83,13 @@ def set_head(self, node: Node) -> None:
             self.insert_before_node(self.head, node)
 
     def set_tail(self, node: Node) -> None:
-        if self.head is None:
-            self.set_head(node)
+        if self.tail is None:
+            self.head = node
+            self.tail = node
         else:
             self.insert_after_node(self.tail, node)
 
-    def insert(self, value: int) -> None:
+    def insert(self, value: DataType) -> None:
         node = Node(value)
         if self.head is None:
             self.set_head(node)
@@ -103,7 +100,7 @@ def insert_before_node(self, node: Node, node_to_insert: Node) -> None:
         node_to_insert.next = node
         node_to_insert.previous = node.previous
 
-        if node.get_previous() is None:
+        if node.previous is None:
             self.head = node_to_insert
         else:
             node.previous.next = node_to_insert
@@ -114,14 +111,14 @@ def insert_after_node(self, node: Node, node_to_insert: Node) -> None:
         node_to_insert.previous = node
         node_to_insert.next = node.next
 
-        if node.get_next() is None:
+        if node.next is None:
             self.tail = node_to_insert
         else:
             node.next.previous = node_to_insert
 
         node.next = node_to_insert
 
-    def insert_at_position(self, position: int, value: int) -> None:
+    def insert_at_position(self, position: int, value: DataType) -> None:
         current_position = 1
         new_node = Node(value)
         node = self.head
@@ -131,32 +128,32 @@ def insert_at_position(self, position: int, value: int) -> None:
                 return
             current_position += 1
             node = node.next
-        self.insert_after_node(self.tail, new_node)
+        self.set_tail(new_node)
 
-    def get_node(self, item: int) -> Node:
+    def get_node(self, item: DataType) -> Node:
         node = self.head
         while node:
-            if node.get_data() == item:
+            if node.data == item:
                 return node
-            node = node.get_next()
+            node = node.next
         raise Exception("Node not found")
 
     def delete_value(self, value):
         if (node := self.get_node(value)) is not None:
             if node == self.head:
-                self.head = self.head.get_next()
+                self.head = self.head.next
 
             if node == self.tail:
-                self.tail = self.tail.get_previous()
+                self.tail = self.tail.previous
 
             self.remove_node_pointers(node)
 
     @staticmethod
     def remove_node_pointers(node: Node) -> None:
-        if node.get_next():
+        if node.next:
             node.next.previous = node.previous
 
-        if node.get_previous():
+        if node.previous:
             node.previous.next = node.next
 
         node.next = None
@@ -241,6 +238,22 @@ def create_linked_list() -> None:
     7
     8
     9
+    >>> linked_list = LinkedList()
+    >>> linked_list.insert_at_position(position=1, value=10)
+    >>> str(linked_list)
+    '10'
+    >>> linked_list.insert_at_position(position=2, value=20)
+    >>> str(linked_list)
+    '10 20'
+    >>> linked_list.insert_at_position(position=1, value=30)
+    >>> str(linked_list)
+    '30 10 20'
+    >>> linked_list.insert_at_position(position=3, value=40)
+    >>> str(linked_list)
+    '30 10 40 20'
+    >>> linked_list.insert_at_position(position=5, value=50)
+    >>> str(linked_list)
+    '30 10 40 20 50'
     """
 
 
diff --git a/data_structures/linked_list/floyds_cycle_detection.py b/data_structures/linked_list/floyds_cycle_detection.py
new file mode 100644
index 000000000000..6c3f13760260
--- /dev/null
+++ b/data_structures/linked_list/floyds_cycle_detection.py
@@ -0,0 +1,150 @@
+"""
+Floyd's cycle detection algorithm is a popular algorithm used to detect cycles
+in a linked list. It uses two pointers, a slow pointer and a fast pointer,
+to traverse the linked list. The slow pointer moves one node at a time while the fast
+pointer moves two nodes at a time. If there is a cycle in the linked list,
+the fast pointer will eventually catch up to the slow pointer and they will
+meet at the same node. If there is no cycle, the fast pointer will reach the end of
+the linked list and the algorithm will terminate.
+
+For more information: https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare
+"""
+
+from collections.abc import Iterator
+from dataclasses import dataclass
+from typing import Any, Self
+
+
+@dataclass
+class Node:
+    """
+    A class representing a node in a singly linked list.
+    """
+
+    data: Any
+    next_node: Self | None = None
+
+
+@dataclass
+class LinkedList:
+    """
+    A class representing a singly linked list.
+    """
+
+    head: Node | None = None
+
+    def __iter__(self) -> Iterator:
+        """
+        Iterates through the linked list.
+
+        Returns:
+            Iterator: An iterator over the linked list.
+
+        Examples:
+        >>> linked_list = LinkedList()
+        >>> list(linked_list)
+        []
+        >>> linked_list.add_node(1)
+        >>> tuple(linked_list)
+        (1,)
+        """
+        visited = []
+        node = self.head
+        while node:
+            # Avoid infinite loop in there's a cycle
+            if node in visited:
+                return
+            visited.append(node)
+            yield node.data
+            node = node.next_node
+
+    def add_node(self, data: Any) -> None:
+        """
+        Adds a new node to the end of the linked list.
+
+        Args:
+            data (Any): The data to be stored in the new node.
+
+        Examples:
+        >>> linked_list = LinkedList()
+        >>> linked_list.add_node(1)
+        >>> linked_list.add_node(2)
+        >>> linked_list.add_node(3)
+        >>> linked_list.add_node(4)
+        >>> tuple(linked_list)
+        (1, 2, 3, 4)
+        """
+        new_node = Node(data)
+
+        if self.head is None:
+            self.head = new_node
+            return
+
+        current_node = self.head
+        while current_node.next_node is not None:
+            current_node = current_node.next_node
+
+        current_node.next_node = new_node
+
+    def detect_cycle(self) -> bool:
+        """
+        Detects if there is a cycle in the linked list using
+        Floyd's cycle detection algorithm.
+
+        Returns:
+            bool: True if there is a cycle, False otherwise.
+
+        Examples:
+        >>> linked_list = LinkedList()
+        >>> linked_list.add_node(1)
+        >>> linked_list.add_node(2)
+        >>> linked_list.add_node(3)
+        >>> linked_list.add_node(4)
+
+        >>> linked_list.detect_cycle()
+        False
+
+        # Create a cycle in the linked list
+        >>> linked_list.head.next_node.next_node.next_node = linked_list.head.next_node
+
+        >>> linked_list.detect_cycle()
+        True
+        """
+        if self.head is None:
+            return False
+
+        slow_pointer: Node | None = self.head
+        fast_pointer: Node | None = self.head
+
+        while fast_pointer is not None and fast_pointer.next_node is not None:
+            slow_pointer = slow_pointer.next_node if slow_pointer else None
+            fast_pointer = fast_pointer.next_node.next_node
+            if slow_pointer == fast_pointer:
+                return True
+
+        return False
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    linked_list = LinkedList()
+    linked_list.add_node(1)
+    linked_list.add_node(2)
+    linked_list.add_node(3)
+    linked_list.add_node(4)
+
+    # Create a cycle in the linked list
+    # It first checks if the head, next_node, and next_node.next_node attributes of the
+    # linked list are not None to avoid any potential type errors.
+    if (
+        linked_list.head
+        and linked_list.head.next_node
+        and linked_list.head.next_node.next_node
+    ):
+        linked_list.head.next_node.next_node.next_node = linked_list.head.next_node
+
+    has_cycle = linked_list.detect_cycle()
+    print(has_cycle)  # Output: True
diff --git a/data_structures/linked_list/from_sequence.py b/data_structures/linked_list/from_sequence.py
index 94b44f15037f..fa43f4d10e08 100644
--- a/data_structures/linked_list/from_sequence.py
+++ b/data_structures/linked_list/from_sequence.py
@@ -1,4 +1,4 @@
-# Recursive Prorgam to create a Linked List from a sequence and
+# Recursive Program to create a Linked List from a sequence and
 # print a string representation of it.
 
 
diff --git a/data_structures/linked_list/has_loop.py b/data_structures/linked_list/has_loop.py
index bc06ffe150e8..f49e01579adc 100644
--- a/data_structures/linked_list/has_loop.py
+++ b/data_structures/linked_list/has_loop.py
@@ -14,11 +14,11 @@ def __init__(self, data: Any) -> None:
 
     def __iter__(self):
         node = self
-        visited = []
+        visited = set()
         while node:
             if node in visited:
                 raise ContainsLoopError
-            visited.append(node)
+            visited.add(node)
             yield node.data
             node = node.next_node
 
diff --git a/data_structures/linked_list/is_palindrome.py b/data_structures/linked_list/is_palindrome.py
index f949d9a2f201..da788e3e5045 100644
--- a/data_structures/linked_list/is_palindrome.py
+++ b/data_structures/linked_list/is_palindrome.py
@@ -171,11 +171,9 @@ def is_palindrome_dict(head: ListNode | None) -> bool:
         if len(v) % 2 != 0:
             middle += 1
         else:
-            step = 0
-            for i in range(len(v)):
+            for step, i in enumerate(range(len(v))):
                 if v[i] + v[len(v) - 1 - step] != checksum:
                     return False
-                step += 1
         if middle > 1:
             return False
     return True
diff --git a/data_structures/linked_list/merge_two_lists.py b/data_structures/linked_list/merge_two_lists.py
index ca0d3bb48540..e47dbdadcf39 100644
--- a/data_structures/linked_list/merge_two_lists.py
+++ b/data_structures/linked_list/merge_two_lists.py
@@ -1,6 +1,7 @@
 """
 Algorithm that merges two sorted linked lists into one sorted linked list.
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterable, Iterator
diff --git a/data_structures/linked_list/rotate_to_the_right.py b/data_structures/linked_list/rotate_to_the_right.py
index 51b10481c0ce..6b1c54f4be4d 100644
--- a/data_structures/linked_list/rotate_to_the_right.py
+++ b/data_structures/linked_list/rotate_to_the_right.py
@@ -63,7 +63,7 @@ def insert_node(head: Node | None, data: int) -> Node:
     while temp_node.next_node:
         temp_node = temp_node.next_node
 
-    temp_node.next_node = new_node  # type: ignore
+    temp_node.next_node = new_node
     return head
 
 
diff --git a/data_structures/linked_list/skip_list.py b/data_structures/linked_list/skip_list.py
index 4413c53e520e..f21ca70bbc82 100644
--- a/data_structures/linked_list/skip_list.py
+++ b/data_structures/linked_list/skip_list.py
@@ -2,16 +2,18 @@
 Based on "Skip Lists: A Probabilistic Alternative to Balanced Trees" by William Pugh
 https://epaperpress.com/sortsearch/download/skiplist.pdf
 """
+
 from __future__ import annotations
 
+from itertools import pairwise
 from random import random
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 KT = TypeVar("KT")
 VT = TypeVar("VT")
 
 
-class Node(Generic[KT, VT]):
+class Node[KT, VT]:
     def __init__(self, key: KT | str = "root", value: VT | None = None):
         self.key = key
         self.value = value
@@ -47,7 +49,7 @@ def level(self) -> int:
         return len(self.forward)
 
 
-class SkipList(Generic[KT, VT]):
+class SkipList[KT, VT]:
     def __init__(self, p: float = 0.5, max_level: int = 16):
         self.head: Node[KT, VT] = Node[KT, VT]()
         self.level = 0
@@ -388,7 +390,7 @@ def traverse_keys(node):
 
 def test_iter_always_yields_sorted_values():
     def is_sorted(lst):
-        return all(next_item >= item for item, next_item in zip(lst, lst[1:]))
+        return all(next_item >= item for item, next_item in pairwise(lst))
 
     skip_list = SkipList()
     for i in range(10):
diff --git a/data_structures/linked_list/swap_nodes.py b/data_structures/linked_list/swap_nodes.py
index 31dcb02bfa9a..d66512087d2d 100644
--- a/data_structures/linked_list/swap_nodes.py
+++ b/data_structures/linked_list/swap_nodes.py
@@ -1,49 +1,73 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+from dataclasses import dataclass
 from typing import Any
 
 
+@dataclass
 class Node:
-    def __init__(self, data: Any) -> None:
-        """
-        Initialize a new Node with the given data.
-
-        Args:
-            data: The data to be stored in the node.
-
-        """
-        self.data = data
-        self.next: Node | None = None  # Reference to the next node
+    data: Any
+    next_node: Node | None = None
 
 
+@dataclass
 class LinkedList:
-    def __init__(self) -> None:
+    head: Node | None = None
+
+    def __iter__(self) -> Iterator:
         """
-        Initialize an empty Linked List.
+        >>> linked_list = LinkedList()
+        >>> list(linked_list)
+        []
+        >>> linked_list.push(0)
+        >>> tuple(linked_list)
+        (0,)
         """
-        self.head: Node | None = None  # Reference to the head (first node)
+        node = self.head
+        while node:
+            yield node.data
+            node = node.next_node
 
-    def print_list(self):
+    def __len__(self) -> int:
         """
-        Print the elements of the Linked List in order.
+        >>> linked_list = LinkedList()
+        >>> len(linked_list)
+        0
+        >>> linked_list.push(0)
+        >>> len(linked_list)
+        1
         """
-        temp = self.head
-        while temp is not None:
-            print(temp.data, end=" ")
-            temp = temp.next
-        print()
+        return sum(1 for _ in self)
 
     def push(self, new_data: Any) -> None:
         """
         Add a new node with the given data to the beginning of the Linked List.
+
         Args:
             new_data (Any): The data to be added to the new node.
+
+        Returns:
+            None
+
+        Examples:
+            >>> linked_list = LinkedList()
+            >>> linked_list.push(5)
+            >>> linked_list.push(4)
+            >>> linked_list.push(3)
+            >>> linked_list.push(2)
+            >>> linked_list.push(1)
+            >>> list(linked_list)
+            [1, 2, 3, 4, 5]
         """
         new_node = Node(new_data)
-        new_node.next = self.head
+        new_node.next_node = self.head
         self.head = new_node
 
-    def swap_nodes(self, node_data_1, node_data_2) -> None:
+    def swap_nodes(self, node_data_1: Any, node_data_2: Any) -> None:
         """
         Swap the positions of two nodes in the Linked List based on their data values.
+
         Args:
             node_data_1: Data value of the first node to be swapped.
             node_data_2: Data value of the second node to be swapped.
@@ -51,34 +75,74 @@ def swap_nodes(self, node_data_1, node_data_2) -> None:
 
         Note:
             If either of the specified data values isn't found then, no swapping occurs.
+
+        Examples:
+        When both values are present in a linked list.
+            >>> linked_list = LinkedList()
+            >>> linked_list.push(5)
+            >>> linked_list.push(4)
+            >>> linked_list.push(3)
+            >>> linked_list.push(2)
+            >>> linked_list.push(1)
+            >>> list(linked_list)
+            [1, 2, 3, 4, 5]
+            >>> linked_list.swap_nodes(1, 5)
+            >>> tuple(linked_list)
+            (5, 2, 3, 4, 1)
+
+        When one value is present and the other isn't in the linked list.
+            >>> second_list = LinkedList()
+            >>> second_list.push(6)
+            >>> second_list.push(7)
+            >>> second_list.push(8)
+            >>> second_list.push(9)
+            >>> second_list.swap_nodes(1, 6) is None
+            True
+
+        When both values are absent in the linked list.
+            >>> second_list = LinkedList()
+            >>> second_list.push(10)
+            >>> second_list.push(9)
+            >>> second_list.push(8)
+            >>> second_list.push(7)
+            >>> second_list.swap_nodes(1, 3) is None
+            True
+
+        When linkedlist is empty.
+            >>> second_list = LinkedList()
+            >>> second_list.swap_nodes(1, 3) is None
+            True
+
+        Returns:
+            None
         """
         if node_data_1 == node_data_2:
             return
-        else:
-            node_1 = self.head
-            while node_1 is not None and node_1.data != node_data_1:
-                node_1 = node_1.next
-
-            node_2 = self.head
-            while node_2 is not None and node_2.data != node_data_2:
-                node_2 = node_2.next
-
-            if node_1 is None or node_2 is None:
-                return
 
-            # Swap the data values of the two nodes
-            node_1.data, node_2.data = node_2.data, node_1.data
+        node_1 = self.head
+        while node_1 and node_1.data != node_data_1:
+            node_1 = node_1.next_node
+        node_2 = self.head
+        while node_2 and node_2.data != node_data_2:
+            node_2 = node_2.next_node
+        if node_1 is None or node_2 is None:
+            return
+        # Swap the data values of the two nodes
+        node_1.data, node_2.data = node_2.data, node_1.data
 
 
 if __name__ == "__main__":
-    ll = LinkedList()
-    for i in range(5, 0, -1):
-        ll.push(i)
+    """
+    Python script that outputs the swap of nodes in a linked list.
+    """
+    from doctest import testmod
 
-    print("Original Linked List:")
-    ll.print_list()
-
-    ll.swap_nodes(1, 4)
-    print("After swapping the nodes whose data is 1 and 4:")
+    testmod()
+    linked_list = LinkedList()
+    for i in range(5, 0, -1):
+        linked_list.push(i)
 
-    ll.print_list()
+    print(f"Original Linked List: {list(linked_list)}")
+    linked_list.swap_nodes(1, 4)
+    print(f"Modified Linked List: {list(linked_list)}")
+    print("After swapping the nodes whose data is 1 and 4.")
diff --git a/data_structures/queues/__init__.py b/data_structures/queues/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/data_structures/queue/circular_queue.py b/data_structures/queues/circular_queue.py
similarity index 79%
rename from data_structures/queue/circular_queue.py
rename to data_structures/queues/circular_queue.py
index 93a6ef805c7c..efbf1efdc42d 100644
--- a/data_structures/queue/circular_queue.py
+++ b/data_structures/queues/circular_queue.py
@@ -17,7 +17,9 @@ def __len__(self) -> int:
         >>> len(cq)
         0
         >>> cq.enqueue("A")  # doctest: +ELLIPSIS
-        <data_structures.queue.circular_queue.CircularQueue object at ...
+        <data_structures.queues.circular_queue.CircularQueue object at ...
+        >>> cq.array
+        ['A', None, None, None, None]
         >>> len(cq)
         1
         """
@@ -25,6 +27,7 @@ def __len__(self) -> int:
 
     def is_empty(self) -> bool:
         """
+        Checks whether the queue is empty or not
         >>> cq = CircularQueue(5)
         >>> cq.is_empty()
         True
@@ -35,6 +38,7 @@ def is_empty(self) -> bool:
 
     def first(self):
         """
+        Returns the first element of the queue
         >>> cq = CircularQueue(5)
         >>> cq.first()
         False
@@ -45,14 +49,17 @@ def first(self):
 
     def enqueue(self, data):
         """
-        This function insert an element in the queue using self.rear value as an index
+        This function inserts an element at the end of the queue using self.rear value
+        as an index.
         >>> cq = CircularQueue(5)
         >>> cq.enqueue("A")  # doctest: +ELLIPSIS
-        <data_structures.queue.circular_queue.CircularQueue object at ...
+        <data_structures.queues.circular_queue.CircularQueue object at ...
         >>> (cq.size, cq.first())
         (1, 'A')
         >>> cq.enqueue("B")  # doctest: +ELLIPSIS
-        <data_structures.queue.circular_queue.CircularQueue object at ...
+        <data_structures.queues.circular_queue.CircularQueue object at ...
+        >>> cq.array
+        ['A', 'B', None, None, None]
         >>> (cq.size, cq.first())
         (2, 'A')
         """
@@ -67,7 +74,7 @@ def enqueue(self, data):
     def dequeue(self):
         """
         This function removes an element from the queue using on self.front value as an
-        index
+        index and returns it
         >>> cq = CircularQueue(5)
         >>> cq.dequeue()
         Traceback (most recent call last):
diff --git a/data_structures/queue/circular_queue_linked_list.py b/data_structures/queues/circular_queue_linked_list.py
similarity index 98%
rename from data_structures/queue/circular_queue_linked_list.py
rename to data_structures/queues/circular_queue_linked_list.py
index 62042c4bce96..da8629678e52 100644
--- a/data_structures/queue/circular_queue_linked_list.py
+++ b/data_structures/queues/circular_queue_linked_list.py
@@ -39,7 +39,7 @@ def create_linked_list(self, initial_capacity: int) -> None:
 
     def is_empty(self) -> bool:
         """
-        Checks where the queue is empty or not
+        Checks whether the queue is empty or not
         >>> cq = CircularQueueLinkedList()
         >>> cq.is_empty()
         True
diff --git a/data_structures/queue/double_ended_queue.py b/data_structures/queues/double_ended_queue.py
similarity index 99%
rename from data_structures/queue/double_ended_queue.py
rename to data_structures/queues/double_ended_queue.py
index 17a23038d288..c28d46c65168 100644
--- a/data_structures/queue/double_ended_queue.py
+++ b/data_structures/queues/double_ended_queue.py
@@ -1,6 +1,7 @@
 """
 Implementation of double ended queue.
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterable
@@ -32,7 +33,7 @@ class Deque:
         the number of nodes
     """
 
-    __slots__ = ("_front", "_back", "_len")
+    __slots__ = ("_back", "_front", "_len")
 
     @dataclass
     class _Node:
diff --git a/data_structures/queue/linked_queue.py b/data_structures/queues/linked_queue.py
similarity index 98%
rename from data_structures/queue/linked_queue.py
rename to data_structures/queues/linked_queue.py
index 3af97d28e4f7..80f6d309af9a 100644
--- a/data_structures/queue/linked_queue.py
+++ b/data_structures/queues/linked_queue.py
@@ -1,4 +1,5 @@
-""" A Queue using a linked list like structure """
+"""A Queue using a linked list like structure"""
+
 from __future__ import annotations
 
 from collections.abc import Iterator
diff --git a/data_structures/queue/priority_queue_using_list.py b/data_structures/queues/priority_queue_using_list.py
similarity index 96%
rename from data_structures/queue/priority_queue_using_list.py
rename to data_structures/queues/priority_queue_using_list.py
index f61b5e8e664d..15e56c557069 100644
--- a/data_structures/queue/priority_queue_using_list.py
+++ b/data_structures/queues/priority_queue_using_list.py
@@ -59,12 +59,12 @@ class FixedPriorityQueue:
     >>> fpq.dequeue()
     Traceback (most recent call last):
         ...
-    data_structures.queue.priority_queue_using_list.UnderFlowError: All queues are empty
+    data_structures.queues.priority_queue_using_list.UnderFlowError: All queues are empty
     >>> print(fpq)
     Priority 0: []
     Priority 1: []
     Priority 2: []
-    """
+    """  # noqa: E501
 
     def __init__(self):
         self.queues = [
@@ -141,7 +141,7 @@ class ElementPriorityQueue:
     >>> epq.dequeue()
     Traceback (most recent call last):
         ...
-    data_structures.queue.priority_queue_using_list.UnderFlowError: The queue is empty
+    data_structures.queues.priority_queue_using_list.UnderFlowError: The queue is empty
     >>> print(epq)
     []
     """
diff --git a/data_structures/queue/queue_by_list.py b/data_structures/queues/queue_by_list.py
similarity index 90%
rename from data_structures/queue/queue_by_list.py
rename to data_structures/queues/queue_by_list.py
index 4b05be9fd08e..182cc4147c47 100644
--- a/data_structures/queue/queue_by_list.py
+++ b/data_structures/queues/queue_by_list.py
@@ -1,13 +1,10 @@
 """Queue represented by a Python list"""
 
 from collections.abc import Iterable
-from typing import Generic, TypeVar
 
-_T = TypeVar("_T")
 
-
-class QueueByList(Generic[_T]):
-    def __init__(self, iterable: Iterable[_T] | None = None) -> None:
+class QueueByList[T]:
+    def __init__(self, iterable: Iterable[T] | None = None) -> None:
         """
         >>> QueueByList()
         Queue(())
@@ -16,7 +13,7 @@ def __init__(self, iterable: Iterable[_T] | None = None) -> None:
         >>> QueueByList((i**2 for i in range(1, 4)))
         Queue((1, 4, 9))
         """
-        self.entries: list[_T] = list(iterable or [])
+        self.entries: list[T] = list(iterable or [])
 
     def __len__(self) -> int:
         """
@@ -58,7 +55,7 @@ def __repr__(self) -> str:
 
         return f"Queue({tuple(self.entries)})"
 
-    def put(self, item: _T) -> None:
+    def put(self, item: T) -> None:
         """Put `item` to the Queue
 
         >>> queue = QueueByList()
@@ -72,7 +69,7 @@ def put(self, item: _T) -> None:
 
         self.entries.append(item)
 
-    def get(self) -> _T:
+    def get(self) -> T:
         """
         Get `item` from the Queue
 
@@ -118,7 +115,7 @@ def rotate(self, rotation: int) -> None:
         for _ in range(rotation):
             put(get(0))
 
-    def get_front(self) -> _T:
+    def get_front(self) -> T:
         """Get the front item from the Queue
 
         >>> queue = QueueByList((10, 20, 30))
diff --git a/data_structures/queue/queue_by_two_stacks.py b/data_structures/queues/queue_by_two_stacks.py
similarity index 88%
rename from data_structures/queue/queue_by_two_stacks.py
rename to data_structures/queues/queue_by_two_stacks.py
index cd62f155a63b..f9e302ffcedd 100644
--- a/data_structures/queue/queue_by_two_stacks.py
+++ b/data_structures/queues/queue_by_two_stacks.py
@@ -1,13 +1,10 @@
 """Queue implementation using two stacks"""
 
 from collections.abc import Iterable
-from typing import Generic, TypeVar
 
-_T = TypeVar("_T")
 
-
-class QueueByTwoStacks(Generic[_T]):
-    def __init__(self, iterable: Iterable[_T] | None = None) -> None:
+class QueueByTwoStacks[T]:
+    def __init__(self, iterable: Iterable[T] | None = None) -> None:
         """
         >>> QueueByTwoStacks()
         Queue(())
@@ -16,8 +13,8 @@ def __init__(self, iterable: Iterable[_T] | None = None) -> None:
         >>> QueueByTwoStacks((i**2 for i in range(1, 4)))
         Queue((1, 4, 9))
         """
-        self._stack1: list[_T] = list(iterable or [])
-        self._stack2: list[_T] = []
+        self._stack1: list[T] = list(iterable or [])
+        self._stack2: list[T] = []
 
     def __len__(self) -> int:
         """
@@ -59,7 +56,7 @@ def __repr__(self) -> str:
         """
         return f"Queue({tuple(self._stack2[::-1] + self._stack1)})"
 
-    def put(self, item: _T) -> None:
+    def put(self, item: T) -> None:
         """
         Put `item` into the Queue
 
@@ -74,7 +71,7 @@ def put(self, item: _T) -> None:
 
         self._stack1.append(item)
 
-    def get(self) -> _T:
+    def get(self) -> T:
         """
         Get `item` from the Queue
 
diff --git a/data_structures/queue/queue_on_pseudo_stack.py b/data_structures/queues/queue_on_pseudo_stack.py
similarity index 96%
rename from data_structures/queue/queue_on_pseudo_stack.py
rename to data_structures/queues/queue_on_pseudo_stack.py
index d9845100008e..2da67ecc263c 100644
--- a/data_structures/queue/queue_on_pseudo_stack.py
+++ b/data_structures/queues/queue_on_pseudo_stack.py
@@ -1,4 +1,5 @@
 """Queue represented by a pseudo stack (represented by a list with pop and append)"""
+
 from typing import Any
 
 
diff --git a/data_structures/stacks/balanced_parentheses.py b/data_structures/stacks/balanced_parentheses.py
index 3c036c220e5c..928815bb2111 100644
--- a/data_structures/stacks/balanced_parentheses.py
+++ b/data_structures/stacks/balanced_parentheses.py
@@ -19,9 +19,10 @@ def balanced_parentheses(parentheses: str) -> bool:
     for bracket in parentheses:
         if bracket in bracket_pairs:
             stack.push(bracket)
-        elif bracket in (")", "]", "}"):
-            if stack.is_empty() or bracket_pairs[stack.pop()] != bracket:
-                return False
+        elif bracket in (")", "]", "}") and (
+            stack.is_empty() or bracket_pairs[stack.pop()] != bracket
+        ):
+            return False
     return stack.is_empty()
 
 
diff --git a/data_structures/stacks/dijkstras_two_stack_algorithm.py b/data_structures/stacks/dijkstras_two_stack_algorithm.py
index 976c9a53c931..94d19156f1c3 100644
--- a/data_structures/stacks/dijkstras_two_stack_algorithm.py
+++ b/data_structures/stacks/dijkstras_two_stack_algorithm.py
@@ -29,6 +29,7 @@
 
 NOTE:   It only works with whole numbers.
 """
+
 __author__ = "Alexander Joslin"
 
 import operator as op
diff --git a/data_structures/stacks/infix_to_prefix_conversion.py b/data_structures/stacks/infix_to_prefix_conversion.py
index beff421c0cfa..878473b93c19 100644
--- a/data_structures/stacks/infix_to_prefix_conversion.py
+++ b/data_structures/stacks/infix_to_prefix_conversion.py
@@ -95,13 +95,12 @@ def infix_2_postfix(infix: str) -> str:
             while stack[-1] != "(":
                 post_fix.append(stack.pop())  # Pop stack & add the content to Postfix
             stack.pop()
-        else:
-            if len(stack) == 0:
-                stack.append(x)  # If stack is empty, push x to stack
-            else:  # while priority of x is not > priority of element in the stack
-                while stack and stack[-1] != "(" and priority[x] <= priority[stack[-1]]:
-                    post_fix.append(stack.pop())  # pop stack & add to Postfix
-                stack.append(x)  # push x to stack
+        elif len(stack) == 0:
+            stack.append(x)  # If stack is empty, push x to stack
+        else:  # while priority of x is not > priority of element in the stack
+            while stack and stack[-1] != "(" and priority[x] <= priority[stack[-1]]:
+                post_fix.append(stack.pop())  # pop stack & add to Postfix
+            stack.append(x)  # push x to stack
 
         print(
             x.center(8),
diff --git a/data_structures/stacks/largest_rectangle_histogram.py b/data_structures/stacks/largest_rectangle_histogram.py
new file mode 100644
index 000000000000..7575bd9f628d
--- /dev/null
+++ b/data_structures/stacks/largest_rectangle_histogram.py
@@ -0,0 +1,39 @@
+def largest_rectangle_area(heights: list[int]) -> int:
+    """
+    Inputs an array of integers representing the heights of bars,
+    and returns the area of the largest rectangle that can be formed
+
+    >>> largest_rectangle_area([2, 1, 5, 6, 2, 3])
+    10
+
+    >>> largest_rectangle_area([2, 4])
+    4
+
+    >>> largest_rectangle_area([6, 2, 5, 4, 5, 1, 6])
+    12
+
+    >>> largest_rectangle_area([1])
+    1
+    """
+    stack: list[int] = []
+    max_area = 0
+    heights = [*heights, 0]  # make a new list by appending the sentinel 0
+    n = len(heights)
+
+    for i in range(n):
+        # make sure the stack remains in increasing order
+        while stack and heights[i] < heights[stack[-1]]:
+            h = heights[stack.pop()]  # height of the bar
+            # if stack is empty, it means entire width can be taken from index 0 to i-1
+            w = i if not stack else i - stack[-1] - 1  # calculate width
+            max_area = max(max_area, h * w)
+
+        stack.append(i)
+
+    return max_area
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/stacks/lexicographical_numbers.py b/data_structures/stacks/lexicographical_numbers.py
new file mode 100644
index 000000000000..6a174e7d9e95
--- /dev/null
+++ b/data_structures/stacks/lexicographical_numbers.py
@@ -0,0 +1,38 @@
+from collections.abc import Iterator
+
+
+def lexical_order(max_number: int) -> Iterator[int]:
+    """
+    Generate numbers in lexical order from 1 to max_number.
+
+    >>> " ".join(map(str, lexical_order(13)))
+    '1 10 11 12 13 2 3 4 5 6 7 8 9'
+    >>> list(lexical_order(1))
+    [1]
+    >>> " ".join(map(str, lexical_order(20)))
+    '1 10 11 12 13 14 15 16 17 18 19 2 20 3 4 5 6 7 8 9'
+    >>> " ".join(map(str, lexical_order(25)))
+    '1 10 11 12 13 14 15 16 17 18 19 2 20 21 22 23 24 25 3 4 5 6 7 8 9'
+    >>> list(lexical_order(12))
+    [1, 10, 11, 12, 2, 3, 4, 5, 6, 7, 8, 9]
+    """
+
+    stack = [1]
+
+    while stack:
+        num = stack.pop()
+        if num > max_number:
+            continue
+
+        yield num
+        if (num % 10) != 9:
+            stack.append(num + 1)
+
+        stack.append(num * 10)
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    print(f"Numbers from 1 to 25 in lexical order: {list(lexical_order(26))}")
diff --git a/data_structures/stacks/next_greater_element.py b/data_structures/stacks/next_greater_element.py
index 7d76d1f47dfa..216850b4b894 100644
--- a/data_structures/stacks/next_greater_element.py
+++ b/data_structures/stacks/next_greater_element.py
@@ -6,9 +6,20 @@
 
 def next_greatest_element_slow(arr: list[float]) -> list[float]:
     """
-    Get the Next Greatest Element (NGE) for all elements in a list.
-    Maximum element present after the current one which is also greater than the
-    current one.
+    Get the Next Greatest Element (NGE) for each element in the array
+    by checking all subsequent elements to find the next greater one.
+
+    This is a brute-force implementation, and it has a time complexity
+    of O(n^2), where n is the size of the array.
+
+    Args:
+        arr: List of numbers for which the NGE is calculated.
+
+    Returns:
+        List containing the next greatest elements. If no
+        greater element is found, -1 is placed in the result.
+
+    Example:
     >>> next_greatest_element_slow(arr) == expect
     True
     """
@@ -28,9 +39,21 @@ def next_greatest_element_slow(arr: list[float]) -> list[float]:
 
 def next_greatest_element_fast(arr: list[float]) -> list[float]:
     """
-    Like next_greatest_element_slow() but changes the loops to use
-    enumerate() instead of range(len()) for the outer loop and
-    for in a slice of arr for the inner loop.
+    Find the Next Greatest Element (NGE) for each element in the array
+    using a more readable approach. This implementation utilizes
+    enumerate() for the outer loop and slicing for the inner loop.
+
+    While this improves readability over next_greatest_element_slow(),
+    it still has a time complexity of O(n^2).
+
+    Args:
+        arr: List of numbers for which the NGE is calculated.
+
+    Returns:
+        List containing the next greatest elements. If no
+        greater element is found, -1 is placed in the result.
+
+    Example:
     >>> next_greatest_element_fast(arr) == expect
     True
     """
@@ -47,14 +70,23 @@ def next_greatest_element_fast(arr: list[float]) -> list[float]:
 
 def next_greatest_element(arr: list[float]) -> list[float]:
     """
-    Get the Next Greatest Element (NGE) for all elements in a list.
-    Maximum element present after the current one which is also greater than the
-    current one.
-
-    A naive way to solve this is to take two loops and check for the next bigger
-    number but that will make the time complexity as O(n^2). The better way to solve
-    this would be to use a stack to keep track of maximum number giving a linear time
-    solution.
+    Efficient solution to find the Next Greatest Element (NGE) for all elements
+    using a stack. The time complexity is reduced to O(n), making it suitable
+    for larger arrays.
+
+    The stack keeps track of elements for which the next greater element hasn't
+    been found yet. By iterating through the array in reverse (from the last
+    element to the first), the stack is used to efficiently determine the next
+    greatest element for each element.
+
+    Args:
+        arr: List of numbers for which the NGE is calculated.
+
+    Returns:
+        List containing the next greatest elements. If no
+        greater element is found, -1 is placed in the result.
+
+    Example:
     >>> next_greatest_element(arr) == expect
     True
     """
diff --git a/data_structures/stacks/prefix_evaluation.py b/data_structures/stacks/prefix_evaluation.py
index f48eca23d7b5..03a70d884725 100644
--- a/data_structures/stacks/prefix_evaluation.py
+++ b/data_structures/stacks/prefix_evaluation.py
@@ -1,8 +1,9 @@
 """
-Python3 program to evaluate a prefix expression.
+Program to evaluate a prefix expression.
+https://en.wikipedia.org/wiki/Polish_notation
 """
 
-calc = {
+operators = {
     "+": lambda x, y: x + y,
     "-": lambda x, y: x - y,
     "*": lambda x, y: x * y,
@@ -31,6 +32,10 @@ def evaluate(expression):
     21
     >>> evaluate("/ * 10 2 + 4 1 ")
     4.0
+    >>> evaluate("2")
+    2
+    >>> evaluate("+ * 2 3 / 8 4")
+    8.0
     """
     stack = []
 
@@ -45,11 +50,39 @@ def evaluate(expression):
             # push the result onto the stack again
             o1 = stack.pop()
             o2 = stack.pop()
-            stack.append(calc[c](o1, o2))
+            stack.append(operators[c](o1, o2))
 
     return stack.pop()
 
 
+def evaluate_recursive(expression: list[str]):
+    """
+    Alternative recursive implementation
+
+    >>> evaluate_recursive(['2'])
+    2
+    >>> expression = ['+', '*', '2', '3', '/', '8', '4']
+    >>> evaluate_recursive(expression)
+    8.0
+    >>> expression
+    []
+    >>> evaluate_recursive(['+', '9', '*', '2', '6'])
+    21
+    >>> evaluate_recursive(['/', '*', '10', '2', '+', '4', '1'])
+    4.0
+    """
+
+    op = expression.pop(0)
+    if is_operand(op):
+        return int(op)
+
+    operation = operators[op]
+
+    a = evaluate_recursive(expression)
+    b = evaluate_recursive(expression)
+    return operation(a, b)
+
+
 # Driver code
 if __name__ == "__main__":
     test_expression = "+ 9 * 2 6"
diff --git a/data_structures/stacks/stack.py b/data_structures/stacks/stack.py
index a14f4648a399..3ffa32d4167f 100644
--- a/data_structures/stacks/stack.py
+++ b/data_structures/stacks/stack.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
@@ -13,7 +13,7 @@ class StackUnderflowError(BaseException):
     pass
 
 
-class Stack(Generic[T]):
+class Stack[T]:
     """A stack is an abstract data type that serves as a collection of
     elements with two principal operations: push() and pop(). push() adds an
     element to the top of the stack, and pop() removes an element from the top
@@ -33,7 +33,23 @@ def __str__(self) -> str:
         return str(self.stack)
 
     def push(self, data: T) -> None:
-        """Push an element to the top of the stack."""
+        """
+        Push an element to the top of the stack.
+
+        >>> S = Stack(2) # stack size = 2
+        >>> S.push(10)
+        >>> S.push(20)
+        >>> print(S)
+        [10, 20]
+
+        >>> S = Stack(1) # stack size = 1
+        >>> S.push(10)
+        >>> S.push(20)
+        Traceback (most recent call last):
+        ...
+        data_structures.stacks.stack.StackOverflowError
+
+        """
         if len(self.stack) >= self.limit:
             raise StackOverflowError
         self.stack.append(data)
@@ -42,6 +58,12 @@ def pop(self) -> T:
         """
         Pop an element off of the top of the stack.
 
+        >>> S = Stack()
+        >>> S.push(-5)
+        >>> S.push(10)
+        >>> S.pop()
+        10
+
         >>> Stack().pop()
         Traceback (most recent call last):
             ...
@@ -55,7 +77,13 @@ def peek(self) -> T:
         """
         Peek at the top-most element of the stack.
 
-        >>> Stack().pop()
+        >>> S = Stack()
+        >>> S.push(-5)
+        >>> S.push(10)
+        >>> S.peek()
+        10
+
+        >>> Stack().peek()
         Traceback (most recent call last):
             ...
         data_structures.stacks.stack.StackUnderflowError
@@ -65,18 +93,68 @@ def peek(self) -> T:
         return self.stack[-1]
 
     def is_empty(self) -> bool:
-        """Check if a stack is empty."""
+        """
+        Check if a stack is empty.
+
+        >>> S = Stack()
+        >>> S.is_empty()
+        True
+
+        >>> S = Stack()
+        >>> S.push(10)
+        >>> S.is_empty()
+        False
+        """
         return not bool(self.stack)
 
     def is_full(self) -> bool:
+        """
+        >>> S = Stack()
+        >>> S.is_full()
+        False
+
+        >>> S = Stack(1)
+        >>> S.push(10)
+        >>> S.is_full()
+        True
+        """
         return self.size() == self.limit
 
     def size(self) -> int:
-        """Return the size of the stack."""
+        """
+        Return the size of the stack.
+
+        >>> S = Stack(3)
+        >>> S.size()
+        0
+
+        >>> S = Stack(3)
+        >>> S.push(10)
+        >>> S.size()
+        1
+
+        >>> S = Stack(3)
+        >>> S.push(10)
+        >>> S.push(20)
+        >>> S.size()
+        2
+        """
         return len(self.stack)
 
     def __contains__(self, item: T) -> bool:
-        """Check if item is in stack"""
+        """
+        Check if item is in stack
+
+        >>> S = Stack(3)
+        >>> S.push(10)
+        >>> 10 in S
+        True
+
+        >>> S = Stack(3)
+        >>> S.push(10)
+        >>> 20 in S
+        False
+        """
         return item in self.stack
 
 
@@ -131,3 +209,7 @@ def test_stack() -> None:
 
 if __name__ == "__main__":
     test_stack()
+
+    import doctest
+
+    doctest.testmod()
diff --git a/data_structures/stacks/stack_with_doubly_linked_list.py b/data_structures/stacks/stack_with_doubly_linked_list.py
index 50c5236e073c..ad01cd7eb6aa 100644
--- a/data_structures/stacks/stack_with_doubly_linked_list.py
+++ b/data_structures/stacks/stack_with_doubly_linked_list.py
@@ -3,19 +3,19 @@
 
 from __future__ import annotations
 
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
 
-class Node(Generic[T]):
+class Node[T]:
     def __init__(self, data: T):
         self.data = data  # Assign data
         self.next: Node[T] | None = None  # Initialize next as null
         self.prev: Node[T] | None = None  # Initialize prev as null
 
 
-class Stack(Generic[T]):
+class Stack[T]:
     """
     >>> stack = Stack()
     >>> stack.is_empty()
diff --git a/data_structures/stacks/stack_with_singly_linked_list.py b/data_structures/stacks/stack_with_singly_linked_list.py
index f5ce83b863ce..57a68679eee1 100644
--- a/data_structures/stacks/stack_with_singly_linked_list.py
+++ b/data_structures/stacks/stack_with_singly_linked_list.py
@@ -1,13 +1,14 @@
-""" A Stack using a linked list like structure """
+"""A Stack using a linked list like structure"""
+
 from __future__ import annotations
 
 from collections.abc import Iterator
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
 
-class Node(Generic[T]):
+class Node[T]:
     def __init__(self, data: T):
         self.data = data
         self.next: Node[T] | None = None
@@ -16,7 +17,7 @@ def __str__(self) -> str:
         return f"{self.data}"
 
 
-class LinkedStack(Generic[T]):
+class LinkedStack[T]:
     """
     Linked List Stack implementing push (to top),
     pop (from top) and is_empty
diff --git a/data_structures/suffix_tree/__init__.py b/data_structures/suffix_tree/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/data_structures/suffix_tree/example/__init__.py b/data_structures/suffix_tree/example/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/data_structures/suffix_tree/example/example_usage.py b/data_structures/suffix_tree/example/example_usage.py
new file mode 100644
index 000000000000..724ac57e8bfb
--- /dev/null
+++ b/data_structures/suffix_tree/example/example_usage.py
@@ -0,0 +1,37 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11554
+#  https://github.com/TheAlgorithms/Python/pull/11554
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+from data_structures.suffix_tree.suffix_tree import SuffixTree
+
+
+def main() -> None:
+    """
+    Demonstrate the usage of the SuffixTree class.
+
+    - Initializes a SuffixTree with a predefined text.
+    - Defines a list of patterns to search for within the suffix tree.
+    - Searches for each pattern in the suffix tree.
+
+    Patterns tested:
+        - "ana" (found) --> True
+        - "ban" (found) --> True
+        - "na" (found) --> True
+        - "xyz" (not found) --> False
+        - "mon" (found) --> True
+    """
+    text = "monkey banana"
+    suffix_tree = SuffixTree(text)
+
+    patterns = ["ana", "ban", "na", "xyz", "mon"]
+    for pattern in patterns:
+        found = suffix_tree.search(pattern)
+        print(f"Pattern '{pattern}' found: {found}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/data_structures/suffix_tree/suffix_tree.py b/data_structures/suffix_tree/suffix_tree.py
new file mode 100644
index 000000000000..ad54fb0ba009
--- /dev/null
+++ b/data_structures/suffix_tree/suffix_tree.py
@@ -0,0 +1,66 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11554
+#  https://github.com/TheAlgorithms/Python/pull/11554
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+from data_structures.suffix_tree.suffix_tree_node import SuffixTreeNode
+
+
+class SuffixTree:
+    def __init__(self, text: str) -> None:
+        """
+        Initializes the suffix tree with the given text.
+
+        Args:
+            text (str): The text for which the suffix tree is to be built.
+        """
+        self.text: str = text
+        self.root: SuffixTreeNode = SuffixTreeNode()
+        self.build_suffix_tree()
+
+    def build_suffix_tree(self) -> None:
+        """
+        Builds the suffix tree for the given text by adding all suffixes.
+        """
+        text = self.text
+        n = len(text)
+        for i in range(n):
+            suffix = text[i:]
+            self._add_suffix(suffix, i)
+
+    def _add_suffix(self, suffix: str, index: int) -> None:
+        """
+        Adds a suffix to the suffix tree.
+
+        Args:
+            suffix (str): The suffix to add.
+            index (int): The starting index of the suffix in the original text.
+        """
+        node = self.root
+        for char in suffix:
+            if char not in node.children:
+                node.children[char] = SuffixTreeNode()
+            node = node.children[char]
+        node.is_end_of_string = True
+        node.start = index
+        node.end = index + len(suffix) - 1
+
+    def search(self, pattern: str) -> bool:
+        """
+        Searches for a pattern in the suffix tree.
+
+        Args:
+            pattern (str): The pattern to search for.
+
+        Returns:
+            bool: True if the pattern is found, False otherwise.
+        """
+        node = self.root
+        for char in pattern:
+            if char not in node.children:
+                return False
+            node = node.children[char]
+        return True
diff --git a/data_structures/suffix_tree/suffix_tree_node.py b/data_structures/suffix_tree/suffix_tree_node.py
new file mode 100644
index 000000000000..e5b628645063
--- /dev/null
+++ b/data_structures/suffix_tree/suffix_tree_node.py
@@ -0,0 +1,36 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11554
+#  https://github.com/TheAlgorithms/Python/pull/11554
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+from __future__ import annotations
+
+
+class SuffixTreeNode:
+    def __init__(
+        self,
+        children: dict[str, SuffixTreeNode] | None = None,
+        is_end_of_string: bool = False,
+        start: int | None = None,
+        end: int | None = None,
+        suffix_link: SuffixTreeNode | None = None,
+    ) -> None:
+        """
+        Initializes a suffix tree node.
+
+        Parameters:
+            children (dict[str, SuffixTreeNode] | None): The children of this node.
+            is_end_of_string (bool): Indicates if this node represents
+                                     the end of a string.
+            start (int | None): The start index of the suffix in the text.
+            end (int | None): The end index of the suffix in the text.
+            suffix_link (SuffixTreeNode | None): Link to another suffix tree node.
+        """
+        self.children = children or {}
+        self.is_end_of_string = is_end_of_string
+        self.start = start
+        self.end = end
+        self.suffix_link = suffix_link
diff --git a/data_structures/suffix_tree/tests/__init__.py b/data_structures/suffix_tree/tests/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/data_structures/suffix_tree/tests/test_suffix_tree.py b/data_structures/suffix_tree/tests/test_suffix_tree.py
new file mode 100644
index 000000000000..c9dbe199d19d
--- /dev/null
+++ b/data_structures/suffix_tree/tests/test_suffix_tree.py
@@ -0,0 +1,59 @@
+#  Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed)
+#  in Pull Request: #11554
+#  https://github.com/TheAlgorithms/Python/pull/11554
+#
+#  Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request
+#  addressing bugs/corrections to this file.
+#  Thank you!
+
+import unittest
+
+from data_structures.suffix_tree.suffix_tree import SuffixTree
+
+
+class TestSuffixTree(unittest.TestCase):
+    def setUp(self) -> None:
+        """Set up the initial conditions for each test."""
+        self.text = "banana"
+        self.suffix_tree = SuffixTree(self.text)
+
+    def test_search_existing_patterns(self) -> None:
+        """Test searching for patterns that exist in the suffix tree."""
+        patterns = ["ana", "ban", "na"]
+        for pattern in patterns:
+            with self.subTest(pattern=pattern):
+                assert self.suffix_tree.search(pattern), (
+                    f"Pattern '{pattern}' should be found."
+                )
+
+    def test_search_non_existing_patterns(self) -> None:
+        """Test searching for patterns that do not exist in the suffix tree."""
+        patterns = ["xyz", "apple", "cat"]
+        for pattern in patterns:
+            with self.subTest(pattern=pattern):
+                assert not self.suffix_tree.search(pattern), (
+                    f"Pattern '{pattern}' should not be found."
+                )
+
+    def test_search_empty_pattern(self) -> None:
+        """Test searching for an empty pattern."""
+        assert self.suffix_tree.search(""), "An empty pattern should be found."
+
+    def test_search_full_text(self) -> None:
+        """Test searching for the full text."""
+        assert self.suffix_tree.search(self.text), (
+            "The full text should be found in the suffix tree."
+        )
+
+    def test_search_substrings(self) -> None:
+        """Test searching for substrings of the full text."""
+        substrings = ["ban", "ana", "a", "na"]
+        for substring in substrings:
+            with self.subTest(substring=substring):
+                assert self.suffix_tree.search(substring), (
+                    f"Substring '{substring}' should be found."
+                )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/data_structures/trie/radix_tree.py b/data_structures/trie/radix_tree.py
index fadc50cb49a7..caf566a6ce30 100644
--- a/data_structures/trie/radix_tree.py
+++ b/data_structures/trie/radix_tree.py
@@ -153,31 +153,30 @@ def delete(self, word: str) -> bool:
             # We have word remaining so we check the next node
             elif remaining_word != "":
                 return incoming_node.delete(remaining_word)
+            # If it is not a leaf, we don't have to delete
+            elif not incoming_node.is_leaf:
+                return False
             else:
-                # If it is not a leaf, we don't have to delete
-                if not incoming_node.is_leaf:
-                    return False
+                # We delete the nodes if no edges go from it
+                if len(incoming_node.nodes) == 0:
+                    del self.nodes[word[0]]
+                    # We merge the current node with its only child
+                    if len(self.nodes) == 1 and not self.is_leaf:
+                        merging_node = next(iter(self.nodes.values()))
+                        self.is_leaf = merging_node.is_leaf
+                        self.prefix += merging_node.prefix
+                        self.nodes = merging_node.nodes
+                # If there is more than 1 edge, we just mark it as non-leaf
+                elif len(incoming_node.nodes) > 1:
+                    incoming_node.is_leaf = False
+                # If there is 1 edge, we merge it with its child
                 else:
-                    # We delete the nodes if no edges go from it
-                    if len(incoming_node.nodes) == 0:
-                        del self.nodes[word[0]]
-                        # We merge the current node with its only child
-                        if len(self.nodes) == 1 and not self.is_leaf:
-                            merging_node = next(iter(self.nodes.values()))
-                            self.is_leaf = merging_node.is_leaf
-                            self.prefix += merging_node.prefix
-                            self.nodes = merging_node.nodes
-                    # If there is more than 1 edge, we just mark it as non-leaf
-                    elif len(incoming_node.nodes) > 1:
-                        incoming_node.is_leaf = False
-                    # If there is 1 edge, we merge it with its child
-                    else:
-                        merging_node = next(iter(incoming_node.nodes.values()))
-                        incoming_node.is_leaf = merging_node.is_leaf
-                        incoming_node.prefix += merging_node.prefix
-                        incoming_node.nodes = merging_node.nodes
-
-                    return True
+                    merging_node = next(iter(incoming_node.nodes.values()))
+                    incoming_node.is_leaf = merging_node.is_leaf
+                    incoming_node.prefix += merging_node.prefix
+                    incoming_node.nodes = merging_node.nodes
+
+                return True
 
     def print_tree(self, height: int = 0) -> None:
         """Print the tree
diff --git a/digital_image_processing/convert_to_negative.py b/digital_image_processing/convert_to_negative.py
index 7df44138973c..9bf2d8f2c075 100644
--- a/digital_image_processing/convert_to_negative.py
+++ b/digital_image_processing/convert_to_negative.py
@@ -1,6 +1,7 @@
 """
-    Implemented an algorithm using opencv to convert a colored image into its negative
+Implemented an algorithm using opencv to convert a colored image into its negative
 """
+
 from cv2 import destroyAllWindows, imread, imshow, waitKey
 
 
diff --git a/digital_image_processing/dithering/burkes.py b/digital_image_processing/dithering/burkes.py
index 35aedc16d404..4b59356d8f08 100644
--- a/digital_image_processing/dithering/burkes.py
+++ b/digital_image_processing/dithering/burkes.py
@@ -1,6 +1,7 @@
 """
 Implementation Burke's algorithm (dithering)
 """
+
 import numpy as np
 from cv2 import destroyAllWindows, imread, imshow, waitKey
 
diff --git a/digital_image_processing/edge_detection/canny.py b/digital_image_processing/edge_detection/canny.py
index f8cbeedb3874..944161c31cfc 100644
--- a/digital_image_processing/edge_detection/canny.py
+++ b/digital_image_processing/edge_detection/canny.py
@@ -74,9 +74,9 @@ def detect_high_low_threshold(
     image_shape, destination, threshold_low, threshold_high, weak, strong
 ):
     """
-    High-Low threshold detection. If an edge pixel’s gradient value is higher
+    High-Low threshold detection. If an edge pixel's gradient value is higher
     than the high threshold value, it is marked as a strong edge pixel. If an
-    edge pixel’s gradient value is smaller than the high threshold value and
+    edge pixel's gradient value is smaller than the high threshold value and
     larger than the low threshold value, it is marked as a weak edge pixel. If
     an edge pixel's value is smaller than the low threshold value, it will be
     suppressed.
diff --git a/digital_image_processing/filters/bilateral_filter.py b/digital_image_processing/filters/bilateral_filter.py
index 199ac4d9939a..6ef4434d959c 100644
--- a/digital_image_processing/filters/bilateral_filter.py
+++ b/digital_image_processing/filters/bilateral_filter.py
@@ -9,6 +9,7 @@
 Output:
     img:A 2d zero padded image with values in between 0 and 1
 """
+
 import math
 import sys
 
diff --git a/digital_image_processing/filters/gabor_filter.py b/digital_image_processing/filters/gabor_filter.py
index 8f9212a35a79..aaec567f4c99 100644
--- a/digital_image_processing/filters/gabor_filter.py
+++ b/digital_image_processing/filters/gabor_filter.py
@@ -48,9 +48,9 @@ def gabor_filter_kernel(
             _y = -sin_theta * px + cos_theta * py
 
             # fill kernel
-            gabor[y, x] = np.exp(
-                -(_x**2 + gamma**2 * _y**2) / (2 * sigma**2)
-            ) * np.cos(2 * np.pi * _x / lambd + psi)
+            gabor[y, x] = np.exp(-(_x**2 + gamma**2 * _y**2) / (2 * sigma**2)) * np.cos(
+                2 * np.pi * _x / lambd + psi
+            )
 
     return gabor
 
diff --git a/digital_image_processing/filters/gaussian_filter.py b/digital_image_processing/filters/gaussian_filter.py
index 87fa67fb65ea..0c34e59fafe5 100644
--- a/digital_image_processing/filters/gaussian_filter.py
+++ b/digital_image_processing/filters/gaussian_filter.py
@@ -1,6 +1,7 @@
 """
 Implementation of gaussian filter algorithm
 """
+
 from itertools import product
 
 from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey
@@ -22,11 +23,9 @@ def gaussian_filter(image, k_size, sigma):
 
     # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows
     image_array = zeros((dst_height * dst_width, k_size * k_size))
-    row = 0
-    for i, j in product(range(dst_height), range(dst_width)):
+    for row, (i, j) in enumerate(product(range(dst_height), range(dst_width))):
         window = ravel(image[i : i + k_size, j : j + k_size])
         image_array[row, :] = window
-        row += 1
 
     #  turn the kernel into shape(k*k, 1)
     gaussian_kernel = gen_gaussian_kernel(k_size, sigma)
diff --git a/digital_image_processing/filters/median_filter.py b/digital_image_processing/filters/median_filter.py
index 174018569d62..fc8b582ef67a 100644
--- a/digital_image_processing/filters/median_filter.py
+++ b/digital_image_processing/filters/median_filter.py
@@ -1,6 +1,7 @@
 """
 Implementation of median filter algorithm
 """
+
 from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey
 from numpy import divide, int8, multiply, ravel, sort, zeros_like
 
diff --git a/digital_image_processing/histogram_equalization/histogram_stretch.py b/digital_image_processing/histogram_equalization/histogram_stretch.py
index 5ea7773e32d9..1270c964dee6 100644
--- a/digital_image_processing/histogram_equalization/histogram_stretch.py
+++ b/digital_image_processing/histogram_equalization/histogram_stretch.py
@@ -3,6 +3,7 @@
 
 @author: Binish125
 """
+
 import copy
 import os
 
diff --git a/digital_image_processing/index_calculation.py b/digital_image_processing/index_calculation.py
index 67830668b0da..988f8e72b9a8 100644
--- a/digital_image_processing/index_calculation.py
+++ b/digital_image_processing/index_calculation.py
@@ -182,7 +182,7 @@ def arv12(self):
         Atmospherically Resistant Vegetation Index 2
         https://www.indexdatabase.de/db/i-single.php?id=396
         :return: index
-            −0.18+1.17*(self.nir−self.red)/(self.nir+self.red)
+            -0.18+1.17*(self.nir-self.red)/(self.nir+self.red)
         """
         return -0.18 + (1.17 * ((self.nir - self.red) / (self.nir + self.red)))
 
diff --git a/digital_image_processing/morphological_operations/__init__.py b/digital_image_processing/morphological_operations/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/digital_image_processing/resize/resize.py b/digital_image_processing/resize/resize.py
index 4836521f9f58..7bde118da69b 100644
--- a/digital_image_processing/resize/resize.py
+++ b/digital_image_processing/resize/resize.py
@@ -1,4 +1,5 @@
-""" Multiple image resizing techniques """
+"""Multiple image resizing techniques"""
+
 import numpy as np
 from cv2 import destroyAllWindows, imread, imshow, waitKey
 
diff --git a/digital_image_processing/sepia.py b/digital_image_processing/sepia.py
index e9dd2c06066d..1924a80451e5 100644
--- a/digital_image_processing/sepia.py
+++ b/digital_image_processing/sepia.py
@@ -1,6 +1,7 @@
 """
-    Implemented an algorithm using opencv to tone an image with sepia technique
+Implemented an algorithm using opencv to tone an image with sepia technique
 """
+
 from cv2 import destroyAllWindows, imread, imshow, waitKey
 
 
diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py
index 7993110d6bdd..d1200f4d65ca 100644
--- a/digital_image_processing/test_digital_image_processing.py
+++ b/digital_image_processing/test_digital_image_processing.py
@@ -1,6 +1,7 @@
 """
 PyTest's for Digital Image Processing
 """
+
 import numpy as np
 from cv2 import COLOR_BGR2GRAY, cvtColor, imread
 from numpy import array, uint8
diff --git a/divide_and_conquer/closest_pair_of_points.py b/divide_and_conquer/closest_pair_of_points.py
index cb7fa00d1c8f..534cbba9b718 100644
--- a/divide_and_conquer/closest_pair_of_points.py
+++ b/divide_and_conquer/closest_pair_of_points.py
@@ -54,8 +54,7 @@ def dis_between_closest_pair(points, points_counts, min_dis=float("inf")):
     for i in range(points_counts - 1):
         for j in range(i + 1, points_counts):
             current_dis = euclidean_distance_sqr(points[i], points[j])
-            if current_dis < min_dis:
-                min_dis = current_dis
+            min_dis = min(min_dis, current_dis)
     return min_dis
 
 
@@ -76,8 +75,7 @@ def dis_between_closest_in_strip(points, points_counts, min_dis=float("inf")):
     for i in range(min(6, points_counts - 1), points_counts):
         for j in range(max(0, i - 6), i):
             current_dis = euclidean_distance_sqr(points[i], points[j])
-            if current_dis < min_dis:
-                min_dis = current_dis
+            min_dis = min(min_dis, current_dis)
     return min_dis
 
 
diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py
index 1d1bf301def5..93f6daf1f88c 100644
--- a/divide_and_conquer/convex_hull.py
+++ b/divide_and_conquer/convex_hull.py
@@ -12,6 +12,7 @@
 which have not been implemented here, yet.
 
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterable
@@ -273,14 +274,13 @@ def convex_hull_bf(points: list[Point]) -> list[Point]:
                         points_left_of_ij = True
                     elif det_k < 0:
                         points_right_of_ij = True
-                    else:
-                        # point[i], point[j], point[k] all lie on a straight line
-                        # if point[k] is to the left of point[i] or it's to the
-                        # right of point[j], then point[i], point[j] cannot be
-                        # part of the convex hull of A
-                        if points[k] < points[i] or points[k] > points[j]:
-                            ij_part_of_convex_hull = False
-                            break
+                    # point[i], point[j], point[k] all lie on a straight line
+                    # if point[k] is to the left of point[i] or it's to the
+                    # right of point[j], then point[i], point[j] cannot be
+                    # part of the convex hull of A
+                    elif points[k] < points[i] or points[k] > points[j]:
+                        ij_part_of_convex_hull = False
+                        break
 
                 if points_left_of_ij and points_right_of_ij:
                     ij_part_of_convex_hull = False
diff --git a/divide_and_conquer/kth_order_statistic.py b/divide_and_conquer/kth_order_statistic.py
index 666ad1a39b8a..23fd8be5ea47 100644
--- a/divide_and_conquer/kth_order_statistic.py
+++ b/divide_and_conquer/kth_order_statistic.py
@@ -8,6 +8,7 @@
 For more information of this algorithm:
 https://web.stanford.edu/class/archive/cs/cs161/cs161.1138/lectures/08/Small08.pdf
 """
+
 from __future__ import annotations
 
 from random import choice
diff --git a/divide_and_conquer/max_subarray.py b/divide_and_conquer/max_subarray.py
index 851ef621a24c..0fad7ab5d920 100644
--- a/divide_and_conquer/max_subarray.py
+++ b/divide_and_conquer/max_subarray.py
@@ -6,6 +6,7 @@
 
 This divide-and-conquer algorithm finds the maximum subarray in O(n log n) time.
 """
+
 from __future__ import annotations
 
 import time
diff --git a/divide_and_conquer/peak.py b/divide_and_conquer/peak.py
index e60f28bfbe29..71ab5ac86574 100644
--- a/divide_and_conquer/peak.py
+++ b/divide_and_conquer/peak.py
@@ -7,6 +7,7 @@
 (From Kleinberg and Tardos. Algorithm Design.
 Addison Wesley 2006: Chapter 5 Solved Exercise 1)
 """
+
 from __future__ import annotations
 
 
diff --git a/divide_and_conquer/power.py b/divide_and_conquer/power.py
index f2e023afd536..492ee6dd12f0 100644
--- a/divide_and_conquer/power.py
+++ b/divide_and_conquer/power.py
@@ -1,18 +1,38 @@
-def actual_power(a: int, b: int):
+def actual_power(a: int, b: int) -> int:
     """
     Function using divide and conquer to calculate a^b.
     It only works for integer a,b.
+
+    :param a: The base of the power operation, an integer.
+    :param b: The exponent of the power operation, a non-negative integer.
+    :return: The result of a^b.
+
+    Examples:
+    >>> actual_power(3, 2)
+    9
+    >>> actual_power(5, 3)
+    125
+    >>> actual_power(2, 5)
+    32
+    >>> actual_power(7, 0)
+    1
     """
     if b == 0:
         return 1
+    half = actual_power(a, b // 2)
+
     if (b % 2) == 0:
-        return actual_power(a, int(b / 2)) * actual_power(a, int(b / 2))
+        return half * half
     else:
-        return a * actual_power(a, int(b / 2)) * actual_power(a, int(b / 2))
+        return a * half * half
 
 
 def power(a: int, b: int) -> float:
     """
+    :param a: The base (integer).
+    :param b: The exponent (integer).
+    :return: The result of a^b, as a float for negative exponents.
+
     >>> power(4,6)
     4096
     >>> power(2,3)
@@ -25,9 +45,9 @@ def power(a: int, b: int) -> float:
     -0.125
     """
     if b < 0:
-        return 1 / actual_power(a, b)
+        return 1 / actual_power(a, -b)
     return actual_power(a, b)
 
 
 if __name__ == "__main__":
-    print(power(-2, -3))
+    print(power(-2, -3))  # output -0.125
diff --git a/docs/__init__.py b/docs/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 000000000000..f2481f107267
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,3 @@
+from sphinx_pyproject import SphinxConfig
+
+project = SphinxConfig("../pyproject.toml", globalns=globals()).name
diff --git a/docs/source/__init__.py b/docs/source/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/dynamic_programming/all_construct.py b/dynamic_programming/all_construct.py
index 6e53a702cbb1..ca00f2beb06a 100644
--- a/dynamic_programming/all_construct.py
+++ b/dynamic_programming/all_construct.py
@@ -2,14 +2,16 @@
 Program to list all the ways a target string can be
 constructed from the given list of substrings
 """
+
 from __future__ import annotations
 
 
 def all_construct(target: str, word_bank: list[str] | None = None) -> list[list[str]]:
     """
-        returns the list containing all the possible
-        combinations a string(target) can be constructed from
-        the given list of substrings(word_bank)
+    returns the list containing all the possible
+    combinations a string(`target`) can be constructed from
+    the given list of substrings(`word_bank`)
+
     >>> all_construct("hello", ["he", "l", "o"])
     [['he', 'l', 'l', 'o']]
     >>> all_construct("purple",["purp","p","ur","le","purpl"])
diff --git a/dynamic_programming/bitmask.py b/dynamic_programming/bitmask.py
index 56bb8e96ba02..4737a3419e8e 100644
--- a/dynamic_programming/bitmask.py
+++ b/dynamic_programming/bitmask.py
@@ -8,6 +8,7 @@
 a person can do only one task and a task is performed only by one person.
 Find the total no of ways in which the tasks can be distributed.
 """
+
 from collections import defaultdict
 
 
@@ -41,7 +42,7 @@ def count_ways_until(self, mask, task_no):
             return self.dp[mask][task_no]
 
         # Number of ways when we don't this task in the arrangement
-        total_ways_util = self.count_ways_until(mask, task_no + 1)
+        total_ways_until = self.count_ways_until(mask, task_no + 1)
 
         # now assign the tasks one by one to all possible persons and recursively
         # assign for the remaining tasks.
@@ -53,10 +54,10 @@ def count_ways_until(self, mask, task_no):
 
                 # assign this task to p and change the mask value. And recursively
                 # assign tasks with the new mask value.
-                total_ways_util += self.count_ways_until(mask | (1 << p), task_no + 1)
+                total_ways_until += self.count_ways_until(mask | (1 << p), task_no + 1)
 
         # save the value.
-        self.dp[mask][task_no] = total_ways_util
+        self.dp[mask][task_no] = total_ways_until
 
         return self.dp[mask][task_no]
 
diff --git a/dynamic_programming/climbing_stairs.py b/dynamic_programming/climbing_stairs.py
index d6273d025f08..38bdb427eedc 100644
--- a/dynamic_programming/climbing_stairs.py
+++ b/dynamic_programming/climbing_stairs.py
@@ -25,9 +25,9 @@ def climb_stairs(number_of_steps: int) -> int:
         ...
     AssertionError: number_of_steps needs to be positive integer, your input -7
     """
-    assert (
-        isinstance(number_of_steps, int) and number_of_steps > 0
-    ), f"number_of_steps needs to be positive integer, your input {number_of_steps}"
+    assert isinstance(number_of_steps, int) and number_of_steps > 0, (
+        f"number_of_steps needs to be positive integer, your input {number_of_steps}"
+    )
     if number_of_steps == 1:
         return 1
     previous, current = 1, 1
diff --git a/dynamic_programming/combination_sum_iv.py b/dynamic_programming/combination_sum_iv.py
index b2aeb0824f64..ed8dcd88e6fd 100644
--- a/dynamic_programming/combination_sum_iv.py
+++ b/dynamic_programming/combination_sum_iv.py
@@ -1,33 +1,34 @@
 """
 Question:
-You are given an array of distinct integers and you have to tell how many
-different ways of selecting the elements from the array are there such that
-the sum of chosen elements is equal to the target number tar.
+    You are given an array of distinct integers and you have to tell how many
+    different ways of selecting the elements from the array are there such that
+    the sum of chosen elements is equal to the target number tar.
 
 Example
 
 Input:
-N = 3
-target = 5
-array = [1, 2, 5]
+    * N = 3
+    * target = 5
+    * array = [1, 2, 5]
 
 Output:
-9
+    9
 
 Approach:
-The basic idea is to go over recursively to find the way such that the sum
-of chosen elements is “tar”. For every element, we have two choices
-    1. Include the element in our set of chosen elements.
-    2. Don’t include the element in our set of chosen elements.
+    The basic idea is to go over recursively to find the way such that the sum
+    of chosen elements is `target`. For every element, we have two choices
+
+        1. Include the element in our set of chosen elements.
+        2. Don't include the element in our set of chosen elements.
 """
 
 
-def combination_sum_iv(n: int, array: list[int], target: int) -> int:
+def combination_sum_iv(array: list[int], target: int) -> int:
     """
     Function checks the all possible combinations, and returns the count
     of possible combination in exponential Time Complexity.
 
-    >>> combination_sum_iv(3, [1,2,5], 5)
+    >>> combination_sum_iv([1,2,5], 5)
     9
     """
 
@@ -41,13 +42,13 @@ def count_of_possible_combinations(target: int) -> int:
     return count_of_possible_combinations(target)
 
 
-def combination_sum_iv_dp_array(n: int, array: list[int], target: int) -> int:
+def combination_sum_iv_dp_array(array: list[int], target: int) -> int:
     """
     Function checks the all possible combinations, and returns the count
     of possible combination in O(N^2) Time Complexity as we are using Dynamic
     programming array here.
 
-    >>> combination_sum_iv_dp_array(3, [1,2,5], 5)
+    >>> combination_sum_iv_dp_array([1,2,5], 5)
     9
     """
 
@@ -96,7 +97,6 @@ def combination_sum_iv_bottom_up(n: int, array: list[int], target: int) -> int:
     import doctest
 
     doctest.testmod()
-    n = 3
     target = 5
     array = [1, 2, 5]
-    print(combination_sum_iv(n, array, target))
+    print(combination_sum_iv(array, target))
diff --git a/dynamic_programming/fast_fibonacci.py b/dynamic_programming/fast_fibonacci.py
index f48186a34c25..d04a5ac8249b 100644
--- a/dynamic_programming/fast_fibonacci.py
+++ b/dynamic_programming/fast_fibonacci.py
@@ -4,6 +4,7 @@
 This program calculates the nth Fibonacci number in O(log(n)).
 It's possible to calculate F(1_000_000) in less than a second.
 """
+
 from __future__ import annotations
 
 import sys
@@ -25,7 +26,7 @@ def _fib(n: int) -> tuple[int, int]:
     if n == 0:  # (F(0), F(1))
         return (0, 1)
 
-    # F(2n) = F(n)[2F(n+1) − F(n)]
+    # F(2n) = F(n)[2F(n+1) - F(n)]
     # F(2n+1) = F(n+1)^2+F(n)^2
     a, b = _fib(n // 2)
     c = a * (b * 2 - a)
diff --git a/dynamic_programming/fizz_buzz.py b/dynamic_programming/fizz_buzz.py
index e29116437a93..0cb48897875b 100644
--- a/dynamic_programming/fizz_buzz.py
+++ b/dynamic_programming/fizz_buzz.py
@@ -3,11 +3,12 @@
 
 def fizz_buzz(number: int, iterations: int) -> str:
     """
-    Plays FizzBuzz.
-    Prints Fizz if number is a multiple of 3.
-    Prints Buzz if its a multiple of 5.
-    Prints FizzBuzz if its a multiple of both 3 and 5 or 15.
-    Else Prints The Number Itself.
+    | Plays FizzBuzz.
+    | Prints Fizz if number is a multiple of ``3``.
+    | Prints Buzz if its a multiple of ``5``.
+    | Prints FizzBuzz if its a multiple of both ``3`` and ``5`` or ``15``.
+    | Else Prints The Number Itself.
+
     >>> fizz_buzz(1,7)
     '1 2 Fizz 4 Buzz Fizz 7 '
     >>> fizz_buzz(1,0)
diff --git a/dynamic_programming/floyd_warshall.py b/dynamic_programming/floyd_warshall.py
index 2331f3e65483..b92c6667fb5c 100644
--- a/dynamic_programming/floyd_warshall.py
+++ b/dynamic_programming/floyd_warshall.py
@@ -12,19 +12,58 @@ def __init__(self, n=0):  # a graph with Node 0,1,...,N-1
         ]  # dp[i][j] stores minimum distance from i to j
 
     def add_edge(self, u, v, w):
+        """
+        Adds a directed edge from node u
+        to node v with weight w.
+
+        >>> g = Graph(3)
+        >>> g.add_edge(0, 1, 5)
+        >>> g.dp[0][1]
+        5
+        """
         self.dp[u][v] = w
 
     def floyd_warshall(self):
+        """
+        Computes the shortest paths between all pairs of
+        nodes using the Floyd-Warshall algorithm.
+
+        >>> g = Graph(3)
+        >>> g.add_edge(0, 1, 1)
+        >>> g.add_edge(1, 2, 2)
+        >>> g.floyd_warshall()
+        >>> g.show_min(0, 2)
+        3
+        >>> g.show_min(2, 0)
+        inf
+        """
         for k in range(self.n):
             for i in range(self.n):
                 for j in range(self.n):
                     self.dp[i][j] = min(self.dp[i][j], self.dp[i][k] + self.dp[k][j])
 
     def show_min(self, u, v):
+        """
+        Returns the minimum distance from node u to node v.
+
+        >>> g = Graph(3)
+        >>> g.add_edge(0, 1, 3)
+        >>> g.add_edge(1, 2, 4)
+        >>> g.floyd_warshall()
+        >>> g.show_min(0, 2)
+        7
+        >>> g.show_min(1, 0)
+        inf
+        """
         return self.dp[u][v]
 
 
 if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    # Example usage
     graph = Graph(5)
     graph.add_edge(0, 2, 9)
     graph.add_edge(0, 4, 10)
@@ -38,5 +77,9 @@ def show_min(self, u, v):
     graph.add_edge(4, 2, 4)
     graph.add_edge(4, 3, 9)
     graph.floyd_warshall()
-    graph.show_min(1, 4)
-    graph.show_min(0, 3)
+    print(
+        graph.show_min(1, 4)
+    )  # Should output the minimum distance from node 1 to node 4
+    print(
+        graph.show_min(0, 3)
+    )  # Should output the minimum distance from node 0 to node 3
diff --git a/dynamic_programming/iterating_through_submasks.py b/dynamic_programming/iterating_through_submasks.py
index 4d0a250e8dfe..efab6dacff3f 100644
--- a/dynamic_programming/iterating_through_submasks.py
+++ b/dynamic_programming/iterating_through_submasks.py
@@ -5,6 +5,7 @@
 its submasks. The mask s is submask of m if only bits that were included in
 bitmask are set
 """
+
 from __future__ import annotations
 
 
@@ -36,9 +37,9 @@ def list_of_submasks(mask: int) -> list[int]:
 
     """
 
-    assert (
-        isinstance(mask, int) and mask > 0
-    ), f"mask needs to be positive integer, your input {mask}"
+    assert isinstance(mask, int) and mask > 0, (
+        f"mask needs to be positive integer, your input {mask}"
+    )
 
     """
     first submask iterated will be mask itself then operation will be performed
diff --git a/dynamic_programming/k_means_clustering_tensorflow.py.DISABLED.txt b/dynamic_programming/k_means_clustering_tensorflow.py
similarity index 100%
rename from dynamic_programming/k_means_clustering_tensorflow.py.DISABLED.txt
rename to dynamic_programming/k_means_clustering_tensorflow.py
diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py
index 489b5ada450a..28c5b19dbe36 100644
--- a/dynamic_programming/knapsack.py
+++ b/dynamic_programming/knapsack.py
@@ -11,7 +11,7 @@ def mf_knapsack(i, wt, val, j):
     """
     This code involves the concept of memory functions. Here we solve the subproblems
     which are needed unlike the below example
-    F is a 2D array with -1s filled up
+    F is a 2D array with ``-1`` s filled up
     """
     global f  # a global dp table for knapsack
     if f[i][j] < 0:
@@ -45,22 +45,24 @@ def knapsack_with_example_solution(w: int, wt: list, val: list):
     the several possible optimal subsets.
 
     Parameters
-    ---------
+    ----------
 
-    W: int, the total maximum weight for the given knapsack problem.
-    wt: list, the vector of weights for all items where wt[i] is the weight
-    of the i-th item.
-    val: list, the vector of values for all items where val[i] is the value
-    of the i-th item
+    * `w`: int, the total maximum weight for the given knapsack problem.
+    * `wt`: list, the vector of weights for all items where ``wt[i]`` is the weight
+       of the ``i``-th item.
+    * `val`: list, the vector of values for all items where ``val[i]`` is the value
+      of the ``i``-th item
 
     Returns
     -------
-    optimal_val: float, the optimal value for the given knapsack problem
-    example_optional_set: set, the indices of one of the optimal subsets
-    which gave rise to the optimal value.
+
+    * `optimal_val`: float, the optimal value for the given knapsack problem
+    * `example_optional_set`: set, the indices of one of the optimal subsets
+      which gave rise to the optimal value.
 
     Examples
-    -------
+    --------
+
     >>> knapsack_with_example_solution(10, [1, 3, 5, 2], [10, 20, 100, 22])
     (142, {2, 3, 4})
     >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4, 4])
@@ -104,19 +106,19 @@ def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set):
     a filled DP table and the vector of weights
 
     Parameters
-    ---------
-
-    dp: list of list, the table of a solved integer weight dynamic programming problem
+    ----------
 
-    wt: list or tuple, the vector of weights of the items
-    i: int, the index of the item under consideration
-    j: int, the current possible maximum weight
-    optimal_set: set, the optimal subset so far. This gets modified by the function.
+    * `dp`: list of list, the table of a solved integer weight dynamic programming
+      problem
+    * `wt`: list or tuple, the vector of weights of the items
+    * `i`: int, the index of the item under consideration
+    * `j`: int, the current possible maximum weight
+    * `optimal_set`: set, the optimal subset so far. This gets modified by the function.
 
     Returns
     -------
-    None
 
+    ``None``
     """
     # for the current item i at a maximum weight j to be part of an optimal subset,
     # the optimal value at (i, j) must be greater than the optimal value at (i-1, j).
diff --git a/dynamic_programming/longest_common_subsequence.py b/dynamic_programming/longest_common_subsequence.py
index 178b4169b213..4a6c880aff61 100644
--- a/dynamic_programming/longest_common_subsequence.py
+++ b/dynamic_programming/longest_common_subsequence.py
@@ -28,6 +28,24 @@ def longest_common_subsequence(x: str, y: str):
     (2, 'ph')
     >>> longest_common_subsequence("computer", "food")
     (1, 'o')
+    >>> longest_common_subsequence("", "abc")  # One string is empty
+    (0, '')
+    >>> longest_common_subsequence("abc", "")  # Other string is empty
+    (0, '')
+    >>> longest_common_subsequence("", "")  # Both strings are empty
+    (0, '')
+    >>> longest_common_subsequence("abc", "def")  # No common subsequence
+    (0, '')
+    >>> longest_common_subsequence("abc", "abc")  # Identical strings
+    (3, 'abc')
+    >>> longest_common_subsequence("a", "a")  # Single character match
+    (1, 'a')
+    >>> longest_common_subsequence("a", "b")  # Single character no match
+    (0, '')
+    >>> longest_common_subsequence("abcdef", "ace")  # Interleaved subsequence
+    (3, 'ace')
+    >>> longest_common_subsequence("ABCD", "ACBD")  # No repeated characters
+    (3, 'ABD')
     """
     # find the length of strings
 
@@ -38,30 +56,30 @@ def longest_common_subsequence(x: str, y: str):
     n = len(y)
 
     # declaring the array for storing the dp values
-    l = [[0] * (n + 1) for _ in range(m + 1)]  # noqa: E741
+    dp = [[0] * (n + 1) for _ in range(m + 1)]
 
     for i in range(1, m + 1):
         for j in range(1, n + 1):
             match = 1 if x[i - 1] == y[j - 1] else 0
 
-            l[i][j] = max(l[i - 1][j], l[i][j - 1], l[i - 1][j - 1] + match)
+            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] + match)
 
     seq = ""
     i, j = m, n
     while i > 0 and j > 0:
         match = 1 if x[i - 1] == y[j - 1] else 0
 
-        if l[i][j] == l[i - 1][j - 1] + match:
+        if dp[i][j] == dp[i - 1][j - 1] + match:
             if match == 1:
                 seq = x[i - 1] + seq
             i -= 1
             j -= 1
-        elif l[i][j] == l[i - 1][j]:
+        elif dp[i][j] == dp[i - 1][j]:
             i -= 1
         else:
             j -= 1
 
-    return l[m][n], seq
+    return dp[m][n], seq
 
 
 if __name__ == "__main__":
diff --git a/dynamic_programming/longest_common_substring.py b/dynamic_programming/longest_common_substring.py
index e2f944a5e336..3ba83f3d9f03 100644
--- a/dynamic_programming/longest_common_substring.py
+++ b/dynamic_programming/longest_common_substring.py
@@ -1,15 +1,19 @@
 """
-Longest Common Substring Problem Statement: Given two sequences, find the
-longest common substring present in both of them. A substring is
-necessarily continuous.
-Example: "abcdef" and "xabded" have two longest common substrings, "ab" or "de".
-Therefore, algorithm should return any one of them.
+Longest Common Substring Problem Statement:
+    Given two sequences, find the
+    longest common substring present in both of them. A substring is
+    necessarily continuous.
+
+Example:
+    ``abcdef`` and ``xabded`` have two longest common substrings, ``ab`` or ``de``.
+    Therefore, algorithm should return any one of them.
 """
 
 
 def longest_common_substring(text1: str, text2: str) -> str:
     """
     Finds the longest common substring between two strings.
+
     >>> longest_common_substring("", "")
     ''
     >>> longest_common_substring("a","")
@@ -39,22 +43,25 @@ def longest_common_substring(text1: str, text2: str) -> str:
     if not (isinstance(text1, str) and isinstance(text2, str)):
         raise ValueError("longest_common_substring() takes two strings for inputs")
 
+    if not text1 or not text2:
+        return ""
+
     text1_length = len(text1)
     text2_length = len(text2)
 
     dp = [[0] * (text2_length + 1) for _ in range(text1_length + 1)]
-    ans_index = 0
-    ans_length = 0
+    end_pos = 0
+    max_length = 0
 
     for i in range(1, text1_length + 1):
         for j in range(1, text2_length + 1):
             if text1[i - 1] == text2[j - 1]:
                 dp[i][j] = 1 + dp[i - 1][j - 1]
-                if dp[i][j] > ans_length:
-                    ans_index = i
-                    ans_length = dp[i][j]
+                if dp[i][j] > max_length:
+                    end_pos = i
+                    max_length = dp[i][j]
 
-    return text1[ans_index - ans_length : ans_index]
+    return text1[end_pos - max_length : end_pos]
 
 
 if __name__ == "__main__":
diff --git a/dynamic_programming/longest_increasing_subsequence.py b/dynamic_programming/longest_increasing_subsequence.py
index d827893763c5..1863a882c41e 100644
--- a/dynamic_programming/longest_increasing_subsequence.py
+++ b/dynamic_programming/longest_increasing_subsequence.py
@@ -4,24 +4,30 @@
 This is a pure Python implementation of Dynamic Programming solution to the longest
 increasing subsequence of a given sequence.
 
-The problem is  :
-Given an array, to find the longest and increasing sub-array in that given array and
-return it.
-Example: [10, 22, 9, 33, 21, 50, 41, 60, 80] as input will return
-         [10, 22, 33, 41, 60, 80] as output
+The problem is:
+    Given an array, to find the longest and increasing sub-array in that given array and
+    return it.
+
+Example:
+    ``[10, 22, 9, 33, 21, 50, 41, 60, 80]`` as input will return
+    ``[10, 22, 33, 41, 60, 80]`` as output
 """
+
 from __future__ import annotations
 
 
 def longest_subsequence(array: list[int]) -> list[int]:  # This function is recursive
     """
     Some examples
+
     >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80])
     [10, 22, 33, 41, 60, 80]
     >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9])
     [1, 2, 3, 9]
+    >>> longest_subsequence([28, 26, 12, 23, 35, 39])
+    [12, 23, 35, 39]
     >>> longest_subsequence([9, 8, 7, 6, 5, 7])
-    [8]
+    [5, 7]
     >>> longest_subsequence([1, 1, 1])
     [1, 1, 1]
     >>> longest_subsequence([])
@@ -40,7 +46,7 @@ def longest_subsequence(array: list[int]) -> list[int]:  # This function is recu
     while not is_found and i < array_length:
         if array[i] < pivot:
             is_found = True
-            temp_array = [element for element in array[i:] if element >= array[i]]
+            temp_array = array[i:]
             temp_array = longest_subsequence(temp_array)
             if len(temp_array) > len(longest_subseq):
                 longest_subseq = temp_array
diff --git a/dynamic_programming/longest_increasing_subsequence_iterative.py b/dynamic_programming/longest_increasing_subsequence_iterative.py
new file mode 100644
index 000000000000..665c86a35d2e
--- /dev/null
+++ b/dynamic_programming/longest_increasing_subsequence_iterative.py
@@ -0,0 +1,72 @@
+"""
+Author  : Sanjay Muthu <https://github.com/XenoBytesX>
+
+This is a pure Python implementation of Dynamic Programming solution to the longest
+increasing subsequence of a given sequence.
+
+The problem is:
+    Given an array, to find the longest and increasing sub-array in that given array and
+    return it.
+
+Example:
+    ``[10, 22, 9, 33, 21, 50, 41, 60, 80]`` as input will return
+    ``[10, 22, 33, 50, 60, 80]`` as output
+"""
+
+from __future__ import annotations
+
+import copy
+
+
+def longest_subsequence(array: list[int]) -> list[int]:
+    """
+    Some examples
+
+    >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80])
+    [10, 22, 33, 50, 60, 80]
+    >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9])
+    [1, 2, 3, 9]
+    >>> longest_subsequence([9, 8, 7, 6, 5, 7])
+    [7, 7]
+    >>> longest_subsequence([28, 26, 12, 23, 35, 39])
+    [12, 23, 35, 39]
+    >>> longest_subsequence([1, 1, 1])
+    [1, 1, 1]
+    >>> longest_subsequence([])
+    []
+    """
+    n = len(array)
+    # The longest increasing subsequence ending at array[i]
+    longest_increasing_subsequence = []
+    for i in range(n):
+        longest_increasing_subsequence.append([array[i]])
+
+    for i in range(1, n):
+        for prev in range(i):
+            # If array[prev] is less than or equal to array[i], then
+            # longest_increasing_subsequence[prev] + array[i]
+            # is a valid increasing subsequence
+
+            # longest_increasing_subsequence[i] is only set to
+            # longest_increasing_subsequence[prev] + array[i] if the length is longer.
+
+            if array[prev] <= array[i] and len(
+                longest_increasing_subsequence[prev]
+            ) + 1 > len(longest_increasing_subsequence[i]):
+                longest_increasing_subsequence[i] = copy.copy(
+                    longest_increasing_subsequence[prev]
+                )
+                longest_increasing_subsequence[i].append(array[i])
+
+    result: list[int] = []
+    for i in range(n):
+        if len(longest_increasing_subsequence[i]) > len(result):
+            result = longest_increasing_subsequence[i]
+
+    return result
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/dynamic_programming/longest_increasing_subsequence_o(nlogn).py b/dynamic_programming/longest_increasing_subsequence_o_nlogn.py
similarity index 85%
rename from dynamic_programming/longest_increasing_subsequence_o(nlogn).py
rename to dynamic_programming/longest_increasing_subsequence_o_nlogn.py
index 5e11d729f395..bbc7a62b6b5c 100644
--- a/dynamic_programming/longest_increasing_subsequence_o(nlogn).py
+++ b/dynamic_programming/longest_increasing_subsequence_o_nlogn.py
@@ -7,14 +7,14 @@
 from __future__ import annotations
 
 
-def ceil_index(v, l, r, key):  # noqa: E741
-    while r - l > 1:
-        m = (l + r) // 2
-        if v[m] >= key:
-            r = m
+def ceil_index(v, left, right, key):
+    while right - left > 1:
+        middle = (left + right) // 2
+        if v[middle] >= key:
+            right = middle
         else:
-            l = m  # noqa: E741
-    return r
+            left = middle
+    return right
 
 
 def longest_increasing_subsequence_length(v: list[int]) -> int:
diff --git a/dynamic_programming/matrix_chain_multiplication.py b/dynamic_programming/matrix_chain_multiplication.py
index 084254a61f6c..4c0c771f9092 100644
--- a/dynamic_programming/matrix_chain_multiplication.py
+++ b/dynamic_programming/matrix_chain_multiplication.py
@@ -1,43 +1,50 @@
 """
-Find the minimum number of multiplications needed to multiply chain of matrices.
-Reference: https://www.geeksforgeeks.org/matrix-chain-multiplication-dp-8/
+| Find the minimum number of multiplications needed to multiply chain of matrices.
+| Reference: https://www.geeksforgeeks.org/matrix-chain-multiplication-dp-8/
 
-The algorithm has interesting real-world applications. Example:
-1. Image transformations in Computer Graphics as images are composed of matrix.
-2. Solve complex polynomial equations in the field of algebra using least processing
-   power.
-3. Calculate overall impact of macroeconomic decisions as economic equations involve a
-   number of variables.
-4. Self-driving car navigation can be made more accurate as matrix multiplication can
-   accurately determine position and orientation of obstacles in short time.
+The algorithm has interesting real-world applications.
 
-Python doctests can be run with the following command:
-python -m doctest -v matrix_chain_multiply.py
+Example:
+  1. Image transformations in Computer Graphics as images are composed of matrix.
+  2. Solve complex polynomial equations in the field of algebra using least processing
+     power.
+  3. Calculate overall impact of macroeconomic decisions as economic equations involve a
+     number of variables.
+  4. Self-driving car navigation can be made more accurate as matrix multiplication can
+     accurately determine position and orientation of obstacles in short time.
 
-Given a sequence arr[] that represents chain of 2D matrices such that the dimension of
-the ith matrix is arr[i-1]*arr[i].
-So suppose arr = [40, 20, 30, 10, 30] means we have 4 matrices of dimensions
-40*20, 20*30, 30*10 and 10*30.
+Python doctests can be run with the following command::
 
-matrix_chain_multiply() returns an integer denoting minimum number of multiplications to
-multiply the chain.
+  python -m doctest -v matrix_chain_multiply.py
+
+Given a sequence ``arr[]`` that represents chain of 2D matrices such that the dimension
+of the ``i`` th matrix is ``arr[i-1]*arr[i]``.
+So suppose ``arr = [40, 20, 30, 10, 30]`` means we have ``4`` matrices of dimensions
+``40*20``, ``20*30``, ``30*10`` and ``10*30``.
+
+``matrix_chain_multiply()`` returns an integer denoting minimum number of
+multiplications to multiply the chain.
 
 We do not need to perform actual multiplication here.
 We only need to decide the order in which to perform the multiplication.
 
 Hints:
-1. Number of multiplications (ie cost) to multiply 2 matrices
-of size m*p and p*n is m*p*n.
-2. Cost of matrix multiplication is associative ie (M1*M2)*M3 != M1*(M2*M3)
-3. Matrix multiplication is not commutative. So, M1*M2 does not mean M2*M1 can be done.
-4. To determine the required order, we can try different combinations.
+  1. Number of multiplications (ie cost) to multiply ``2`` matrices
+     of size ``m*p`` and ``p*n`` is ``m*p*n``.
+  2. Cost of matrix multiplication is not associative ie ``(M1*M2)*M3 != M1*(M2*M3)``
+  3. Matrix multiplication is not commutative. So, ``M1*M2`` does not mean ``M2*M1``
+     can be done.
+  4. To determine the required order, we can try different combinations.
+
 So, this problem has overlapping sub-problems and can be solved using recursion.
 We use Dynamic Programming for optimal time complexity.
 
 Example input:
-arr = [40, 20, 30, 10, 30]
-output: 26000
+    ``arr = [40, 20, 30, 10, 30]``
+output:
+    ``26000``
 """
+
 from collections.abc import Iterator
 from contextlib import contextmanager
 from functools import cache
@@ -49,25 +56,25 @@ def matrix_chain_multiply(arr: list[int]) -> int:
     Find the minimum number of multiplcations required to multiply the chain of matrices
 
     Args:
-        arr: The input array of integers.
+        `arr`: The input array of integers.
 
     Returns:
         Minimum number of multiplications needed to multiply the chain
 
     Examples:
-        >>> matrix_chain_multiply([1, 2, 3, 4, 3])
-        30
-        >>> matrix_chain_multiply([10])
-        0
-        >>> matrix_chain_multiply([10, 20])
-        0
-        >>> matrix_chain_multiply([19, 2, 19])
-        722
-        >>> matrix_chain_multiply(list(range(1, 100)))
-        323398
-
-        # >>> matrix_chain_multiply(list(range(1, 251)))
-        # 2626798
+
+    >>> matrix_chain_multiply([1, 2, 3, 4, 3])
+    30
+    >>> matrix_chain_multiply([10])
+    0
+    >>> matrix_chain_multiply([10, 20])
+    0
+    >>> matrix_chain_multiply([19, 2, 19])
+    722
+    >>> matrix_chain_multiply(list(range(1, 100)))
+    323398
+    >>> # matrix_chain_multiply(list(range(1, 251)))
+    # 2626798
     """
     if len(arr) < 2:
         return 0
@@ -92,8 +99,10 @@ def matrix_chain_multiply(arr: list[int]) -> int:
 def matrix_chain_order(dims: list[int]) -> int:
     """
     Source: https://en.wikipedia.org/wiki/Matrix_chain_multiplication
+
     The dynamic programming solution is faster than cached the recursive solution and
     can handle larger inputs.
+
     >>> matrix_chain_order([1, 2, 3, 4, 3])
     30
     >>> matrix_chain_order([10])
@@ -104,8 +113,7 @@ def matrix_chain_order(dims: list[int]) -> int:
     722
     >>> matrix_chain_order(list(range(1, 100)))
     323398
-
-    # >>> matrix_chain_order(list(range(1, 251)))  # Max before RecursionError is raised
+    >>> # matrix_chain_order(list(range(1, 251)))  # Max before RecursionError is raised
     # 2626798
     """
 
@@ -126,7 +134,7 @@ def elapsed_time(msg: str) -> Iterator:
 
     start = perf_counter_ns()
     yield
-    print(f"Finished: {msg} in {(perf_counter_ns() - start) / 10 ** 9} seconds.")
+    print(f"Finished: {msg} in {(perf_counter_ns() - start) / 10**9} seconds.")
 
 
 if __name__ == "__main__":
diff --git a/dynamic_programming/matrix_chain_order.py b/dynamic_programming/matrix_chain_order.py
index d612aea7b99d..6df43e84be28 100644
--- a/dynamic_programming/matrix_chain_order.py
+++ b/dynamic_programming/matrix_chain_order.py
@@ -5,13 +5,19 @@
 Implementation of Matrix Chain Multiplication
 Time Complexity: O(n^3)
 Space Complexity: O(n^2)
+
+Reference: https://en.wikipedia.org/wiki/Matrix_chain_multiplication
 """
 
 
-def matrix_chain_order(array):
+def matrix_chain_order(array: list[int]) -> tuple[list[list[int]], list[list[int]]]:
+    """
+    >>> matrix_chain_order([10, 30, 5])
+    ([[0, 0, 0], [0, 0, 1500], [0, 0, 0]], [[0, 0, 0], [0, 0, 1], [0, 0, 0]])
+    """
     n = len(array)
-    matrix = [[0 for x in range(n)] for x in range(n)]
-    sol = [[0 for x in range(n)] for x in range(n)]
+    matrix = [[0 for _ in range(n)] for _ in range(n)]
+    sol = [[0 for _ in range(n)] for _ in range(n)]
 
     for chain_length in range(2, n):
         for a in range(1, n - chain_length + 1):
@@ -28,26 +34,33 @@ def matrix_chain_order(array):
     return matrix, sol
 
 
-# Print order of matrix with Ai as Matrix
-def print_optiomal_solution(optimal_solution, i, j):
+def print_optimal_solution(optimal_solution: list[list[int]], i: int, j: int):
+    """
+    Print order of matrix with Ai as Matrix.
+    """
+
     if i == j:
         print("A" + str(i), end=" ")
     else:
         print("(", end=" ")
-        print_optiomal_solution(optimal_solution, i, optimal_solution[i][j])
-        print_optiomal_solution(optimal_solution, optimal_solution[i][j] + 1, j)
+        print_optimal_solution(optimal_solution, i, optimal_solution[i][j])
+        print_optimal_solution(optimal_solution, optimal_solution[i][j] + 1, j)
         print(")", end=" ")
 
 
 def main():
+    """
+    Size of matrix created from array [30, 35, 15, 5, 10, 20, 25] will be:
+    30*35 35*15 15*5 5*10 10*20 20*25
+    """
+
     array = [30, 35, 15, 5, 10, 20, 25]
     n = len(array)
-    # Size of matrix created from above array will be
-    # 30*35 35*15 15*5 5*10 10*20 20*25
+
     matrix, optimal_solution = matrix_chain_order(array)
 
     print("No. of Operation required: " + str(matrix[1][n - 1]))
-    print_optiomal_solution(optimal_solution, 1, n - 1)
+    print_optimal_solution(optimal_solution, 1, n - 1)
 
 
 if __name__ == "__main__":
diff --git a/dynamic_programming/max_product_subarray.py b/dynamic_programming/max_product_subarray.py
index 425859bc03e3..6f4f38e38942 100644
--- a/dynamic_programming/max_product_subarray.py
+++ b/dynamic_programming/max_product_subarray.py
@@ -1,9 +1,10 @@
 def max_product_subarray(numbers: list[int]) -> int:
     """
     Returns the maximum product that can be obtained by multiplying a
-    contiguous subarray of the given integer list `nums`.
+    contiguous subarray of the given integer list `numbers`.
 
     Example:
+
     >>> max_product_subarray([2, 3, -2, 4])
     6
     >>> max_product_subarray((-2, 0, -1))
diff --git a/dynamic_programming/max_subarray_sum.py b/dynamic_programming/max_subarray_sum.py
index c76943472b97..8c1dc0889a85 100644
--- a/dynamic_programming/max_subarray_sum.py
+++ b/dynamic_programming/max_subarray_sum.py
@@ -9,6 +9,7 @@
 
 Reference: https://en.wikipedia.org/wiki/Maximum_subarray_problem
 """
+
 from collections.abc import Sequence
 
 
diff --git a/dynamic_programming/minimum_squares_to_represent_a_number.py b/dynamic_programming/minimum_squares_to_represent_a_number.py
index bf5849f5bcb3..98c0602fa831 100644
--- a/dynamic_programming/minimum_squares_to_represent_a_number.py
+++ b/dynamic_programming/minimum_squares_to_represent_a_number.py
@@ -5,6 +5,7 @@
 def minimum_squares_to_represent_a_number(number: int) -> int:
     """
     Count the number of minimum squares to represent a number
+
     >>> minimum_squares_to_represent_a_number(25)
     1
     >>> minimum_squares_to_represent_a_number(37)
diff --git a/dynamic_programming/range_sum_query.py b/dynamic_programming/range_sum_query.py
new file mode 100644
index 000000000000..484fcf785fda
--- /dev/null
+++ b/dynamic_programming/range_sum_query.py
@@ -0,0 +1,92 @@
+"""
+Author: Sanjay Muthu <https://github.com/XenoBytesX>
+
+This is an implementation of the Dynamic Programming solution to the Range Sum Query.
+
+The problem statement is:
+    Given an array and q queries,
+    each query stating you to find the sum of elements from l to r (inclusive)
+
+Example:
+    arr = [1, 4, 6, 2, 61, 12]
+    queries = 3
+    l_1 = 2, r_1 = 5
+    l_2 = 1, r_2 = 5
+    l_3 = 3, r_3 = 4
+
+    as input will return
+
+    [81, 85, 63]
+
+    as output
+
+0-indexing:
+NOTE: 0-indexing means the indexing of the array starts from 0
+Example: a = [1, 2, 3, 4, 5, 6]
+         Here, the 0th index of a is 1,
+               the 1st index of a is 2,
+               and so forth
+
+Time Complexity: O(N + Q)
+* O(N) pre-calculation time to calculate the prefix sum array
+* and O(1) time per each query = O(1 * Q) = O(Q) time
+
+Space Complexity: O(N)
+* O(N) to store the prefix sum
+
+Algorithm:
+So, first we calculate the prefix sum (dp) of the array.
+The prefix sum of the index i is the sum of all elements indexed
+from 0 to i (inclusive).
+The prefix sum of the index i is the prefix sum of index (i - 1) + the current element.
+So, the state of the dp is dp[i] = dp[i - 1] + a[i].
+
+After we calculate the prefix sum,
+for each query [l, r]
+the answer is dp[r] - dp[l - 1] (we need to be careful because l might be 0).
+For example take this array:
+    [4, 2, 1, 6, 3]
+The prefix sum calculated for this array would be:
+    [4, 4 + 2, 4 + 2 + 1, 4 + 2 + 1 + 6, 4 + 2 + 1 + 6 + 3]
+    ==> [4, 6, 7, 13, 16]
+If the query was l = 3, r = 4,
+the answer would be 6 + 3 = 9 but this would require O(r - l + 1) time ≈ O(N) time
+
+If we use prefix sums we can find it in O(1) by using the formula
+prefix[r] - prefix[l - 1].
+This formula works because prefix[r] is the sum of elements from [0, r]
+and prefix[l - 1] is the sum of elements from [0, l - 1],
+so if we do prefix[r] - prefix[l - 1] it will be
+[0, r] - [0, l - 1] = [0, l - 1] + [l, r] - [0, l - 1] = [l, r]
+"""
+
+
+def prefix_sum(array: list[int], queries: list[tuple[int, int]]) -> list[int]:
+    """
+    >>> prefix_sum([1, 4, 6, 2, 61, 12], [(2, 5), (1, 5), (3, 4)])
+    [81, 85, 63]
+    >>> prefix_sum([4, 2, 1, 6, 3], [(3, 4), (1, 3), (0, 2)])
+    [9, 9, 7]
+    """
+    # The prefix sum array
+    dp = [0] * len(array)
+    dp[0] = array[0]
+    for i in range(1, len(array)):
+        dp[i] = dp[i - 1] + array[i]
+
+    # See Algorithm section (Line 44)
+    result = []
+    for query in queries:
+        left, right = query
+        res = dp[right]
+        if left > 0:
+            res -= dp[left - 1]
+        result.append(res)
+
+    return result
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/dynamic_programming/regex_match.py b/dynamic_programming/regex_match.py
index 200a882831c0..e94d82093c8b 100644
--- a/dynamic_programming/regex_match.py
+++ b/dynamic_programming/regex_match.py
@@ -1,23 +1,25 @@
 """
 Regex matching check if a text matches pattern or not.
 Pattern:
-    '.' Matches any single character.
-    '*' Matches zero or more of the preceding element.
+
+    1. ``.`` Matches any single character.
+    2. ``*`` Matches zero or more of the preceding element.
+
 More info:
     https://medium.com/trick-the-interviwer/regular-expression-matching-9972eb74c03
 """
 
 
 def recursive_match(text: str, pattern: str) -> bool:
-    """
+    r"""
     Recursive matching algorithm.
 
-    Time complexity: O(2 ^ (|text| + |pattern|))
-    Space complexity: Recursion depth is O(|text| + |pattern|).
+    | Time complexity: O(2^(\|text\| + \|pattern\|))
+    | Space complexity: Recursion depth is O(\|text\| + \|pattern\|).
 
     :param text: Text to match.
     :param pattern: Pattern to match.
-    :return: True if text matches pattern, False otherwise.
+    :return: ``True`` if `text` matches `pattern`, ``False`` otherwise.
 
     >>> recursive_match('abc', 'a.c')
     True
@@ -48,15 +50,15 @@ def recursive_match(text: str, pattern: str) -> bool:
 
 
 def dp_match(text: str, pattern: str) -> bool:
-    """
+    r"""
     Dynamic programming matching algorithm.
 
-    Time complexity: O(|text| * |pattern|)
-    Space complexity: O(|text| * |pattern|)
+    | Time complexity: O(\|text\| * \|pattern\|)
+    | Space complexity: O(\|text\| * \|pattern\|)
 
     :param text: Text to match.
     :param pattern: Pattern to match.
-    :return: True if text matches pattern, False otherwise.
+    :return: ``True`` if `text` matches `pattern`, ``False`` otherwise.
 
     >>> dp_match('abc', 'a.c')
     True
diff --git a/dynamic_programming/rod_cutting.py b/dynamic_programming/rod_cutting.py
index f80fa440ae86..d12c759dc928 100644
--- a/dynamic_programming/rod_cutting.py
+++ b/dynamic_programming/rod_cutting.py
@@ -1,7 +1,7 @@
 """
 This module provides two implementations for the rod-cutting problem:
-1. A naive recursive implementation which has an exponential runtime
-2. Two dynamic programming implementations which have quadratic runtime
+  1. A naive recursive implementation which has an exponential runtime
+  2. Two dynamic programming implementations which have quadratic runtime
 
 The rod-cutting problem is the problem of finding the maximum possible revenue
 obtainable from a rod of length ``n`` given a list of prices for each integral piece
@@ -20,18 +20,21 @@ def naive_cut_rod_recursive(n: int, prices: list):
     Runtime: O(2^n)
 
     Arguments
-    -------
-    n: int, the length of the rod
-    prices: list, the prices for each piece of rod. ``p[i-i]`` is the
-    price for a rod of length ``i``
+    ---------
+
+    * `n`: int, the length of the rod
+    * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the
+      price for a rod of length ``i``
 
     Returns
     -------
-    The maximum revenue obtainable for a rod of length n given the list of prices
+
+    The maximum revenue obtainable for a rod of length `n` given the list of prices
     for each piece.
 
     Examples
     --------
+
     >>> naive_cut_rod_recursive(4, [1, 5, 8, 9])
     10
     >>> naive_cut_rod_recursive(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30])
@@ -54,28 +57,30 @@ def top_down_cut_rod(n: int, prices: list):
     """
     Constructs a top-down dynamic programming solution for the rod-cutting
     problem via memoization. This function serves as a wrapper for
-    _top_down_cut_rod_recursive
+    ``_top_down_cut_rod_recursive``
 
     Runtime: O(n^2)
 
     Arguments
-    --------
-    n: int, the length of the rod
-    prices: list, the prices for each piece of rod. ``p[i-i]`` is the
-    price for a rod of length ``i``
+    ---------
 
-    Note
-    ----
-    For convenience and because Python's lists using 0-indexing, length(max_rev) =
-    n + 1, to accommodate for the revenue obtainable from a rod of length 0.
+    * `n`: int, the length of the rod
+    * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the
+      price for a rod of length ``i``
+
+    .. note::
+      For convenience and because Python's lists using ``0``-indexing, ``length(max_rev)
+      = n + 1``, to accommodate for the revenue obtainable from a rod of length ``0``.
 
     Returns
     -------
-    The maximum revenue obtainable for a rod of length n given the list of prices
+
+    The maximum revenue obtainable for a rod of length `n` given the list of prices
     for each piece.
 
     Examples
-    -------
+    --------
+
     >>> top_down_cut_rod(4, [1, 5, 8, 9])
     10
     >>> top_down_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30])
@@ -94,16 +99,18 @@ def _top_down_cut_rod_recursive(n: int, prices: list, max_rev: list):
     Runtime: O(n^2)
 
     Arguments
-    --------
-    n: int, the length of the rod
-    prices: list, the prices for each piece of rod. ``p[i-i]`` is the
-    price for a rod of length ``i``
-    max_rev: list, the computed maximum revenue for a piece of rod.
-    ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i``
+    ---------
+
+    * `n`: int, the length of the rod
+    * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the
+      price for a rod of length ``i``
+    * `max_rev`: list, the computed maximum revenue for a piece of rod.
+      ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i``
 
     Returns
     -------
-    The maximum revenue obtainable for a rod of length n given the list of prices
+
+    The maximum revenue obtainable for a rod of length `n` given the list of prices
     for each piece.
     """
     if max_rev[n] >= 0:
@@ -130,18 +137,21 @@ def bottom_up_cut_rod(n: int, prices: list):
     Runtime: O(n^2)
 
     Arguments
-    ----------
-    n: int, the maximum length of the rod.
-    prices: list, the prices for each piece of rod. ``p[i-i]`` is the
-    price for a rod of length ``i``
+    ---------
+
+    * `n`: int, the maximum length of the rod.
+    * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the
+      price for a rod of length ``i``
 
     Returns
     -------
-    The maximum revenue obtainable from cutting a rod of length n given
+
+    The maximum revenue obtainable from cutting a rod of length `n` given
     the prices for each piece of rod p.
 
     Examples
-    -------
+    --------
+
     >>> bottom_up_cut_rod(4, [1, 5, 8, 9])
     10
     >>> bottom_up_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30])
@@ -168,13 +178,12 @@ def _enforce_args(n: int, prices: list):
     """
     Basic checks on the arguments to the rod-cutting algorithms
 
-    n: int, the length of the rod
-    prices: list, the price list for each piece of rod.
-
-    Throws ValueError:
+    * `n`: int, the length of the rod
+    * `prices`: list, the price list for each piece of rod.
 
-    if n is negative or there are fewer items in the price list than the length of
-    the rod
+    Throws ``ValueError``:
+        if `n` is negative or there are fewer items in the price list than the length of
+        the rod
     """
     if n < 0:
         msg = f"n must be greater than or equal to 0. Got n = {n}"
diff --git a/dynamic_programming/subset_generation.py b/dynamic_programming/subset_generation.py
index 1be412b9374d..08daaac6f88a 100644
--- a/dynamic_programming/subset_generation.py
+++ b/dynamic_programming/subset_generation.py
@@ -1,38 +1,41 @@
 def subset_combinations(elements: list[int], n: int) -> list:
     """
     Compute n-element combinations from a given list using dynamic programming.
+
     Args:
-        elements: The list of elements from which combinations will be generated.
-        n: The number of elements in each combination.
+        * `elements`: The list of elements from which combinations will be generated.
+        * `n`: The number of elements in each combination.
+
     Returns:
-        A list of tuples, each representing a combination of n elements.
-        >>> subset_combinations(elements=[10, 20, 30, 40], n=2)
-        [(10, 20), (10, 30), (10, 40), (20, 30), (20, 40), (30, 40)]
-        >>> subset_combinations(elements=[1, 2, 3], n=1)
-        [(1,), (2,), (3,)]
-        >>> subset_combinations(elements=[1, 2, 3], n=3)
-        [(1, 2, 3)]
-        >>> subset_combinations(elements=[42], n=1)
-        [(42,)]
-        >>> subset_combinations(elements=[6, 7, 8, 9], n=4)
-        [(6, 7, 8, 9)]
-        >>> subset_combinations(elements=[10, 20, 30, 40, 50], n=0)
-        [()]
-        >>> subset_combinations(elements=[1, 2, 3, 4], n=2)
-        [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
-        >>> subset_combinations(elements=[1, 'apple', 3.14], n=2)
-        [(1, 'apple'), (1, 3.14), ('apple', 3.14)]
-        >>> subset_combinations(elements=['single'], n=0)
-        [()]
-        >>> subset_combinations(elements=[], n=9)
-        []
-        >>> from itertools import combinations
-        >>> all(subset_combinations(items, n) == list(combinations(items, n))
-        ...     for items, n in (
-        ...         ([10, 20, 30, 40], 2), ([1, 2, 3], 1), ([1, 2, 3], 3), ([42], 1),
-        ...         ([6, 7, 8, 9], 4), ([10, 20, 30, 40, 50], 1), ([1, 2, 3, 4], 2),
-        ...         ([1, 'apple', 3.14], 2), (['single'], 0), ([], 9)))
-        True
+        A list of tuples, each representing a combination of `n` elements.
+
+    >>> subset_combinations(elements=[10, 20, 30, 40], n=2)
+    [(10, 20), (10, 30), (10, 40), (20, 30), (20, 40), (30, 40)]
+    >>> subset_combinations(elements=[1, 2, 3], n=1)
+    [(1,), (2,), (3,)]
+    >>> subset_combinations(elements=[1, 2, 3], n=3)
+    [(1, 2, 3)]
+    >>> subset_combinations(elements=[42], n=1)
+    [(42,)]
+    >>> subset_combinations(elements=[6, 7, 8, 9], n=4)
+    [(6, 7, 8, 9)]
+    >>> subset_combinations(elements=[10, 20, 30, 40, 50], n=0)
+    [()]
+    >>> subset_combinations(elements=[1, 2, 3, 4], n=2)
+    [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+    >>> subset_combinations(elements=[1, 'apple', 3.14], n=2)
+    [(1, 'apple'), (1, 3.14), ('apple', 3.14)]
+    >>> subset_combinations(elements=['single'], n=0)
+    [()]
+    >>> subset_combinations(elements=[], n=9)
+    []
+    >>> from itertools import combinations
+    >>> all(subset_combinations(items, n) == list(combinations(items, n))
+    ...     for items, n in (
+    ...         ([10, 20, 30, 40], 2), ([1, 2, 3], 1), ([1, 2, 3], 3), ([42], 1),
+    ...         ([6, 7, 8, 9], 4), ([10, 20, 30, 40, 50], 1), ([1, 2, 3, 4], 2),
+    ...         ([1, 'apple', 3.14], 2), (['single'], 0), ([], 9)))
+    True
     """
     r = len(elements)
     if n > r:
@@ -45,7 +48,7 @@ def subset_combinations(elements: list[int], n: int) -> list:
     for i in range(1, r + 1):
         for j in range(i, 0, -1):
             for prev_combination in dp[j - 1]:
-                dp[j].append(tuple(prev_combination) + (elements[i - 1],))
+                dp[j].append((*prev_combination, elements[i - 1]))
 
     try:
         return sorted(dp[n])
diff --git a/dynamic_programming/viterbi.py b/dynamic_programming/viterbi.py
index 764d45dc2c05..5b78fa9e46d0 100644
--- a/dynamic_programming/viterbi.py
+++ b/dynamic_programming/viterbi.py
@@ -9,119 +9,102 @@ def viterbi(
     emission_probabilities: dict,
 ) -> list:
     """
-        Viterbi Algorithm, to find the most likely path of
-        states from the start and the expected output.
-        https://en.wikipedia.org/wiki/Viterbi_algorithm
-    sdafads
-        Wikipedia example
-        >>> observations = ["normal", "cold", "dizzy"]
-        >>> states = ["Healthy", "Fever"]
-        >>> start_p = {"Healthy": 0.6, "Fever": 0.4}
-        >>> trans_p = {
-        ...     "Healthy": {"Healthy": 0.7, "Fever": 0.3},
-        ...     "Fever": {"Healthy": 0.4, "Fever": 0.6},
-        ... }
-        >>> emit_p = {
-        ...     "Healthy": {"normal": 0.5, "cold": 0.4, "dizzy": 0.1},
-        ...     "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6},
-        ... }
-        >>> viterbi(observations, states, start_p, trans_p, emit_p)
-        ['Healthy', 'Healthy', 'Fever']
+    Viterbi Algorithm, to find the most likely path of
+    states from the start and the expected output.
 
-        >>> viterbi((), states, start_p, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: There's an empty parameter
-
-        >>> viterbi(observations, (), start_p, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: There's an empty parameter
-
-        >>> viterbi(observations, states, {}, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: There's an empty parameter
-
-        >>> viterbi(observations, states, start_p, {}, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: There's an empty parameter
-
-        >>> viterbi(observations, states, start_p, trans_p, {})
-        Traceback (most recent call last):
-            ...
-        ValueError: There's an empty parameter
-
-        >>> viterbi("invalid", states, start_p, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: observations_space must be a list
+    https://en.wikipedia.org/wiki/Viterbi_algorithm
 
-        >>> viterbi(["valid", 123], states, start_p, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: observations_space must be a list of strings
-
-        >>> viterbi(observations, "invalid", start_p, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: states_space must be a list
-
-        >>> viterbi(observations, ["valid", 123], start_p, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: states_space must be a list of strings
-
-        >>> viterbi(observations, states, "invalid", trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: initial_probabilities must be a dict
-
-        >>> viterbi(observations, states, {2:2}, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: initial_probabilities all keys must be strings
-
-        >>> viterbi(observations, states, {"a":2}, trans_p, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: initial_probabilities all values must be float
+    Wikipedia example
 
-        >>> viterbi(observations, states, start_p, "invalid", emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: transition_probabilities must be a dict
-
-        >>> viterbi(observations, states, start_p, {"a":2}, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: transition_probabilities all values must be dict
-
-        >>> viterbi(observations, states, start_p, {2:{2:2}}, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: transition_probabilities all keys must be strings
-
-        >>> viterbi(observations, states, start_p, {"a":{2:2}}, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: transition_probabilities all keys must be strings
-
-        >>> viterbi(observations, states, start_p, {"a":{"b":2}}, emit_p)
-        Traceback (most recent call last):
-            ...
-        ValueError: transition_probabilities nested dictionary all values must be float
-
-        >>> viterbi(observations, states, start_p, trans_p, "invalid")
-        Traceback (most recent call last):
-            ...
-        ValueError: emission_probabilities must be a dict
-
-        >>> viterbi(observations, states, start_p, trans_p, None)
-        Traceback (most recent call last):
-            ...
-        ValueError: There's an empty parameter
+    >>> observations = ["normal", "cold", "dizzy"]
+    >>> states = ["Healthy", "Fever"]
+    >>> start_p = {"Healthy": 0.6, "Fever": 0.4}
+    >>> trans_p = {
+    ...     "Healthy": {"Healthy": 0.7, "Fever": 0.3},
+    ...     "Fever": {"Healthy": 0.4, "Fever": 0.6},
+    ... }
+    >>> emit_p = {
+    ...     "Healthy": {"normal": 0.5, "cold": 0.4, "dizzy": 0.1},
+    ...     "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6},
+    ... }
+    >>> viterbi(observations, states, start_p, trans_p, emit_p)
+    ['Healthy', 'Healthy', 'Fever']
+    >>> viterbi((), states, start_p, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: There's an empty parameter
+    >>> viterbi(observations, (), start_p, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: There's an empty parameter
+    >>> viterbi(observations, states, {}, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: There's an empty parameter
+    >>> viterbi(observations, states, start_p, {}, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: There's an empty parameter
+    >>> viterbi(observations, states, start_p, trans_p, {})
+    Traceback (most recent call last):
+        ...
+    ValueError: There's an empty parameter
+    >>> viterbi("invalid", states, start_p, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: observations_space must be a list
+    >>> viterbi(["valid", 123], states, start_p, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: observations_space must be a list of strings
+    >>> viterbi(observations, "invalid", start_p, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: states_space must be a list
+    >>> viterbi(observations, ["valid", 123], start_p, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: states_space must be a list of strings
+    >>> viterbi(observations, states, "invalid", trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: initial_probabilities must be a dict
+    >>> viterbi(observations, states, {2:2}, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: initial_probabilities all keys must be strings
+    >>> viterbi(observations, states, {"a":2}, trans_p, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: initial_probabilities all values must be float
+    >>> viterbi(observations, states, start_p, "invalid", emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: transition_probabilities must be a dict
+    >>> viterbi(observations, states, start_p, {"a":2}, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: transition_probabilities all values must be dict
+    >>> viterbi(observations, states, start_p, {2:{2:2}}, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: transition_probabilities all keys must be strings
+    >>> viterbi(observations, states, start_p, {"a":{2:2}}, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: transition_probabilities all keys must be strings
+    >>> viterbi(observations, states, start_p, {"a":{"b":2}}, emit_p)
+    Traceback (most recent call last):
+        ...
+    ValueError: transition_probabilities nested dictionary all values must be float
+    >>> viterbi(observations, states, start_p, trans_p, "invalid")
+    Traceback (most recent call last):
+        ...
+    ValueError: emission_probabilities must be a dict
+    >>> viterbi(observations, states, start_p, trans_p, None)
+    Traceback (most recent call last):
+        ...
+    ValueError: There's an empty parameter
 
     """
     _validation(
@@ -213,7 +196,6 @@ def _validation(
     ...     "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6},
     ... }
     >>> _validation(observations, states, start_p, trans_p, emit_p)
-
     >>> _validation([], states, start_p, trans_p, emit_p)
     Traceback (most recent call last):
             ...
@@ -242,7 +224,6 @@ def _validate_not_empty(
     """
     >>> _validate_not_empty(["a"], ["b"], {"c":0.5},
     ... {"d": {"e": 0.6}}, {"f": {"g": 0.7}})
-
     >>> _validate_not_empty(["a"], ["b"], {"c":0.5}, {}, {"f": {"g": 0.7}})
     Traceback (most recent call last):
             ...
@@ -267,12 +248,10 @@ def _validate_not_empty(
 def _validate_lists(observations_space: Any, states_space: Any) -> None:
     """
     >>> _validate_lists(["a"], ["b"])
-
     >>> _validate_lists(1234, ["b"])
     Traceback (most recent call last):
             ...
     ValueError: observations_space must be a list
-
     >>> _validate_lists(["a"], [3])
     Traceback (most recent call last):
             ...
@@ -285,7 +264,6 @@ def _validate_lists(observations_space: Any, states_space: Any) -> None:
 def _validate_list(_object: Any, var_name: str) -> None:
     """
     >>> _validate_list(["a"], "mock_name")
-
     >>> _validate_list("a", "mock_name")
     Traceback (most recent call last):
             ...
@@ -294,7 +272,6 @@ def _validate_list(_object: Any, var_name: str) -> None:
     Traceback (most recent call last):
             ...
     ValueError: mock_name must be a list of strings
-
     """
     if not isinstance(_object, list):
         msg = f"{var_name} must be a list"
@@ -313,7 +290,6 @@ def _validate_dicts(
 ) -> None:
     """
     >>> _validate_dicts({"c":0.5}, {"d": {"e": 0.6}}, {"f": {"g": 0.7}})
-
     >>> _validate_dicts("invalid", {"d": {"e": 0.6}}, {"f": {"g": 0.7}})
     Traceback (most recent call last):
             ...
@@ -339,7 +315,6 @@ def _validate_dicts(
 def _validate_nested_dict(_object: Any, var_name: str) -> None:
     """
     >>> _validate_nested_dict({"a":{"b": 0.5}}, "mock_name")
-
     >>> _validate_nested_dict("invalid", "mock_name")
     Traceback (most recent call last):
             ...
@@ -367,7 +342,6 @@ def _validate_dict(
 ) -> None:
     """
     >>> _validate_dict({"b": 0.5}, "mock_name", float)
-
     >>> _validate_dict("invalid", "mock_name", float)
     Traceback (most recent call last):
             ...
diff --git a/dynamic_programming/wildcard_matching.py b/dynamic_programming/wildcard_matching.py
index 4ffc4b5d46aa..d9a1392720bd 100644
--- a/dynamic_programming/wildcard_matching.py
+++ b/dynamic_programming/wildcard_matching.py
@@ -1,62 +1,68 @@
 """
-Given two strings, an input string and a pattern,
-this program checks if the input string matches the pattern.
+Author  : ilyas dahhou
+Date    : Oct 7, 2023
 
-Example :
-input_string = "baaabab"
-pattern = "*****ba*****ab"
-Output: True
+Task:
+Given an input string and a pattern, implement wildcard pattern matching with support
+for '?' and '*' where:
+'?' matches any single character.
+'*' matches any sequence of characters (including the empty sequence).
+The matching should cover the entire input string (not partial).
 
-This problem can be solved using the concept of "DYNAMIC PROGRAMMING".
-
-We create a 2D boolean matrix, where each entry match_matrix[i][j] is True
-if the first i characters in input_string match the first j characters
-of pattern. We initialize the first row and first column based on specific
-rules, then fill up the rest of the matrix using a bottom-up dynamic
-programming approach.
-
-The amount of match that will be determined is equal to match_matrix[n][m]
-where n and m are lengths of the input_string and pattern respectively.
+Runtime complexity: O(m * n)
 
+The implementation was tested on the
+leetcode: https://leetcode.com/problems/wildcard-matching/
 """
 
 
-def is_pattern_match(input_string: str, pattern: str) -> bool:
+def is_match(string: str, pattern: str) -> bool:
     """
-    >>> is_pattern_match('baaabab','*****ba*****ba')
+    >>> is_match("", "")
+    True
+    >>> is_match("aa", "a")
     False
-    >>> is_pattern_match('baaabab','*****ba*****ab')
+    >>> is_match("abc", "abc")
+    True
+    >>> is_match("abc", "*c")
+    True
+    >>> is_match("abc", "a*")
     True
-    >>> is_pattern_match('aa','*')
+    >>> is_match("abc", "*a*")
+    True
+    >>> is_match("abc", "?b?")
+    True
+    >>> is_match("abc", "*?")
+    True
+    >>> is_match("abc", "a*d")
+    False
+    >>> is_match("abc", "a*c?")
+    False
+    >>> is_match('baaabab','*****ba*****ba')
+    False
+    >>> is_match('baaabab','*****ba*****ab')
+    True
+    >>> is_match('aa','*')
     True
     """
-
-    input_length = len(input_string)
-    pattern_length = len(pattern)
-
-    match_matrix = [[False] * (pattern_length + 1) for _ in range(input_length + 1)]
-
-    match_matrix[0][0] = True
-
-    for j in range(1, pattern_length + 1):
-        if pattern[j - 1] == "*":
-            match_matrix[0][j] = match_matrix[0][j - 1]
-
-    for i in range(1, input_length + 1):
-        for j in range(1, pattern_length + 1):
-            if pattern[j - 1] in ("?", input_string[i - 1]):
-                match_matrix[i][j] = match_matrix[i - 1][j - 1]
+    dp = [[False] * (len(pattern) + 1) for _ in string + "1"]
+    dp[0][0] = True
+    # Fill in the first row
+    for j, char in enumerate(pattern, 1):
+        if char == "*":
+            dp[0][j] = dp[0][j - 1]
+    # Fill in the rest of the DP table
+    for i, s_char in enumerate(string, 1):
+        for j, p_char in enumerate(pattern, 1):
+            if p_char in (s_char, "?"):
+                dp[i][j] = dp[i - 1][j - 1]
             elif pattern[j - 1] == "*":
-                match_matrix[i][j] = match_matrix[i - 1][j] or match_matrix[i][j - 1]
-            else:
-                match_matrix[i][j] = False
-
-    return match_matrix[input_length][pattern_length]
+                dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
+    return dp[len(string)][len(pattern)]
 
 
 if __name__ == "__main__":
     import doctest
 
     doctest.testmod()
-
-    print(f"{is_pattern_match('baaabab','*****ba*****ab')}")
+    print(f"{is_match('baaabab','*****ba*****ab') = }")
diff --git a/electronics/__init__.py b/electronics/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/electronics/capacitor_equivalence.py b/electronics/capacitor_equivalence.py
new file mode 100644
index 000000000000..274b18afb3ef
--- /dev/null
+++ b/electronics/capacitor_equivalence.py
@@ -0,0 +1,53 @@
+# https://farside.ph.utexas.edu/teaching/316/lectures/node46.html
+
+from __future__ import annotations
+
+
+def capacitor_parallel(capacitors: list[float]) -> float:
+    """
+    Ceq = C1 + C2 + ... + Cn
+    Calculate the equivalent resistance for any number of capacitors in parallel.
+    >>> capacitor_parallel([5.71389, 12, 3])
+    20.71389
+    >>> capacitor_parallel([5.71389, 12, -3])
+    Traceback (most recent call last):
+        ...
+    ValueError: Capacitor at index 2 has a negative value!
+    """
+    sum_c = 0.0
+    for index, capacitor in enumerate(capacitors):
+        if capacitor < 0:
+            msg = f"Capacitor at index {index} has a negative value!"
+            raise ValueError(msg)
+        sum_c += capacitor
+    return sum_c
+
+
+def capacitor_series(capacitors: list[float]) -> float:
+    """
+    Ceq = 1/ (1/C1 + 1/C2 + ... + 1/Cn)
+    >>> capacitor_series([5.71389, 12, 3])
+    1.6901062252507735
+    >>> capacitor_series([5.71389, 12, -3])
+    Traceback (most recent call last):
+        ...
+    ValueError: Capacitor at index 2 has a negative or zero value!
+    >>> capacitor_series([5.71389, 12, 0.000])
+    Traceback (most recent call last):
+        ...
+    ValueError: Capacitor at index 2 has a negative or zero value!
+    """
+
+    first_sum = 0.0
+    for index, capacitor in enumerate(capacitors):
+        if capacitor <= 0:
+            msg = f"Capacitor at index {index} has a negative or zero value!"
+            raise ValueError(msg)
+        first_sum += 1 / capacitor
+    return 1 / first_sum
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/electronics/charging_capacitor.py b/electronics/charging_capacitor.py
index 4029b0ecf267..0021e4e345e0 100644
--- a/electronics/charging_capacitor.py
+++ b/electronics/charging_capacitor.py
@@ -14,6 +14,7 @@
 time 't' from the initiation of charging a capacitor with the help of the exponential
 function containing RC.  Both at charging and discharging of a capacitor.
 """
+
 from math import exp  # value of exp = 2.718281828459…
 
 
diff --git a/electronics/charging_inductor.py b/electronics/charging_inductor.py
index e5c0126c248a..8a3bbc0bbfcd 100644
--- a/electronics/charging_inductor.py
+++ b/electronics/charging_inductor.py
@@ -25,6 +25,7 @@
 in its 'magnetic field'.with the help 'RL-time-constant' we can find current at any time
 in inductor while it is charging.
 """
+
 from math import exp  # value of exp = 2.718281828459…
 
 
diff --git a/electronics/circular_convolution.py b/electronics/circular_convolution.py
index f2e35742e944..d06e76be759b 100644
--- a/electronics/circular_convolution.py
+++ b/electronics/circular_convolution.py
@@ -37,10 +37,9 @@ def circular_convolution(self) -> list[float]:
         using matrix method
 
         Usage:
-        >>> import circular_convolution as cc
-        >>> convolution = cc.CircularConvolution()
+        >>> convolution = CircularConvolution()
         >>> convolution.circular_convolution()
-        [10, 10, 6, 14]
+        [10.0, 10.0, 6.0, 14.0]
 
         >>> convolution.first_signal = [0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6]
         >>> convolution.second_signal = [0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5]
@@ -55,7 +54,7 @@ def circular_convolution(self) -> list[float]:
         >>> convolution.first_signal = [1, -1, 2, 3, -1]
         >>> convolution.second_signal = [1, 2, 3]
         >>> convolution.circular_convolution()
-        [8, -2, 3, 4, 11]
+        [8.0, -2.0, 3.0, 4.0, 11.0]
 
         """
 
@@ -92,7 +91,7 @@ def circular_convolution(self) -> list[float]:
         final_signal = np.matmul(np.transpose(matrix), np.transpose(self.first_signal))
 
         # rounding-off to two decimal places
-        return [round(i, 2) for i in final_signal]
+        return [float(round(i, 2)) for i in final_signal]
 
 
 if __name__ == "__main__":
diff --git a/electronics/coulombs_law.py b/electronics/coulombs_law.py
index 18c1a8179eb6..74bbea5ea8ec 100644
--- a/electronics/coulombs_law.py
+++ b/electronics/coulombs_law.py
@@ -20,8 +20,8 @@ def couloumbs_law(
 
     Reference
     ----------
-    Coulomb (1785) "Premier mémoire sur l’électricité et le magnétisme,"
-    Histoire de l’Académie Royale des Sciences, pp. 569–577.
+    Coulomb (1785) "Premier mémoire sur l'électricité et le magnétisme,"
+    Histoire de l'Académie Royale des Sciences, pp. 569-577.
 
     Parameters
     ----------
diff --git a/electronics/electric_conductivity.py b/electronics/electric_conductivity.py
index 11f2a607d214..65bb6c5ceaf0 100644
--- a/electronics/electric_conductivity.py
+++ b/electronics/electric_conductivity.py
@@ -21,6 +21,26 @@ def electric_conductivity(
     ('conductivity', 5.12672e-14)
     >>> electric_conductivity(conductivity=1000, electron_conc=0, mobility=1200)
     ('electron_conc', 5.201506356240767e+18)
+    >>> electric_conductivity(conductivity=-10, electron_conc=100, mobility=0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Conductivity cannot be negative
+    >>> electric_conductivity(conductivity=50, electron_conc=-10, mobility=0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Electron concentration cannot be negative
+    >>> electric_conductivity(conductivity=50, electron_conc=0, mobility=-10)
+    Traceback (most recent call last):
+        ...
+    ValueError: mobility cannot be negative
+    >>> electric_conductivity(conductivity=50, electron_conc=0, mobility=0)
+    Traceback (most recent call last):
+        ...
+    ValueError: You cannot supply more or less than 2 values
+    >>> electric_conductivity(conductivity=50, electron_conc=200, mobility=300)
+    Traceback (most recent call last):
+        ...
+    ValueError: You cannot supply more or less than 2 values
     """
     if (conductivity, electron_conc, mobility).count(0) != 1:
         raise ValueError("You cannot supply more or less than 2 values")
diff --git a/electronics/electric_power.py b/electronics/electric_power.py
index 8b92e320ace3..8e3454e39c3f 100644
--- a/electronics/electric_power.py
+++ b/electronics/electric_power.py
@@ -23,20 +23,22 @@ def electric_power(voltage: float, current: float, power: float) -> tuple:
     >>> electric_power(voltage=2, current=4, power=2)
     Traceback (most recent call last):
         ...
-    ValueError: Only one argument must be 0
+    ValueError: Exactly one argument must be 0
     >>> electric_power(voltage=0, current=0, power=2)
     Traceback (most recent call last):
         ...
-    ValueError: Only one argument must be 0
+    ValueError: Exactly one argument must be 0
     >>> electric_power(voltage=0, current=2, power=-4)
     Traceback (most recent call last):
         ...
     ValueError: Power cannot be negative in any electrical/electronics system
     >>> electric_power(voltage=2.2, current=2.2, power=0)
     Result(name='power', value=4.84)
+    >>> electric_power(current=0, power=6, voltage=2)
+    Result(name='current', value=3.0)
     """
     if (voltage, current, power).count(0) != 1:
-        raise ValueError("Only one argument must be 0")
+        raise ValueError("Exactly one argument must be 0")
     elif power < 0:
         raise ValueError(
             "Power cannot be negative in any electrical/electronics system"
@@ -48,7 +50,7 @@ def electric_power(voltage: float, current: float, power: float) -> tuple:
     elif power == 0:
         return Result("power", float(round(abs(voltage * current), 2)))
     else:
-        raise ValueError("Exactly one argument must be 0")
+        raise AssertionError
 
 
 if __name__ == "__main__":
diff --git a/electronics/electrical_impedance.py b/electronics/electrical_impedance.py
index 44041ff790b6..4f4f1d308293 100644
--- a/electronics/electrical_impedance.py
+++ b/electronics/electrical_impedance.py
@@ -6,7 +6,7 @@
 
 from __future__ import annotations
 
-from math import pow, sqrt
+from math import pow, sqrt  # noqa: A004
 
 
 def electrical_impedance(
diff --git a/electronics/resistor_color_code.py b/electronics/resistor_color_code.py
index b0534b813def..189d19946d9d 100644
--- a/electronics/resistor_color_code.py
+++ b/electronics/resistor_color_code.py
@@ -58,6 +58,7 @@
     https://learn.parallax.com/support/reference/resistor-color-codes
     https://byjus.com/physics/resistor-colour-codes/
 """
+
 valid_colors: list = [
     "Black",
     "Brown",
diff --git a/electronics/resistor_equivalence.py b/electronics/resistor_equivalence.py
index 55e7f2d6b5d2..c4ea7d4b757e 100644
--- a/electronics/resistor_equivalence.py
+++ b/electronics/resistor_equivalence.py
@@ -20,13 +20,11 @@ def resistor_parallel(resistors: list[float]) -> float:
     """
 
     first_sum = 0.00
-    index = 0
-    for resistor in resistors:
+    for index, resistor in enumerate(resistors):
         if resistor <= 0:
             msg = f"Resistor at index {index} has a negative or zero value!"
             raise ValueError(msg)
         first_sum += 1 / float(resistor)
-        index += 1
     return 1 / first_sum
 
 
@@ -44,13 +42,11 @@ def resistor_series(resistors: list[float]) -> float:
     ValueError: Resistor at index 2 has a negative value!
     """
     sum_r = 0.00
-    index = 0
-    for resistor in resistors:
+    for index, resistor in enumerate(resistors):
         sum_r += resistor
         if resistor < 0:
             msg = f"Resistor at index {index} has a negative value!"
             raise ValueError(msg)
-        index += 1
     return sum_r
 
 
diff --git a/financial/ABOUT.md b/financial/README.md
similarity index 97%
rename from financial/ABOUT.md
rename to financial/README.md
index f6b0647f8201..e5d3a84c8381 100644
--- a/financial/ABOUT.md
+++ b/financial/README.md
@@ -1,4 +1,4 @@
-### Interest
+# Interest
 
 * Compound Interest: "Compound interest is calculated by multiplying the initial principal amount by one plus the annual interest rate raised to the number of compound periods minus one." [Compound Interest](https://www.investopedia.com/)
 * Simple Interest: "Simple interest paid or received over a certain period is a fixed percentage of the principal amount that was borrowed or lent. " [Simple Interest](https://www.investopedia.com/)
diff --git a/financial/exponential_moving_average.py b/financial/exponential_moving_average.py
index 0b6cea3b4c91..b56eb2712415 100644
--- a/financial/exponential_moving_average.py
+++ b/financial/exponential_moving_average.py
@@ -1,12 +1,12 @@
 """
-    Calculate the exponential moving average (EMA) on the series of stock prices.
-    Wikipedia Reference: https://en.wikipedia.org/wiki/Exponential_smoothing
-    https://www.investopedia.com/terms/e/ema.asp#toc-what-is-an-exponential
-    -moving-average-ema
-
-    Exponential moving average is used in finance to analyze changes stock prices.
-    EMA is used in conjunction with Simple moving average (SMA), EMA reacts to the
-    changes in the value quicker than SMA, which is one of the advantages of using EMA.
+Calculate the exponential moving average (EMA) on the series of stock prices.
+Wikipedia Reference: https://en.wikipedia.org/wiki/Exponential_smoothing
+https://www.investopedia.com/terms/e/ema.asp#toc-what-is-an-exponential
+-moving-average-ema
+
+Exponential moving average is used in finance to analyze changes stock prices.
+EMA is used in conjunction with Simple moving average (SMA), EMA reacts to the
+changes in the value quicker than SMA, which is one of the advantages of using EMA.
 """
 
 from collections.abc import Iterator
diff --git a/financial/simple_moving_average.py b/financial/simple_moving_average.py
new file mode 100644
index 000000000000..f5ae444fd027
--- /dev/null
+++ b/financial/simple_moving_average.py
@@ -0,0 +1,69 @@
+"""
+The Simple Moving Average (SMA) is a statistical calculation used to analyze data points
+by creating a constantly updated average price over a specific time period.
+In finance, SMA is often used in time series analysis to smooth out price data
+and identify trends.
+
+Reference: https://en.wikipedia.org/wiki/Moving_average
+"""
+
+from collections.abc import Sequence
+
+
+def simple_moving_average(
+    data: Sequence[float], window_size: int
+) -> list[float | None]:
+    """
+    Calculate the simple moving average (SMA) for some given time series data.
+
+    :param data: A list of numerical data points.
+    :param window_size: An integer representing the size of the SMA window.
+    :return: A list of SMA values with the same length as the input data.
+
+    Examples:
+    >>> sma = simple_moving_average([10, 12, 15, 13, 14, 16, 18, 17, 19, 21], 3)
+    >>> [round(value, 2) if value is not None else None for value in sma]
+    [None, None, 12.33, 13.33, 14.0, 14.33, 16.0, 17.0, 18.0, 19.0]
+    >>> simple_moving_average([10, 12, 15], 5)
+    [None, None, None]
+    >>> simple_moving_average([10, 12, 15, 13, 14, 16, 18, 17, 19, 21], 0)
+    Traceback (most recent call last):
+    ...
+    ValueError: Window size must be a positive integer
+    """
+    if window_size < 1:
+        raise ValueError("Window size must be a positive integer")
+
+    sma: list[float | None] = []
+
+    for i in range(len(data)):
+        if i < window_size - 1:
+            sma.append(None)  # SMA not available for early data points
+        else:
+            window = data[i - window_size + 1 : i + 1]
+            sma_value = sum(window) / window_size
+            sma.append(sma_value)
+    return sma
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    # Example data (replace with your own time series data)
+    data = [10, 12, 15, 13, 14, 16, 18, 17, 19, 21]
+
+    # Specify the window size for the SMA
+    window_size = 3
+
+    # Calculate the Simple Moving Average
+    sma_values = simple_moving_average(data, window_size)
+
+    # Print the SMA values
+    print("Simple Moving Average (SMA) Values:")
+    for i, value in enumerate(sma_values):
+        if value is not None:
+            print(f"Day {i + 1}: {value:.2f}")
+        else:
+            print(f"Day {i + 1}: Not enough data for SMA")
diff --git a/financial/straight_line_depreciation.py b/financial/straight_line_depreciation.py
new file mode 100644
index 000000000000..e11e1c1364ce
--- /dev/null
+++ b/financial/straight_line_depreciation.py
@@ -0,0 +1,103 @@
+"""
+In accounting, depreciation refers to the decreases in the value
+of a fixed asset during the asset's useful life.
+When an organization purchases a fixed asset,
+the purchase expenditure is not recognized as an expense immediately.
+Instead, the decreases in the asset's value are recognized as expenses
+over the years during which the asset is used.
+
+The following methods are widely used
+for depreciation calculation in accounting:
+- Straight-line method
+- Diminishing balance method
+- Units-of-production method
+
+The straight-line method is the simplest and most widely used.
+This method calculates depreciation by spreading the cost evenly
+over the asset's useful life.
+
+The following formula shows how to calculate the yearly depreciation expense:
+
+- annual depreciation expense =
+    (purchase cost of asset - residual value) / useful life of asset(years)
+
+Further information on:
+https://en.wikipedia.org/wiki/Depreciation
+
+The function, straight_line_depreciation, returns a list of
+the depreciation expenses over the given period.
+"""
+
+
+def straight_line_depreciation(
+    useful_years: int,
+    purchase_value: float,
+    residual_value: float = 0.0,
+) -> list[float]:
+    """
+    Calculate the depreciation expenses over the given period
+    :param useful_years: Number of years the asset will be used
+    :param purchase_value: Purchase expenditure for the asset
+    :param residual_value: Residual value of the asset at the end of its useful life
+    :return: A list of annual depreciation expenses over the asset's useful life
+    >>> straight_line_depreciation(10, 1100.0, 100.0)
+    [100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]
+    >>> straight_line_depreciation(6, 1250.0, 50.0)
+    [200.0, 200.0, 200.0, 200.0, 200.0, 200.0]
+    >>> straight_line_depreciation(4, 1001.0)
+    [250.25, 250.25, 250.25, 250.25]
+    >>> straight_line_depreciation(11, 380.0, 50.0)
+    [30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0]
+    >>> straight_line_depreciation(1, 4985, 100)
+    [4885.0]
+    """
+
+    if not isinstance(useful_years, int):
+        raise TypeError("Useful years must be an integer")
+
+    if useful_years < 1:
+        raise ValueError("Useful years cannot be less than 1")
+
+    if not isinstance(purchase_value, (float, int)):
+        raise TypeError("Purchase value must be numeric")
+
+    if not isinstance(residual_value, (float, int)):
+        raise TypeError("Residual value must be numeric")
+
+    if purchase_value < 0.0:
+        raise ValueError("Purchase value cannot be less than zero")
+
+    if purchase_value < residual_value:
+        raise ValueError("Purchase value cannot be less than residual value")
+
+    # Calculate annual depreciation expense
+    depreciable_cost = purchase_value - residual_value
+    annual_depreciation_expense = depreciable_cost / useful_years
+
+    # List of annual depreciation expenses
+    list_of_depreciation_expenses = []
+    accumulated_depreciation_expense = 0.0
+    for period in range(useful_years):
+        if period != useful_years - 1:
+            accumulated_depreciation_expense += annual_depreciation_expense
+            list_of_depreciation_expenses.append(annual_depreciation_expense)
+        else:
+            depreciation_expense_in_end_year = (
+                depreciable_cost - accumulated_depreciation_expense
+            )
+            list_of_depreciation_expenses.append(depreciation_expense_in_end_year)
+
+    return list_of_depreciation_expenses
+
+
+if __name__ == "__main__":
+    user_input_useful_years = int(input("Please Enter Useful Years:\n > "))
+    user_input_purchase_value = float(input("Please Enter Purchase Value:\n > "))
+    user_input_residual_value = float(input("Please Enter Residual Value:\n > "))
+    print(
+        straight_line_depreciation(
+            user_input_useful_years,
+            user_input_purchase_value,
+            user_input_residual_value,
+        )
+    )
diff --git a/financial/time_and_half_pay.py b/financial/time_and_half_pay.py
new file mode 100644
index 000000000000..c5dff1bc1ce1
--- /dev/null
+++ b/financial/time_and_half_pay.py
@@ -0,0 +1,40 @@
+"""
+Calculate time and a half pay
+"""
+
+
+def pay(hours_worked: float, pay_rate: float, hours: float = 40) -> float:
+    """
+    hours_worked = The total hours worked
+    pay_rate = Amount of money per hour
+    hours = Number of hours that must be worked before you receive time and a half
+
+    >>> pay(41, 1)
+    41.5
+    >>> pay(65, 19)
+    1472.5
+    >>> pay(10, 1)
+    10.0
+    """
+    # Check that all input parameters are float or integer
+    assert isinstance(hours_worked, (float, int)), (
+        "Parameter 'hours_worked' must be of type 'int' or 'float'"
+    )
+    assert isinstance(pay_rate, (float, int)), (
+        "Parameter 'pay_rate' must be of type 'int' or 'float'"
+    )
+    assert isinstance(hours, (float, int)), (
+        "Parameter 'hours' must be of type 'int' or 'float'"
+    )
+
+    normal_pay = hours_worked * pay_rate
+    over_time = max(0, hours_worked - hours)
+    over_time_pay = over_time * pay_rate / 2
+    return normal_pay + over_time_pay
+
+
+if __name__ == "__main__":
+    # Test
+    import doctest
+
+    doctest.testmod()
diff --git a/fractals/__init__.py b/fractals/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/fractals/julia_sets.py b/fractals/julia_sets.py
index 482e1eddfecc..bea599d44339 100644
--- a/fractals/julia_sets.py
+++ b/fractals/julia_sets.py
@@ -25,8 +25,8 @@
 from collections.abc import Callable
 from typing import Any
 
-import numpy
-from matplotlib import pyplot
+import matplotlib.pyplot as plt
+import numpy as np
 
 c_cauliflower = 0.25 + 0.0j
 c_polynomial_1 = -0.4 + 0.6j
@@ -37,22 +37,20 @@
 nb_pixels = 666
 
 
-def eval_exponential(c_parameter: complex, z_values: numpy.ndarray) -> numpy.ndarray:
+def eval_exponential(c_parameter: complex, z_values: np.ndarray) -> np.ndarray:
     """
     Evaluate $e^z + c$.
-    >>> eval_exponential(0, 0)
+    >>> float(eval_exponential(0, 0))
     1.0
-    >>> abs(eval_exponential(1, numpy.pi*1.j)) < 1e-15
+    >>> bool(abs(eval_exponential(1, np.pi*1.j)) < 1e-15)
     True
-    >>> abs(eval_exponential(1.j, 0)-1-1.j) < 1e-15
+    >>> bool(abs(eval_exponential(1.j, 0)-1-1.j) < 1e-15)
     True
     """
-    return numpy.exp(z_values) + c_parameter
+    return np.exp(z_values) + c_parameter
 
 
-def eval_quadratic_polynomial(
-    c_parameter: complex, z_values: numpy.ndarray
-) -> numpy.ndarray:
+def eval_quadratic_polynomial(c_parameter: complex, z_values: np.ndarray) -> np.ndarray:
     """
     >>> eval_quadratic_polynomial(0, 2)
     4
@@ -66,7 +64,7 @@ def eval_quadratic_polynomial(
     return z_values * z_values + c_parameter
 
 
-def prepare_grid(window_size: float, nb_pixels: int) -> numpy.ndarray:
+def prepare_grid(window_size: float, nb_pixels: int) -> np.ndarray:
     """
     Create a grid of complex values of size nb_pixels*nb_pixels with real and
      imaginary parts ranging from -window_size to window_size (inclusive).
@@ -77,20 +75,20 @@ def prepare_grid(window_size: float, nb_pixels: int) -> numpy.ndarray:
            [ 0.-1.j,  0.+0.j,  0.+1.j],
            [ 1.-1.j,  1.+0.j,  1.+1.j]])
     """
-    x = numpy.linspace(-window_size, window_size, nb_pixels)
+    x = np.linspace(-window_size, window_size, nb_pixels)
     x = x.reshape((nb_pixels, 1))
-    y = numpy.linspace(-window_size, window_size, nb_pixels)
+    y = np.linspace(-window_size, window_size, nb_pixels)
     y = y.reshape((1, nb_pixels))
     return x + 1.0j * y
 
 
 def iterate_function(
-    eval_function: Callable[[Any, numpy.ndarray], numpy.ndarray],
+    eval_function: Callable[[Any, np.ndarray], np.ndarray],
     function_params: Any,
     nb_iterations: int,
-    z_0: numpy.ndarray,
+    z_0: np.ndarray,
     infinity: float | None = None,
-) -> numpy.ndarray:
+) -> np.ndarray:
     """
     Iterate the function "eval_function" exactly nb_iterations times.
     The first argument of the function is a parameter which is contained in
@@ -98,22 +96,22 @@ def iterate_function(
     values to iterate from.
     This function returns the final iterates.
 
-    >>> iterate_function(eval_quadratic_polynomial, 0, 3, numpy.array([0,1,2])).shape
+    >>> iterate_function(eval_quadratic_polynomial, 0, 3, np.array([0,1,2])).shape
     (3,)
-    >>> numpy.round(iterate_function(eval_quadratic_polynomial,
+    >>> complex(np.round(iterate_function(eval_quadratic_polynomial,
     ... 0,
     ... 3,
-    ... numpy.array([0,1,2]))[0])
+    ... np.array([0,1,2]))[0]))
     0j
-    >>> numpy.round(iterate_function(eval_quadratic_polynomial,
+    >>> complex(np.round(iterate_function(eval_quadratic_polynomial,
     ... 0,
     ... 3,
-    ... numpy.array([0,1,2]))[1])
+    ... np.array([0,1,2]))[1]))
     (1+0j)
-    >>> numpy.round(iterate_function(eval_quadratic_polynomial,
+    >>> complex(np.round(iterate_function(eval_quadratic_polynomial,
     ... 0,
     ... 3,
-    ... numpy.array([0,1,2]))[2])
+    ... np.array([0,1,2]))[2]))
     (256+0j)
     """
 
@@ -121,8 +119,8 @@ def iterate_function(
     for _ in range(nb_iterations):
         z_n = eval_function(function_params, z_n)
         if infinity is not None:
-            numpy.nan_to_num(z_n, copy=False, nan=infinity)
-            z_n[abs(z_n) == numpy.inf] = infinity
+            np.nan_to_num(z_n, copy=False, nan=infinity)
+            z_n[abs(z_n) == np.inf] = infinity
     return z_n
 
 
@@ -130,21 +128,21 @@ def show_results(
     function_label: str,
     function_params: Any,
     escape_radius: float,
-    z_final: numpy.ndarray,
+    z_final: np.ndarray,
 ) -> None:
     """
     Plots of whether the absolute value of z_final is greater than
     the value of escape_radius. Adds the function_label and function_params to
     the title.
 
-    >>> show_results('80', 0, 1, numpy.array([[0,1,.5],[.4,2,1.1],[.2,1,1.3]]))
+    >>> show_results('80', 0, 1, np.array([[0,1,.5],[.4,2,1.1],[.2,1,1.3]]))
     """
 
     abs_z_final = (abs(z_final)).transpose()
     abs_z_final[:, :] = abs_z_final[::-1, :]
-    pyplot.matshow(abs_z_final < escape_radius)
-    pyplot.title(f"Julia set of ${function_label}$, $c={function_params}$")
-    pyplot.show()
+    plt.matshow(abs_z_final < escape_radius)
+    plt.title(f"Julia set of ${function_label}$, $c={function_params}$")
+    plt.show()
 
 
 def ignore_overflow_warnings() -> None:
diff --git a/fractals/koch_snowflake.py b/fractals/koch_snowflake.py
index b0aaa86b11d8..724b78f41a69 100644
--- a/fractals/koch_snowflake.py
+++ b/fractals/koch_snowflake.py
@@ -20,28 +20,27 @@
     - numpy
 """
 
-
 from __future__ import annotations
 
-import matplotlib.pyplot as plt  # type: ignore
-import numpy
+import matplotlib.pyplot as plt
+import numpy as np
 
 # initial triangle of Koch snowflake
-VECTOR_1 = numpy.array([0, 0])
-VECTOR_2 = numpy.array([0.5, 0.8660254])
-VECTOR_3 = numpy.array([1, 0])
+VECTOR_1 = np.array([0, 0])
+VECTOR_2 = np.array([0.5, 0.8660254])
+VECTOR_3 = np.array([1, 0])
 INITIAL_VECTORS = [VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_1]
 
 # uncomment for simple Koch curve instead of Koch snowflake
 # INITIAL_VECTORS = [VECTOR_1, VECTOR_3]
 
 
-def iterate(initial_vectors: list[numpy.ndarray], steps: int) -> list[numpy.ndarray]:
+def iterate(initial_vectors: list[np.ndarray], steps: int) -> list[np.ndarray]:
     """
     Go through the number of iterations determined by the argument "steps".
     Be careful with high values (above 5) since the time to calculate increases
     exponentially.
-    >>> iterate([numpy.array([0, 0]), numpy.array([1, 0])], 1)
+    >>> iterate([np.array([0, 0]), np.array([1, 0])], 1)
     [array([0, 0]), array([0.33333333, 0.        ]), array([0.5       , \
 0.28867513]), array([0.66666667, 0.        ]), array([1, 0])]
     """
@@ -51,13 +50,13 @@ def iterate(initial_vectors: list[numpy.ndarray], steps: int) -> list[numpy.ndar
     return vectors
 
 
-def iteration_step(vectors: list[numpy.ndarray]) -> list[numpy.ndarray]:
+def iteration_step(vectors: list[np.ndarray]) -> list[np.ndarray]:
     """
     Loops through each pair of adjacent vectors. Each line between two adjacent
     vectors is divided into 4 segments by adding 3 additional vectors in-between
     the original two vectors. The vector in the middle is constructed through a
     60 degree rotation so it is bent outwards.
-    >>> iteration_step([numpy.array([0, 0]), numpy.array([1, 0])])
+    >>> iteration_step([np.array([0, 0]), np.array([1, 0])])
     [array([0, 0]), array([0.33333333, 0.        ]), array([0.5       , \
 0.28867513]), array([0.66666667, 0.        ]), array([1, 0])]
     """
@@ -75,22 +74,22 @@ def iteration_step(vectors: list[numpy.ndarray]) -> list[numpy.ndarray]:
     return new_vectors
 
 
-def rotate(vector: numpy.ndarray, angle_in_degrees: float) -> numpy.ndarray:
+def rotate(vector: np.ndarray, angle_in_degrees: float) -> np.ndarray:
     """
     Standard rotation of a 2D vector with a rotation matrix
     (see https://en.wikipedia.org/wiki/Rotation_matrix )
-    >>> rotate(numpy.array([1, 0]), 60)
+    >>> rotate(np.array([1, 0]), 60)
     array([0.5      , 0.8660254])
-    >>> rotate(numpy.array([1, 0]), 90)
+    >>> rotate(np.array([1, 0]), 90)
     array([6.123234e-17, 1.000000e+00])
     """
-    theta = numpy.radians(angle_in_degrees)
-    c, s = numpy.cos(theta), numpy.sin(theta)
-    rotation_matrix = numpy.array(((c, -s), (s, c)))
-    return numpy.dot(rotation_matrix, vector)
+    theta = np.radians(angle_in_degrees)
+    c, s = np.cos(theta), np.sin(theta)
+    rotation_matrix = np.array(((c, -s), (s, c)))
+    return np.dot(rotation_matrix, vector)
 
 
-def plot(vectors: list[numpy.ndarray]) -> None:
+def plot(vectors: list[np.ndarray]) -> None:
     """
     Utility function to plot the vectors using matplotlib.pyplot
     No doctest was implemented since this function does not have a return value
diff --git a/fractals/mandelbrot.py b/fractals/mandelbrot.py
index 84dbda997562..359d965a882d 100644
--- a/fractals/mandelbrot.py
+++ b/fractals/mandelbrot.py
@@ -15,10 +15,9 @@
 (see also https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set )
 """
 
-
 import colorsys
 
-from PIL import Image  # type: ignore
+from PIL import Image
 
 
 def get_distance(x: float, y: float, max_step: int) -> float:
diff --git a/fractals/sierpinski_triangle.py b/fractals/sierpinski_triangle.py
index 45f7ab84cfff..ceb2001b681d 100644
--- a/fractals/sierpinski_triangle.py
+++ b/fractals/sierpinski_triangle.py
@@ -22,6 +22,7 @@
     This code was written by editing the code from
     https://www.riannetrujillo.com/blog/python-fractal/
 """
+
 import sys
 import turtle
 
diff --git a/fractals/vicsek.py b/fractals/vicsek.py
new file mode 100644
index 000000000000..290fe95b79b4
--- /dev/null
+++ b/fractals/vicsek.py
@@ -0,0 +1,76 @@
+"""Authors Bastien Capiaux & Mehdi Oudghiri
+
+The Vicsek fractal algorithm is a recursive algorithm that creates a
+pattern known as the Vicsek fractal or the Vicsek square.
+It is based on the concept of self-similarity, where the pattern at each
+level of recursion resembles the overall pattern.
+The algorithm involves dividing a square into 9 equal smaller squares,
+removing the center square, and then repeating this process on the remaining 8 squares.
+This results in a pattern that exhibits self-similarity and has a
+square-shaped outline with smaller squares within it.
+
+Source: https://en.wikipedia.org/wiki/Vicsek_fractal
+"""
+
+import turtle
+
+
+def draw_cross(x: float, y: float, length: float):
+    """
+    Draw a cross at the specified position and with the specified length.
+    """
+    turtle.up()
+    turtle.goto(x - length / 2, y - length / 6)
+    turtle.down()
+    turtle.seth(0)
+    turtle.begin_fill()
+    for _ in range(4):
+        turtle.fd(length / 3)
+        turtle.right(90)
+        turtle.fd(length / 3)
+        turtle.left(90)
+        turtle.fd(length / 3)
+        turtle.left(90)
+    turtle.end_fill()
+
+
+def draw_fractal_recursive(x: float, y: float, length: float, depth: float):
+    """
+    Recursively draw the Vicsek fractal at the specified position, with the
+    specified length and depth.
+    """
+    if depth == 0:
+        draw_cross(x, y, length)
+        return
+
+    draw_fractal_recursive(x, y, length / 3, depth - 1)
+    draw_fractal_recursive(x + length / 3, y, length / 3, depth - 1)
+    draw_fractal_recursive(x - length / 3, y, length / 3, depth - 1)
+    draw_fractal_recursive(x, y + length / 3, length / 3, depth - 1)
+    draw_fractal_recursive(x, y - length / 3, length / 3, depth - 1)
+
+
+def set_color(rgb: str):
+    turtle.color(rgb)
+
+
+def draw_vicsek_fractal(x: float, y: float, length: float, depth: float, color="blue"):
+    """
+    Draw the Vicsek fractal at the specified position, with the specified
+    length and depth.
+    """
+    turtle.speed(0)
+    turtle.hideturtle()
+    set_color(color)
+    draw_fractal_recursive(x, y, length, depth)
+    turtle.Screen().update()
+
+
+def main():
+    draw_vicsek_fractal(0, 0, 800, 4)
+
+    turtle.done()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzy_logic/fuzzy_operations.py b/fuzzy_logic/fuzzy_operations.py
new file mode 100644
index 000000000000..c5e4cbde019d
--- /dev/null
+++ b/fuzzy_logic/fuzzy_operations.py
@@ -0,0 +1,195 @@
+"""
+By @Shreya123714
+
+https://en.wikipedia.org/wiki/Fuzzy_set
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+@dataclass
+class FuzzySet:
+    """
+    A class for representing and manipulating triangular fuzzy sets.
+    Attributes:
+        name: The name or label of the fuzzy set.
+        left_boundary: The left boundary of the fuzzy set.
+        peak: The peak (central) value of the fuzzy set.
+        right_boundary: The right boundary of the fuzzy set.
+    Methods:
+        membership(x): Calculate the membership value of an input 'x' in the fuzzy set.
+        union(other): Calculate the union of this fuzzy set with another fuzzy set.
+        intersection(other): Calculate the intersection of this fuzzy set with another.
+        complement(): Calculate the complement (negation) of this fuzzy set.
+        plot(): Plot the membership function of the fuzzy set.
+
+    >>> sheru = FuzzySet("Sheru", 0.4, 1, 0.6)
+    >>> sheru
+    FuzzySet(name='Sheru', left_boundary=0.4, peak=1, right_boundary=0.6)
+    >>> str(sheru)
+    'Sheru: [0.4, 1, 0.6]'
+
+    >>> siya = FuzzySet("Siya", 0.5, 1, 0.7)
+    >>> siya
+    FuzzySet(name='Siya', left_boundary=0.5, peak=1, right_boundary=0.7)
+
+    # Complement Operation
+    >>> sheru.complement()
+    FuzzySet(name='¬Sheru', left_boundary=0.4, peak=0.6, right_boundary=0)
+    >>> siya.complement()  # doctest: +NORMALIZE_WHITESPACE
+    FuzzySet(name='¬Siya', left_boundary=0.30000000000000004, peak=0.5,
+     right_boundary=0)
+
+    # Intersection Operation
+    >>> siya.intersection(sheru)
+    FuzzySet(name='Siya ∩ Sheru', left_boundary=0.5, peak=0.6, right_boundary=1.0)
+
+    # Membership Operation
+    >>> sheru.membership(0.5)
+    0.16666666666666663
+    >>> sheru.membership(0.6)
+    0.0
+
+    # Union Operations
+    >>> siya.union(sheru)
+    FuzzySet(name='Siya U Sheru', left_boundary=0.4, peak=0.7, right_boundary=1.0)
+    """
+
+    name: str
+    left_boundary: float
+    peak: float
+    right_boundary: float
+
+    def __str__(self) -> str:
+        """
+        >>> FuzzySet("fuzzy_set", 0.1, 0.2, 0.3)
+        FuzzySet(name='fuzzy_set', left_boundary=0.1, peak=0.2, right_boundary=0.3)
+        """
+        return (
+            f"{self.name}: [{self.left_boundary}, {self.peak}, {self.right_boundary}]"
+        )
+
+    def complement(self) -> FuzzySet:
+        """
+        Calculate the complement (negation) of this fuzzy set.
+        Returns:
+            FuzzySet: A new fuzzy set representing the complement.
+
+        >>> FuzzySet("fuzzy_set", 0.1, 0.2, 0.3).complement()
+        FuzzySet(name='¬fuzzy_set', left_boundary=0.7, peak=0.9, right_boundary=0.8)
+        """
+        return FuzzySet(
+            f"¬{self.name}",
+            1 - self.right_boundary,
+            1 - self.left_boundary,
+            1 - self.peak,
+        )
+
+    def intersection(self, other) -> FuzzySet:
+        """
+        Calculate the intersection of this fuzzy set
+        with another fuzzy set.
+        Args:
+            other: Another fuzzy set to intersect with.
+        Returns:
+            A new fuzzy set representing the intersection.
+
+        >>> FuzzySet("a", 0.1, 0.2, 0.3).intersection(FuzzySet("b", 0.4, 0.5, 0.6))
+        FuzzySet(name='a ∩ b', left_boundary=0.4, peak=0.3, right_boundary=0.35)
+        """
+        return FuzzySet(
+            f"{self.name} ∩ {other.name}",
+            max(self.left_boundary, other.left_boundary),
+            min(self.right_boundary, other.right_boundary),
+            (self.peak + other.peak) / 2,
+        )
+
+    def membership(self, x: float) -> float:
+        """
+        Calculate the membership value of an input 'x' in the fuzzy set.
+        Returns:
+            The membership value of 'x' in the fuzzy set.
+
+        >>> a = FuzzySet("a", 0.1, 0.2, 0.3)
+        >>> a.membership(0.09)
+        0.0
+        >>> a.membership(0.1)
+        0.0
+        >>> a.membership(0.11)
+        0.09999999999999995
+        >>> a.membership(0.4)
+        0.0
+        >>> FuzzySet("A", 0, 0.5, 1).membership(0.1)
+        0.2
+        >>> FuzzySet("B", 0.2, 0.7, 1).membership(0.6)
+        0.8
+        """
+        if x <= self.left_boundary or x >= self.right_boundary:
+            return 0.0
+        elif self.left_boundary < x <= self.peak:
+            return (x - self.left_boundary) / (self.peak - self.left_boundary)
+        elif self.peak < x < self.right_boundary:
+            return (self.right_boundary - x) / (self.right_boundary - self.peak)
+        msg = f"Invalid value {x} for fuzzy set {self}"
+        raise ValueError(msg)
+
+    def union(self, other) -> FuzzySet:
+        """
+        Calculate the union of this fuzzy set with another fuzzy set.
+        Args:
+            other (FuzzySet): Another fuzzy set to union with.
+        Returns:
+            FuzzySet: A new fuzzy set representing the union.
+
+        >>> FuzzySet("a", 0.1, 0.2, 0.3).union(FuzzySet("b", 0.4, 0.5, 0.6))
+        FuzzySet(name='a U b', left_boundary=0.1, peak=0.6, right_boundary=0.35)
+        """
+        return FuzzySet(
+            f"{self.name} U {other.name}",
+            min(self.left_boundary, other.left_boundary),
+            max(self.right_boundary, other.right_boundary),
+            (self.peak + other.peak) / 2,
+        )
+
+    def plot(self):
+        """
+        Plot the membership function of the fuzzy set.
+        """
+        x = np.linspace(0, 1, 1000)
+        y = [self.membership(xi) for xi in x]
+
+        plt.plot(x, y, label=self.name)
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    a = FuzzySet("A", 0, 0.5, 1)
+    b = FuzzySet("B", 0.2, 0.7, 1)
+
+    a.plot()
+    b.plot()
+
+    plt.xlabel("x")
+    plt.ylabel("Membership")
+    plt.legend()
+    plt.show()
+
+    union_ab = a.union(b)
+    intersection_ab = a.intersection(b)
+    complement_a = a.complement()
+
+    union_ab.plot()
+    intersection_ab.plot()
+    complement_a.plot()
+
+    plt.xlabel("x")
+    plt.ylabel("Membership")
+    plt.legend()
+    plt.show()
diff --git a/fuzzy_logic/fuzzy_operations.py.DISABLED.txt b/fuzzy_logic/fuzzy_operations.py.DISABLED.txt
index 0786ef8b0c67..67fd587f4baf 100644
--- a/fuzzy_logic/fuzzy_operations.py.DISABLED.txt
+++ b/fuzzy_logic/fuzzy_operations.py.DISABLED.txt
@@ -28,7 +28,7 @@ if __name__ == "__main__":
     union = fuzz.fuzzy_or(X, young, X, middle_aged)[1]
     # 2. Intersection = min(µA(x), µB(x))
     intersection = fuzz.fuzzy_and(X, young, X, middle_aged)[1]
-    # 3. Complement (A) = (1- min(µA(x))
+    # 3. Complement (A) = (1 - min(µA(x)))
     complement_a = fuzz.fuzzy_not(young)
     # 4. Difference (A/B) = min(µA(x),(1- µB(x)))
     difference = fuzz.fuzzy_and(X, young, X, fuzz.fuzzy_not(middle_aged)[1])[1]
diff --git a/genetic_algorithm/basic_string.py b/genetic_algorithm/basic_string.py
index 089c5c99a1ec..b75491d9a949 100644
--- a/genetic_algorithm/basic_string.py
+++ b/genetic_algorithm/basic_string.py
@@ -33,7 +33,12 @@ def evaluate(item: str, main_target: str) -> tuple[str, float]:
 
 
 def crossover(parent_1: str, parent_2: str) -> tuple[str, str]:
-    """Slice and combine two string at a random point."""
+    """
+    Slice and combine two strings at a random point.
+    >>> random.seed(42)
+    >>> crossover("123456", "abcdef")
+    ('12345f', 'abcde6')
+    """
     random_slice = random.randint(0, len(parent_1) - 1)
     child_1 = parent_1[:random_slice] + parent_2[random_slice:]
     child_2 = parent_2[:random_slice] + parent_1[random_slice:]
@@ -41,7 +46,12 @@ def crossover(parent_1: str, parent_2: str) -> tuple[str, str]:
 
 
 def mutate(child: str, genes: list[str]) -> str:
-    """Mutate a random gene of a child with another one from the list."""
+    """
+    Mutate a random gene of a child with another one from the list.
+    >>> random.seed(123)
+    >>> mutate("123456", list("ABCDEF"))
+    '12345A'
+    """
     child_list = list(child)
     if random.uniform(0, 1) < MUTATION_PROBABILITY:
         child_list[random.randint(0, len(child)) - 1] = random.choice(genes)
@@ -54,7 +64,22 @@ def select(
     population_score: list[tuple[str, float]],
     genes: list[str],
 ) -> list[str]:
-    """Select the second parent and generate new population"""
+    """
+    Select the second parent and generate new population
+
+    >>> random.seed(42)
+    >>> parent_1 = ("123456", 8.0)
+    >>> population_score = [("abcdef", 4.0), ("ghijkl", 5.0), ("mnopqr", 7.0)]
+    >>> genes = list("ABCDEF")
+    >>> child_n = int(min(parent_1[1] + 1, 10))
+    >>> population = []
+    >>> for _ in range(child_n):
+    ...     parent_2 = population_score[random.randrange(len(population_score))][0]
+    ...     child_1, child_2 = crossover(parent_1[0], parent_2)
+    ...     population.extend((mutate(child_1, genes), mutate(child_2, genes)))
+    >>> len(population) == (int(parent_1[1]) + 1) * 2
+    True
+    """
     pop = []
     # Generate more children proportionally to the fitness score.
     child_n = int(parent_1[1] * 100) + 1
@@ -119,18 +144,18 @@ def basic(target: str, genes: list[str], debug: bool = True) -> tuple[int, int,
 
         # Random population created. Now it's time to evaluate.
 
-        # Adding a bit of concurrency can make everything faster,
+        # (Option 1) Adding a bit of concurrency can make everything faster,
         #
         # import concurrent.futures
         # population_score: list[tuple[str, float]] = []
         # with concurrent.futures.ThreadPoolExecutor(
         #                                   max_workers=NUM_WORKERS) as executor:
-        #     futures = {executor.submit(evaluate, item) for item in population}
+        #     futures = {executor.submit(evaluate, item, target) for item in population}
         #     concurrent.futures.wait(futures)
         #     population_score = [item.result() for item in futures]
         #
         # but with a simple algorithm like this, it will probably be slower.
-        # We just need to call evaluate for every item inside the population.
+        # (Option 2) We just need to call evaluate for every item inside the population.
         population_score = [evaluate(item, target) for item in population]
 
         # Check if there is a matching evolution.
diff --git a/geodesy/haversine_distance.py b/geodesy/haversine_distance.py
index 93e625770f9d..39cd250af965 100644
--- a/geodesy/haversine_distance.py
+++ b/geodesy/haversine_distance.py
@@ -21,10 +21,11 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
     computation like Haversine can be handy for shorter range distances.
 
     Args:
-        lat1, lon1: latitude and longitude of coordinate 1
-        lat2, lon2: latitude and longitude of coordinate 2
+        * `lat1`, `lon1`: latitude and longitude of coordinate 1
+        * `lat2`, `lon2`: latitude and longitude of coordinate 2
     Returns:
         geographical distance between two points in metres
+
     >>> from collections import namedtuple
     >>> point_2d = namedtuple("point_2d", "lat lon")
     >>> SAN_FRANCISCO = point_2d(37.774856, -122.424227)
diff --git a/geometry/__init__.py b/geometry/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/geometry/geometry.py b/geometry/geometry.py
new file mode 100644
index 000000000000..a0be8eb3befc
--- /dev/null
+++ b/geometry/geometry.py
@@ -0,0 +1,288 @@
+from __future__ import annotations
+
+import math
+from dataclasses import dataclass, field
+from types import NoneType
+from typing import Self
+
+# Building block classes
+
+
+@dataclass
+class Angle:
+    """
+    An Angle in degrees (unit of measurement)
+
+    >>> Angle()
+    Angle(degrees=90)
+    >>> Angle(45.5)
+    Angle(degrees=45.5)
+    >>> Angle(-1)
+    Traceback (most recent call last):
+        ...
+    TypeError: degrees must be a numeric value between 0 and 360.
+    >>> Angle(361)
+    Traceback (most recent call last):
+        ...
+    TypeError: degrees must be a numeric value between 0 and 360.
+    """
+
+    degrees: float = 90
+
+    def __post_init__(self) -> None:
+        if not isinstance(self.degrees, (int, float)) or not 0 <= self.degrees <= 360:
+            raise TypeError("degrees must be a numeric value between 0 and 360.")
+
+
+@dataclass
+class Side:
+    """
+    A side of a two dimensional Shape such as Polygon, etc.
+    adjacent_sides: a list of sides which are adjacent to the current side
+    angle: the angle in degrees between each adjacent side
+    length: the length of the current side in meters
+
+    >>> Side(5)
+    Side(length=5, angle=Angle(degrees=90), next_side=None)
+    >>> Side(5, Angle(45.6))
+    Side(length=5, angle=Angle(degrees=45.6), next_side=None)
+    >>> Side(5, Angle(45.6), Side(1, Angle(2)))  # doctest: +ELLIPSIS
+    Side(length=5, angle=Angle(degrees=45.6), next_side=Side(length=1, angle=Angle(d...
+    >>> Side(-1)
+    Traceback (most recent call last):
+        ...
+    TypeError: length must be a positive numeric value.
+    >>> Side(5, None)
+    Traceback (most recent call last):
+        ...
+    TypeError: angle must be an Angle object.
+    >>> Side(5, Angle(90), "Invalid next_side")
+    Traceback (most recent call last):
+        ...
+    TypeError: next_side must be a Side or None.
+    """
+
+    length: float
+    angle: Angle = field(default_factory=Angle)
+    next_side: Side | None = None
+
+    def __post_init__(self) -> None:
+        if not isinstance(self.length, (int, float)) or self.length <= 0:
+            raise TypeError("length must be a positive numeric value.")
+        if not isinstance(self.angle, Angle):
+            raise TypeError("angle must be an Angle object.")
+        if not isinstance(self.next_side, (Side, NoneType)):
+            raise TypeError("next_side must be a Side or None.")
+
+
+@dataclass
+class Ellipse:
+    """
+    A geometric Ellipse on a 2D surface
+
+    >>> Ellipse(5, 10)
+    Ellipse(major_radius=5, minor_radius=10)
+    >>> Ellipse(5, 10) is Ellipse(5, 10)
+    False
+    >>> Ellipse(5, 10) == Ellipse(5, 10)
+    True
+    """
+
+    major_radius: float
+    minor_radius: float
+
+    @property
+    def area(self) -> float:
+        """
+        >>> Ellipse(5, 10).area
+        157.07963267948966
+        """
+        return math.pi * self.major_radius * self.minor_radius
+
+    @property
+    def perimeter(self) -> float:
+        """
+        >>> Ellipse(5, 10).perimeter
+        47.12388980384689
+        """
+        return math.pi * (self.major_radius + self.minor_radius)
+
+
+class Circle(Ellipse):
+    """
+    A geometric Circle on a 2D surface
+
+    >>> Circle(5)
+    Circle(radius=5)
+    >>> Circle(5) is Circle(5)
+    False
+    >>> Circle(5) == Circle(5)
+    True
+    >>> Circle(5).area
+    78.53981633974483
+    >>> Circle(5).perimeter
+    31.41592653589793
+    """
+
+    def __init__(self, radius: float) -> None:
+        super().__init__(radius, radius)
+        self.radius = radius
+
+    def __repr__(self) -> str:
+        return f"Circle(radius={self.radius})"
+
+    @property
+    def diameter(self) -> float:
+        """
+        >>> Circle(5).diameter
+        10
+        """
+        return self.radius * 2
+
+    def max_parts(self, num_cuts: float) -> float:
+        """
+        Return the maximum number of parts that circle can be divided into if cut
+        'num_cuts' times.
+
+        >>> circle = Circle(5)
+        >>> circle.max_parts(0)
+        1.0
+        >>> circle.max_parts(7)
+        29.0
+        >>> circle.max_parts(54)
+        1486.0
+        >>> circle.max_parts(22.5)
+        265.375
+        >>> circle.max_parts(-222)
+        Traceback (most recent call last):
+            ...
+        TypeError: num_cuts must be a positive numeric value.
+        >>> circle.max_parts("-222")
+        Traceback (most recent call last):
+            ...
+        TypeError: num_cuts must be a positive numeric value.
+        """
+        if not isinstance(num_cuts, (int, float)) or num_cuts < 0:
+            raise TypeError("num_cuts must be a positive numeric value.")
+        return (num_cuts + 2 + num_cuts**2) * 0.5
+
+
+@dataclass
+class Polygon:
+    """
+    An abstract class which represents Polygon on a 2D surface.
+
+    >>> Polygon()
+    Polygon(sides=[])
+    >>> polygon = Polygon()
+    >>> polygon.add_side(Side(5)).get_side(0)
+    Side(length=5, angle=Angle(degrees=90), next_side=None)
+    >>> polygon.get_side(1)
+    Traceback (most recent call last):
+        ...
+    IndexError: list index out of range
+    >>> polygon.set_side(0, Side(10)).get_side(0)
+    Side(length=10, angle=Angle(degrees=90), next_side=None)
+    >>> polygon.set_side(1, Side(10))
+    Traceback (most recent call last):
+        ...
+    IndexError: list assignment index out of range
+    """
+
+    sides: list[Side] = field(default_factory=list)
+
+    def add_side(self, side: Side) -> Self:
+        """
+        >>> Polygon().add_side(Side(5))
+        Polygon(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None)])
+        """
+        self.sides.append(side)
+        return self
+
+    def get_side(self, index: int) -> Side:
+        """
+        >>> Polygon().get_side(0)
+        Traceback (most recent call last):
+            ...
+        IndexError: list index out of range
+        >>> Polygon().add_side(Side(5)).get_side(-1)
+        Side(length=5, angle=Angle(degrees=90), next_side=None)
+        """
+        return self.sides[index]
+
+    def set_side(self, index: int, side: Side) -> Self:
+        """
+        >>> Polygon().set_side(0, Side(5))
+        Traceback (most recent call last):
+            ...
+        IndexError: list assignment index out of range
+        >>> Polygon().add_side(Side(5)).set_side(0, Side(10))
+        Polygon(sides=[Side(length=10, angle=Angle(degrees=90), next_side=None)])
+        """
+        self.sides[index] = side
+        return self
+
+
+class Rectangle(Polygon):
+    """
+    A geometric rectangle on a 2D surface.
+
+    >>> rectangle_one = Rectangle(5, 10)
+    >>> rectangle_one.perimeter()
+    30
+    >>> rectangle_one.area()
+    50
+    >>> Rectangle(-5, 10)
+    Traceback (most recent call last):
+        ...
+    TypeError: length must be a positive numeric value.
+    """
+
+    def __init__(self, short_side_length: float, long_side_length: float) -> None:
+        super().__init__()
+        self.short_side_length = short_side_length
+        self.long_side_length = long_side_length
+        self.post_init()
+
+    def post_init(self) -> None:
+        """
+        >>> Rectangle(5, 10)  # doctest: +NORMALIZE_WHITESPACE
+        Rectangle(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None),
+        Side(length=10, angle=Angle(degrees=90), next_side=None)])
+        """
+        self.short_side = Side(self.short_side_length)
+        self.long_side = Side(self.long_side_length)
+        super().add_side(self.short_side)
+        super().add_side(self.long_side)
+
+    def perimeter(self) -> float:
+        return (self.short_side.length + self.long_side.length) * 2
+
+    def area(self) -> float:
+        return self.short_side.length * self.long_side.length
+
+
+@dataclass
+class Square(Rectangle):
+    """
+    a structure which represents a
+    geometrical square on a 2D surface
+    >>> square_one = Square(5)
+    >>> square_one.perimeter()
+    20
+    >>> square_one.area()
+    25
+    """
+
+    def __init__(self, side_length: float) -> None:
+        super().__init__(side_length, side_length)
+
+    def perimeter(self) -> float:
+        return super().perimeter()
+
+    def area(self) -> float:
+        return super().area()
+
+
+if __name__ == "__main__":
+    __import__("doctest").testmod()
diff --git a/graphics/bezier_curve.py b/graphics/bezier_curve.py
index 7c22329ad8b4..6c7dcd4f06e7 100644
--- a/graphics/bezier_curve.py
+++ b/graphics/bezier_curve.py
@@ -2,7 +2,7 @@
 # https://www.tutorialspoint.com/computer_graphics/computer_graphics_curves.htm
 from __future__ import annotations
 
-from scipy.special import comb  # type: ignore
+from scipy.special import comb
 
 
 class BezierCurve:
@@ -30,9 +30,9 @@ def basis_function(self, t: float) -> list[float]:
         returns the x, y values of basis function at time t
 
         >>> curve = BezierCurve([(1,1), (1,2)])
-        >>> curve.basis_function(0)
+        >>> [float(x) for x in curve.basis_function(0)]
         [1.0, 0.0]
-        >>> curve.basis_function(1)
+        >>> [float(x) for x in curve.basis_function(1)]
         [0.0, 1.0]
         """
         assert 0 <= t <= 1, "Time t must be between 0 and 1."
@@ -55,9 +55,9 @@ def bezier_curve_function(self, t: float) -> tuple[float, float]:
             The last point in the curve is when t = 1.
 
         >>> curve = BezierCurve([(1,1), (1,2)])
-        >>> curve.bezier_curve_function(0)
+        >>> tuple(float(x) for x in curve.bezier_curve_function(0))
         (1.0, 1.0)
-        >>> curve.bezier_curve_function(1)
+        >>> tuple(float(x) for x in curve.bezier_curve_function(1))
         (1.0, 2.0)
         """
 
@@ -78,7 +78,7 @@ def plot_curve(self, step_size: float = 0.01):
             step_size: defines the step(s) at which to evaluate the Bezier curve.
             The smaller the step size, the finer the curve produced.
         """
-        from matplotlib import pyplot as plt  # type: ignore
+        from matplotlib import pyplot as plt
 
         to_plot_x: list[float] = []  # x coordinates of points to plot
         to_plot_y: list[float] = []  # y coordinates of points to plot
diff --git a/graphics/butterfly_pattern.py b/graphics/butterfly_pattern.py
new file mode 100644
index 000000000000..7913b03a7e95
--- /dev/null
+++ b/graphics/butterfly_pattern.py
@@ -0,0 +1,46 @@
+def butterfly_pattern(n: int) -> str:
+    """
+    Creates a butterfly pattern of size n and returns it as a string.
+
+    >>> print(butterfly_pattern(3))
+    *   *
+    ** **
+    *****
+    ** **
+    *   *
+    >>> print(butterfly_pattern(5))
+    *       *
+    **     **
+    ***   ***
+    **** ****
+    *********
+    **** ****
+    ***   ***
+    **     **
+    *       *
+    """
+    result = []
+
+    # Upper part
+    for i in range(1, n):
+        left_stars = "*" * i
+        spaces = " " * (2 * (n - i) - 1)
+        right_stars = "*" * i
+        result.append(left_stars + spaces + right_stars)
+
+    # Middle part
+    result.append("*" * (2 * n - 1))
+
+    # Lower part
+    for i in range(n - 1, 0, -1):
+        left_stars = "*" * i
+        spaces = " " * (2 * (n - i) - 1)
+        right_stars = "*" * i
+        result.append(left_stars + spaces + right_stars)
+
+    return "\n".join(result)
+
+
+if __name__ == "__main__":
+    n = int(input("Enter the size of the butterfly pattern: "))
+    print(butterfly_pattern(n))
diff --git a/graphics/digital_differential_analyzer_line.py b/graphics/digital_differential_analyzer_line.py
new file mode 100644
index 000000000000..f7269ab09856
--- /dev/null
+++ b/graphics/digital_differential_analyzer_line.py
@@ -0,0 +1,52 @@
+import matplotlib.pyplot as plt
+
+
+def digital_differential_analyzer_line(
+    p1: tuple[int, int], p2: tuple[int, int]
+) -> list[tuple[int, int]]:
+    """
+    Draws a line between two points using the DDA algorithm.
+
+    Args:
+    - p1: Coordinates of the starting point.
+    - p2: Coordinates of the ending point.
+    Returns:
+    - List of coordinate points that form the line.
+
+    >>> digital_differential_analyzer_line((1, 1), (4, 4))
+    [(2, 2), (3, 3), (4, 4)]
+    """
+    x1, y1 = p1
+    x2, y2 = p2
+    dx = x2 - x1
+    dy = y2 - y1
+    steps = max(abs(dx), abs(dy))
+    x_increment = dx / float(steps)
+    y_increment = dy / float(steps)
+    coordinates = []
+    x: float = x1
+    y: float = y1
+    for _ in range(steps):
+        x += x_increment
+        y += y_increment
+        coordinates.append((round(x), round(y)))
+    return coordinates
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    x1 = int(input("Enter the x-coordinate of the starting point: "))
+    y1 = int(input("Enter the y-coordinate of the starting point: "))
+    x2 = int(input("Enter the x-coordinate of the ending point: "))
+    y2 = int(input("Enter the y-coordinate of the ending point: "))
+    coordinates = digital_differential_analyzer_line((x1, y1), (x2, y2))
+    x_points, y_points = zip(*coordinates)
+    plt.plot(x_points, y_points, marker="o")
+    plt.title("Digital Differential Analyzer Line Drawing Algorithm")
+    plt.xlabel("X-axis")
+    plt.ylabel("Y-axis")
+    plt.grid()
+    plt.show()
diff --git a/graphs/a_star.py b/graphs/a_star.py
index 06da3b5cd863..1d7063ccc55a 100644
--- a/graphs/a_star.py
+++ b/graphs/a_star.py
@@ -75,13 +75,19 @@ def search(
                 for i in range(len(DIRECTIONS)):  # to try out different valid actions
                     x2 = x + DIRECTIONS[i][0]
                     y2 = y + DIRECTIONS[i][1]
-                    if x2 >= 0 and x2 < len(grid) and y2 >= 0 and y2 < len(grid[0]):
-                        if closed[x2][y2] == 0 and grid[x2][y2] == 0:
-                            g2 = g + cost
-                            f2 = g2 + heuristic[x2][y2]
-                            cell.append([f2, g2, x2, y2])
-                            closed[x2][y2] = 1
-                            action[x2][y2] = i
+                    if (
+                        x2 >= 0
+                        and x2 < len(grid)
+                        and y2 >= 0
+                        and y2 < len(grid[0])
+                        and closed[x2][y2] == 0
+                        and grid[x2][y2] == 0
+                    ):
+                        g2 = g + cost
+                        f2 = g2 + heuristic[x2][y2]
+                        cell.append([f2, g2, x2, y2])
+                        closed[x2][y2] = 1
+                        action[x2][y2] = i
     invpath = []
     x = goal[0]
     y = goal[1]
diff --git a/graphs/ant_colony_optimization_algorithms.py b/graphs/ant_colony_optimization_algorithms.py
new file mode 100644
index 000000000000..753f4c0962c8
--- /dev/null
+++ b/graphs/ant_colony_optimization_algorithms.py
@@ -0,0 +1,224 @@
+"""
+Use an ant colony optimization algorithm to solve the travelling salesman problem (TSP)
+which asks the following question:
+"Given a list of cities and the distances between each pair of cities, what is the
+ shortest possible route that visits each city exactly once and returns to the origin
+ city?"
+
+https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms
+https://en.wikipedia.org/wiki/Travelling_salesman_problem
+
+Author: Clark
+"""
+
+import copy
+import random
+
+cities = {
+    0: [0, 0],
+    1: [0, 5],
+    2: [3, 8],
+    3: [8, 10],
+    4: [12, 8],
+    5: [12, 4],
+    6: [8, 0],
+    7: [6, 2],
+}
+
+
+def main(
+    cities: dict[int, list[int]],
+    ants_num: int,
+    iterations_num: int,
+    pheromone_evaporation: float,
+    alpha: float,
+    beta: float,
+    q: float,  # Pheromone system parameters Q, which is a constant
+) -> tuple[list[int], float]:
+    """
+    Ant colony algorithm main function
+    >>> main(cities=cities, ants_num=10, iterations_num=20,
+    ...      pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10)
+    ([0, 1, 2, 3, 4, 5, 6, 7, 0], 37.909778143828696)
+    >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5,
+    ...      pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10)
+    ([0, 1, 0], 5.656854249492381)
+    >>> main(cities={0: [0, 0], 1: [2, 2], 4: [4, 4]}, ants_num=5, iterations_num=5,
+    ...      pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10)
+    Traceback (most recent call last):
+      ...
+    IndexError: list index out of range
+    >>> main(cities={}, ants_num=5, iterations_num=5,
+    ...      pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10)
+    Traceback (most recent call last):
+      ...
+    StopIteration
+    >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=0, iterations_num=5,
+    ...      pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10)
+    ([], inf)
+    >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=0,
+    ...      pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10)
+    ([], inf)
+    >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5,
+    ...      pheromone_evaporation=1, alpha=1.0, beta=5.0, q=10)
+    ([0, 1, 0], 5.656854249492381)
+    >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5,
+    ...      pheromone_evaporation=0, alpha=1.0, beta=5.0, q=10)
+    ([0, 1, 0], 5.656854249492381)
+    """
+    # Initialize the pheromone matrix
+    cities_num = len(cities)
+    pheromone = [[1.0] * cities_num] * cities_num
+
+    best_path: list[int] = []
+    best_distance = float("inf")
+    for _ in range(iterations_num):
+        ants_route = []
+        for _ in range(ants_num):
+            unvisited_cities = copy.deepcopy(cities)
+            current_city = {next(iter(cities.keys())): next(iter(cities.values()))}
+            del unvisited_cities[next(iter(current_city.keys()))]
+            ant_route = [next(iter(current_city.keys()))]
+            while unvisited_cities:
+                current_city, unvisited_cities = city_select(
+                    pheromone, current_city, unvisited_cities, alpha, beta
+                )
+                ant_route.append(next(iter(current_city.keys())))
+            ant_route.append(0)
+            ants_route.append(ant_route)
+
+        pheromone, best_path, best_distance = pheromone_update(
+            pheromone,
+            cities,
+            pheromone_evaporation,
+            ants_route,
+            q,
+            best_path,
+            best_distance,
+        )
+    return best_path, best_distance
+
+
+def distance(city1: list[int], city2: list[int]) -> float:
+    """
+    Calculate the distance between two coordinate points
+    >>> distance([0, 0], [3, 4] )
+    5.0
+    >>> distance([0, 0], [-3, 4] )
+    5.0
+    >>> distance([0, 0], [-3, -4] )
+    5.0
+    """
+    return (((city1[0] - city2[0]) ** 2) + ((city1[1] - city2[1]) ** 2)) ** 0.5
+
+
+def pheromone_update(
+    pheromone: list[list[float]],
+    cities: dict[int, list[int]],
+    pheromone_evaporation: float,
+    ants_route: list[list[int]],
+    q: float,  # Pheromone system parameters Q, which is a constant
+    best_path: list[int],
+    best_distance: float,
+) -> tuple[list[list[float]], list[int], float]:
+    """
+    Update pheromones on the route and update the best route
+    >>>
+    >>> pheromone_update(pheromone=[[1.0, 1.0], [1.0, 1.0]],
+    ...                  cities={0: [0,0], 1: [2,2]}, pheromone_evaporation=0.7,
+    ...                  ants_route=[[0, 1, 0]], q=10, best_path=[],
+    ...                  best_distance=float("inf"))
+    ([[0.7, 4.235533905932737], [4.235533905932737, 0.7]], [0, 1, 0], 5.656854249492381)
+    >>> pheromone_update(pheromone=[],
+    ...                  cities={0: [0,0], 1: [2,2]}, pheromone_evaporation=0.7,
+    ...                  ants_route=[[0, 1, 0]], q=10, best_path=[],
+    ...                  best_distance=float("inf"))
+    Traceback (most recent call last):
+      ...
+    IndexError: list index out of range
+    >>> pheromone_update(pheromone=[[1.0, 1.0], [1.0, 1.0]],
+    ...                  cities={}, pheromone_evaporation=0.7,
+    ...                  ants_route=[[0, 1, 0]], q=10, best_path=[],
+    ...                  best_distance=float("inf"))
+    Traceback (most recent call last):
+      ...
+    KeyError: 0
+    """
+    for a in range(len(cities)):  # Update the volatilization of pheromone on all routes
+        for b in range(len(cities)):
+            pheromone[a][b] *= pheromone_evaporation
+    for ant_route in ants_route:
+        total_distance = 0.0
+        for i in range(len(ant_route) - 1):  # Calculate total distance
+            total_distance += distance(cities[ant_route[i]], cities[ant_route[i + 1]])
+        delta_pheromone = q / total_distance
+        for i in range(len(ant_route) - 1):  # Update pheromones
+            pheromone[ant_route[i]][ant_route[i + 1]] += delta_pheromone
+            pheromone[ant_route[i + 1]][ant_route[i]] = pheromone[ant_route[i]][
+                ant_route[i + 1]
+            ]
+
+        if total_distance < best_distance:
+            best_path = ant_route
+            best_distance = total_distance
+
+    return pheromone, best_path, best_distance
+
+
+def city_select(
+    pheromone: list[list[float]],
+    current_city: dict[int, list[int]],
+    unvisited_cities: dict[int, list[int]],
+    alpha: float,
+    beta: float,
+) -> tuple[dict[int, list[int]], dict[int, list[int]]]:
+    """
+    Choose the next city for ants
+    >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={0: [0, 0]},
+    ...             unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0)
+    ({1: [2, 2]}, {})
+    >>> city_select(pheromone=[], current_city={0: [0,0]},
+    ...             unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0)
+    Traceback (most recent call last):
+      ...
+    IndexError: list index out of range
+    >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={},
+    ...             unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0)
+    Traceback (most recent call last):
+      ...
+    StopIteration
+    >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={0: [0, 0]},
+    ...             unvisited_cities={}, alpha=1.0, beta=5.0)
+    Traceback (most recent call last):
+      ...
+    IndexError: list index out of range
+    """
+    probabilities = []
+    for city, value in unvisited_cities.items():
+        city_distance = distance(value, next(iter(current_city.values())))
+        probability = (pheromone[city][next(iter(current_city.keys()))] ** alpha) * (
+            (1 / city_distance) ** beta
+        )
+        probabilities.append(probability)
+
+    chosen_city_i = random.choices(
+        list(unvisited_cities.keys()), weights=probabilities
+    )[0]
+    chosen_city = {chosen_city_i: unvisited_cities[chosen_city_i]}
+    del unvisited_cities[next(iter(chosen_city.keys()))]
+    return chosen_city, unvisited_cities
+
+
+if __name__ == "__main__":
+    best_path, best_distance = main(
+        cities=cities,
+        ants_num=10,
+        iterations_num=20,
+        pheromone_evaporation=0.7,
+        alpha=1.0,
+        beta=5.0,
+        q=10,
+    )
+
+    print(f"{best_path = }")
+    print(f"{best_distance = }")
diff --git a/graphs/articulation_points.py b/graphs/articulation_points.py
index d28045282425..0bf16e55bc04 100644
--- a/graphs/articulation_points.py
+++ b/graphs/articulation_points.py
@@ -1,6 +1,6 @@
 # Finding Articulation Points in Undirected Graph
-def compute_ap(l):  # noqa: E741
-    n = len(l)
+def compute_ap(graph):
+    n = len(graph)
     out_edge_count = 0
     low = [0] * n
     visited = [False] * n
@@ -12,7 +12,7 @@ def dfs(root, at, parent, out_edge_count):
         visited[at] = True
         low[at] = at
 
-        for to in l[at]:
+        for to in graph[at]:
             if to == parent:
                 pass
             elif not visited[to]:
@@ -41,7 +41,7 @@ def dfs(root, at, parent, out_edge_count):
 
 
 # Adjacency list of graph
-data = {
+graph = {
     0: [1, 2],
     1: [0, 2],
     2: [0, 1, 3, 5],
@@ -52,4 +52,4 @@ def dfs(root, at, parent, out_edge_count):
     7: [6, 8],
     8: [5, 7],
 }
-compute_ap(data)
+compute_ap(graph)
diff --git a/graphs/basic_graphs.py b/graphs/basic_graphs.py
index 25c8045b3d2b..286e9b195796 100644
--- a/graphs/basic_graphs.py
+++ b/graphs/basic_graphs.py
@@ -77,6 +77,14 @@ def initialize_weighted_undirected_graph(
 
 
 def dfs(g, s):
+    """
+    >>> dfs({1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []}, 1)
+    1
+    2
+    4
+    5
+    3
+    """
     vis, _s = {s}, [s]
     print(s)
     while _s:
@@ -104,6 +112,17 @@ def dfs(g, s):
 
 
 def bfs(g, s):
+    """
+    >>> bfs({1: [2, 3], 2: [4, 5], 3: [6, 7], 4: [], 5: [8], 6: [], 7: [], 8: []}, 1)
+    1
+    2
+    3
+    4
+    5
+    6
+    7
+    8
+    """
     vis, q = {s}, deque([s])
     print(s)
     while q:
@@ -128,23 +147,36 @@ def bfs(g, s):
 
 
 def dijk(g, s):
+    """
+    dijk({1: [(2, 7), (3, 9), (6, 14)],
+        2: [(1, 7), (3, 10), (4, 15)],
+        3: [(1, 9), (2, 10), (4, 11), (6, 2)],
+        4: [(2, 15), (3, 11), (5, 6)],
+        5: [(4, 6), (6, 9)],
+        6: [(1, 14), (3, 2), (5, 9)]}, 1)
+    7
+    9
+    11
+    20
+    20
+    """
     dist, known, path = {s: 0}, set(), {s: 0}
     while True:
         if len(known) == len(g) - 1:
             break
         mini = 100000
-        for i in dist:
-            if i not in known and dist[i] < mini:
-                mini = dist[i]
-                u = i
+        for key, value in dist:
+            if key not in known and value < mini:
+                mini = value
+                u = key
         known.add(u)
         for v in g[u]:
             if v[0] not in known and dist[u] + v[1] < dist.get(v[0], 100000):
                 dist[v[0]] = dist[u] + v[1]
                 path[v[0]] = u
-    for i in dist:
-        if i != s:
-            print(dist[i])
+    for key, value in dist.items():
+        if key != s:
+            print(value)
 
 
 """
@@ -255,10 +287,10 @@ def prim(g, s):
         if len(known) == len(g) - 1:
             break
         mini = 100000
-        for i in dist:
-            if i not in known and dist[i] < mini:
-                mini = dist[i]
-                u = i
+        for key, value in dist.items():
+            if key not in known and value < mini:
+                mini = value
+                u = key
         known.add(u)
         for v in g[u]:
             if v[0] not in known and v[1] < dist.get(v[0], 100000):
diff --git a/graphs/bi_directional_dijkstra.py b/graphs/bi_directional_dijkstra.py
index 529a235db625..d2c4030b921b 100644
--- a/graphs/bi_directional_dijkstra.py
+++ b/graphs/bi_directional_dijkstra.py
@@ -10,7 +10,6 @@
 
 # Author: Swayam Singh (https://github.com/practice404)
 
-
 from queue import PriorityQueue
 from typing import Any
 
@@ -37,9 +36,11 @@ def pass_and_relaxation(
             queue.put((new_cost_f, nxt))
             cst_fwd[nxt] = new_cost_f
             parent[nxt] = v
-        if nxt in visited_backward:
-            if cst_fwd[v] + d + cst_bwd[nxt] < shortest_distance:
-                shortest_distance = cst_fwd[v] + d + cst_bwd[nxt]
+        if (
+            nxt in visited_backward
+            and cst_fwd[v] + d + cst_bwd[nxt] < shortest_distance
+        ):
+            shortest_distance = cst_fwd[v] + d + cst_bwd[nxt]
     return shortest_distance
 
 
diff --git a/graphs/bidirectional_a_star.py b/graphs/bidirectional_a_star.py
index 373d67142aa9..00f623de3493 100644
--- a/graphs/bidirectional_a_star.py
+++ b/graphs/bidirectional_a_star.py
@@ -1,6 +1,7 @@
 """
 https://en.wikipedia.org/wiki/Bidirectional_search
 """
+
 from __future__ import annotations
 
 import time
diff --git a/graphs/bidirectional_breadth_first_search.py b/graphs/bidirectional_breadth_first_search.py
index 511b080a9add..71c5a9aff08f 100644
--- a/graphs/bidirectional_breadth_first_search.py
+++ b/graphs/bidirectional_breadth_first_search.py
@@ -1,6 +1,7 @@
 """
 https://en.wikipedia.org/wiki/Bidirectional_search
 """
+
 from __future__ import annotations
 
 import time
diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py
new file mode 100644
index 000000000000..b3ff9f75fd44
--- /dev/null
+++ b/graphs/bidirectional_search.py
@@ -0,0 +1,201 @@
+"""
+Bidirectional Search Algorithm.
+
+This algorithm searches from both the source and target nodes simultaneously,
+meeting somewhere in the middle. This approach can significantly reduce the
+search space compared to a traditional one-directional search.
+
+Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
+Space Complexity: O(b^(d/2))
+
+https://en.wikipedia.org/wiki/Bidirectional_search
+"""
+
+from collections import deque
+
+
+def expand_search(
+    graph: dict[int, list[int]],
+    queue: deque[int],
+    parents: dict[int, int | None],
+    opposite_direction_parents: dict[int, int | None],
+) -> int | None:
+    if not queue:
+        return None
+
+    current = queue.popleft()
+    for neighbor in graph[current]:
+        if neighbor in parents:
+            continue
+
+        parents[neighbor] = current
+        queue.append(neighbor)
+
+        # Check if this creates an intersection
+        if neighbor in opposite_direction_parents:
+            return neighbor
+
+    return None
+
+
+def construct_path(current: int | None, parents: dict[int, int | None]) -> list[int]:
+    path: list[int] = []
+    while current is not None:
+        path.append(current)
+        current = parents[current]
+    return path
+
+
+def bidirectional_search(
+    graph: dict[int, list[int]], start: int, goal: int
+) -> list[int] | None:
+    """
+    Perform bidirectional search on a graph to find the shortest path.
+
+    Args:
+        graph: A dictionary where keys are nodes and values are lists of adjacent nodes
+        start: The starting node
+        goal: The target node
+
+    Returns:
+        A list representing the path from start to goal, or None if no path exists
+
+    Examples:
+        >>> graph = {
+        ...     0: [1, 2],
+        ...     1: [0, 3, 4],
+        ...     2: [0, 5, 6],
+        ...     3: [1, 7],
+        ...     4: [1, 8],
+        ...     5: [2, 9],
+        ...     6: [2, 10],
+        ...     7: [3, 11],
+        ...     8: [4, 11],
+        ...     9: [5, 11],
+        ...     10: [6, 11],
+        ...     11: [7, 8, 9, 10],
+        ... }
+        >>> bidirectional_search(graph=graph, start=0, goal=11)
+        [0, 1, 3, 7, 11]
+        >>> bidirectional_search(graph=graph, start=5, goal=5)
+        [5]
+        >>> disconnected_graph = {
+        ...     0: [1, 2],
+        ...     1: [0],
+        ...     2: [0],
+        ...     3: [4],
+        ...     4: [3],
+        ... }
+        >>> bidirectional_search(graph=disconnected_graph, start=0, goal=3) is None
+        True
+    """
+    if start == goal:
+        return [start]
+
+    # Check if start and goal are in the graph
+    if start not in graph or goal not in graph:
+        return None
+
+    # Initialize forward and backward search dictionaries
+    # Each maps a node to its parent in the search
+    forward_parents: dict[int, int | None] = {start: None}
+    backward_parents: dict[int, int | None] = {goal: None}
+
+    # Initialize forward and backward search queues
+    forward_queue = deque([start])
+    backward_queue = deque([goal])
+
+    # Intersection node (where the two searches meet)
+    intersection = None
+
+    # Continue until both queues are empty or an intersection is found
+    while forward_queue and backward_queue and intersection is None:
+        # Expand forward search
+        intersection = expand_search(
+            graph=graph,
+            queue=forward_queue,
+            parents=forward_parents,
+            opposite_direction_parents=backward_parents,
+        )
+
+        # If no intersection found, expand backward search
+        if intersection is not None:
+            break
+
+        intersection = expand_search(
+            graph=graph,
+            queue=backward_queue,
+            parents=backward_parents,
+            opposite_direction_parents=forward_parents,
+        )
+
+    # If no intersection found, there's no path
+    if intersection is None:
+        return None
+
+    # Construct path from start to intersection
+    forward_path: list[int] = construct_path(
+        current=intersection, parents=forward_parents
+    )
+    forward_path.reverse()
+
+    # Construct path from intersection to goal
+    backward_path: list[int] = construct_path(
+        current=backward_parents[intersection], parents=backward_parents
+    )
+
+    # Return the complete path
+    return forward_path + backward_path
+
+
+def main() -> None:
+    """
+    Run example of bidirectional search algorithm.
+
+    Examples:
+        >>> main()  # doctest: +NORMALIZE_WHITESPACE
+        Path from 0 to 11: [0, 1, 3, 7, 11]
+        Path from 5 to 5: [5]
+        Path from 0 to 3: None
+    """
+    # Example graph represented as an adjacency list
+    example_graph = {
+        0: [1, 2],
+        1: [0, 3, 4],
+        2: [0, 5, 6],
+        3: [1, 7],
+        4: [1, 8],
+        5: [2, 9],
+        6: [2, 10],
+        7: [3, 11],
+        8: [4, 11],
+        9: [5, 11],
+        10: [6, 11],
+        11: [7, 8, 9, 10],
+    }
+
+    # Test case 1: Path exists
+    start, goal = 0, 11
+    path = bidirectional_search(graph=example_graph, start=start, goal=goal)
+    print(f"Path from {start} to {goal}: {path}")
+
+    # Test case 2: Start and goal are the same
+    start, goal = 5, 5
+    path = bidirectional_search(graph=example_graph, start=start, goal=goal)
+    print(f"Path from {start} to {goal}: {path}")
+
+    # Test case 3: No path exists (disconnected graph)
+    disconnected_graph = {
+        0: [1, 2],
+        1: [0],
+        2: [0],
+        3: [4],
+        4: [3],
+    }
+    start, goal = 0, 3
+    path = bidirectional_search(graph=disconnected_graph, start=start, goal=goal)
+    print(f"Path from {start} to {goal}: {path}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/graphs/boruvka.py b/graphs/boruvka.py
index 2715a3085948..3dc059ff6a62 100644
--- a/graphs/boruvka.py
+++ b/graphs/boruvka.py
@@ -1,29 +1,30 @@
 """Borůvka's algorithm.
 
-    Determines the minimum spanning tree (MST) of a graph using the Borůvka's algorithm.
-    Borůvka's algorithm is a greedy algorithm for finding a minimum spanning tree in a
-    connected graph, or a minimum spanning forest if a graph that is not connected.
+Determines the minimum spanning tree (MST) of a graph using the Borůvka's algorithm.
+Borůvka's algorithm is a greedy algorithm for finding a minimum spanning tree in a
+connected graph, or a minimum spanning forest if a graph that is not connected.
 
-    The time complexity of this algorithm is O(ELogV), where E represents the number
-    of edges, while V represents the number of nodes.
-    O(number_of_edges Log number_of_nodes)
+The time complexity of this algorithm is O(ELogV), where E represents the number
+of edges, while V represents the number of nodes.
+O(number_of_edges Log number_of_nodes)
 
-    The space complexity of this algorithm is O(V + E), since we have to keep a couple
-    of lists whose sizes are equal to the number of nodes, as well as keep all the
-    edges of a graph inside of the data structure itself.
+The space complexity of this algorithm is O(V + E), since we have to keep a couple
+of lists whose sizes are equal to the number of nodes, as well as keep all the
+edges of a graph inside of the data structure itself.
 
-    Borůvka's algorithm gives us pretty much the same result as other MST Algorithms -
-    they all find the minimum spanning tree, and the time complexity is approximately
-    the same.
+Borůvka's algorithm gives us pretty much the same result as other MST Algorithms -
+they all find the minimum spanning tree, and the time complexity is approximately
+the same.
 
-    One advantage that Borůvka's algorithm has compared to the alternatives is that it
-    doesn't need to presort the edges or maintain a priority queue in order to find the
-    minimum spanning tree.
-    Even though that doesn't help its complexity, since it still passes the edges logE
-    times, it is a bit simpler to code.
+One advantage that Borůvka's algorithm has compared to the alternatives is that it
+doesn't need to presort the edges or maintain a priority queue in order to find the
+minimum spanning tree.
+Even though that doesn't help its complexity, since it still passes the edges logE
+times, it is a bit simpler to code.
 
-    Details: https://en.wikipedia.org/wiki/Bor%C5%AFvka%27s_algorithm
+Details: https://en.wikipedia.org/wiki/Bor%C5%AFvka%27s_algorithm
 """
+
 from __future__ import annotations
 
 from typing import Any
diff --git a/graphs/breadth_first_search.py b/graphs/breadth_first_search.py
index 171d3875f3c5..cab79be39ed3 100644
--- a/graphs/breadth_first_search.py
+++ b/graphs/breadth_first_search.py
@@ -1,6 +1,7 @@
 #!/usr/bin/python
 
-""" Author: OMKAR PATHAK """
+"""Author: OMKAR PATHAK"""
+
 from __future__ import annotations
 
 from queue import Queue
diff --git a/graphs/breadth_first_search_2.py b/graphs/breadth_first_search_2.py
index a0b92b90b456..ccadfa346bf1 100644
--- a/graphs/breadth_first_search_2.py
+++ b/graphs/breadth_first_search_2.py
@@ -12,6 +12,7 @@
             mark w as explored
             add w to Q (at the end)
 """
+
 from __future__ import annotations
 
 from collections import deque
diff --git a/graphs/breadth_first_search_shortest_path.py b/graphs/breadth_first_search_shortest_path.py
index d489b110b3a7..c06440bccef3 100644
--- a/graphs/breadth_first_search_shortest_path.py
+++ b/graphs/breadth_first_search_shortest_path.py
@@ -1,6 +1,7 @@
 """Breath First Search (BFS) can be used when finding the shortest path
 from a given source node to a target node in an unweighted graph.
 """
+
 from __future__ import annotations
 
 graph = {
diff --git a/graphs/breadth_first_search_shortest_path_2.py b/graphs/breadth_first_search_shortest_path_2.py
index b0c8d353ba04..4f9b6e65bdf3 100644
--- a/graphs/breadth_first_search_shortest_path_2.py
+++ b/graphs/breadth_first_search_shortest_path_2.py
@@ -1,9 +1,10 @@
 """Breadth-first search shortest path implementations.
-    doctest:
-    python -m doctest -v bfs_shortest_path.py
-    Manual test:
-    python bfs_shortest_path.py
+doctest:
+python -m doctest -v bfs_shortest_path.py
+Manual test:
+python bfs_shortest_path.py
 """
+
 demo_graph = {
     "A": ["B", "C", "E"],
     "B": ["A", "D", "E"],
diff --git a/graphs/breadth_first_search_zero_one_shortest_path.py b/graphs/breadth_first_search_zero_one_shortest_path.py
index 78047c5d2237..d3a255bac1ef 100644
--- a/graphs/breadth_first_search_zero_one_shortest_path.py
+++ b/graphs/breadth_first_search_zero_one_shortest_path.py
@@ -3,6 +3,7 @@
 0-1-graph is the weighted graph with the weights equal to 0 or 1.
 Link: https://codeforces.com/blog/entry/22276
 """
+
 from __future__ import annotations
 
 from collections import deque
diff --git a/graphs/check_bipatrite.py b/graphs/check_bipatrite.py
index 10b9cc965251..213f3f9480b5 100644
--- a/graphs/check_bipatrite.py
+++ b/graphs/check_bipatrite.py
@@ -6,16 +6,17 @@ def is_bipartite_dfs(graph: defaultdict[int, list[int]]) -> bool:
     Check if a graph is bipartite using depth-first search (DFS).
 
     Args:
-        graph: Adjacency list representing the graph.
+        `graph`: Adjacency list representing the graph.
 
     Returns:
-        True if bipartite, False otherwise.
+        ``True`` if bipartite, ``False`` otherwise.
 
     Checks if the graph can be divided into two sets of vertices, such that no two
     vertices within the same set are connected by an edge.
 
     Examples:
-    # FIXME: This test should pass.
+
+    >>> # FIXME: This test should pass.
     >>> is_bipartite_dfs(defaultdict(list, {0: [1, 2], 1: [0, 3], 2: [0, 4]}))
     Traceback (most recent call last):
         ...
@@ -37,7 +38,7 @@ def is_bipartite_dfs(graph: defaultdict[int, list[int]]) -> bool:
         ...
     KeyError: 0
 
-    # FIXME: This test should fails with KeyError: 4.
+    >>> # FIXME: This test should fails with KeyError: 4.
     >>> is_bipartite_dfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 9: [0]})
     False
     >>> is_bipartite_dfs({0: [-1, 3], 1: [0, -2]})
@@ -51,7 +52,8 @@ def is_bipartite_dfs(graph: defaultdict[int, list[int]]) -> bool:
         ...
     KeyError: 0
 
-    # FIXME: This test should fails with TypeError: list indices must be integers or...
+    >>> # FIXME: This test should fails with
+    >>> # TypeError: list indices must be integers or...
     >>> is_bipartite_dfs({0: [1.0, 3.0], 1.0: [0, 2.0], 2.0: [1.0, 3.0], 3.0: [0, 2.0]})
     True
     >>> is_bipartite_dfs({"a": [1, 3], "b": [0, 2], "c": [1, 3], "d": [0, 2]})
@@ -95,16 +97,17 @@ def is_bipartite_bfs(graph: defaultdict[int, list[int]]) -> bool:
     Check if a graph is bipartite using a breadth-first search (BFS).
 
     Args:
-        graph: Adjacency list representing the graph.
+        `graph`: Adjacency list representing the graph.
 
     Returns:
-        True if bipartite, False otherwise.
+        ``True`` if bipartite, ``False`` otherwise.
 
     Check if the graph can be divided into two sets of vertices, such that no two
     vertices within the same set are connected by an edge.
 
     Examples:
-    # FIXME: This test should pass.
+
+    >>> # FIXME: This test should pass.
     >>> is_bipartite_bfs(defaultdict(list, {0: [1, 2], 1: [0, 3], 2: [0, 4]}))
     Traceback (most recent call last):
         ...
@@ -126,7 +129,7 @@ def is_bipartite_bfs(graph: defaultdict[int, list[int]]) -> bool:
         ...
     KeyError: 0
 
-    # FIXME: This test should fails with KeyError: 4.
+    >>> # FIXME: This test should fails with KeyError: 4.
     >>> is_bipartite_bfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 9: [0]})
     False
     >>> is_bipartite_bfs({0: [-1, 3], 1: [0, -2]})
@@ -140,7 +143,8 @@ def is_bipartite_bfs(graph: defaultdict[int, list[int]]) -> bool:
         ...
     KeyError: 0
 
-    # FIXME: This test should fails with TypeError: list indices must be integers or...
+    >>> # FIXME: This test should fails with
+    >>> # TypeError: list indices must be integers or...
     >>> is_bipartite_bfs({0: [1.0, 3.0], 1.0: [0, 2.0], 2.0: [1.0, 3.0], 3.0: [0, 2.0]})
     True
     >>> is_bipartite_bfs({"a": [1, 3], "b": [0, 2], "c": [1, 3], "d": [0, 2]})
diff --git a/graphs/deep_clone_graph.py b/graphs/deep_clone_graph.py
index 55678b4c01ec..18ea99c6a52d 100644
--- a/graphs/deep_clone_graph.py
+++ b/graphs/deep_clone_graph.py
@@ -9,6 +9,7 @@
 Each node in the graph contains a value (int) and a list (List[Node]) of its
 neighbors.
 """
+
 from dataclasses import dataclass
 
 
diff --git a/graphs/depth_first_search.py b/graphs/depth_first_search.py
index f20a503ca395..a666e74ce607 100644
--- a/graphs/depth_first_search.py
+++ b/graphs/depth_first_search.py
@@ -1,4 +1,5 @@
 """Non recursive implementation of a DFS algorithm."""
+
 from __future__ import annotations
 
 
diff --git a/graphs/depth_first_search_2.py b/graphs/depth_first_search_2.py
index 5ff13af33168..8fe48b7f2b42 100644
--- a/graphs/depth_first_search_2.py
+++ b/graphs/depth_first_search_2.py
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 
-""" Author: OMKAR PATHAK """
+"""Author: OMKAR PATHAK"""
 
 
 class Graph:
diff --git a/graphs/dijkstra.py b/graphs/dijkstra.py
index b0bdfab60649..87e9d2233bb2 100644
--- a/graphs/dijkstra.py
+++ b/graphs/dijkstra.py
@@ -30,6 +30,7 @@
 distance between each vertex that makes up the path from start vertex to target
 vertex.
 """
+
 import heapq
 
 
diff --git a/graphs/dijkstra_algorithm.py b/graphs/dijkstra_algorithm.py
index 452138fe904b..51412b790bac 100644
--- a/graphs/dijkstra_algorithm.py
+++ b/graphs/dijkstra_algorithm.py
@@ -11,35 +11,127 @@
 class PriorityQueue:
     # Based on Min Heap
     def __init__(self):
+        """
+        Priority queue class constructor method.
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.cur_size
+        0
+        >>> priority_queue_test.array
+        []
+        >>> priority_queue_test.pos
+        {}
+        """
         self.cur_size = 0
         self.array = []
         self.pos = {}  # To store the pos of node in array
 
     def is_empty(self):
+        """
+        Conditional boolean method to determine if the priority queue is empty or not.
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.is_empty()
+        True
+        >>> priority_queue_test.insert((2, 'A'))
+        >>> priority_queue_test.is_empty()
+        False
+        """
         return self.cur_size == 0
 
     def min_heapify(self, idx):
+        """
+        Sorts the queue array so that the minimum element is root.
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.cur_size = 3
+        >>> priority_queue_test.pos = {'A': 0, 'B': 1, 'C': 2}
+
+        >>> priority_queue_test.array = [(5, 'A'), (10, 'B'), (15, 'C')]
+        >>> priority_queue_test.min_heapify(0)
+        Traceback (most recent call last):
+            ...
+        TypeError: 'list' object is not callable
+        >>> priority_queue_test.array
+        [(5, 'A'), (10, 'B'), (15, 'C')]
+
+        >>> priority_queue_test.array = [(10, 'A'), (5, 'B'), (15, 'C')]
+        >>> priority_queue_test.min_heapify(0)
+        Traceback (most recent call last):
+            ...
+        TypeError: 'list' object is not callable
+        >>> priority_queue_test.array
+        [(10, 'A'), (5, 'B'), (15, 'C')]
+
+        >>> priority_queue_test.array = [(10, 'A'), (15, 'B'), (5, 'C')]
+        >>> priority_queue_test.min_heapify(0)
+        Traceback (most recent call last):
+            ...
+        TypeError: 'list' object is not callable
+        >>> priority_queue_test.array
+        [(10, 'A'), (15, 'B'), (5, 'C')]
+
+        >>> priority_queue_test.array = [(10, 'A'), (5, 'B')]
+        >>> priority_queue_test.cur_size = len(priority_queue_test.array)
+        >>> priority_queue_test.pos = {'A': 0, 'B': 1}
+        >>> priority_queue_test.min_heapify(0)
+        Traceback (most recent call last):
+            ...
+        TypeError: 'list' object is not callable
+        >>> priority_queue_test.array
+        [(10, 'A'), (5, 'B')]
+        """
         lc = self.left(idx)
         rc = self.right(idx)
-        if lc < self.cur_size and self.array(lc)[0] < self.array(idx)[0]:
+        if lc < self.cur_size and self.array(lc)[0] < self.array[idx][0]:
             smallest = lc
         else:
             smallest = idx
-        if rc < self.cur_size and self.array(rc)[0] < self.array(smallest)[0]:
+        if rc < self.cur_size and self.array(rc)[0] < self.array[smallest][0]:
             smallest = rc
         if smallest != idx:
             self.swap(idx, smallest)
             self.min_heapify(smallest)
 
     def insert(self, tup):
-        # Inserts a node into the Priority Queue
+        """
+        Inserts a node into the Priority Queue.
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.insert((10, 'A'))
+        >>> priority_queue_test.array
+        [(10, 'A')]
+        >>> priority_queue_test.insert((15, 'B'))
+        >>> priority_queue_test.array
+        [(10, 'A'), (15, 'B')]
+        >>> priority_queue_test.insert((5, 'C'))
+        >>> priority_queue_test.array
+        [(5, 'C'), (10, 'A'), (15, 'B')]
+        """
         self.pos[tup[1]] = self.cur_size
         self.cur_size += 1
         self.array.append((sys.maxsize, tup[1]))
         self.decrease_key((sys.maxsize, tup[1]), tup[0])
 
     def extract_min(self):
-        # Removes and returns the min element at top of priority queue
+        """
+        Removes and returns the min element at top of priority queue.
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.array = [(10, 'A'), (15, 'B')]
+        >>> priority_queue_test.cur_size = len(priority_queue_test.array)
+        >>> priority_queue_test.pos = {'A': 0, 'B': 1}
+        >>> priority_queue_test.insert((5, 'C'))
+        >>> priority_queue_test.extract_min()
+        'C'
+        >>> priority_queue_test.array[0]
+        (15, 'B')
+        """
         min_node = self.array[0][1]
         self.array[0] = self.array[self.cur_size - 1]
         self.cur_size -= 1
@@ -48,20 +140,61 @@ def extract_min(self):
         return min_node
 
     def left(self, i):
-        # returns the index of left child
+        """
+        Returns the index of left child
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.left(0)
+        1
+        >>> priority_queue_test.left(1)
+        3
+        """
         return 2 * i + 1
 
     def right(self, i):
-        # returns the index of right child
+        """
+        Returns the index of right child
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.right(0)
+        2
+        >>> priority_queue_test.right(1)
+        4
+        """
         return 2 * i + 2
 
     def par(self, i):
-        # returns the index of parent
+        """
+        Returns the index of parent
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.par(1)
+        0
+        >>> priority_queue_test.par(2)
+        1
+        >>> priority_queue_test.par(4)
+        2
+        """
         return math.floor(i / 2)
 
     def swap(self, i, j):
-        # swaps array elements at indices i and j
-        # update the pos{}
+        """
+        Swaps array elements at indices i and j, update the pos{}
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.array = [(10, 'A'), (15, 'B')]
+        >>> priority_queue_test.cur_size = len(priority_queue_test.array)
+        >>> priority_queue_test.pos = {'A': 0, 'B': 1}
+        >>> priority_queue_test.swap(0, 1)
+        >>> priority_queue_test.array
+        [(15, 'B'), (10, 'A')]
+        >>> priority_queue_test.pos
+        {'A': 1, 'B': 0}
+        """
         self.pos[self.array[i][1]] = j
         self.pos[self.array[j][1]] = i
         temp = self.array[i]
@@ -69,8 +202,20 @@ def swap(self, i, j):
         self.array[j] = temp
 
     def decrease_key(self, tup, new_d):
+        """
+        Decrease the key value for a given tuple, assuming the new_d is at most old_d.
+
+        Examples:
+        >>> priority_queue_test = PriorityQueue()
+        >>> priority_queue_test.array = [(10, 'A'), (15, 'B')]
+        >>> priority_queue_test.cur_size = len(priority_queue_test.array)
+        >>> priority_queue_test.pos = {'A': 0, 'B': 1}
+        >>> priority_queue_test.decrease_key((10, 'A'), 5)
+        >>> priority_queue_test.array
+        [(5, 'A'), (15, 'B')]
+        """
         idx = self.pos[tup[1]]
-        # assuming the new_d is atmost old_d
+        # assuming the new_d is at most old_d
         self.array[idx] = (new_d, tup[1])
         while idx > 0 and self.array[self.par(idx)][0] > self.array[idx][0]:
             self.swap(idx, self.par(idx))
@@ -79,6 +224,20 @@ def decrease_key(self, tup, new_d):
 
 class Graph:
     def __init__(self, num):
+        """
+        Graph class constructor
+
+        Examples:
+        >>> graph_test = Graph(1)
+        >>> graph_test.num_nodes
+        1
+        >>> graph_test.dist
+        [0]
+        >>> graph_test.par
+        [-1]
+        >>> graph_test.adjList
+        {}
+        """
         self.adjList = {}  # To store graph: u -> (v,w)
         self.num_nodes = num  # Number of nodes in graph
         # To store the distance from source vertex
@@ -86,8 +245,16 @@ def __init__(self, num):
         self.par = [-1] * self.num_nodes  # To store the path
 
     def add_edge(self, u, v, w):
-        #  Edge going from node u to v and v to u with weight w
-        # u (w)-> v, v (w) -> u
+        """
+        Add edge going from node u to v and v to u with weight w: u (w)-> v, v (w) -> u
+
+        Examples:
+        >>> graph_test = Graph(1)
+        >>> graph_test.add_edge(1, 2, 1)
+        >>> graph_test.add_edge(2, 3, 2)
+        >>> graph_test.adjList
+        {1: [(2, 1)], 2: [(1, 1), (3, 2)], 3: [(2, 2)]}
+        """
         # Check if u already in graph
         if u in self.adjList:
             self.adjList[u].append((v, w))
@@ -101,11 +268,99 @@ def add_edge(self, u, v, w):
             self.adjList[v] = [(u, w)]
 
     def show_graph(self):
-        # u -> v(w)
+        """
+        Show the graph: u -> v(w)
+
+        Examples:
+        >>> graph_test = Graph(1)
+        >>> graph_test.add_edge(1, 2, 1)
+        >>> graph_test.show_graph()
+        1 -> 2(1)
+        2 -> 1(1)
+        >>> graph_test.add_edge(2, 3, 2)
+        >>> graph_test.show_graph()
+        1 -> 2(1)
+        2 -> 1(1) -> 3(2)
+        3 -> 2(2)
+        """
         for u in self.adjList:
             print(u, "->", " -> ".join(str(f"{v}({w})") for v, w in self.adjList[u]))
 
     def dijkstra(self, src):
+        """
+        Dijkstra algorithm
+
+        Examples:
+        >>> graph_test = Graph(3)
+        >>> graph_test.add_edge(0, 1, 2)
+        >>> graph_test.add_edge(1, 2, 2)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 2
+        Node 2 has distance: 4
+        >>> graph_test.dist
+        [0, 2, 4]
+
+        >>> graph_test = Graph(2)
+        >>> graph_test.add_edge(0, 1, 2)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 2
+        >>> graph_test.dist
+        [0, 2]
+
+        >>> graph_test = Graph(3)
+        >>> graph_test.add_edge(0, 1, 2)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 2
+        Node 2 has distance: 0
+        >>> graph_test.dist
+        [0, 2, 0]
+
+        >>> graph_test = Graph(3)
+        >>> graph_test.add_edge(0, 1, 2)
+        >>> graph_test.add_edge(1, 2, 2)
+        >>> graph_test.add_edge(0, 2, 1)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 2
+        Node 2 has distance: 1
+        >>> graph_test.dist
+        [0, 2, 1]
+
+        >>> graph_test = Graph(4)
+        >>> graph_test.add_edge(0, 1, 4)
+        >>> graph_test.add_edge(1, 2, 2)
+        >>> graph_test.add_edge(2, 3, 1)
+        >>> graph_test.add_edge(0, 2, 3)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 4
+        Node 2 has distance: 3
+        Node 3 has distance: 4
+        >>> graph_test.dist
+        [0, 4, 3, 4]
+
+        >>> graph_test = Graph(4)
+        >>> graph_test.add_edge(0, 1, 4)
+        >>> graph_test.add_edge(1, 2, 2)
+        >>> graph_test.add_edge(2, 3, 1)
+        >>> graph_test.add_edge(0, 2, 7)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 4
+        Node 2 has distance: 6
+        Node 3 has distance: 7
+        >>> graph_test.dist
+        [0, 4, 6, 7]
+        """
         # Flush old junk values in par[]
         self.par = [-1] * self.num_nodes
         # src is the source node
@@ -135,13 +390,40 @@ def dijkstra(self, src):
         self.show_distances(src)
 
     def show_distances(self, src):
+        """
+        Show the distances from src to all other nodes in a graph
+
+        Examples:
+        >>> graph_test = Graph(1)
+        >>> graph_test.show_distances(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        """
         print(f"Distance from node: {src}")
         for u in range(self.num_nodes):
             print(f"Node {u} has distance: {self.dist[u]}")
 
     def show_path(self, src, dest):
-        # To show the shortest path from src to dest
-        # WARNING: Use it *after* calling dijkstra
+        """
+        Shows the shortest path from src to dest.
+        WARNING: Use it *after* calling dijkstra.
+
+        Examples:
+        >>> graph_test = Graph(4)
+        >>> graph_test.add_edge(0, 1, 1)
+        >>> graph_test.add_edge(1, 2, 2)
+        >>> graph_test.add_edge(2, 3, 3)
+        >>> graph_test.dijkstra(0)
+        Distance from node: 0
+        Node 0 has distance: 0
+        Node 1 has distance: 1
+        Node 2 has distance: 3
+        Node 3 has distance: 6
+        >>> graph_test.show_path(0, 3)  # doctest: +NORMALIZE_WHITESPACE
+        ----Path to reach 3 from 0----
+        0 -> 1 -> 2 -> 3
+        Total cost of path:  6
+        """
         path = []
         cost = 0
         temp = dest
@@ -167,6 +449,9 @@ def show_path(self, src, dest):
 
 
 if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
     graph = Graph(9)
     graph.add_edge(0, 1, 4)
     graph.add_edge(0, 7, 8)
diff --git a/graphs/dijkstra_binary_grid.py b/graphs/dijkstra_binary_grid.py
index c23d8234328a..06293a87da2d 100644
--- a/graphs/dijkstra_binary_grid.py
+++ b/graphs/dijkstra_binary_grid.py
@@ -69,7 +69,7 @@ def dijkstra(
                 x, y = predecessors[x, y]
             path.append(source)  # add the source manually
             path.reverse()
-            return matrix[destination], path
+            return float(matrix[destination]), path
 
         for i in range(len(dx)):
             nx, ny = x + dx[i], y + dy[i]
diff --git a/graphs/dinic.py b/graphs/dinic.py
index aaf3a119525c..7919e6bc060a 100644
--- a/graphs/dinic.py
+++ b/graphs/dinic.py
@@ -37,7 +37,7 @@ def depth_first_search(self, vertex, sink, flow):
     # Here we calculate the flow that reaches the sink
     def max_flow(self, source, sink):
         flow, self.q[0] = 0, source
-        for l in range(31):  # noqa: E741  l = 30 maybe faster for random data
+        for l in range(31):  # l = 30 maybe faster for random data  # noqa: E741
             while True:
                 self.lvl, self.ptr = [0] * len(self.q), [0] * len(self.q)
                 qi, qe, self.lvl[source] = 0, 1, 1
diff --git a/graphs/directed_and_undirected_(weighted)_graph.py b/graphs/directed_and_undirected_weighted_graph.py
similarity index 100%
rename from graphs/directed_and_undirected_(weighted)_graph.py
rename to graphs/directed_and_undirected_weighted_graph.py
diff --git a/graphs/eulerian_path_and_circuit_for_undirected_graph.py b/graphs/eulerian_path_and_circuit_for_undirected_graph.py
index 6b4ea8e21e8b..5b146eaa845b 100644
--- a/graphs/eulerian_path_and_circuit_for_undirected_graph.py
+++ b/graphs/eulerian_path_and_circuit_for_undirected_graph.py
@@ -56,7 +56,7 @@ def main():
     g4 = {1: [2, 3], 2: [1, 3], 3: [1, 2]}
     g5 = {
         1: [],
-        2: []
+        2: [],
         # all degree is zero
     }
     max_node = 10
diff --git a/graphs/even_tree.py b/graphs/even_tree.py
index 92ffb4b232f7..7d47899527a7 100644
--- a/graphs/even_tree.py
+++ b/graphs/even_tree.py
@@ -12,6 +12,7 @@
 Note: The tree input will be such that it can always be decomposed into
 components containing an even number of nodes.
 """
+
 # pylint: disable=invalid-name
 from collections import defaultdict
 
diff --git a/graphs/frequent_pattern_graph_miner.py b/graphs/frequent_pattern_graph_miner.py
index 208e57f9b32f..f8da73f3438e 100644
--- a/graphs/frequent_pattern_graph_miner.py
+++ b/graphs/frequent_pattern_graph_miner.py
@@ -8,6 +8,7 @@
 
 URL: https://www.researchgate.net/publication/235255851
 """
+
 # fmt: off
 edge_array = [
     ['ab-e1', 'ac-e3', 'ad-e5', 'bc-e4', 'bd-e2', 'be-e6', 'bh-e12', 'cd-e2', 'ce-e4',
diff --git a/graphs/graph_adjacency_list.py b/graphs/graph_adjacency_list.py
index d0b94f03e9b4..244e59e0e1bf 100644
--- a/graphs/graph_adjacency_list.py
+++ b/graphs/graph_adjacency_list.py
@@ -15,19 +15,20 @@
 - Make edge weights and vertex values customizable to store whatever the client wants
 - Support multigraph functionality if the client wants it
 """
+
 from __future__ import annotations
 
 import random
 import unittest
 from pprint import pformat
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 import pytest
 
 T = TypeVar("T")
 
 
-class GraphAdjacencyList(Generic[T]):
+class GraphAdjacencyList[T]:
     def __init__(
         self, vertices: list[T], edges: list[list[T]], directed: bool = True
     ) -> None:
diff --git a/graphs/graph_adjacency_matrix.py b/graphs/graph_adjacency_matrix.py
index cdef388d9098..8eeeae786513 100644
--- a/graphs/graph_adjacency_matrix.py
+++ b/graphs/graph_adjacency_matrix.py
@@ -15,19 +15,20 @@
 - Make edge weights and vertex values customizable to store whatever the client wants
 - Support multigraph functionality if the client wants it
 """
+
 from __future__ import annotations
 
 import random
 import unittest
 from pprint import pformat
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 import pytest
 
 T = TypeVar("T")
 
 
-class GraphAdjacencyMatrix(Generic[T]):
+class GraphAdjacencyMatrix[T]:
     def __init__(
         self, vertices: list[T], edges: list[list[T]], directed: bool = True
     ) -> None:
@@ -155,9 +156,11 @@ def remove_vertex(self, vertex: T) -> None:
         self.vertex_to_index.pop(vertex)
 
         # decrement indices for vertices shifted by the deleted vertex in the adj matrix
-        for vertex in self.vertex_to_index:
-            if self.vertex_to_index[vertex] >= start_index:
-                self.vertex_to_index[vertex] = self.vertex_to_index[vertex] - 1
+        for inner_vertex in self.vertex_to_index:
+            if self.vertex_to_index[inner_vertex] >= start_index:
+                self.vertex_to_index[inner_vertex] = (
+                    self.vertex_to_index[inner_vertex] - 1
+                )
 
     def contains_vertex(self, vertex: T) -> bool:
         """
diff --git a/graphs/graph_list.py b/graphs/graph_list.py
index e871f3b8a9d6..6b63590bf55a 100644
--- a/graphs/graph_list.py
+++ b/graphs/graph_list.py
@@ -6,12 +6,12 @@
 from __future__ import annotations
 
 from pprint import pformat
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
 
-class GraphAdjacencyList(Generic[T]):
+class GraphAdjacencyList[T]:
     """
     Adjacency List type Graph Data Structure that accounts for directed and undirected
     Graphs.  Initialize graph object indicating whether it's directed or undirected.
@@ -120,29 +120,29 @@ def add_edge(
             else:
                 self.adj_list[source_vertex] = [destination_vertex]
                 self.adj_list[destination_vertex] = [source_vertex]
-        else:  # For directed graphs
-            # if both source vertex and destination vertex are present in adjacency
-            # list, add destination vertex to source vertex list of adjacent vertices.
-            if source_vertex in self.adj_list and destination_vertex in self.adj_list:
-                self.adj_list[source_vertex].append(destination_vertex)
-            # if only source vertex is present in adjacency list, add destination
-            # vertex to source vertex list of adjacent vertices and create a new vertex
-            # with destination vertex as key, which has no adjacent vertex
-            elif source_vertex in self.adj_list:
-                self.adj_list[source_vertex].append(destination_vertex)
-                self.adj_list[destination_vertex] = []
-            # if only destination vertex is present in adjacency list, create a new
-            # vertex with source vertex as key and assign a list containing destination
-            # vertex as first adjacent vertex
-            elif destination_vertex in self.adj_list:
-                self.adj_list[source_vertex] = [destination_vertex]
-            # if both source vertex and destination vertex are not present in adjacency
-            # list, create a new vertex with source vertex as key and a list containing
-            # destination vertex as it's first adjacent vertex. Then create a new vertex
-            # with destination vertex as key, which has no adjacent vertex
-            else:
-                self.adj_list[source_vertex] = [destination_vertex]
-                self.adj_list[destination_vertex] = []
+        # For directed graphs
+        # if both source vertex and destination vertex are present in adjacency
+        # list, add destination vertex to source vertex list of adjacent vertices.
+        elif source_vertex in self.adj_list and destination_vertex in self.adj_list:
+            self.adj_list[source_vertex].append(destination_vertex)
+        # if only source vertex is present in adjacency list, add destination
+        # vertex to source vertex list of adjacent vertices and create a new vertex
+        # with destination vertex as key, which has no adjacent vertex
+        elif source_vertex in self.adj_list:
+            self.adj_list[source_vertex].append(destination_vertex)
+            self.adj_list[destination_vertex] = []
+        # if only destination vertex is present in adjacency list, create a new
+        # vertex with source vertex as key and assign a list containing destination
+        # vertex as first adjacent vertex
+        elif destination_vertex in self.adj_list:
+            self.adj_list[source_vertex] = [destination_vertex]
+        # if both source vertex and destination vertex are not present in adjacency
+        # list, create a new vertex with source vertex as key and a list containing
+        # destination vertex as it's first adjacent vertex. Then create a new vertex
+        # with destination vertex as key, which has no adjacent vertex
+        else:
+            self.adj_list[source_vertex] = [destination_vertex]
+            self.adj_list[destination_vertex] = []
 
         return self
 
diff --git a/graphs/graphs_floyd_warshall.py b/graphs/graphs_floyd_warshall.py
index 56cf8b9e382b..aaed9ac5df8b 100644
--- a/graphs/graphs_floyd_warshall.py
+++ b/graphs/graphs_floyd_warshall.py
@@ -1,7 +1,7 @@
 # floyd_warshall.py
 """
-    The problem is to find the shortest distance between all pairs of vertices in a
-    weighted directed graph that can have negative edge weights.
+The problem is to find the shortest distance between all pairs of vertices in a
+weighted directed graph that can have negative edge weights.
 """
 
 
diff --git a/graphs/kahns_algorithm_long.py b/graphs/kahns_algorithm_long.py
index 63cbeb909a8a..1f16b90c0745 100644
--- a/graphs/kahns_algorithm_long.py
+++ b/graphs/kahns_algorithm_long.py
@@ -17,8 +17,7 @@ def longest_distance(graph):
         for x in graph[vertex]:
             indegree[x] -= 1
 
-            if long_dist[vertex] + 1 > long_dist[x]:
-                long_dist[x] = long_dist[vertex] + 1
+            long_dist[x] = max(long_dist[x], long_dist[vertex] + 1)
 
             if indegree[x] == 0:
                 queue.append(x)
diff --git a/graphs/kahns_algorithm_topo.py b/graphs/kahns_algorithm_topo.py
index b1260bd5bd9b..c956cf9f48fd 100644
--- a/graphs/kahns_algorithm_topo.py
+++ b/graphs/kahns_algorithm_topo.py
@@ -1,36 +1,61 @@
-def topological_sort(graph):
+def topological_sort(graph: dict[int, list[int]]) -> list[int] | None:
     """
-    Kahn's Algorithm is used to find Topological ordering of Directed Acyclic Graph
-    using BFS
+    Perform topological sorting of a Directed Acyclic Graph (DAG)
+    using Kahn's Algorithm via Breadth-First Search (BFS).
+
+    Topological sorting is a linear ordering of vertices in a graph such that for
+    every directed edge u → v, vertex u comes before vertex v in the ordering.
+
+    Parameters:
+    graph: Adjacency list representing the directed graph where keys are
+           vertices, and values are lists of adjacent vertices.
+
+    Returns:
+    The topologically sorted order of vertices if the graph is a DAG.
+    Returns None if the graph contains a cycle.
+
+    Example:
+    >>> graph = {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [], 5: []}
+    >>> topological_sort(graph)
+    [0, 1, 2, 3, 4, 5]
+
+    >>> graph_with_cycle = {0: [1], 1: [2], 2: [0]}
+    >>> topological_sort(graph_with_cycle)
     """
+
     indegree = [0] * len(graph)
     queue = []
-    topo = []
-    cnt = 0
+    topo_order = []
+    processed_vertices_count = 0
 
+    # Calculate the indegree of each vertex
     for values in graph.values():
         for i in values:
             indegree[i] += 1
 
+    # Add all vertices with 0 indegree to the queue
     for i in range(len(indegree)):
         if indegree[i] == 0:
             queue.append(i)
 
+    # Perform BFS
     while queue:
         vertex = queue.pop(0)
-        cnt += 1
-        topo.append(vertex)
-        for x in graph[vertex]:
-            indegree[x] -= 1
-            if indegree[x] == 0:
-                queue.append(x)
-
-    if cnt != len(graph):
-        print("Cycle exists")
-    else:
-        print(topo)
-
-
-# Adjacency List of Graph
-graph = {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [], 5: []}
-topological_sort(graph)
+        processed_vertices_count += 1
+        topo_order.append(vertex)
+
+        # Traverse neighbors
+        for neighbor in graph[vertex]:
+            indegree[neighbor] -= 1
+            if indegree[neighbor] == 0:
+                queue.append(neighbor)
+
+    if processed_vertices_count != len(graph):
+        return None  # no topological ordering exists due to cycle
+    return topo_order  # valid topological ordering
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/graphs/lanczos_eigenvectors.py b/graphs/lanczos_eigenvectors.py
new file mode 100644
index 000000000000..581a81a1127f
--- /dev/null
+++ b/graphs/lanczos_eigenvectors.py
@@ -0,0 +1,206 @@
+"""
+Lanczos Method for Finding Eigenvalues and Eigenvectors of a Graph.
+
+This module demonstrates the Lanczos method to approximate the largest eigenvalues
+and corresponding eigenvectors of a symmetric matrix represented as a graph's
+adjacency list. The method efficiently handles large, sparse matrices by converting
+the graph to a tridiagonal matrix, whose eigenvalues and eigenvectors are then
+computed.
+
+Key Functions:
+- `find_lanczos_eigenvectors`: Computes the k largest eigenvalues and vectors.
+- `lanczos_iteration`: Constructs the tridiagonal matrix and orthonormal basis vectors.
+- `multiply_matrix_vector`: Multiplies an adjacency list graph with a vector.
+
+Complexity:
+- Time: O(k * n), where k is the number of eigenvalues and n is the matrix size.
+- Space: O(n), due to sparse representation and tridiagonal matrix structure.
+
+Further Reading:
+- Lanczos Algorithm: https://en.wikipedia.org/wiki/Lanczos_algorithm
+- Eigenvector Centrality: https://en.wikipedia.org/wiki/Eigenvector_centrality
+
+Example Usage:
+Given a graph represented by an adjacency list, the `find_lanczos_eigenvectors`
+function returns the largest eigenvalues and eigenvectors. This can be used to
+analyze graph centrality.
+"""
+
+import numpy as np
+
+
+def validate_adjacency_list(graph: list[list[int | None]]) -> None:
+    """Validates the adjacency list format for the graph.
+
+    Args:
+        graph: A list of lists where each sublist contains the neighbors of a node.
+
+    Raises:
+        ValueError: If the graph is not a list of lists, or if any node has
+                    invalid neighbors (e.g., out-of-range or non-integer values).
+
+    >>> validate_adjacency_list([[1, 2], [0], [0, 1]])
+    >>> validate_adjacency_list([[]])  # No neighbors, valid case
+    >>> validate_adjacency_list([[1], [2], [-1]])  # Invalid neighbor
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid neighbor -1 in node 2 adjacency list.
+    """
+    if not isinstance(graph, list):
+        raise ValueError("Graph should be a list of lists.")
+
+    for node_index, neighbors in enumerate(graph):
+        if not isinstance(neighbors, list):
+            no_neighbors_message: str = (
+                f"Node {node_index} should have a list of neighbors."
+            )
+            raise ValueError(no_neighbors_message)
+        for neighbor_index in neighbors:
+            if (
+                not isinstance(neighbor_index, int)
+                or neighbor_index < 0
+                or neighbor_index >= len(graph)
+            ):
+                invalid_neighbor_message: str = (
+                    f"Invalid neighbor {neighbor_index} in node {node_index} "
+                    f"adjacency list."
+                )
+                raise ValueError(invalid_neighbor_message)
+
+
+def lanczos_iteration(
+    graph: list[list[int | None]], num_eigenvectors: int
+) -> tuple[np.ndarray, np.ndarray]:
+    """Constructs the tridiagonal matrix and orthonormal basis vectors using the
+    Lanczos method.
+
+    Args:
+        graph: The graph represented as a list of adjacency lists.
+        num_eigenvectors: The number of largest eigenvalues and eigenvectors
+                          to approximate.
+
+    Returns:
+        A tuple containing:
+            - tridiagonal_matrix: A (num_eigenvectors x num_eigenvectors) symmetric
+                                  matrix.
+            - orthonormal_basis: A (num_nodes x num_eigenvectors) matrix of orthonormal
+                                 basis vectors.
+
+    Raises:
+        ValueError: If num_eigenvectors is less than 1 or greater than the number of
+                    nodes.
+
+    >>> graph = [[1, 2], [0, 2], [0, 1]]
+    >>> T, Q = lanczos_iteration(graph, 2)
+    >>> T.shape == (2, 2) and Q.shape == (3, 2)
+    True
+    """
+    num_nodes: int = len(graph)
+    if not (1 <= num_eigenvectors <= num_nodes):
+        raise ValueError(
+            "Number of eigenvectors must be between 1 and the number of "
+            "nodes in the graph."
+        )
+
+    orthonormal_basis: np.ndarray = np.zeros((num_nodes, num_eigenvectors))
+    tridiagonal_matrix: np.ndarray = np.zeros((num_eigenvectors, num_eigenvectors))
+
+    rng = np.random.default_rng()
+    initial_vector: np.ndarray = rng.random(num_nodes)
+    initial_vector /= np.sqrt(np.dot(initial_vector, initial_vector))
+    orthonormal_basis[:, 0] = initial_vector
+
+    prev_beta: float = 0.0
+    for iter_index in range(num_eigenvectors):
+        result_vector: np.ndarray = multiply_matrix_vector(
+            graph, orthonormal_basis[:, iter_index]
+        )
+        if iter_index > 0:
+            result_vector -= prev_beta * orthonormal_basis[:, iter_index - 1]
+        alpha_value: float = np.dot(orthonormal_basis[:, iter_index], result_vector)
+        result_vector -= alpha_value * orthonormal_basis[:, iter_index]
+
+        prev_beta = np.sqrt(np.dot(result_vector, result_vector))
+        if iter_index < num_eigenvectors - 1 and prev_beta > 1e-10:
+            orthonormal_basis[:, iter_index + 1] = result_vector / prev_beta
+        tridiagonal_matrix[iter_index, iter_index] = alpha_value
+        if iter_index < num_eigenvectors - 1:
+            tridiagonal_matrix[iter_index, iter_index + 1] = prev_beta
+            tridiagonal_matrix[iter_index + 1, iter_index] = prev_beta
+    return tridiagonal_matrix, orthonormal_basis
+
+
+def multiply_matrix_vector(
+    graph: list[list[int | None]], vector: np.ndarray
+) -> np.ndarray:
+    """Performs multiplication of a graph's adjacency list representation with a vector.
+
+    Args:
+        graph: The adjacency list of the graph.
+        vector: A 1D numpy array representing the vector to multiply.
+
+    Returns:
+        A numpy array representing the product of the adjacency list and the vector.
+
+    Raises:
+        ValueError: If the vector's length does not match the number of nodes in the
+                    graph.
+
+    >>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([1, 1, 1]))
+    array([2., 2., 2.])
+    >>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([0, 1, 0]))
+    array([1., 0., 1.])
+    """
+    num_nodes: int = len(graph)
+    if vector.shape[0] != num_nodes:
+        raise ValueError("Vector length must match the number of nodes in the graph.")
+
+    result: np.ndarray = np.zeros(num_nodes)
+    for node_index, neighbors in enumerate(graph):
+        for neighbor_index in neighbors:
+            result[node_index] += vector[neighbor_index]
+    return result
+
+
+def find_lanczos_eigenvectors(
+    graph: list[list[int | None]], num_eigenvectors: int
+) -> tuple[np.ndarray, np.ndarray]:
+    """Computes the largest eigenvalues and their corresponding eigenvectors using the
+    Lanczos method.
+
+    Args:
+        graph: The graph as a list of adjacency lists.
+        num_eigenvectors: Number of largest eigenvalues and eigenvectors to compute.
+
+    Returns:
+        A tuple containing:
+            - eigenvalues: 1D array of the largest eigenvalues in descending order.
+            - eigenvectors: 2D array where each column is an eigenvector corresponding
+                            to an eigenvalue.
+
+    Raises:
+        ValueError: If the graph format is invalid or num_eigenvectors is out of bounds.
+
+    >>> eigenvalues, eigenvectors = find_lanczos_eigenvectors(
+    ...     [[1, 2], [0, 2], [0, 1]], 2
+    ... )
+    >>> len(eigenvalues) == 2 and eigenvectors.shape[1] == 2
+    True
+    """
+    validate_adjacency_list(graph)
+    tridiagonal_matrix, orthonormal_basis = lanczos_iteration(graph, num_eigenvectors)
+    eigenvalues, eigenvectors = np.linalg.eigh(tridiagonal_matrix)
+    return eigenvalues[::-1], np.dot(orthonormal_basis, eigenvectors[:, ::-1])
+
+
+def main() -> None:
+    """
+    Main driver function for testing the implementation with doctests.
+    """
+    import doctest
+
+    doctest.testmod()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/graphs/minimum_spanning_tree_boruvka.py b/graphs/minimum_spanning_tree_boruvka.py
index 3c6888037948..f234d65ab765 100644
--- a/graphs/minimum_spanning_tree_boruvka.py
+++ b/graphs/minimum_spanning_tree_boruvka.py
@@ -185,12 +185,12 @@ def boruvka_mst(graph):
 
                     if cheap_edge[set2] == -1 or cheap_edge[set2][2] > weight:
                         cheap_edge[set2] = [head, tail, weight]
-            for vertex in cheap_edge:
-                if cheap_edge[vertex] != -1:
-                    head, tail, weight = cheap_edge[vertex]
+            for head_tail_weight in cheap_edge.values():
+                if head_tail_weight != -1:
+                    head, tail, weight = head_tail_weight
                     if union_find.find(head) != union_find.find(tail):
                         union_find.union(head, tail)
-                        mst_edges.append(cheap_edge[vertex])
+                        mst_edges.append(head_tail_weight)
                         num_components = num_components - 1
         mst = Graph.build(edges=mst_edges)
         return mst
diff --git a/graphs/minimum_spanning_tree_kruskal2.py b/graphs/minimum_spanning_tree_kruskal2.py
index 0ddb43ce8e6e..1f6d7255683b 100644
--- a/graphs/minimum_spanning_tree_kruskal2.py
+++ b/graphs/minimum_spanning_tree_kruskal2.py
@@ -1,11 +1,11 @@
 from __future__ import annotations
 
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
 
-class DisjointSetTreeNode(Generic[T]):
+class DisjointSetTreeNode[T]:
     # Disjoint Set Node to store the parent and rank
     def __init__(self, data: T) -> None:
         self.data = data
@@ -13,7 +13,7 @@ def __init__(self, data: T) -> None:
         self.rank = 0
 
 
-class DisjointSetTree(Generic[T]):
+class DisjointSetTree[T]:
     # Disjoint Set DataStructure
     def __init__(self) -> None:
         # map from node name to the node object
@@ -46,7 +46,7 @@ def union(self, data1: T, data2: T) -> None:
         self.link(self.find_set(data1), self.find_set(data2))
 
 
-class GraphUndirectedWeighted(Generic[T]):
+class GraphUndirectedWeighted[T]:
     def __init__(self) -> None:
         # connections: map from the node to the neighbouring nodes (with weights)
         self.connections: dict[T, dict[T, int]] = {}
diff --git a/graphs/minimum_spanning_tree_prims.py b/graphs/minimum_spanning_tree_prims.py
index 5a08ec57ff4d..d0b45d7ef139 100644
--- a/graphs/minimum_spanning_tree_prims.py
+++ b/graphs/minimum_spanning_tree_prims.py
@@ -16,13 +16,12 @@ def top_to_bottom(self, heap, start, size, positions):
         if start > size // 2 - 1:
             return
         else:
-            if 2 * start + 2 >= size:
+            if 2 * start + 2 >= size:  # noqa: SIM114
+                smallest_child = 2 * start + 1
+            elif heap[2 * start + 1] < heap[2 * start + 2]:
                 smallest_child = 2 * start + 1
             else:
-                if heap[2 * start + 1] < heap[2 * start + 2]:
-                    smallest_child = 2 * start + 1
-                else:
-                    smallest_child = 2 * start + 2
+                smallest_child = 2 * start + 2
             if heap[smallest_child] < heap[start]:
                 temp, temp1 = heap[smallest_child], positions[smallest_child]
                 heap[smallest_child], positions[smallest_child] = (
diff --git a/graphs/minimum_spanning_tree_prims2.py b/graphs/minimum_spanning_tree_prims2.py
index 81f30ef615fe..d22111289a48 100644
--- a/graphs/minimum_spanning_tree_prims2.py
+++ b/graphs/minimum_spanning_tree_prims2.py
@@ -6,10 +6,11 @@
 at a time, from an arbitrary starting vertex, at each step adding the cheapest possible
 connection from the tree to another vertex.
 """
+
 from __future__ import annotations
 
 from sys import maxsize
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
@@ -46,7 +47,7 @@ def get_child_right_position(position: int) -> int:
     return (2 * position) + 2
 
 
-class MinPriorityQueue(Generic[T]):
+class MinPriorityQueue[T]:
     """
     Minimum Priority Queue Class
 
@@ -183,7 +184,7 @@ def _swap_nodes(self, node1_pos: int, node2_pos: int) -> None:
         self.position_map[node2_elem] = node1_pos
 
 
-class GraphUndirectedWeighted(Generic[T]):
+class GraphUndirectedWeighted[T]:
     """
     Graph Undirected Weighted Class
 
@@ -216,7 +217,7 @@ def add_edge(self, node1: T, node2: T, weight: int) -> None:
         self.connections[node2][node1] = weight
 
 
-def prims_algo(
+def prims_algo[T](
     graph: GraphUndirectedWeighted[T],
 ) -> tuple[dict[T, int], dict[T, T | None]]:
     """
@@ -238,8 +239,8 @@ def prims_algo(
     13
     """
     # prim's algorithm for minimum spanning tree
-    dist: dict[T, int] = {node: maxsize for node in graph.connections}
-    parent: dict[T, T | None] = {node: None for node in graph.connections}
+    dist: dict[T, int] = dict.fromkeys(graph.connections, maxsize)
+    parent: dict[T, T | None] = dict.fromkeys(graph.connections)
 
     priority_queue: MinPriorityQueue[T] = MinPriorityQueue()
     for node, weight in dist.items():
diff --git a/graphs/multi_heuristic_astar.py b/graphs/multi_heuristic_astar.py
index 0a18ede6ed41..38b07e1ca675 100644
--- a/graphs/multi_heuristic_astar.py
+++ b/graphs/multi_heuristic_astar.py
@@ -79,7 +79,7 @@ def key(start: TPos, i: int, goal: TPos, g_function: dict[TPos, float]):
 
 
 def do_something(back_pointer, goal, start):
-    grid = np.chararray((n, n))
+    grid = np.char.chararray((n, n))
     for i in range(n):
         for j in range(n):
             grid[i][j] = "*"
@@ -123,9 +123,7 @@ def do_something(back_pointer, goal, start):
 def valid(p: TPos):
     if p[0] < 0 or p[0] > n - 1:
         return False
-    if p[1] < 0 or p[1] > n - 1:
-        return False
-    return True
+    return not (p[1] < 0 or p[1] > n - 1)
 
 
 def expand_state(
@@ -270,24 +268,23 @@ def multi_a_star(start: TPos, goal: TPos, n_heuristic: int):
                         back_pointer,
                     )
                     close_list_inad.append(get_s)
+            elif g_function[goal] <= open_list[0].minkey():
+                if g_function[goal] < float("inf"):
+                    do_something(back_pointer, goal, start)
             else:
-                if g_function[goal] <= open_list[0].minkey():
-                    if g_function[goal] < float("inf"):
-                        do_something(back_pointer, goal, start)
-                else:
-                    get_s = open_list[0].top_show()
-                    visited.add(get_s)
-                    expand_state(
-                        get_s,
-                        0,
-                        visited,
-                        g_function,
-                        close_list_anchor,
-                        close_list_inad,
-                        open_list,
-                        back_pointer,
-                    )
-                    close_list_anchor.append(get_s)
+                get_s = open_list[0].top_show()
+                visited.add(get_s)
+                expand_state(
+                    get_s,
+                    0,
+                    visited,
+                    g_function,
+                    close_list_anchor,
+                    close_list_inad,
+                    open_list,
+                    back_pointer,
+                )
+                close_list_anchor.append(get_s)
     print("No path found to goal")
     print()
     for i in range(n - 1, -1, -1):
diff --git a/graphs/page_rank.py b/graphs/page_rank.py
index b9e4c4a72a93..c0ce3a94c76b 100644
--- a/graphs/page_rank.py
+++ b/graphs/page_rank.py
@@ -1,6 +1,7 @@
 """
 Author: https://github.com/bhushan-borole
 """
+
 """
 The input graph for the algorithm is:
 
diff --git a/graphs/prim.py b/graphs/prim.py
index 6cb1a6def359..5b3ce04441ec 100644
--- a/graphs/prim.py
+++ b/graphs/prim.py
@@ -1,8 +1,8 @@
 """Prim's Algorithm.
 
-    Determines the minimum spanning tree(MST) of a graph using the Prim's Algorithm.
+Determines the minimum spanning tree(MST) of a graph using the Prim's Algorithm.
 
-    Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm
+Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm
 """
 
 import heapq as hq
diff --git a/graphs/strongly_connected_components.py b/graphs/strongly_connected_components.py
index 325e5c1f33a3..4d4cf88035b5 100644
--- a/graphs/strongly_connected_components.py
+++ b/graphs/strongly_connected_components.py
@@ -38,7 +38,7 @@ def find_components(
     reversed_graph: dict[int, list[int]], vert: int, visited: list[bool]
 ) -> list[int]:
     """
-    Use depth first search to find strongliy connected
+    Use depth first search to find strongly connected
     vertices. Now graph is reversed
     >>> find_components({0: [1], 1: [2], 2: [0]}, 0, 5 * [False])
     [0, 1, 2]
diff --git a/graphs/tarjans_scc.py b/graphs/tarjans_scc.py
index dfd2e52704d5..b4a3bd5c4c35 100644
--- a/graphs/tarjans_scc.py
+++ b/graphs/tarjans_scc.py
@@ -1,7 +1,7 @@
 from collections import deque
 
 
-def tarjan(g):
+def tarjan(g: list[list[int]]) -> list[list[int]]:
     """
     Tarjan's algo for finding strongly connected components in a directed graph
 
@@ -19,15 +19,30 @@ def tarjan(g):
     Complexity: strong_connect() is called at most once for each node and has a
     complexity of O(|E|) as it is DFS.
     Therefore this has complexity O(|V| + |E|) for a graph G = (V, E)
+
+    >>> tarjan([[2, 3, 4], [2, 3, 4], [0, 1, 3], [0, 1, 2], [1]])
+    [[4, 3, 1, 2, 0]]
+    >>> tarjan([[], [], [], []])
+    [[0], [1], [2], [3]]
+    >>> a = [0, 1, 2, 3, 4, 5, 4]
+    >>> b = [1, 0, 3, 2, 5, 4, 0]
+    >>> n = 7
+    >>> sorted(tarjan(create_graph(n, list(zip(a, b))))) == sorted(
+    ...     tarjan(create_graph(n, list(zip(a[::-1], b[::-1])))))
+    True
+    >>> a = [0, 1, 2, 3, 4, 5, 6]
+    >>> b = [0, 1, 2, 3, 4, 5, 6]
+    >>> sorted(tarjan(create_graph(n, list(zip(a, b)))))
+    [[0], [1], [2], [3], [4], [5], [6]]
     """
 
     n = len(g)
-    stack = deque()
+    stack: deque[int] = deque()
     on_stack = [False for _ in range(n)]
     index_of = [-1 for _ in range(n)]
     lowlink_of = index_of[:]
 
-    def strong_connect(v, index, components):
+    def strong_connect(v: int, index: int, components: list[list[int]]) -> int:
         index_of[v] = index  # the number when this node is seen
         lowlink_of[v] = index  # lowest rank node reachable from here
         index += 1
@@ -57,7 +72,7 @@ def strong_connect(v, index, components):
             components.append(component)
         return index
 
-    components = []
+    components: list[list[int]] = []
     for v in range(n):
         if index_of[v] == -1:
             strong_connect(v, 0, components)
@@ -65,8 +80,16 @@ def strong_connect(v, index, components):
     return components
 
 
-def create_graph(n, edges):
-    g = [[] for _ in range(n)]
+def create_graph(n: int, edges: list[tuple[int, int]]) -> list[list[int]]:
+    """
+    >>> n = 7
+    >>> source = [0, 0, 1, 2, 3, 3, 4, 4, 6]
+    >>> target = [1, 3, 2, 0, 1, 4, 5, 6, 5]
+    >>> edges = list(zip(source, target))
+    >>> create_graph(n, edges)
+    [[1, 3], [2], [0], [1, 4], [5, 6], [], [5]]
+    """
+    g: list[list[int]] = [[] for _ in range(n)]
     for u, v in edges:
         g[u].append(v)
     return g
@@ -80,4 +103,4 @@ def create_graph(n, edges):
     edges = list(zip(source, target))
     g = create_graph(n_vertices, edges)
 
-    assert [[5], [6], [4], [3, 2, 1, 0]] == tarjan(g)
+    assert tarjan(g) == [[5], [6], [4], [3, 2, 1, 0]]
diff --git a/greedy_methods/__init__.py b/greedy_methods/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/greedy_methods/fractional_knapsack.py b/greedy_methods/fractional_knapsack.py
index 58976d40c02b..d52b56f23569 100644
--- a/greedy_methods/fractional_knapsack.py
+++ b/greedy_methods/fractional_knapsack.py
@@ -6,6 +6,30 @@ def frac_knapsack(vl, wt, w, n):
     """
     >>> frac_knapsack([60, 100, 120], [10, 20, 30], 50, 3)
     240.0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 10, 4)
+    105.0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 4)
+    95.0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6], 8, 4)
+    60.0
+    >>> frac_knapsack([10, 40, 30], [5, 4, 6, 3], 8, 4)
+    60.0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 0, 4)
+    0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 0)
+    95.0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], -8, 4)
+    0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, -4)
+    95.0
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 800, 4)
+    130
+    >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 400)
+    95.0
+    >>> frac_knapsack("ABCD", [5, 4, 6, 3], 8, 400)
+    Traceback (most recent call last):
+        ...
+    TypeError: unsupported operand type(s) for /: 'str' and 'int'
     """
 
     r = sorted(zip(vl, wt), key=lambda x: x[0] / x[1], reverse=True)
diff --git a/greedy_methods/gas_station.py b/greedy_methods/gas_station.py
index 2427375d2664..6391ce379329 100644
--- a/greedy_methods/gas_station.py
+++ b/greedy_methods/gas_station.py
@@ -23,6 +23,7 @@
 start checking from the next station.
 
 """
+
 from dataclasses import dataclass
 
 
diff --git a/greedy_methods/smallest_range.py b/greedy_methods/smallest_range.py
new file mode 100644
index 000000000000..9adb12bf9029
--- /dev/null
+++ b/greedy_methods/smallest_range.py
@@ -0,0 +1,72 @@
+"""
+smallest_range function takes a list of sorted integer lists and finds the smallest
+range that includes at least one number from each list, using a min heap for efficiency.
+"""
+
+from heapq import heappop, heappush
+from sys import maxsize
+
+
+def smallest_range(nums: list[list[int]]) -> list[int]:
+    """
+    Find the smallest range from each list in nums.
+
+    Uses min heap for efficiency. The range includes at least one number from each list.
+
+    Args:
+        `nums`: List of k sorted integer lists.
+
+    Returns:
+        list: Smallest range as a two-element list.
+
+    Examples:
+
+    >>> smallest_range([[4, 10, 15, 24, 26], [0, 9, 12, 20], [5, 18, 22, 30]])
+    [20, 24]
+    >>> smallest_range([[1, 2, 3], [1, 2, 3], [1, 2, 3]])
+    [1, 1]
+    >>> smallest_range(((1, 2, 3), (1, 2, 3), (1, 2, 3)))
+    [1, 1]
+    >>> smallest_range(((-3, -2, -1), (0, 0, 0), (1, 2, 3)))
+    [-1, 1]
+    >>> smallest_range([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+    [3, 7]
+    >>> smallest_range([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+    [0, 0]
+    >>> smallest_range([[], [], []])
+    Traceback (most recent call last):
+        ...
+    IndexError: list index out of range
+    """
+
+    min_heap: list[tuple[int, int, int]] = []
+    current_max = -maxsize - 1
+
+    for i, items in enumerate(nums):
+        heappush(min_heap, (items[0], i, 0))
+        current_max = max(current_max, items[0])
+
+    # Initialize smallest_range with large integer values
+    smallest_range = [-maxsize - 1, maxsize]
+
+    while min_heap:
+        current_min, list_index, element_index = heappop(min_heap)
+
+        if current_max - current_min < smallest_range[1] - smallest_range[0]:
+            smallest_range = [current_min, current_max]
+
+        if element_index == len(nums[list_index]) - 1:
+            break
+
+        next_element = nums[list_index][element_index + 1]
+        heappush(min_heap, (next_element, list_index, element_index + 1))
+        current_max = max(current_max, next_element)
+
+    return smallest_range
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    print(f"{smallest_range([[1, 2, 3], [1, 2, 3], [1, 2, 3]])}")  # Output: [1, 1]
diff --git a/hashes/adler32.py b/hashes/adler32.py
index 611ebc88b80f..38d76ab12aa0 100644
--- a/hashes/adler32.py
+++ b/hashes/adler32.py
@@ -1,11 +1,11 @@
 """
-    Adler-32 is a checksum algorithm which was invented by Mark Adler in 1995.
-    Compared to a cyclic redundancy check of the same length, it trades reliability for
-    speed (preferring the latter).
-    Adler-32 is more reliable than Fletcher-16, and slightly less reliable than
-    Fletcher-32.[2]
+Adler-32 is a checksum algorithm which was invented by Mark Adler in 1995.
+Compared to a cyclic redundancy check of the same length, it trades reliability for
+speed (preferring the latter).
+Adler-32 is more reliable than Fletcher-16, and slightly less reliable than
+Fletcher-32.[2]
 
-    source: https://en.wikipedia.org/wiki/Adler-32
+source: https://en.wikipedia.org/wiki/Adler-32
 """
 
 MOD_ADLER = 65521
diff --git a/hashes/enigma_machine.py b/hashes/enigma_machine.py
index d95437d12c34..0da8e4113de9 100644
--- a/hashes/enigma_machine.py
+++ b/hashes/enigma_machine.py
@@ -15,12 +15,12 @@ def rotator():
     gear_one.append(i)
     del gear_one[0]
     gear_one_pos += 1
-    if gear_one_pos % int(len(alphabets)) == 0:
+    if gear_one_pos % len(alphabets) == 0:
         i = gear_two[0]
         gear_two.append(i)
         del gear_two[0]
         gear_two_pos += 1
-        if gear_two_pos % int(len(alphabets)) == 0:
+        if gear_two_pos % len(alphabets) == 0:
             i = gear_three[0]
             gear_three.append(i)
             del gear_three[0]
diff --git a/hashes/fletcher16.py b/hashes/fletcher16.py
index 7c23c98d72c5..add8e185bc06 100644
--- a/hashes/fletcher16.py
+++ b/hashes/fletcher16.py
@@ -1,6 +1,6 @@
 """
 The Fletcher checksum is an algorithm for computing a position-dependent
-checksum devised by John G. Fletcher (1934–2012) at Lawrence Livermore Labs
+checksum devised by John G. Fletcher (1934-2012) at Lawrence Livermore Labs
 in the late 1970s.[1] The objective of the Fletcher checksum was to
 provide error-detection properties approaching those of a cyclic
 redundancy check but with the lower computational effort associated
diff --git a/hashes/hamming_code.py b/hashes/hamming_code.py
index 8498ca920b36..b3095852ac51 100644
--- a/hashes/hamming_code.py
+++ b/hashes/hamming_code.py
@@ -4,44 +4,44 @@
 # Black: True
 
 """
-    * This code implement the Hamming code:
-        https://en.wikipedia.org/wiki/Hamming_code - In telecommunication,
-    Hamming codes are a family of linear error-correcting codes. Hamming
-    codes can detect up to two-bit errors or correct one-bit errors
-    without detection of uncorrected errors. By contrast, the simple
-    parity code cannot correct errors, and can detect only an odd number
-    of bits in error. Hamming codes are perfect codes, that is, they
-    achieve the highest possible rate for codes with their block length
-    and minimum distance of three.
-
-    * the implemented code consists of:
-        * a function responsible for encoding the message (emitterConverter)
-            * return the encoded message
-        * a function responsible for decoding the message (receptorConverter)
-            * return the decoded message and a ack of data integrity
-
-    * how to use:
-            to be used you must declare how many parity bits (sizePari)
-        you want to include in the message.
-            it is desired (for test purposes) to select a bit to be set
-        as an error. This serves to check whether the code is working correctly.
-            Lastly, the variable of the message/word that must be desired to be
-        encoded (text).
-
-    * how this work:
-            declaration of variables (sizePari, be, text)
-
-            converts the message/word (text) to binary using the
-        text_to_bits function
-            encodes the message using the rules of hamming encoding
-            decodes the message using the rules of hamming encoding
-            print the original message, the encoded message and the
-        decoded message
-
-            forces an error in the coded text variable
-            decodes the message that was forced the error
-            print the original message, the encoded message, the bit changed
-        message and the decoded message
+* This code implement the Hamming code:
+    https://en.wikipedia.org/wiki/Hamming_code - In telecommunication,
+Hamming codes are a family of linear error-correcting codes. Hamming
+codes can detect up to two-bit errors or correct one-bit errors
+without detection of uncorrected errors. By contrast, the simple
+parity code cannot correct errors, and can detect only an odd number
+of bits in error. Hamming codes are perfect codes, that is, they
+achieve the highest possible rate for codes with their block length
+and minimum distance of three.
+
+* the implemented code consists of:
+    * a function responsible for encoding the message (emitterConverter)
+        * return the encoded message
+    * a function responsible for decoding the message (receptorConverter)
+        * return the decoded message and a ack of data integrity
+
+* how to use:
+        to be used you must declare how many parity bits (sizePari)
+    you want to include in the message.
+        it is desired (for test purposes) to select a bit to be set
+    as an error. This serves to check whether the code is working correctly.
+        Lastly, the variable of the message/word that must be desired to be
+    encoded (text).
+
+* how this work:
+        declaration of variables (sizePari, be, text)
+
+        converts the message/word (text) to binary using the
+    text_to_bits function
+        encodes the message using the rules of hamming encoding
+        decodes the message using the rules of hamming encoding
+        print the original message, the encoded message and the
+    decoded message
+
+        forces an error in the coded text variable
+        decodes the message that was forced the error
+        print the original message, the encoded message, the bit changed
+    message and the decoded message
 """
 
 # Imports
@@ -77,6 +77,10 @@ def emitter_converter(size_par, data):
 
     >>> emitter_converter(4, "101010111111")
     ['1', '1', '1', '1', '0', '1', '0', '0', '1', '0', '1', '1', '1', '1', '1', '1']
+    >>> emitter_converter(5, "101010111111")
+    Traceback (most recent call last):
+        ...
+    ValueError: size of parity don't match with size of data
     """
     if size_par + len(data) <= 2**size_par - (len(data) - 1):
         raise ValueError("size of parity don't match with size of data")
@@ -119,8 +123,7 @@ def emitter_converter(size_par, data):
         # Bit counter one for a given parity
         cont_bo = 0
         # counter to control the loop reading
-        cont_loop = 0
-        for x in data_ord:
+        for cont_loop, x in enumerate(data_ord):
             if x is not None:
                 try:
                     aux = (bin_pos[cont_loop])[-1 * (bp)]
@@ -128,7 +131,6 @@ def emitter_converter(size_par, data):
                     aux = "0"
                 if aux == "1" and x == "1":
                     cont_bo += 1
-            cont_loop += 1
         parity.append(cont_bo % 2)
 
         qtd_bp += 1
@@ -160,10 +162,10 @@ def receptor_converter(size_par, data):
     parity_received = []
     data_output = []
 
-    for x in range(1, len(data) + 1):
+    for i, item in enumerate(data, 1):
         # Performs a template of bit positions - who should be given,
         #  and who should be parity
-        if qtd_bp < size_par and (np.log(x) / np.log(2)).is_integer():
+        if qtd_bp < size_par and (np.log(i) / np.log(2)).is_integer():
             data_out_gab.append("P")
             qtd_bp = qtd_bp + 1
         else:
@@ -171,10 +173,9 @@ def receptor_converter(size_par, data):
 
         # Sorts the data to the new output size
         if data_out_gab[-1] == "D":
-            data_output.append(data[cont_data])
+            data_output.append(item)
         else:
-            parity_received.append(data[cont_data])
-        cont_data += 1
+            parity_received.append(item)
 
     # -----------calculates the parity with the data
     data_out = []
@@ -211,9 +212,7 @@ def receptor_converter(size_par, data):
     for bp in range(1, size_par + 1):
         # Bit counter one for a certain parity
         cont_bo = 0
-        # Counter to control loop reading
-        cont_loop = 0
-        for x in data_ord:
+        for cont_loop, x in enumerate(data_ord):
             if x is not None:
                 try:
                     aux = (bin_pos[cont_loop])[-1 * (bp)]
@@ -221,7 +220,6 @@ def receptor_converter(size_par, data):
                     aux = "0"
                 if aux == "1" and x == "1":
                     cont_bo += 1
-            cont_loop += 1
         parity.append(str(cont_bo % 2))
 
         qtd_bp += 1
diff --git a/hashes/luhn.py b/hashes/luhn.py
index bb77fd05c556..a29bf39e3d82 100644
--- a/hashes/luhn.py
+++ b/hashes/luhn.py
@@ -1,4 +1,5 @@
-""" Luhn Algorithm """
+"""Luhn Algorithm"""
+
 from __future__ import annotations
 
 
diff --git a/hashes/md5.py b/hashes/md5.py
index 2187006ec8a9..f9d802ff0308 100644
--- a/hashes/md5.py
+++ b/hashes/md5.py
@@ -82,8 +82,8 @@ def reformat_hex(i: int) -> bytes:
 
     hex_rep = format(i, "08x")[-8:]
     little_endian_hex = b""
-    for i in [3, 2, 1, 0]:
-        little_endian_hex += hex_rep[2 * i : 2 * i + 2].encode("utf-8")
+    for j in [3, 2, 1, 0]:
+        little_endian_hex += hex_rep[2 * j : 2 * j + 2].encode("utf-8")
     return little_endian_hex
 
 
@@ -131,7 +131,7 @@ def preprocess(message: bytes) -> bytes:
     return bit_string
 
 
-def get_block_words(bit_string: bytes) -> Generator[list[int], None, None]:
+def get_block_words(bit_string: bytes) -> Generator[list[int]]:
     """
     Splits bit string into blocks of 512 chars and yields each block as a list
     of 32-bit words
diff --git a/hashes/sdbm.py b/hashes/sdbm.py
index a5432874ba7d..a5abc6f3185b 100644
--- a/hashes/sdbm.py
+++ b/hashes/sdbm.py
@@ -1,21 +1,21 @@
 """
-    This algorithm was created for sdbm (a public-domain reimplementation of ndbm)
-    database library.
-    It was found to do well in scrambling bits, causing better distribution of the keys
-    and fewer splits.
-    It also happens to be a good general hashing function with good distribution.
-    The actual function (pseudo code) is:
-        for i in i..len(str):
-            hash(i) = hash(i - 1) * 65599 + str[i];
+This algorithm was created for sdbm (a public-domain reimplementation of ndbm)
+database library.
+It was found to do well in scrambling bits, causing better distribution of the keys
+and fewer splits.
+It also happens to be a good general hashing function with good distribution.
+The actual function (pseudo code) is:
+    for i in i..len(str):
+        hash(i) = hash(i - 1) * 65599 + str[i];
 
-    What is included below is the faster version used in gawk. [there is even a faster,
-    duff-device version]
-    The magic constant 65599 was picked out of thin air while experimenting with
-    different constants.
-    It turns out to be a prime.
-    This is one of the algorithms used in berkeley db (see sleepycat) and elsewhere.
+What is included below is the faster version used in gawk. [there is even a faster,
+duff-device version]
+The magic constant 65599 was picked out of thin air while experimenting with
+different constants.
+It turns out to be a prime.
+This is one of the algorithms used in berkeley db (see sleepycat) and elsewhere.
 
-    source: http://www.cse.yorku.ca/~oz/hash.html
+source: http://www.cse.yorku.ca/~oz/hash.html
 """
 
 
diff --git a/hashes/sha1.py b/hashes/sha1.py
index a0fa688f863e..75a1423e9b5f 100644
--- a/hashes/sha1.py
+++ b/hashes/sha1.py
@@ -25,6 +25,7 @@
 
 Reference: https://deadhacker.com/2006/02/21/sha-1-illustrated/
 """
+
 import argparse
 import hashlib  # hashlib is only used inside the Test class
 import struct
diff --git a/index.md b/index.md
new file mode 100644
index 000000000000..134520cb94aa
--- /dev/null
+++ b/index.md
@@ -0,0 +1,10 @@
+# TheAlgorithms/Python
+```{toctree}
+:maxdepth: 2
+:caption: index.md
+
+<!-- CONTRIBUTING.md must be the FIRST doc and README.md can come after. -->
+CONTRIBUTING.md
+README.md
+LICENSE.md
+```
diff --git a/knapsack/knapsack.py b/knapsack/knapsack.py
index 18a36c3bcdda..bb507be1ba3c 100644
--- a/knapsack/knapsack.py
+++ b/knapsack/knapsack.py
@@ -1,6 +1,7 @@
-""" A naive recursive implementation of 0-1 Knapsack Problem
-    https://en.wikipedia.org/wiki/Knapsack_problem
+"""A naive recursive implementation of 0-1 Knapsack Problem
+https://en.wikipedia.org/wiki/Knapsack_problem
 """
+
 from __future__ import annotations
 
 
diff --git a/knapsack/tests/test_knapsack.py b/knapsack/tests/test_knapsack.py
index 6932bbb3536b..7bfb8780627b 100644
--- a/knapsack/tests/test_knapsack.py
+++ b/knapsack/tests/test_knapsack.py
@@ -6,6 +6,7 @@
 
 This file contains the test-suite for the knapsack problem.
 """
+
 import unittest
 
 from knapsack import knapsack as k
diff --git a/arithmetic_analysis/gaussian_elimination.py b/linear_algebra/gaussian_elimination.py
similarity index 79%
rename from arithmetic_analysis/gaussian_elimination.py
rename to linear_algebra/gaussian_elimination.py
index a1a35131b157..6f4075b710fd 100644
--- a/arithmetic_analysis/gaussian_elimination.py
+++ b/linear_algebra/gaussian_elimination.py
@@ -1,9 +1,8 @@
 """
-Gaussian elimination method for solving a system of linear equations.
-Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination
+| Gaussian elimination method for solving a system of linear equations.
+| Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination
 """
 
-
 import numpy as np
 from numpy import float64
 from numpy.typing import NDArray
@@ -14,12 +13,17 @@ def retroactive_resolution(
 ) -> NDArray[float64]:
     """
     This function performs a retroactive linear system resolution
-        for triangular matrix
+    for triangular matrix
 
     Examples:
-        2x1 + 2x2 - 1x3 = 5         2x1 + 2x2 = -1
-        0x1 - 2x2 - 1x3 = -7        0x1 - 2x2 = -1
-        0x1 + 0x2 + 5x3 = 15
+        1.
+            * 2x1 + 2x2 - 1x3 = 5
+            * 0x1 - 2x2 - 1x3 = -7
+            * 0x1 + 0x2 + 5x3 = 15
+        2.
+            * 2x1 + 2x2 = -1
+            * 0x1 - 2x2 = -1
+
     >>> gaussian_elimination([[2, 2, -1], [0, -2, -1], [0, 0, 5]], [[5], [-7], [15]])
     array([[2.],
            [2.],
@@ -46,9 +50,14 @@ def gaussian_elimination(
     This function performs Gaussian elimination method
 
     Examples:
-        1x1 - 4x2 - 2x3 = -2        1x1 + 2x2 = 5
-        5x1 + 2x2 - 2x3 = -3        5x1 + 2x2 = 5
-        1x1 - 1x2 + 0x3 = 4
+        1.
+            * 1x1 - 4x2 - 2x3 = -2
+            * 5x1 + 2x2 - 2x3 = -3
+            * 1x1 - 1x2 + 0x3 = 4
+        2.
+            * 1x1 + 2x2 = 5
+            * 5x1 + 2x2 = 5
+
     >>> gaussian_elimination([[1, -4, -2], [5, 2, -2], [1, -1, 0]], [[-2], [-3], [4]])
     array([[ 2.3 ],
            [-1.7 ],
diff --git a/arithmetic_analysis/jacobi_iteration_method.py b/linear_algebra/jacobi_iteration_method.py
similarity index 96%
rename from arithmetic_analysis/jacobi_iteration_method.py
rename to linear_algebra/jacobi_iteration_method.py
index 44c52dd44640..2cc9c103018b 100644
--- a/arithmetic_analysis/jacobi_iteration_method.py
+++ b/linear_algebra/jacobi_iteration_method.py
@@ -1,203 +1,204 @@
-"""
-Jacobi Iteration Method - https://en.wikipedia.org/wiki/Jacobi_method
-"""
-from __future__ import annotations
-
-import numpy as np
-from numpy import float64
-from numpy.typing import NDArray
-
-
-# Method to find solution of system of linear equations
-def jacobi_iteration_method(
-    coefficient_matrix: NDArray[float64],
-    constant_matrix: NDArray[float64],
-    init_val: list[float],
-    iterations: int,
-) -> list[float]:
-    """
-    Jacobi Iteration Method:
-    An iterative algorithm to determine the solutions of strictly diagonally dominant
-    system of linear equations
-
-    4x1 +  x2 +  x3 =  2
-     x1 + 5x2 + 2x3 = -6
-     x1 + 2x2 + 4x3 = -4
-
-    x_init = [0.5, -0.5 , -0.5]
-
-    Examples:
-
-    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
-    >>> constant = np.array([[2], [-6], [-4]])
-    >>> init_val = [0.5, -0.5, -0.5]
-    >>> iterations = 3
-    >>> jacobi_iteration_method(coefficient, constant, init_val, iterations)
-    [0.909375, -1.14375, -0.7484375]
-
-
-    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2]])
-    >>> constant = np.array([[2], [-6], [-4]])
-    >>> init_val = [0.5, -0.5, -0.5]
-    >>> iterations = 3
-    >>> jacobi_iteration_method(coefficient, constant, init_val, iterations)
-    Traceback (most recent call last):
-        ...
-    ValueError: Coefficient matrix dimensions must be nxn but received 2x3
-
-    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
-    >>> constant = np.array([[2], [-6]])
-    >>> init_val = [0.5, -0.5, -0.5]
-    >>> iterations = 3
-    >>> jacobi_iteration_method(
-    ...     coefficient, constant, init_val, iterations
-    ... )  # doctest: +NORMALIZE_WHITESPACE
-    Traceback (most recent call last):
-        ...
-    ValueError: Coefficient and constant matrices dimensions must be nxn and nx1 but
-                received 3x3 and 2x1
-
-    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
-    >>> constant = np.array([[2], [-6], [-4]])
-    >>> init_val = [0.5, -0.5]
-    >>> iterations = 3
-    >>> jacobi_iteration_method(
-    ...     coefficient, constant, init_val, iterations
-    ... )  # doctest: +NORMALIZE_WHITESPACE
-    Traceback (most recent call last):
-        ...
-    ValueError: Number of initial values must be equal to number of rows in coefficient
-                matrix but received 2 and 3
-
-    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
-    >>> constant = np.array([[2], [-6], [-4]])
-    >>> init_val = [0.5, -0.5, -0.5]
-    >>> iterations = 0
-    >>> jacobi_iteration_method(coefficient, constant, init_val, iterations)
-    Traceback (most recent call last):
-        ...
-    ValueError: Iterations must be at least 1
-    """
-
-    rows1, cols1 = coefficient_matrix.shape
-    rows2, cols2 = constant_matrix.shape
-
-    if rows1 != cols1:
-        msg = f"Coefficient matrix dimensions must be nxn but received {rows1}x{cols1}"
-        raise ValueError(msg)
-
-    if cols2 != 1:
-        msg = f"Constant matrix must be nx1 but received {rows2}x{cols2}"
-        raise ValueError(msg)
-
-    if rows1 != rows2:
-        msg = (
-            "Coefficient and constant matrices dimensions must be nxn and nx1 but "
-            f"received {rows1}x{cols1} and {rows2}x{cols2}"
-        )
-        raise ValueError(msg)
-
-    if len(init_val) != rows1:
-        msg = (
-            "Number of initial values must be equal to number of rows in coefficient "
-            f"matrix but received {len(init_val)} and {rows1}"
-        )
-        raise ValueError(msg)
-
-    if iterations <= 0:
-        raise ValueError("Iterations must be at least 1")
-
-    table: NDArray[float64] = np.concatenate(
-        (coefficient_matrix, constant_matrix), axis=1
-    )
-
-    rows, cols = table.shape
-
-    strictly_diagonally_dominant(table)
-
-    """
-    # Iterates the whole matrix for given number of times
-    for _ in range(iterations):
-        new_val = []
-        for row in range(rows):
-            temp = 0
-            for col in range(cols):
-                if col == row:
-                    denom = table[row][col]
-                elif col == cols - 1:
-                    val = table[row][col]
-                else:
-                    temp += (-1) * table[row][col] * init_val[col]
-            temp = (temp + val) / denom
-            new_val.append(temp)
-        init_val = new_val
-    """
-
-    # denominator - a list of values along the diagonal
-    denominator = np.diag(coefficient_matrix)
-
-    # val_last - values of the last column of the table array
-    val_last = table[:, -1]
-
-    # masks - boolean mask of all strings without diagonal
-    # elements array coefficient_matrix
-    masks = ~np.eye(coefficient_matrix.shape[0], dtype=bool)
-
-    # no_diagonals - coefficient_matrix array values without diagonal elements
-    no_diagonals = coefficient_matrix[masks].reshape(-1, rows - 1)
-
-    # Here we get 'i_col' - these are the column numbers, for each row
-    # without diagonal elements, except for the last column.
-    i_row, i_col = np.where(masks)
-    ind = i_col.reshape(-1, rows - 1)
-
-    #'i_col' is converted to a two-dimensional list 'ind', which will be
-    # used to make selections from 'init_val' ('arr' array see below).
-
-    # Iterates the whole matrix for given number of times
-    for _ in range(iterations):
-        arr = np.take(init_val, ind)
-        sum_product_rows = np.sum((-1) * no_diagonals * arr, axis=1)
-        new_val = (sum_product_rows + val_last) / denominator
-        init_val = new_val
-
-    return new_val.tolist()
-
-
-# Checks if the given matrix is strictly diagonally dominant
-def strictly_diagonally_dominant(table: NDArray[float64]) -> bool:
-    """
-    >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 4, -4]])
-    >>> strictly_diagonally_dominant(table)
-    True
-
-    >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 3, -4]])
-    >>> strictly_diagonally_dominant(table)
-    Traceback (most recent call last):
-        ...
-    ValueError: Coefficient matrix is not strictly diagonally dominant
-    """
-
-    rows, cols = table.shape
-
-    is_diagonally_dominant = True
-
-    for i in range(rows):
-        total = 0
-        for j in range(cols - 1):
-            if i == j:
-                continue
-            else:
-                total += table[i][j]
-
-        if table[i][i] <= total:
-            raise ValueError("Coefficient matrix is not strictly diagonally dominant")
-
-    return is_diagonally_dominant
-
-
-# Test Cases
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
+"""
+Jacobi Iteration Method - https://en.wikipedia.org/wiki/Jacobi_method
+"""
+
+from __future__ import annotations
+
+import numpy as np
+from numpy import float64
+from numpy.typing import NDArray
+
+
+# Method to find solution of system of linear equations
+def jacobi_iteration_method(
+    coefficient_matrix: NDArray[float64],
+    constant_matrix: NDArray[float64],
+    init_val: list[float],
+    iterations: int,
+) -> list[float]:
+    """
+    Jacobi Iteration Method:
+    An iterative algorithm to determine the solutions of strictly diagonally dominant
+    system of linear equations
+
+    4x1 +  x2 +  x3 =  2
+     x1 + 5x2 + 2x3 = -6
+     x1 + 2x2 + 4x3 = -4
+
+    x_init = [0.5, -0.5 , -0.5]
+
+    Examples:
+
+    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
+    >>> constant = np.array([[2], [-6], [-4]])
+    >>> init_val = [0.5, -0.5, -0.5]
+    >>> iterations = 3
+    >>> jacobi_iteration_method(coefficient, constant, init_val, iterations)
+    [0.909375, -1.14375, -0.7484375]
+
+
+    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2]])
+    >>> constant = np.array([[2], [-6], [-4]])
+    >>> init_val = [0.5, -0.5, -0.5]
+    >>> iterations = 3
+    >>> jacobi_iteration_method(coefficient, constant, init_val, iterations)
+    Traceback (most recent call last):
+        ...
+    ValueError: Coefficient matrix dimensions must be nxn but received 2x3
+
+    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
+    >>> constant = np.array([[2], [-6]])
+    >>> init_val = [0.5, -0.5, -0.5]
+    >>> iterations = 3
+    >>> jacobi_iteration_method(
+    ...     coefficient, constant, init_val, iterations
+    ... )  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+        ...
+    ValueError: Coefficient and constant matrices dimensions must be nxn and nx1 but
+                received 3x3 and 2x1
+
+    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
+    >>> constant = np.array([[2], [-6], [-4]])
+    >>> init_val = [0.5, -0.5]
+    >>> iterations = 3
+    >>> jacobi_iteration_method(
+    ...     coefficient, constant, init_val, iterations
+    ... )  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+        ...
+    ValueError: Number of initial values must be equal to number of rows in coefficient
+                matrix but received 2 and 3
+
+    >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]])
+    >>> constant = np.array([[2], [-6], [-4]])
+    >>> init_val = [0.5, -0.5, -0.5]
+    >>> iterations = 0
+    >>> jacobi_iteration_method(coefficient, constant, init_val, iterations)
+    Traceback (most recent call last):
+        ...
+    ValueError: Iterations must be at least 1
+    """
+
+    rows1, cols1 = coefficient_matrix.shape
+    rows2, cols2 = constant_matrix.shape
+
+    if rows1 != cols1:
+        msg = f"Coefficient matrix dimensions must be nxn but received {rows1}x{cols1}"
+        raise ValueError(msg)
+
+    if cols2 != 1:
+        msg = f"Constant matrix must be nx1 but received {rows2}x{cols2}"
+        raise ValueError(msg)
+
+    if rows1 != rows2:
+        msg = (
+            "Coefficient and constant matrices dimensions must be nxn and nx1 but "
+            f"received {rows1}x{cols1} and {rows2}x{cols2}"
+        )
+        raise ValueError(msg)
+
+    if len(init_val) != rows1:
+        msg = (
+            "Number of initial values must be equal to number of rows in coefficient "
+            f"matrix but received {len(init_val)} and {rows1}"
+        )
+        raise ValueError(msg)
+
+    if iterations <= 0:
+        raise ValueError("Iterations must be at least 1")
+
+    table: NDArray[float64] = np.concatenate(
+        (coefficient_matrix, constant_matrix), axis=1
+    )
+
+    rows, cols = table.shape
+
+    strictly_diagonally_dominant(table)
+
+    """
+    # Iterates the whole matrix for given number of times
+    for _ in range(iterations):
+        new_val = []
+        for row in range(rows):
+            temp = 0
+            for col in range(cols):
+                if col == row:
+                    denom = table[row][col]
+                elif col == cols - 1:
+                    val = table[row][col]
+                else:
+                    temp += (-1) * table[row][col] * init_val[col]
+            temp = (temp + val) / denom
+            new_val.append(temp)
+        init_val = new_val
+    """
+
+    # denominator - a list of values along the diagonal
+    denominator = np.diag(coefficient_matrix)
+
+    # val_last - values of the last column of the table array
+    val_last = table[:, -1]
+
+    # masks - boolean mask of all strings without diagonal
+    # elements array coefficient_matrix
+    masks = ~np.eye(coefficient_matrix.shape[0], dtype=bool)
+
+    # no_diagonals - coefficient_matrix array values without diagonal elements
+    no_diagonals = coefficient_matrix[masks].reshape(-1, rows - 1)
+
+    # Here we get 'i_col' - these are the column numbers, for each row
+    # without diagonal elements, except for the last column.
+    i_row, i_col = np.where(masks)
+    ind = i_col.reshape(-1, rows - 1)
+
+    #'i_col' is converted to a two-dimensional list 'ind', which will be
+    # used to make selections from 'init_val' ('arr' array see below).
+
+    # Iterates the whole matrix for given number of times
+    for _ in range(iterations):
+        arr = np.take(init_val, ind)
+        sum_product_rows = np.sum((-1) * no_diagonals * arr, axis=1)
+        new_val = (sum_product_rows + val_last) / denominator
+        init_val = new_val
+
+    return new_val.tolist()
+
+
+# Checks if the given matrix is strictly diagonally dominant
+def strictly_diagonally_dominant(table: NDArray[float64]) -> bool:
+    """
+    >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 4, -4]])
+    >>> strictly_diagonally_dominant(table)
+    True
+
+    >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 3, -4]])
+    >>> strictly_diagonally_dominant(table)
+    Traceback (most recent call last):
+        ...
+    ValueError: Coefficient matrix is not strictly diagonally dominant
+    """
+
+    rows, cols = table.shape
+
+    is_diagonally_dominant = True
+
+    for i in range(rows):
+        total = 0
+        for j in range(cols - 1):
+            if i == j:
+                continue
+            else:
+                total += table[i][j]
+
+        if table[i][i] <= total:
+            raise ValueError("Coefficient matrix is not strictly diagonally dominant")
+
+    return is_diagonally_dominant
+
+
+# Test Cases
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/arithmetic_analysis/lu_decomposition.py b/linear_algebra/lu_decomposition.py
similarity index 83%
rename from arithmetic_analysis/lu_decomposition.py
rename to linear_algebra/lu_decomposition.py
index 094b20abfecc..3d89b53a48fb 100644
--- a/arithmetic_analysis/lu_decomposition.py
+++ b/linear_algebra/lu_decomposition.py
@@ -1,20 +1,22 @@
 """
-Lower–upper (LU) decomposition factors a matrix as a product of a lower
+Lower-upper (LU) decomposition factors a matrix as a product of a lower
 triangular matrix and an upper triangular matrix. A square matrix has an LU
 decomposition under the following conditions:
+
     - If the matrix is invertible, then it has an LU decomposition if and only
-    if all of its leading principal minors are non-zero (see
-    https://en.wikipedia.org/wiki/Minor_(linear_algebra) for an explanation of
-    leading principal minors of a matrix).
+      if all of its leading principal minors are non-zero (see
+      https://en.wikipedia.org/wiki/Minor_(linear_algebra) for an explanation of
+      leading principal minors of a matrix).
     - If the matrix is singular (i.e., not invertible) and it has a rank of k
-    (i.e., it has k linearly independent columns), then it has an LU
-    decomposition if its first k leading principal minors are non-zero.
+      (i.e., it has k linearly independent columns), then it has an LU
+      decomposition if its first k leading principal minors are non-zero.
 
 This algorithm will simply attempt to perform LU decomposition on any square
 matrix and raise an error if no such decomposition exists.
 
 Reference: https://en.wikipedia.org/wiki/LU_decomposition
 """
+
 from __future__ import annotations
 
 import numpy as np
@@ -24,6 +26,7 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray
     """
     Perform LU decomposition on a given matrix and raises an error if the matrix
     isn't square or if no such decomposition exists
+
     >>> matrix = np.array([[2, -2, 1], [0, 1, 2], [5, 3, 1]])
     >>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
     >>> lower_mat
@@ -44,7 +47,7 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray
     array([[ 4. ,  3. ],
            [ 0. , -1.5]])
 
-    # Matrix is not square
+    >>> # Matrix is not square
     >>> matrix = np.array([[2, -2, 1], [0, 1, 2]])
     >>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
     Traceback (most recent call last):
@@ -53,14 +56,14 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray
     [[ 2 -2  1]
      [ 0  1  2]]
 
-    # Matrix is invertible, but its first leading principal minor is 0
+    >>> # Matrix is invertible, but its first leading principal minor is 0
     >>> matrix = np.array([[0, 1], [1, 0]])
     >>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
     Traceback (most recent call last):
     ...
     ArithmeticError: No LU decomposition exists
 
-    # Matrix is singular, but its first leading principal minor is 1
+    >>> # Matrix is singular, but its first leading principal minor is 1
     >>> matrix = np.array([[1, 0], [1, 0]])
     >>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
     >>> lower_mat
@@ -70,7 +73,7 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray
     array([[1., 0.],
            [0., 0.]])
 
-    # Matrix is singular, but its first leading principal minor is 0
+    >>> # Matrix is singular, but its first leading principal minor is 0
     >>> matrix = np.array([[0, 1], [0, 1]])
     >>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
     Traceback (most recent call last):
diff --git a/linear_algebra/matrix_inversion.py b/linear_algebra/matrix_inversion.py
new file mode 100644
index 000000000000..50dae1c2e825
--- /dev/null
+++ b/linear_algebra/matrix_inversion.py
@@ -0,0 +1,36 @@
+import numpy as np
+
+
+def invert_matrix(matrix: list[list[float]]) -> list[list[float]]:
+    """
+    Returns the inverse of a square matrix using NumPy.
+
+    Parameters:
+    matrix (list[list[float]]): A square matrix.
+
+    Returns:
+    list[list[float]]: Inverted matrix if invertible, else raises error.
+
+    >>> invert_matrix([[4.0, 7.0], [2.0, 6.0]])
+    [[0.6000000000000001, -0.7000000000000001], [-0.2, 0.4]]
+    >>> invert_matrix([[1.0, 2.0], [0.0, 0.0]])
+    Traceback (most recent call last):
+        ...
+    ValueError: Matrix is not invertible
+    """
+    np_matrix = np.array(matrix)
+
+    try:
+        inv_matrix = np.linalg.inv(np_matrix)
+    except np.linalg.LinAlgError:
+        raise ValueError("Matrix is not invertible")
+
+    return inv_matrix.tolist()
+
+
+if __name__ == "__main__":
+    mat = [[4.0, 7.0], [2.0, 6.0]]
+    print("Original Matrix:")
+    print(mat)
+    print("Inverted Matrix:")
+    print(invert_matrix(mat))
diff --git a/linear_algebra/src/conjugate_gradient.py b/linear_algebra/src/conjugate_gradient.py
index 4cf566ec9e36..45da35813978 100644
--- a/linear_algebra/src/conjugate_gradient.py
+++ b/linear_algebra/src/conjugate_gradient.py
@@ -3,6 +3,7 @@
 - https://en.wikipedia.org/wiki/Conjugate_gradient_method
 - https://en.wikipedia.org/wiki/Definite_symmetric_matrix
 """
+
 from typing import Any
 
 import numpy as np
@@ -60,7 +61,8 @@ def _create_spd_matrix(dimension: int) -> Any:
     >>> _is_matrix_spd(spd_matrix)
     True
     """
-    random_matrix = np.random.randn(dimension, dimension)
+    rng = np.random.default_rng()
+    random_matrix = rng.normal(size=(dimension, dimension))
     spd_matrix = np.dot(random_matrix, random_matrix.T)
     assert _is_matrix_spd(spd_matrix)
     return spd_matrix
@@ -156,7 +158,8 @@ def test_conjugate_gradient() -> None:
     # Create linear system with SPD matrix and known solution x_true.
     dimension = 3
     spd_matrix = _create_spd_matrix(dimension)
-    x_true = np.random.randn(dimension, 1)
+    rng = np.random.default_rng()
+    x_true = rng.normal(size=(dimension, 1))
     b = np.dot(spd_matrix, x_true)
 
     # Numpy solution.
diff --git a/linear_algebra/src/gaussian_elimination_pivoting.py b/linear_algebra/src/gaussian_elimination_pivoting.py
new file mode 100644
index 000000000000..540f57b0cff6
--- /dev/null
+++ b/linear_algebra/src/gaussian_elimination_pivoting.py
@@ -0,0 +1,88 @@
+import numpy as np
+
+
+def solve_linear_system(matrix: np.ndarray) -> np.ndarray:
+    """
+    Solve a linear system of equations using Gaussian elimination with partial pivoting
+
+    Args:
+      - `matrix`: Coefficient matrix with the last column representing the constants.
+
+    Returns:
+      - Solution vector.
+
+    Raises:
+      - ``ValueError``: If the matrix is not correct (i.e., singular).
+
+    https://courses.engr.illinois.edu/cs357/su2013/lect.htm Lecture 7
+
+    Example:
+
+    >>> A = np.array([[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], dtype=float)
+    >>> B = np.array([8, -11, -3], dtype=float)
+    >>> solution = solve_linear_system(np.column_stack((A, B)))
+    >>> np.allclose(solution, np.array([2., 3., -1.]))
+    True
+    >>> solve_linear_system(np.array([[0, 0, 0]], dtype=float))
+    Traceback (most recent call last):
+        ...
+    ValueError: Matrix is not square
+    >>> solve_linear_system(np.array([[0, 0, 0], [0, 0, 0]], dtype=float))
+    Traceback (most recent call last):
+        ...
+    ValueError: Matrix is singular
+    """
+    ab = np.copy(matrix)
+    num_of_rows = ab.shape[0]
+    num_of_columns = ab.shape[1] - 1
+    x_lst: list[float] = []
+
+    if num_of_rows != num_of_columns:
+        raise ValueError("Matrix is not square")
+
+    for column_num in range(num_of_rows):
+        # Lead element search
+        for i in range(column_num, num_of_columns):
+            if abs(ab[i][column_num]) > abs(ab[column_num][column_num]):
+                ab[[column_num, i]] = ab[[i, column_num]]
+
+        # Upper triangular matrix
+        if abs(ab[column_num, column_num]) < 1e-8:
+            raise ValueError("Matrix is singular")
+
+        if column_num != 0:
+            for i in range(column_num, num_of_rows):
+                ab[i, :] -= (
+                    ab[i, column_num - 1]
+                    / ab[column_num - 1, column_num - 1]
+                    * ab[column_num - 1, :]
+                )
+
+    # Find x vector (Back Substitution)
+    for column_num in range(num_of_rows - 1, -1, -1):
+        x = ab[column_num, -1] / ab[column_num, column_num]
+        x_lst.insert(0, x)
+        for i in range(column_num - 1, -1, -1):
+            ab[i, -1] -= ab[i, column_num] * x
+
+    # Return the solution vector
+    return np.asarray(x_lst)
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+
+    example_matrix = np.array(
+        [
+            [5.0, -5.0, -3.0, 4.0, -11.0],
+            [1.0, -4.0, 6.0, -4.0, -10.0],
+            [-2.0, -5.0, 4.0, -5.0, -12.0],
+            [-3.0, -3.0, 5.0, -5.0, 8.0],
+        ],
+        dtype=float,
+    )
+
+    print(f"Matrix:\n{example_matrix}")
+    print(f"{solve_linear_system(example_matrix) = }")
diff --git a/linear_algebra/src/lib.py b/linear_algebra/src/lib.py
index 5074faf31d1d..0d6a348475cd 100644
--- a/linear_algebra/src/lib.py
+++ b/linear_algebra/src/lib.py
@@ -18,6 +18,7 @@
 - function square_zero_matrix(N)
 - function random_matrix(W, H, a, b)
 """
+
 from __future__ import annotations
 
 import math
@@ -45,7 +46,6 @@ class Vector:
         change_component(pos: int, value: float): changes specified component
         euclidean_length(): returns the euclidean length of the vector
         angle(other: Vector, deg: bool): returns the angle between two vectors
-        TODO: compare-operator
     """
 
     def __init__(self, components: Collection[float] | None = None) -> None:
@@ -95,13 +95,21 @@ def __sub__(self, other: Vector) -> Vector:
         else:  # error case
             raise Exception("must have the same size")
 
+    def __eq__(self, other: object) -> bool:
+        """
+        performs the comparison between two vectors
+        """
+        if not isinstance(other, Vector):
+            return NotImplemented
+        if len(self) != len(other):
+            return False
+        return all(self.component(i) == other.component(i) for i in range(len(self)))
+
     @overload
-    def __mul__(self, other: float) -> Vector:
-        ...
+    def __mul__(self, other: float) -> Vector: ...
 
     @overload
-    def __mul__(self, other: Vector) -> float:
-        ...
+    def __mul__(self, other: Vector) -> float: ...
 
     def __mul__(self, other: float | Vector) -> float | Vector:
         """
@@ -309,12 +317,10 @@ def __sub__(self, other: Matrix) -> Matrix:
             raise Exception("matrices must have the same dimension!")
 
     @overload
-    def __mul__(self, other: float) -> Matrix:
-        ...
+    def __mul__(self, other: float) -> Matrix: ...
 
     @overload
-    def __mul__(self, other: Vector) -> Vector:
-        ...
+    def __mul__(self, other: Vector) -> Vector: ...
 
     def __mul__(self, other: float | Vector) -> Vector | Matrix:
         """
diff --git a/linear_algebra/src/polynom_for_points.py b/linear_algebra/src/polynom_for_points.py
index a9a9a8117c18..452f3edd4aee 100644
--- a/linear_algebra/src/polynom_for_points.py
+++ b/linear_algebra/src/polynom_for_points.py
@@ -3,30 +3,36 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str:
     coordinates is a two dimensional matrix: [[x, y], [x, y], ...]
     number of points you want to use
 
-    >>> print(points_to_polynomial([]))
+    >>> points_to_polynomial([])
     Traceback (most recent call last):
         ...
     ValueError: The program cannot work out a fitting polynomial.
-    >>> print(points_to_polynomial([[]]))
+    >>> points_to_polynomial([[]])
+    Traceback (most recent call last):
+        ...
+    ValueError: The program cannot work out a fitting polynomial.
+    >>> points_to_polynomial([[1, 0], [2, 0], [3, 0]])
+    'f(x)=x^2*0.0+x^1*-0.0+x^0*0.0'
+    >>> points_to_polynomial([[1, 1], [2, 1], [3, 1]])
+    'f(x)=x^2*0.0+x^1*-0.0+x^0*1.0'
+    >>> points_to_polynomial([[1, 3], [2, 3], [3, 3]])
+    'f(x)=x^2*0.0+x^1*-0.0+x^0*3.0'
+    >>> points_to_polynomial([[1, 1], [2, 2], [3, 3]])
+    'f(x)=x^2*0.0+x^1*1.0+x^0*0.0'
+    >>> points_to_polynomial([[1, 1], [2, 4], [3, 9]])
+    'f(x)=x^2*1.0+x^1*-0.0+x^0*0.0'
+    >>> points_to_polynomial([[1, 3], [2, 6], [3, 11]])
+    'f(x)=x^2*1.0+x^1*-0.0+x^0*2.0'
+    >>> points_to_polynomial([[1, -3], [2, -6], [3, -11]])
+    'f(x)=x^2*-1.0+x^1*-0.0+x^0*-2.0'
+    >>> points_to_polynomial([[1, 5], [2, 2], [3, 9]])
+    'f(x)=x^2*5.0+x^1*-18.0+x^0*18.0'
+    >>> points_to_polynomial([[1, 1], [1, 2], [1, 3]])
+    'x=1'
+    >>> points_to_polynomial([[1, 1], [2, 2], [2, 2]])
     Traceback (most recent call last):
         ...
     ValueError: The program cannot work out a fitting polynomial.
-    >>> print(points_to_polynomial([[1, 0], [2, 0], [3, 0]]))
-    f(x)=x^2*0.0+x^1*-0.0+x^0*0.0
-    >>> print(points_to_polynomial([[1, 1], [2, 1], [3, 1]]))
-    f(x)=x^2*0.0+x^1*-0.0+x^0*1.0
-    >>> print(points_to_polynomial([[1, 3], [2, 3], [3, 3]]))
-    f(x)=x^2*0.0+x^1*-0.0+x^0*3.0
-    >>> print(points_to_polynomial([[1, 1], [2, 2], [3, 3]]))
-    f(x)=x^2*0.0+x^1*1.0+x^0*0.0
-    >>> print(points_to_polynomial([[1, 1], [2, 4], [3, 9]]))
-    f(x)=x^2*1.0+x^1*-0.0+x^0*0.0
-    >>> print(points_to_polynomial([[1, 3], [2, 6], [3, 11]]))
-    f(x)=x^2*1.0+x^1*-0.0+x^0*2.0
-    >>> print(points_to_polynomial([[1, -3], [2, -6], [3, -11]]))
-    f(x)=x^2*-1.0+x^1*-0.0+x^0*-2.0
-    >>> print(points_to_polynomial([[1, 5], [2, 2], [3, 9]]))
-    f(x)=x^2*5.0+x^1*-18.0+x^0*18.0
     """
     if len(coordinates) == 0 or not all(len(pair) == 2 for pair in coordinates):
         raise ValueError("The program cannot work out a fitting polynomial.")
diff --git a/linear_algebra/src/power_iteration.py b/linear_algebra/src/power_iteration.py
index 24fbd9a5e002..83c2ce48c3a0 100644
--- a/linear_algebra/src/power_iteration.py
+++ b/linear_algebra/src/power_iteration.py
@@ -78,7 +78,7 @@ def power_iteration(
     if is_complex:
         lambda_ = np.real(lambda_)
 
-    return lambda_, vector
+    return float(lambda_), vector
 
 
 def test_power_iteration() -> None:
diff --git a/linear_algebra/src/rank_of_matrix.py b/linear_algebra/src/rank_of_matrix.py
index 7ff3c1699a69..2c4fe2a8d1da 100644
--- a/linear_algebra/src/rank_of_matrix.py
+++ b/linear_algebra/src/rank_of_matrix.py
@@ -8,11 +8,15 @@
 def rank_of_matrix(matrix: list[list[int | float]]) -> int:
     """
     Finds the rank of a matrix.
+
     Args:
-        matrix: The matrix as a list of lists.
+        `matrix`: The matrix as a list of lists.
+
     Returns:
         The rank of the matrix.
+
     Example:
+
     >>> matrix1 = [[1, 2, 3],
     ...            [4, 5, 6],
     ...            [7, 8, 9]]
diff --git a/linear_algebra/src/rayleigh_quotient.py b/linear_algebra/src/rayleigh_quotient.py
index 4773429cbf1b..46bf1671d2b1 100644
--- a/linear_algebra/src/rayleigh_quotient.py
+++ b/linear_algebra/src/rayleigh_quotient.py
@@ -1,6 +1,7 @@
 """
 https://en.wikipedia.org/wiki/Rayleigh_quotient
 """
+
 from typing import Any
 
 import numpy as np
diff --git a/linear_algebra/src/schur_complement.py b/linear_algebra/src/schur_complement.py
index 1cc084043856..74ac75e3fce2 100644
--- a/linear_algebra/src/schur_complement.py
+++ b/linear_algebra/src/schur_complement.py
@@ -12,13 +12,14 @@ def schur_complement(
 ) -> np.ndarray:
     """
     Schur complement of a symmetric matrix X given as a 2x2 block matrix
-    consisting of matrices A, B and C.
-    Matrix A must be quadratic and non-singular.
-    In case A is singular, a pseudo-inverse may be provided using
-    the pseudo_inv argument.
+    consisting of matrices `A`, `B` and `C`.
+    Matrix `A` must be quadratic and non-singular.
+    In case `A` is singular, a pseudo-inverse may be provided using
+    the `pseudo_inv` argument.
+
+    | Link to Wiki: https://en.wikipedia.org/wiki/Schur_complement
+    | See also Convex Optimization - Boyd and Vandenberghe, A.5.5
 
-    Link to Wiki: https://en.wikipedia.org/wiki/Schur_complement
-    See also Convex Optimization – Boyd and Vandenberghe, A.5.5
     >>> import numpy as np
     >>> a = np.array([[1, 2], [2, 1]])
     >>> b = np.array([[0, 3], [3, 0]])
diff --git a/linear_algebra/src/test_linear_algebra.py b/linear_algebra/src/test_linear_algebra.py
index 95ab408b3d86..5209c152013e 100644
--- a/linear_algebra/src/test_linear_algebra.py
+++ b/linear_algebra/src/test_linear_algebra.py
@@ -6,6 +6,7 @@
 
 This file contains the test-suite for the linear algebra library.
 """
+
 import unittest
 
 import pytest
@@ -180,7 +181,7 @@ def test_component_matrix(self) -> None:
         test for Matrix method component()
         """
         a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3)
-        assert a.component(2, 1) == 7, 0.01
+        assert a.component(2, 1) == 7, "0.01"
 
     def test__add__matrix(self) -> None:
         """
diff --git a/linear_algebra/src/transformations_2d.py b/linear_algebra/src/transformations_2d.py
index cdf42100d5d9..5dee59024752 100644
--- a/linear_algebra/src/transformations_2d.py
+++ b/linear_algebra/src/transformations_2d.py
@@ -3,14 +3,17 @@
 
 I have added the codes for reflection, projection, scaling and rotation 2D matrices.
 
+.. code-block:: python
+
     scaling(5) = [[5.0, 0.0], [0.0, 5.0]]
-  rotation(45) = [[0.5253219888177297, -0.8509035245341184],
-                  [0.8509035245341184, 0.5253219888177297]]
-projection(45) = [[0.27596319193541496, 0.446998331800279],
-                  [0.446998331800279, 0.7240368080645851]]
-reflection(45) = [[0.05064397763545947, 0.893996663600558],
-                  [0.893996663600558, 0.7018070490682369]]
+    rotation(45) = [[0.5253219888177297, -0.8509035245341184],
+                    [0.8509035245341184, 0.5253219888177297]]
+    projection(45) = [[0.27596319193541496, 0.446998331800279],
+                      [0.446998331800279, 0.7240368080645851]]
+    reflection(45) = [[0.05064397763545947, 0.893996663600558],
+                      [0.893996663600558, 0.7018070490682369]]
 """
+
 from math import cos, sin
 
 
diff --git a/linear_programming/__init__.py b/linear_programming/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py
index bbc97d8e22bf..a8affe1b72d2 100644
--- a/linear_programming/simplex.py
+++ b/linear_programming/simplex.py
@@ -12,6 +12,7 @@
 https://en.wikipedia.org/wiki/Simplex_algorithm
 https://tinyurl.com/simplex4beginners
 """
+
 from typing import Any
 
 import numpy as np
@@ -106,8 +107,8 @@ def generate_col_titles(self) -> list[str]:
 
     def find_pivot(self) -> tuple[Any, Any]:
         """Finds the pivot row and column.
-        >>> Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], [1,2,0,1,7.]]),
-        ... 2, 0).find_pivot()
+        >>> tuple(int(x) for x in Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6],
+        ... [1,2,0,1,7.]]), 2, 0).find_pivot())
         (1, 0)
         """
         objective = self.objectives[-1]
@@ -214,8 +215,8 @@ def run_simplex(self) -> dict[Any, Any]:
         Max:  x1 +  x2
         ST:   x1 + 3x2 <= 4
              3x1 +  x2 <= 4
-        >>> Tableau(np.array([[-1,-1,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]),
-        ... 2, 0).run_simplex()
+        >>> {key: float(value) for key, value in Tableau(np.array([[-1,-1,0,0,0],
+        ... [1,3,1,0,4],[3,1,0,1,4.]]), 2, 0).run_simplex().items()}
         {'P': 2.0, 'x1': 1.0, 'x2': 1.0}
 
         # Standard linear program with 3 variables:
@@ -223,21 +224,21 @@ def run_simplex(self) -> dict[Any, Any]:
         ST:  2x1 +  x2 +  x3 ≤ 2
               x1 + 2x2 + 3x3 ≤ 5
              2x1 + 2x2 +  x3 ≤ 6
-        >>> Tableau(np.array([
+        >>> {key: float(value) for key, value in Tableau(np.array([
         ... [-3,-1,-3,0,0,0,0],
         ... [2,1,1,1,0,0,2],
         ... [1,2,3,0,1,0,5],
         ... [2,2,1,0,0,1,6.]
-        ... ]),3,0).run_simplex() # doctest: +ELLIPSIS
+        ... ]),3,0).run_simplex().items()} # doctest: +ELLIPSIS
         {'P': 5.4, 'x1': 0.199..., 'x3': 1.6}
 
 
         # Optimal tableau input:
-        >>> Tableau(np.array([
+        >>> {key: float(value) for key, value in Tableau(np.array([
         ... [0, 0, 0.25, 0.25, 2],
         ... [0, 1, 0.375, -0.125, 1],
         ... [1, 0, -0.125, 0.375, 1]
-        ... ]), 2, 0).run_simplex()
+        ... ]), 2, 0).run_simplex().items()}
         {'P': 2.0, 'x1': 1.0, 'x2': 1.0}
 
         # Non-standard: >= constraints
@@ -245,25 +246,25 @@ def run_simplex(self) -> dict[Any, Any]:
         ST:   x1 +  x2 +  x3 <= 40
              2x1 +  x2 -  x3 >= 10
                  -  x2 +  x3 >= 10
-        >>> Tableau(np.array([
+        >>> {key: float(value) for key, value in Tableau(np.array([
         ... [2, 0, 0, 0, -1, -1, 0, 0, 20],
         ... [-2, -3, -1, 0, 0, 0, 0, 0, 0],
         ... [1, 1, 1, 1, 0, 0, 0, 0, 40],
         ... [2, 1, -1, 0, -1, 0, 1, 0, 10],
         ... [0, -1, 1, 0, 0, -1, 0, 1, 10.]
-        ... ]), 3, 2).run_simplex()
+        ... ]), 3, 2).run_simplex().items()}
         {'P': 70.0, 'x1': 10.0, 'x2': 10.0, 'x3': 20.0}
 
         # Non standard: minimisation and equalities
         Min: x1 +  x2
         ST: 2x1 +  x2 = 12
             6x1 + 5x2 = 40
-        >>> Tableau(np.array([
+        >>> {key: float(value) for key, value in Tableau(np.array([
         ... [8, 6, 0, 0, 52],
         ... [1, 1, 0, 0, 0],
         ... [2, 1, 1, 0, 12],
         ... [6, 5, 0, 1, 40.],
-        ... ]), 2, 2).run_simplex()
+        ... ]), 2, 2).run_simplex().items()}
         {'P': 7.0, 'x1': 5.0, 'x2': 2.0}
 
 
@@ -274,7 +275,7 @@ def run_simplex(self) -> dict[Any, Any]:
              2x1 + 4x2 <= 48
               x1 +  x2 >= 10
              x1        >= 2
-        >>> Tableau(np.array([
+        >>> {key: float(value) for key, value in Tableau(np.array([
         ... [2, 1, 0, 0, 0, -1, -1, 0, 0, 12.0],
         ... [-8, -6, 0, 0, 0, 0, 0, 0, 0, 0.0],
         ... [1, 3, 1, 0, 0, 0, 0, 0, 0, 33.0],
@@ -282,7 +283,7 @@ def run_simplex(self) -> dict[Any, Any]:
         ... [2, 4, 0, 0, 1, 0, 0, 0, 0, 48.0],
         ... [1, 1, 0, 0, 0, -1, 0, 1, 0, 10.0],
         ... [1, 0, 0, 0, 0, 0, -1, 0, 1, 2.0]
-        ... ]), 2, 2).run_simplex() # doctest: +ELLIPSIS
+        ... ]), 2, 2).run_simplex().items()} # doctest: +ELLIPSIS
         {'P': 132.0, 'x1': 12.000... 'x2': 5.999...}
         """
         # Stop simplex algorithm from cycling.
@@ -306,11 +307,11 @@ def run_simplex(self) -> dict[Any, Any]:
     def interpret_tableau(self) -> dict[str, float]:
         """Given the final tableau, add the corresponding values of the basic
         decision variables to the `output_dict`
-        >>> Tableau(np.array([
+        >>> {key: float(value) for key, value in Tableau(np.array([
         ... [0,0,0.875,0.375,5],
         ... [0,1,0.375,-0.125,1],
         ... [1,0,-0.125,0.375,1]
-        ... ]),2, 0).interpret_tableau()
+        ... ]),2, 0).interpret_tableau().items()}
         {'P': 5.0, 'x1': 1.0, 'x2': 1.0}
         """
         # P = RHS of final tableau
diff --git a/machine_learning/apriori_algorithm.py b/machine_learning/apriori_algorithm.py
index d9fd1f82ea3c..09a89ac236bd 100644
--- a/machine_learning/apriori_algorithm.py
+++ b/machine_learning/apriori_algorithm.py
@@ -10,6 +10,7 @@
 WIKI: https://en.wikipedia.org/wiki/Apriori_algorithm
 Examples: https://www.kaggle.com/code/earthian/apriori-association-rules-mining
 """
+
 from itertools import combinations
 
 
diff --git a/machine_learning/astar.py b/machine_learning/astar.py
index 7a60ed225a2d..a5859e51fe70 100644
--- a/machine_learning/astar.py
+++ b/machine_learning/astar.py
@@ -12,6 +12,7 @@
 
 https://en.wikipedia.org/wiki/A*_search_algorithm
 """
+
 import numpy as np
 
 
@@ -57,7 +58,7 @@ def __init__(self, world_size=(5, 5)):
     def show(self):
         print(self.w)
 
-    def get_neigbours(self, cell):
+    def get_neighbours(self, cell):
         """
         Return the neighbours of cell
         """
@@ -110,7 +111,7 @@ def astar(world, start, goal):
         _closed.append(_open.pop(min_f))
         if current == goal:
             break
-        for n in world.get_neigbours(current):
+        for n in world.get_neighbours(current):
             for c in _closed:
                 if c == n:
                     continue
diff --git a/machine_learning/automatic_differentiation.py b/machine_learning/automatic_differentiation.py
new file mode 100644
index 000000000000..5c2708247c21
--- /dev/null
+++ b/machine_learning/automatic_differentiation.py
@@ -0,0 +1,328 @@
+"""
+Demonstration of the Automatic Differentiation (Reverse mode).
+
+Reference: https://en.wikipedia.org/wiki/Automatic_differentiation
+
+Author: Poojan Smart
+Email: smrtpoojan@gmail.com
+"""
+
+from __future__ import annotations
+
+from collections import defaultdict
+from enum import Enum
+from types import TracebackType
+from typing import Any
+
+import numpy as np
+from typing_extensions import Self  # noqa: UP035
+
+
+class OpType(Enum):
+    """
+    Class represents list of supported operations on Variable for gradient calculation.
+    """
+
+    ADD = 0
+    SUB = 1
+    MUL = 2
+    DIV = 3
+    MATMUL = 4
+    POWER = 5
+    NOOP = 6
+
+
+class Variable:
+    """
+    Class represents n-dimensional object which is used to wrap numpy array on which
+    operations will be performed and the gradient will be calculated.
+
+    Examples:
+    >>> Variable(5.0)
+    Variable(5.0)
+    >>> Variable([5.0, 2.9])
+    Variable([5.  2.9])
+    >>> Variable([5.0, 2.9]) + Variable([1.0, 5.5])
+    Variable([6.  8.4])
+    >>> Variable([[8.0, 10.0]])
+    Variable([[ 8. 10.]])
+    """
+
+    def __init__(self, value: Any) -> None:
+        self.value = np.array(value)
+
+        # pointers to the operations to which the Variable is input
+        self.param_to: list[Operation] = []
+        # pointer to the operation of which the Variable is output of
+        self.result_of: Operation = Operation(OpType.NOOP)
+
+    def __repr__(self) -> str:
+        return f"Variable({self.value})"
+
+    def to_ndarray(self) -> np.ndarray:
+        return self.value
+
+    def __add__(self, other: Variable) -> Variable:
+        result = Variable(self.value + other.value)
+
+        with GradientTracker() as tracker:
+            # if tracker is enabled, computation graph will be updated
+            if tracker.enabled:
+                tracker.append(OpType.ADD, params=[self, other], output=result)
+        return result
+
+    def __sub__(self, other: Variable) -> Variable:
+        result = Variable(self.value - other.value)
+
+        with GradientTracker() as tracker:
+            # if tracker is enabled, computation graph will be updated
+            if tracker.enabled:
+                tracker.append(OpType.SUB, params=[self, other], output=result)
+        return result
+
+    def __mul__(self, other: Variable) -> Variable:
+        result = Variable(self.value * other.value)
+
+        with GradientTracker() as tracker:
+            # if tracker is enabled, computation graph will be updated
+            if tracker.enabled:
+                tracker.append(OpType.MUL, params=[self, other], output=result)
+        return result
+
+    def __truediv__(self, other: Variable) -> Variable:
+        result = Variable(self.value / other.value)
+
+        with GradientTracker() as tracker:
+            # if tracker is enabled, computation graph will be updated
+            if tracker.enabled:
+                tracker.append(OpType.DIV, params=[self, other], output=result)
+        return result
+
+    def __matmul__(self, other: Variable) -> Variable:
+        result = Variable(self.value @ other.value)
+
+        with GradientTracker() as tracker:
+            # if tracker is enabled, computation graph will be updated
+            if tracker.enabled:
+                tracker.append(OpType.MATMUL, params=[self, other], output=result)
+        return result
+
+    def __pow__(self, power: int) -> Variable:
+        result = Variable(self.value**power)
+
+        with GradientTracker() as tracker:
+            # if tracker is enabled, computation graph will be updated
+            if tracker.enabled:
+                tracker.append(
+                    OpType.POWER,
+                    params=[self],
+                    output=result,
+                    other_params={"power": power},
+                )
+        return result
+
+    def add_param_to(self, param_to: Operation) -> None:
+        self.param_to.append(param_to)
+
+    def add_result_of(self, result_of: Operation) -> None:
+        self.result_of = result_of
+
+
+class Operation:
+    """
+    Class represents operation between single or two Variable objects.
+    Operation objects contains type of operation, pointers to input Variable
+    objects and pointer to resulting Variable from the operation.
+    """
+
+    def __init__(
+        self,
+        op_type: OpType,
+        other_params: dict | None = None,
+    ) -> None:
+        self.op_type = op_type
+        self.other_params = {} if other_params is None else other_params
+
+    def add_params(self, params: list[Variable]) -> None:
+        self.params = params
+
+    def add_output(self, output: Variable) -> None:
+        self.output = output
+
+    def __eq__(self, value) -> bool:
+        return self.op_type == value if isinstance(value, OpType) else False
+
+
+class GradientTracker:
+    """
+    Class contains methods to compute partial derivatives of Variable
+    based on the computation graph.
+
+    Examples:
+
+    >>> with GradientTracker() as tracker:
+    ...     a = Variable([2.0, 5.0])
+    ...     b = Variable([1.0, 2.0])
+    ...     m = Variable([1.0, 2.0])
+    ...     c = a + b
+    ...     d = a * b
+    ...     e = c / d
+    >>> tracker.gradient(e, a)
+    array([-0.25, -0.04])
+    >>> tracker.gradient(e, b)
+    array([-1.  , -0.25])
+    >>> tracker.gradient(e, m) is None
+    True
+
+    >>> with GradientTracker() as tracker:
+    ...     a = Variable([[2.0, 5.0]])
+    ...     b = Variable([[1.0], [2.0]])
+    ...     c = a @ b
+    >>> tracker.gradient(c, a)
+    array([[1., 2.]])
+    >>> tracker.gradient(c, b)
+    array([[2.],
+           [5.]])
+
+    >>> with GradientTracker() as tracker:
+    ...     a = Variable([[2.0, 5.0]])
+    ...     b = a ** 3
+    >>> tracker.gradient(b, a)
+    array([[12., 75.]])
+    """
+
+    instance = None
+
+    def __new__(cls) -> Self:
+        """
+        Executes at the creation of class object and returns if
+        object is already created. This class follows singleton
+        design pattern.
+        """
+        if cls.instance is None:
+            cls.instance = super().__new__(cls)
+        return cls.instance
+
+    def __init__(self) -> None:
+        self.enabled = False
+
+    def __enter__(self) -> Self:
+        self.enabled = True
+        return self
+
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc: BaseException | None,
+        traceback: TracebackType | None,
+    ) -> None:
+        self.enabled = False
+
+    def append(
+        self,
+        op_type: OpType,
+        params: list[Variable],
+        output: Variable,
+        other_params: dict | None = None,
+    ) -> None:
+        """
+        Adds Operation object to the related Variable objects for
+        creating computational graph for calculating gradients.
+
+        Args:
+            op_type: Operation type
+            params: Input parameters to the operation
+            output: Output variable of the operation
+        """
+        operation = Operation(op_type, other_params=other_params)
+        param_nodes = []
+        for param in params:
+            param.add_param_to(operation)
+            param_nodes.append(param)
+        output.add_result_of(operation)
+
+        operation.add_params(param_nodes)
+        operation.add_output(output)
+
+    def gradient(self, target: Variable, source: Variable) -> np.ndarray | None:
+        """
+        Reverse accumulation of partial derivatives to calculate gradients
+        of target variable with respect to source variable.
+
+        Args:
+            target: target variable for which gradients are calculated.
+            source: source variable with respect to which the gradients are
+            calculated.
+
+        Returns:
+            Gradient of the source variable with respect to the target variable
+        """
+
+        # partial derivatives with respect to target
+        partial_deriv = defaultdict(lambda: 0)
+        partial_deriv[target] = np.ones_like(target.to_ndarray())
+
+        # iterating through each operations in the computation graph
+        operation_queue = [target.result_of]
+        while len(operation_queue) > 0:
+            operation = operation_queue.pop()
+            for param in operation.params:
+                # as per the chain rule, multiplying partial derivatives
+                # of variables with respect to the target
+                dparam_doutput = self.derivative(param, operation)
+                dparam_dtarget = dparam_doutput * partial_deriv[operation.output]
+                partial_deriv[param] += dparam_dtarget
+
+                if param.result_of and param.result_of != OpType.NOOP:
+                    operation_queue.append(param.result_of)
+
+        return partial_deriv.get(source)
+
+    def derivative(self, param: Variable, operation: Operation) -> np.ndarray:
+        """
+        Compute the derivative of given operation/function
+
+        Args:
+            param: variable to be differentiated
+            operation: function performed on the input variable
+
+        Returns:
+            Derivative of input variable with respect to the output of
+            the operation
+        """
+        params = operation.params
+
+        if operation == OpType.ADD:
+            return np.ones_like(params[0].to_ndarray(), dtype=np.float64)
+        if operation == OpType.SUB:
+            if params[0] == param:
+                return np.ones_like(params[0].to_ndarray(), dtype=np.float64)
+            return -np.ones_like(params[1].to_ndarray(), dtype=np.float64)
+        if operation == OpType.MUL:
+            return (
+                params[1].to_ndarray().T
+                if params[0] == param
+                else params[0].to_ndarray().T
+            )
+        if operation == OpType.DIV:
+            if params[0] == param:
+                return 1 / params[1].to_ndarray()
+            return -params[0].to_ndarray() / (params[1].to_ndarray() ** 2)
+        if operation == OpType.MATMUL:
+            return (
+                params[1].to_ndarray().T
+                if params[0] == param
+                else params[0].to_ndarray().T
+            )
+        if operation == OpType.POWER:
+            power = operation.other_params["power"]
+            return power * (params[0].to_ndarray() ** (power - 1))
+
+        err_msg = f"invalid operation type: {operation.op_type}"
+        raise ValueError(err_msg)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/machine_learning/data_transformations.py b/machine_learning/data_transformations.py
index ecfd3b9e27c2..a1c28d514fd5 100644
--- a/machine_learning/data_transformations.py
+++ b/machine_learning/data_transformations.py
@@ -25,6 +25,7 @@
     2. non-gaussian (non-normal) distributions work better with normalization
     3. If a column or list of values has extreme values / outliers, use standardization
 """
+
 from statistics import mean, stdev
 
 
diff --git a/machine_learning/decision_tree.py b/machine_learning/decision_tree.py
index 7cd1b02c4181..72970431c3fc 100644
--- a/machine_learning/decision_tree.py
+++ b/machine_learning/decision_tree.py
@@ -3,6 +3,7 @@
 Input data set: The input data set must be 1-dimensional with continuous labels.
 Output: The decision tree maps a real number input to a real number output.
 """
+
 import numpy as np
 
 
@@ -18,22 +19,22 @@ def __init__(self, depth=5, min_leaf_size=5):
     def mean_squared_error(self, labels, prediction):
         """
         mean_squared_error:
-        @param labels: a one dimensional numpy array
+        @param labels: a one-dimensional numpy array
         @param prediction: a floating point value
         return value: mean_squared_error calculates the error if prediction is used to
             estimate the labels
         >>> tester = DecisionTree()
         >>> test_labels = np.array([1,2,3,4,5,6,7,8,9,10])
         >>> test_prediction = float(6)
-        >>> tester.mean_squared_error(test_labels, test_prediction) == (
+        >>> bool(tester.mean_squared_error(test_labels, test_prediction) == (
         ...     TestDecisionTree.helper_mean_squared_error_test(test_labels,
-        ...         test_prediction))
+        ...         test_prediction)))
         True
         >>> test_labels = np.array([1,2,3])
         >>> test_prediction = float(2)
-        >>> tester.mean_squared_error(test_labels, test_prediction) == (
+        >>> bool(tester.mean_squared_error(test_labels, test_prediction) == (
         ...     TestDecisionTree.helper_mean_squared_error_test(test_labels,
-        ...         test_prediction))
+        ...         test_prediction)))
         True
         """
         if labels.ndim != 1:
@@ -44,26 +45,47 @@ def mean_squared_error(self, labels, prediction):
     def train(self, x, y):
         """
         train:
-        @param x: a one dimensional numpy array
-        @param y: a one dimensional numpy array.
+        @param x: a one-dimensional numpy array
+        @param y: a one-dimensional numpy array.
         The contents of y are the labels for the corresponding X values
 
-        train does not have a return value
-        """
-
-        """
-        this section is to check that the inputs conform to our dimensionality
+        train() does not have a return value
+
+        Examples:
+        1. Try to train when x & y are of same length & 1 dimensions (No errors)
+        >>> dt = DecisionTree()
+        >>> dt.train(np.array([10,20,30,40,50]),np.array([0,0,0,1,1]))
+
+        2. Try to train when x is 2 dimensions
+        >>> dt = DecisionTree()
+        >>> dt.train(np.array([[1,2,3,4,5],[1,2,3,4,5]]),np.array([0,0,0,1,1]))
+        Traceback (most recent call last):
+            ...
+        ValueError: Input data set must be one-dimensional
+
+        3. Try to train when x and y are not of the same length
+        >>> dt = DecisionTree()
+        >>> dt.train(np.array([1,2,3,4,5]),np.array([[0,0,0,1,1],[0,0,0,1,1]]))
+        Traceback (most recent call last):
+            ...
+        ValueError: x and y have different lengths
+
+        4. Try to train when x & y are of the same length but different dimensions
+        >>> dt = DecisionTree()
+        >>> dt.train(np.array([1,2,3,4,5]),np.array([[1],[2],[3],[4],[5]]))
+        Traceback (most recent call last):
+            ...
+        ValueError: Data set labels must be one-dimensional
+
+        This section is to check that the inputs conform to our dimensionality
         constraints
         """
         if x.ndim != 1:
-            print("Error: Input data set must be one dimensional")
-            return
+            raise ValueError("Input data set must be one-dimensional")
         if len(x) != len(y):
-            print("Error: X and y have different lengths")
-            return
+            raise ValueError("x and y have different lengths")
         if y.ndim != 1:
-            print("Error: Data set labels must be one dimensional")
-            return
+            raise ValueError("Data set labels must be one-dimensional")
 
         if len(x) < 2 * self.min_leaf_size:
             self.prediction = np.mean(y)
@@ -83,7 +105,7 @@ def train(self, x, y):
         the predictor
         """
         for i in range(len(x)):
-            if len(x[:i]) < self.min_leaf_size:
+            if len(x[:i]) < self.min_leaf_size:  # noqa: SIM114
                 continue
             elif len(x[i:]) < self.min_leaf_size:
                 continue
@@ -165,7 +187,8 @@ def main():
     tree = DecisionTree(depth=10, min_leaf_size=10)
     tree.train(x, y)
 
-    test_cases = (np.random.rand(10) * 2) - 1
+    rng = np.random.default_rng()
+    test_cases = (rng.random(10) * 2) - 1
     predictions = np.array([tree.predict(x) for x in test_cases])
     avg_error = np.mean((predictions - test_cases) ** 2)
 
diff --git a/machine_learning/forecasting/run.py b/machine_learning/forecasting/run.py
index 64e719daacc2..9d81b03cd09e 100644
--- a/machine_learning/forecasting/run.py
+++ b/machine_learning/forecasting/run.py
@@ -28,7 +28,7 @@ def linear_regression_prediction(
     input : training data (date, total_user, total_event) in list of float
     output : list of total user prediction in float
     >>> n = linear_regression_prediction([2,3,4,5], [5,3,4,6], [3,1,2,4], [2,1], [2,2])
-    >>> abs(n - 5.0) < 1e-6  # Checking precision because of floating point errors
+    >>> bool(abs(n - 5.0) < 1e-6)  # Checking precision because of floating point errors
     True
     """
     x = np.array([[1, item, train_mtch[i]] for i, item in enumerate(train_dt)])
@@ -56,7 +56,7 @@ def sarimax_predictor(train_user: list, train_match: list, test_match: list) ->
     )
     model_fit = model.fit(disp=False, maxiter=600, method="nm")
     result = model_fit.predict(1, len(test_match), exog=[test_match])
-    return result[0]
+    return float(result[0])
 
 
 def support_vector_regressor(x_train: list, x_test: list, train_user: list) -> float:
@@ -75,7 +75,7 @@ def support_vector_regressor(x_train: list, x_test: list, train_user: list) -> f
     regressor = SVR(kernel="rbf", C=1, gamma=0.1, epsilon=0.1)
     regressor.fit(x_train, train_user)
     y_pred = regressor.predict(x_test)
-    return y_pred[0]
+    return float(y_pred[0])
 
 
 def interquartile_range_checker(train_user: list) -> float:
@@ -92,7 +92,7 @@ def interquartile_range_checker(train_user: list) -> float:
     q3 = np.percentile(train_user, 75)
     iqr = q3 - q1
     low_lim = q1 - (iqr * 0.1)
-    return low_lim
+    return float(low_lim)
 
 
 def data_safety_checker(list_vote: list, actual_result: float) -> bool:
@@ -113,11 +113,10 @@ def data_safety_checker(list_vote: list, actual_result: float) -> bool:
     for i in list_vote:
         if i > actual_result:
             safe = not_safe + 1
+        elif abs(abs(i) - abs(actual_result)) <= 0.1:
+            safe += 1
         else:
-            if abs(abs(i) - abs(actual_result)) <= 0.1:
-                safe += 1
-            else:
-                not_safe += 1
+            not_safe += 1
     return safe > not_safe
 
 
diff --git a/machine_learning/frequent_pattern_growth.py b/machine_learning/frequent_pattern_growth.py
new file mode 100644
index 000000000000..fae2df16efb1
--- /dev/null
+++ b/machine_learning/frequent_pattern_growth.py
@@ -0,0 +1,350 @@
+"""
+The Frequent Pattern Growth algorithm (FP-Growth) is a widely used data mining
+technique for discovering frequent itemsets in large transaction databases.
+
+It overcomes some of the limitations of traditional methods such as Apriori by
+efficiently constructing the FP-Tree
+
+WIKI: https://athena.ecs.csus.edu/~mei/associationcw/FpGrowth.html
+
+Examples: https://www.javatpoint.com/fp-growth-algorithm-in-data-mining
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+
+@dataclass
+class TreeNode:
+    """
+    A node in a Frequent Pattern tree.
+
+    Args:
+        name: The name of this node.
+        num_occur: The number of occurrences of the node.
+        parent_node: The parent node.
+
+    Example:
+    >>> parent = TreeNode("Parent", 1, None)
+    >>> child = TreeNode("Child", 2, parent)
+    >>> child.name
+    'Child'
+    >>> child.count
+    2
+    """
+
+    name: str
+    count: int
+    parent: TreeNode | None = None
+    children: dict[str, TreeNode] = field(default_factory=dict)
+    node_link: TreeNode | None = None
+
+    def __repr__(self) -> str:
+        return f"TreeNode({self.name!r}, {self.count!r}, {self.parent!r})"
+
+    def inc(self, num_occur: int) -> None:
+        self.count += num_occur
+
+    def disp(self, ind: int = 1) -> None:
+        print(f"{'  ' * ind} {self.name}  {self.count}")
+        for child in self.children.values():
+            child.disp(ind + 1)
+
+
+def create_tree(data_set: list, min_sup: int = 1) -> tuple[TreeNode, dict]:
+    """
+    Create Frequent Pattern tree
+
+    Args:
+        data_set: A list of transactions, where each transaction is a list of items.
+        min_sup: The minimum support threshold.
+        Items with support less than this will be pruned. Default is 1.
+
+    Returns:
+        The root of the FP-Tree.
+        header_table: The header table dictionary with item information.
+
+    Example:
+    >>> data_set = [
+    ...    ['A', 'B', 'C'],
+    ...    ['A', 'C'],
+    ...    ['A', 'B', 'E'],
+    ...    ['A', 'B', 'C', 'E'],
+    ...    ['B', 'E']
+    ... ]
+    >>> min_sup = 2
+    >>> fp_tree, header_table = create_tree(data_set, min_sup)
+    >>> fp_tree
+    TreeNode('Null Set', 1, None)
+    >>> len(header_table)
+    4
+    >>> header_table["A"]
+    [[4, None], TreeNode('A', 4, TreeNode('Null Set', 1, None))]
+    >>> header_table["E"][1]  # doctest: +NORMALIZE_WHITESPACE
+    TreeNode('E', 1, TreeNode('B', 3, TreeNode('A', 4, TreeNode('Null Set', 1, None))))
+    >>> sorted(header_table)
+    ['A', 'B', 'C', 'E']
+    >>> fp_tree.name
+    'Null Set'
+    >>> sorted(fp_tree.children)
+    ['A', 'B']
+    >>> fp_tree.children['A'].name
+    'A'
+    >>> sorted(fp_tree.children['A'].children)
+    ['B', 'C']
+    """
+    header_table: dict = {}
+    for trans in data_set:
+        for item in trans:
+            header_table[item] = header_table.get(item, [0, None])
+            header_table[item][0] += 1
+
+    for k in list(header_table):
+        if header_table[k][0] < min_sup:
+            del header_table[k]
+
+    if not (freq_item_set := set(header_table)):
+        return TreeNode("Null Set", 1, None), {}
+
+    for key, value in header_table.items():
+        header_table[key] = [value, None]
+
+    fp_tree = TreeNode("Null Set", 1, None)  # Parent is None for the root node
+    for tran_set in data_set:
+        local_d = {
+            item: header_table[item][0] for item in tran_set if item in freq_item_set
+        }
+        if local_d:
+            sorted_items = sorted(
+                local_d.items(), key=lambda item_info: item_info[1], reverse=True
+            )
+            ordered_items = [item[0] for item in sorted_items]
+            update_tree(ordered_items, fp_tree, header_table, 1)
+
+    return fp_tree, header_table
+
+
+def update_tree(items: list, in_tree: TreeNode, header_table: dict, count: int) -> None:
+    """
+    Update the FP-Tree with a transaction.
+
+    Args:
+        items: List of items in the transaction.
+        in_tree: The current node in the FP-Tree.
+        header_table: The header table dictionary with item information.
+        count: The count of the transaction.
+
+    Example:
+    >>> data_set = [
+    ...    ['A', 'B', 'C'],
+    ...    ['A', 'C'],
+    ...    ['A', 'B', 'E'],
+    ...    ['A', 'B', 'C', 'E'],
+    ...    ['B', 'E']
+    ... ]
+    >>> min_sup = 2
+    >>> fp_tree, header_table = create_tree(data_set, min_sup)
+    >>> fp_tree
+    TreeNode('Null Set', 1, None)
+    >>> transaction = ['A', 'B', 'E']
+    >>> update_tree(transaction, fp_tree, header_table, 1)
+    >>> fp_tree
+    TreeNode('Null Set', 1, None)
+    >>> fp_tree.children['A'].children['B'].children['E'].children
+    {}
+    >>> fp_tree.children['A'].children['B'].children['E'].count
+    2
+    >>> header_table['E'][1].name
+    'E'
+    """
+    if items[0] in in_tree.children:
+        in_tree.children[items[0]].inc(count)
+    else:
+        in_tree.children[items[0]] = TreeNode(items[0], count, in_tree)
+        if header_table[items[0]][1] is None:
+            header_table[items[0]][1] = in_tree.children[items[0]]
+        else:
+            update_header(header_table[items[0]][1], in_tree.children[items[0]])
+    if len(items) > 1:
+        update_tree(items[1:], in_tree.children[items[0]], header_table, count)
+
+
+def update_header(node_to_test: TreeNode, target_node: TreeNode) -> TreeNode:
+    """
+    Update the header table with a node link.
+
+    Args:
+        node_to_test: The node to be updated in the header table.
+        target_node: The node to link to.
+
+    Example:
+    >>> data_set = [
+    ...    ['A', 'B', 'C'],
+    ...    ['A', 'C'],
+    ...    ['A', 'B', 'E'],
+    ...    ['A', 'B', 'C', 'E'],
+    ...    ['B', 'E']
+    ... ]
+    >>> min_sup = 2
+    >>> fp_tree, header_table = create_tree(data_set, min_sup)
+    >>> fp_tree
+    TreeNode('Null Set', 1, None)
+    >>> node1 = TreeNode("A", 3, None)
+    >>> node2 = TreeNode("B", 4, None)
+    >>> node1
+    TreeNode('A', 3, None)
+    >>> node1 = update_header(node1, node2)
+    >>> node1
+    TreeNode('A', 3, None)
+    >>> node1.node_link
+    TreeNode('B', 4, None)
+    >>> node2.node_link is None
+    True
+    """
+    while node_to_test.node_link is not None:
+        node_to_test = node_to_test.node_link
+    if node_to_test.node_link is None:
+        node_to_test.node_link = target_node
+    # Return the updated node
+    return node_to_test
+
+
+def ascend_tree(leaf_node: TreeNode, prefix_path: list[str]) -> None:
+    """
+    Ascend the FP-Tree from a leaf node to its root, adding item names to the prefix
+    path.
+
+    Args:
+        leaf_node: The leaf node to start ascending from.
+        prefix_path: A list to store the item as they are ascended.
+
+    Example:
+    >>> data_set = [
+    ...    ['A', 'B', 'C'],
+    ...    ['A', 'C'],
+    ...    ['A', 'B', 'E'],
+    ...    ['A', 'B', 'C', 'E'],
+    ...    ['B', 'E']
+    ... ]
+    >>> min_sup = 2
+    >>> fp_tree, header_table = create_tree(data_set, min_sup)
+
+    >>> path = []
+    >>> ascend_tree(fp_tree.children['A'], path)
+    >>> path # ascending from a leaf node 'A'
+    ['A']
+    """
+    if leaf_node.parent is not None:
+        prefix_path.append(leaf_node.name)
+        ascend_tree(leaf_node.parent, prefix_path)
+
+
+def find_prefix_path(base_pat: frozenset, tree_node: TreeNode | None) -> dict:  # noqa: ARG001
+    """
+    Find the conditional pattern base for a given base pattern.
+
+    Args:
+        base_pat: The base pattern for which to find the conditional pattern base.
+        tree_node: The node in the FP-Tree.
+
+    Example:
+    >>> data_set = [
+    ...    ['A', 'B', 'C'],
+    ...    ['A', 'C'],
+    ...    ['A', 'B', 'E'],
+    ...    ['A', 'B', 'C', 'E'],
+    ...    ['B', 'E']
+    ... ]
+    >>> min_sup = 2
+    >>> fp_tree, header_table = create_tree(data_set, min_sup)
+    >>> fp_tree
+    TreeNode('Null Set', 1, None)
+    >>> len(header_table)
+    4
+    >>> base_pattern = frozenset(['A'])
+    >>> sorted(find_prefix_path(base_pattern, fp_tree.children['A']))
+    []
+    """
+    cond_pats: dict = {}
+    while tree_node is not None:
+        prefix_path: list = []
+        ascend_tree(tree_node, prefix_path)
+        if len(prefix_path) > 1:
+            cond_pats[frozenset(prefix_path[1:])] = tree_node.count
+        tree_node = tree_node.node_link
+    return cond_pats
+
+
+def mine_tree(
+    in_tree: TreeNode,  # noqa: ARG001
+    header_table: dict,
+    min_sup: int,
+    pre_fix: set,
+    freq_item_list: list,
+) -> None:
+    """
+    Mine the FP-Tree recursively to discover frequent itemsets.
+
+    Args:
+        in_tree: The FP-Tree to mine.
+        header_table: The header table dictionary with item information.
+        min_sup: The minimum support threshold.
+        pre_fix: A set of items as a prefix for the itemsets being mined.
+        freq_item_list: A list to store the frequent itemsets.
+
+    Example:
+    >>> data_set = [
+    ...    ['A', 'B', 'C'],
+    ...    ['A', 'C'],
+    ...    ['A', 'B', 'E'],
+    ...    ['A', 'B', 'C', 'E'],
+    ...    ['B', 'E']
+    ... ]
+    >>> min_sup = 2
+    >>> fp_tree, header_table = create_tree(data_set, min_sup)
+    >>> fp_tree
+    TreeNode('Null Set', 1, None)
+    >>> frequent_itemsets = []
+    >>> mine_tree(fp_tree, header_table, min_sup, set([]), frequent_itemsets)
+    >>> expe_itm = [{'C'}, {'C', 'A'}, {'E'}, {'A', 'E'}, {'E', 'B'}, {'A'}, {'B'}]
+    >>> all(expected in frequent_itemsets for expected in expe_itm)
+    True
+    """
+    sorted_items = sorted(header_table.items(), key=lambda item_info: item_info[1][0])
+    big_l = [item[0] for item in sorted_items]
+    for base_pat in big_l:
+        new_freq_set = pre_fix.copy()
+        new_freq_set.add(base_pat)
+        freq_item_list.append(new_freq_set)
+        cond_patt_bases = find_prefix_path(base_pat, header_table[base_pat][1])
+        my_cond_tree, my_head = create_tree(list(cond_patt_bases), min_sup)
+        if my_head is not None:
+            # Pass header_table[base_pat][1] as node_to_test to update_header
+            header_table[base_pat][1] = update_header(
+                header_table[base_pat][1], my_cond_tree
+            )
+            mine_tree(my_cond_tree, my_head, min_sup, new_freq_set, freq_item_list)
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    data_set: list[frozenset] = [
+        frozenset(["bread", "milk", "cheese"]),
+        frozenset(["bread", "milk"]),
+        frozenset(["bread", "diapers"]),
+        frozenset(["bread", "milk", "diapers"]),
+        frozenset(["milk", "diapers"]),
+        frozenset(["milk", "cheese"]),
+        frozenset(["diapers", "cheese"]),
+        frozenset(["bread", "milk", "cheese", "diapers"]),
+    ]
+    print(f"{len(data_set) = }")
+    fp_tree, header_table = create_tree(data_set, min_sup=3)
+    print(f"{fp_tree = }")
+    print(f"{len(header_table) = }")
+    freq_items: list = []
+    mine_tree(fp_tree, header_table, 3, set(), freq_items)
+    print(f"{freq_items = }")
diff --git a/machine_learning/gradient_boosting_classifier.py b/machine_learning/gradient_boosting_classifier.py
new file mode 100644
index 000000000000..2902394d8226
--- /dev/null
+++ b/machine_learning/gradient_boosting_classifier.py
@@ -0,0 +1,118 @@
+import numpy as np
+from sklearn.datasets import load_iris
+from sklearn.metrics import accuracy_score
+from sklearn.model_selection import train_test_split
+from sklearn.tree import DecisionTreeRegressor
+
+
+class GradientBoostingClassifier:
+    def __init__(self, n_estimators: int = 100, learning_rate: float = 0.1) -> None:
+        """
+        Initialize a GradientBoostingClassifier.
+
+        Parameters:
+        - n_estimators (int): The number of weak learners to train.
+        - learning_rate (float): The learning rate for updating the model.
+
+        Attributes:
+        - n_estimators (int): The number of weak learners.
+        - learning_rate (float): The learning rate.
+        - models (list): A list to store the trained weak learners.
+        """
+        self.n_estimators = n_estimators
+        self.learning_rate = learning_rate
+        self.models: list[tuple[DecisionTreeRegressor, float]] = []
+
+    def fit(self, features: np.ndarray, target: np.ndarray) -> None:
+        """
+        Fit the GradientBoostingClassifier to the training data.
+
+        Parameters:
+        - features (np.ndarray): The training features.
+        - target (np.ndarray): The target values.
+
+        Returns:
+        None
+
+        >>> import numpy as np
+        >>> from sklearn.datasets import load_iris
+        >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1)
+        >>> iris = load_iris()
+        >>> X, y = iris.data, iris.target
+        >>> clf.fit(X, y)
+        >>> # Check if the model is trained
+        >>> len(clf.models) == 100
+        True
+        """
+        for _ in range(self.n_estimators):
+            # Calculate the pseudo-residuals
+            residuals = -self.gradient(target, self.predict(features))
+            # Fit a weak learner (e.g., decision tree) to the residuals
+            model = DecisionTreeRegressor(max_depth=1)
+            model.fit(features, residuals)
+            # Update the model by adding the weak learner with a learning rate
+            self.models.append((model, self.learning_rate))
+
+    def predict(self, features: np.ndarray) -> np.ndarray:
+        """
+        Make predictions on input data.
+
+        Parameters:
+        - features (np.ndarray): The input data for making predictions.
+
+        Returns:
+        - np.ndarray: An array of binary predictions (-1 or 1).
+
+        >>> import numpy as np
+        >>> from sklearn.datasets import load_iris
+        >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1)
+        >>> iris = load_iris()
+        >>> X, y = iris.data, iris.target
+        >>> clf.fit(X, y)
+        >>> y_pred = clf.predict(X)
+        >>> # Check if the predictions have the correct shape
+        >>> y_pred.shape == y.shape
+        True
+        """
+        # Initialize predictions with zeros
+        predictions = np.zeros(features.shape[0])
+        for model, learning_rate in self.models:
+            predictions += learning_rate * model.predict(features)
+        return np.sign(predictions)  # Convert to binary predictions (-1 or 1)
+
+    def gradient(self, target: np.ndarray, y_pred: np.ndarray) -> np.ndarray:
+        """
+        Calculate the negative gradient (pseudo-residuals) for logistic loss.
+
+        Parameters:
+        - target (np.ndarray): The target values.
+        - y_pred (np.ndarray): The predicted values.
+
+        Returns:
+        - np.ndarray: An array of pseudo-residuals.
+
+        >>> import numpy as np
+        >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1)
+        >>> target = np.array([0, 1, 0, 1])
+        >>> y_pred = np.array([0.2, 0.8, 0.3, 0.7])
+        >>> residuals = clf.gradient(target, y_pred)
+        >>> # Check if residuals have the correct shape
+        >>> residuals.shape == target.shape
+        True
+        """
+        return -target / (1 + np.exp(target * y_pred))
+
+
+if __name__ == "__main__":
+    iris = load_iris()
+    X, y = iris.data, iris.target
+    X_train, X_test, y_train, y_test = train_test_split(
+        X, y, test_size=0.2, random_state=42
+    )
+
+    clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1)
+    clf.fit(X_train, y_train)
+
+    y_pred = clf.predict(X_test)
+    accuracy = accuracy_score(y_test, y_pred)
+    print(f"Accuracy: {accuracy:.2f}")
diff --git a/machine_learning/gradient_descent.py b/machine_learning/gradient_descent.py
index 9ffc02bbc284..95463faf5635 100644
--- a/machine_learning/gradient_descent.py
+++ b/machine_learning/gradient_descent.py
@@ -2,7 +2,8 @@
 Implementation of gradient descent algorithm for minimizing cost of a linear hypothesis
 function.
 """
-import numpy
+
+import numpy as np
 
 # List of input, output pairs
 train_data = (
@@ -115,7 +116,7 @@ def run_gradient_descent():
             temp_parameter_vector[i] = (
                 parameter_vector[i] - LEARNING_RATE * cost_derivative
             )
-        if numpy.allclose(
+        if np.allclose(
             parameter_vector,
             temp_parameter_vector,
             atol=absolute_error_limit,
diff --git a/machine_learning/k_means_clust.py b/machine_learning/k_means_clust.py
index ebad66ac8e8f..a926362fc18b 100644
--- a/machine_learning/k_means_clust.py
+++ b/machine_learning/k_means_clust.py
@@ -40,6 +40,7 @@
   5. Transfers Dataframe into excel format it must have feature called
       'Clust' with k means clustering numbers in it.
 """
+
 import warnings
 
 import numpy as np
@@ -54,12 +55,12 @@
 
 def get_initial_centroids(data, k, seed=None):
     """Randomly choose k data points as initial centroids"""
-    if seed is not None:  # useful for obtaining consistent results
-        np.random.seed(seed)
+    # useful for obtaining consistent results
+    rng = np.random.default_rng(seed)
     n = data.shape[0]  # number of data points
 
     # Pick K indices from range [0, N).
-    rand_indices = np.random.randint(0, n, k)
+    rand_indices = rng.integers(0, n, k)
 
     # Keep centroids as dense format, as many entries will be nonzero due to averaging.
     # As long as at least one document in a cluster contains a word,
@@ -237,7 +238,7 @@ def report_generator(
             [
                 ("sum", "sum"),
                 ("mean_with_zeros", lambda x: np.mean(np.nan_to_num(x))),
-                ("mean_without_zeros", lambda x: x.replace(0, np.NaN).mean()),
+                ("mean_without_zeros", lambda x: x.replace(0, np.nan).mean()),
                 (
                     "mean_25-75",
                     lambda x: np.mean(
diff --git a/machine_learning/k_nearest_neighbours.py b/machine_learning/k_nearest_neighbours.py
index a43757c5c20e..fbc1b8bd227e 100644
--- a/machine_learning/k_nearest_neighbours.py
+++ b/machine_learning/k_nearest_neighbours.py
@@ -42,7 +42,7 @@ def _euclidean_distance(a: np.ndarray[float], b: np.ndarray[float]) -> float:
         >>> KNN._euclidean_distance(np.array([1, 2, 3]), np.array([1, 8, 11]))
         10.0
         """
-        return np.linalg.norm(a - b)
+        return float(np.linalg.norm(a - b))
 
     def classify(self, pred_point: np.ndarray[float], k: int = 5) -> str:
         """
diff --git a/machine_learning/linear_discriminant_analysis.py b/machine_learning/linear_discriminant_analysis.py
index 88c047157893..8528ccbbae51 100644
--- a/machine_learning/linear_discriminant_analysis.py
+++ b/machine_learning/linear_discriminant_analysis.py
@@ -1,47 +1,48 @@
 """
-    Linear Discriminant Analysis
+Linear Discriminant Analysis
 
 
 
-    Assumptions About Data :
-        1. The input variables has a gaussian distribution.
-        2. The variance calculated for each input variables by class grouping is the
-           same.
-        3. The mix of classes in your training set is representative of the problem.
+Assumptions About Data :
+    1. The input variables has a gaussian distribution.
+    2. The variance calculated for each input variables by class grouping is the
+       same.
+    3. The mix of classes in your training set is representative of the problem.
 
 
-    Learning The Model :
-        The LDA model requires the estimation of statistics from the training data :
-            1. Mean of each input value for each class.
-            2. Probability of an instance belong to each class.
-            3. Covariance for the input data for each class
+Learning The Model :
+    The LDA model requires the estimation of statistics from the training data :
+        1. Mean of each input value for each class.
+        2. Probability of an instance belong to each class.
+        3. Covariance for the input data for each class
 
-        Calculate the class means :
-            mean(x) = 1/n ( for i = 1 to i = n --> sum(xi))
+    Calculate the class means :
+        mean(x) = 1/n ( for i = 1 to i = n --> sum(xi))
 
-        Calculate the class probabilities :
-            P(y = 0) = count(y = 0) / (count(y = 0) + count(y = 1))
-            P(y = 1) = count(y = 1) / (count(y = 0) + count(y = 1))
+    Calculate the class probabilities :
+        P(y = 0) = count(y = 0) / (count(y = 0) + count(y = 1))
+        P(y = 1) = count(y = 1) / (count(y = 0) + count(y = 1))
 
-        Calculate the variance :
-            We can calculate the variance for dataset in two steps :
-                1. Calculate the squared difference for each input variable from the
-                   group mean.
-                2. Calculate the mean of the squared difference.
-                ------------------------------------------------
-                Squared_Difference = (x - mean(k)) ** 2
-                Variance = (1 / (count(x) - count(classes))) *
-                    (for i = 1 to i = n --> sum(Squared_Difference(xi)))
+    Calculate the variance :
+        We can calculate the variance for dataset in two steps :
+            1. Calculate the squared difference for each input variable from the
+               group mean.
+            2. Calculate the mean of the squared difference.
+            ------------------------------------------------
+            Squared_Difference = (x - mean(k)) ** 2
+            Variance = (1 / (count(x) - count(classes))) *
+                (for i = 1 to i = n --> sum(Squared_Difference(xi)))
 
-    Making Predictions :
-        discriminant(x) = x * (mean / variance) -
-            ((mean ** 2) / (2 * variance)) + Ln(probability)
-        ---------------------------------------------------------------------------
-        After calculating the discriminant value for each class, the class with the
-        largest discriminant value is taken as the prediction.
+Making Predictions :
+    discriminant(x) = x * (mean / variance) -
+        ((mean ** 2) / (2 * variance)) + Ln(probability)
+    ---------------------------------------------------------------------------
+    After calculating the discriminant value for each class, the class with the
+    largest discriminant value is taken as the prediction.
 
-    Author: @EverLookNeverSee
+Author: @EverLookNeverSee
 """
+
 from collections.abc import Callable
 from math import log
 from os import name, system
@@ -255,7 +256,7 @@ def valid_input(
     input_type: Callable[[object], num],  # Usually float or int
     input_msg: str,
     err_msg: str,
-    condition: Callable[[num], bool] = lambda x: True,
+    condition: Callable[[num], bool] = lambda _: True,
     default: str | None = None,
 ) -> num:
     """
@@ -321,7 +322,7 @@ def main():
             user_count = valid_input(
                 input_type=int,
                 condition=lambda x: x > 0,
-                input_msg=(f"Enter The number of instances for class_{i+1}: "),
+                input_msg=(f"Enter The number of instances for class_{i + 1}: "),
                 err_msg="Number of instances should be positive!",
             )
             counts.append(user_count)
@@ -332,7 +333,7 @@ def main():
         for a in range(n_classes):
             user_mean = valid_input(
                 input_type=float,
-                input_msg=(f"Enter the value of mean for class_{a+1}: "),
+                input_msg=(f"Enter the value of mean for class_{a + 1}: "),
                 err_msg="This is an invalid value.",
             )
             user_means.append(user_mean)
diff --git a/machine_learning/linear_regression.py b/machine_learning/linear_regression.py
index 0847112ad538..5b1e663116cc 100644
--- a/machine_learning/linear_regression.py
+++ b/machine_learning/linear_regression.py
@@ -7,8 +7,17 @@
 fit our dataset. In this particular code, I had used a CSGO dataset (ADR vs
 Rating). We try to best fit a line through dataset and estimate the parameters.
 """
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+#     "numpy",
+# ]
+# ///
+
+import httpx
 import numpy as np
-import requests
 
 
 def collect_dataset():
@@ -16,9 +25,10 @@ def collect_dataset():
     The dataset contains ADR vs Rating of a Player
     :return : dataset obtained from the link, as matrix
     """
-    response = requests.get(
+    response = httpx.get(
         "https://raw.githubusercontent.com/yashLadha/The_Math_of_Intelligence/"
-        "master/Week1/ADRvsRating.csv"
+        "master/Week1/ADRvsRating.csv",
+        timeout=10,
     )
     lines = response.text.splitlines()
     data = []
@@ -39,6 +49,14 @@ def run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta):
     :param theta    : Feature vector (weight's for our model)
     ;param return    : Updated Feature's, using
                        curr_features - alpha_ * gradient(w.r.t. feature)
+    >>> import numpy as np
+    >>> data_x = np.array([[1, 2], [3, 4]])
+    >>> data_y = np.array([5, 6])
+    >>> len_data = len(data_x)
+    >>> alpha = 0.01
+    >>> theta = np.array([0.1, 0.2])
+    >>> run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta)
+    array([0.196, 0.343])
     """
     n = len_data
 
@@ -56,6 +74,12 @@ def sum_of_square_error(data_x, data_y, len_data, theta):
     :param len_data  : len of the dataset
     :param theta     : contains the feature vector
     :return          : sum of square error computed from given feature's
+
+    Example:
+    >>> vc_x = np.array([[1.1], [2.1], [3.1]])
+    >>> vc_y = np.array([1.2, 2.2, 3.2])
+    >>> round(sum_of_square_error(vc_x, vc_y, 3, np.array([1])),3)
+    np.float64(0.005)
     """
     prod = np.dot(theta, data_x.transpose())
     prod -= data_y.transpose()
@@ -91,6 +115,11 @@ def mean_absolute_error(predicted_y, original_y):
     :param predicted_y   : contains the output of prediction (result vector)
     :param original_y    : contains values of expected outcome
     :return          : mean absolute error computed from given feature's
+
+    >>> predicted_y = [3, -0.5, 2, 7]
+    >>> original_y = [2.5, 0.0, 2, 8]
+    >>> mean_absolute_error(predicted_y, original_y)
+    0.5
     """
     total = sum(abs(y - predicted_y[i]) for i, y in enumerate(original_y))
     return total / len(original_y)
@@ -112,4 +141,7 @@ def main():
 
 
 if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
     main()
diff --git a/machine_learning/local_weighted_learning/local_weighted_learning.md b/machine_learning/local_weighted_learning/README.md
similarity index 100%
rename from machine_learning/local_weighted_learning/local_weighted_learning.md
rename to machine_learning/local_weighted_learning/README.md
diff --git a/machine_learning/logistic_regression.py b/machine_learning/logistic_regression.py
index 87bc8f6681cc..496026631fbe 100644
--- a/machine_learning/logistic_regression.py
+++ b/machine_learning/logistic_regression.py
@@ -14,6 +14,7 @@
 Coursera ML course
 https://medium.com/@martinpella/logistic-regression-from-scratch-in-python-124c5636b8ac
 """
+
 import numpy as np
 from matplotlib import pyplot as plt
 from sklearn import datasets
@@ -27,12 +28,79 @@
 # classification problems
 
 
-def sigmoid_function(z):
+def sigmoid_function(z: float | np.ndarray) -> float | np.ndarray:
+    """
+    Also known as Logistic Function.
+
+                1
+    f(x) =   -------
+              1 + e⁻ˣ
+
+    The sigmoid function approaches a value of 1 as its input 'x' becomes
+    increasing positive. Opposite for negative values.
+
+    Reference: https://en.wikipedia.org/wiki/Sigmoid_function
+
+    @param z:  input to the function
+    @returns: returns value in the range 0 to 1
+
+    Examples:
+    >>> float(sigmoid_function(4))
+    0.9820137900379085
+    >>> sigmoid_function(np.array([-3, 3]))
+    array([0.04742587, 0.95257413])
+    >>> sigmoid_function(np.array([-3, 3, 1]))
+    array([0.04742587, 0.95257413, 0.73105858])
+    >>> sigmoid_function(np.array([-0.01, -2, -1.9]))
+    array([0.49750002, 0.11920292, 0.13010847])
+    >>> sigmoid_function(np.array([-1.3, 5.3, 12]))
+    array([0.21416502, 0.9950332 , 0.99999386])
+    >>> sigmoid_function(np.array([0.01, 0.02, 4.1]))
+    array([0.50249998, 0.50499983, 0.9836975 ])
+    >>> sigmoid_function(np.array([0.8]))
+    array([0.68997448])
+    """
     return 1 / (1 + np.exp(-z))
 
 
-def cost_function(h, y):
-    return (-y * np.log(h) - (1 - y) * np.log(1 - h)).mean()
+def cost_function(h: np.ndarray, y: np.ndarray) -> float:
+    """
+    Cost function quantifies the error between predicted and expected values.
+    The cost function used in Logistic Regression is called Log Loss
+    or Cross Entropy Function.
+
+    J(θ) = (1/m) * Σ [ -y * log(hθ(x)) - (1 - y) * log(1 - hθ(x)) ]
+
+    Where:
+       - J(θ) is the cost that we want to minimize during training
+       - m is the number of training examples
+       - Σ represents the summation over all training examples
+       - y is the actual binary label (0 or 1) for a given example
+       - hθ(x) is the predicted probability that x belongs to the positive class
+
+    @param h: the output of sigmoid function. It is the estimated probability
+    that the input example 'x' belongs to the positive class
+
+    @param y: the actual binary label associated with input example 'x'
+
+    Examples:
+    >>> estimations = sigmoid_function(np.array([0.3, -4.3, 8.1]))
+    >>> cost_function(h=estimations,y=np.array([1, 0, 1]))
+    0.18937868932131605
+    >>> estimations = sigmoid_function(np.array([4, 3, 1]))
+    >>> cost_function(h=estimations,y=np.array([1, 0, 0]))
+    1.459999655669926
+    >>> estimations = sigmoid_function(np.array([4, -3, -1]))
+    >>> cost_function(h=estimations,y=np.array([1,0,0]))
+    0.1266663223365915
+    >>> estimations = sigmoid_function(0)
+    >>> cost_function(h=estimations,y=np.array([1]))
+    0.6931471805599453
+
+    References:
+       - https://en.wikipedia.org/wiki/Logistic_regression
+    """
+    return float((-y * np.log(h) - (1 - y) * np.log(1 - h)).mean())
 
 
 def log_likelihood(x, y, weights):
@@ -60,6 +128,10 @@ def logistic_reg(alpha, x, y, max_iterations=70000):
 # In[68]:
 
 if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
     iris = datasets.load_iris()
     x = iris.data[:, :2]
     y = (iris.target != 0) * 1
diff --git a/machine_learning/loss_functions.py b/machine_learning/loss_functions.py
new file mode 100644
index 000000000000..0bd9aa8b5401
--- /dev/null
+++ b/machine_learning/loss_functions.py
@@ -0,0 +1,669 @@
+import numpy as np
+
+
+def binary_cross_entropy(
+    y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15
+) -> float:
+    """
+    Calculate the mean binary cross-entropy (BCE) loss between true labels and predicted
+    probabilities.
+
+    BCE loss quantifies dissimilarity between true labels (0 or 1) and predicted
+    probabilities. It's widely used in binary classification tasks.
+
+    BCE = -Σ(y_true * ln(y_pred) + (1 - y_true) * ln(1 - y_pred))
+
+    Reference: https://en.wikipedia.org/wiki/Cross_entropy
+
+    Parameters:
+    - y_true: True binary labels (0 or 1)
+    - y_pred: Predicted probabilities for class 1
+    - epsilon: Small constant to avoid numerical instability
+
+    >>> true_labels = np.array([0, 1, 1, 0, 1])
+    >>> predicted_probs = np.array([0.2, 0.7, 0.9, 0.3, 0.8])
+    >>> float(binary_cross_entropy(true_labels, predicted_probs))
+    0.2529995012327421
+    >>> true_labels = np.array([0, 1, 1, 0, 1])
+    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
+    >>> binary_cross_entropy(true_labels, predicted_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+
+    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)  # Clip predictions to avoid log(0)
+    bce_loss = -(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
+    return np.mean(bce_loss)
+
+
+def binary_focal_cross_entropy(
+    y_true: np.ndarray,
+    y_pred: np.ndarray,
+    gamma: float = 2.0,
+    alpha: float = 0.25,
+    epsilon: float = 1e-15,
+) -> float:
+    """
+    Calculate the mean binary focal cross-entropy (BFCE) loss between true labels
+    and predicted probabilities.
+
+    BFCE loss quantifies dissimilarity between true labels (0 or 1) and predicted
+    probabilities. It's a variation of binary cross-entropy that addresses class
+    imbalance by focusing on hard examples.
+
+    BCFE = -Σ(alpha * (1 - y_pred)**gamma * y_true * log(y_pred)
+                + (1 - alpha) * y_pred**gamma * (1 - y_true) * log(1 - y_pred))
+
+    Reference: [Lin et al., 2018](https://arxiv.org/pdf/1708.02002.pdf)
+
+    Parameters:
+    - y_true: True binary labels (0 or 1).
+    - y_pred: Predicted probabilities for class 1.
+    - gamma: Focusing parameter for modulating the loss (default: 2.0).
+    - alpha: Weighting factor for class 1 (default: 0.25).
+    - epsilon: Small constant to avoid numerical instability.
+
+    >>> true_labels = np.array([0, 1, 1, 0, 1])
+    >>> predicted_probs = np.array([0.2, 0.7, 0.9, 0.3, 0.8])
+    >>> float(binary_focal_cross_entropy(true_labels, predicted_probs))
+    0.008257977659239775
+    >>> true_labels = np.array([0, 1, 1, 0, 1])
+    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
+    >>> binary_focal_cross_entropy(true_labels, predicted_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+    # Clip predicted probabilities to avoid log(0)
+    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
+
+    bcfe_loss = -(
+        alpha * (1 - y_pred) ** gamma * y_true * np.log(y_pred)
+        + (1 - alpha) * y_pred**gamma * (1 - y_true) * np.log(1 - y_pred)
+    )
+
+    return np.mean(bcfe_loss)
+
+
+def categorical_cross_entropy(
+    y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15
+) -> float:
+    """
+    Calculate categorical cross-entropy (CCE) loss between true class labels and
+    predicted class probabilities.
+
+    CCE = -Σ(y_true * ln(y_pred))
+
+    Reference: https://en.wikipedia.org/wiki/Cross_entropy
+
+    Parameters:
+    - y_true: True class labels (one-hot encoded)
+    - y_pred: Predicted class probabilities
+    - epsilon: Small constant to avoid numerical instability
+
+    >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]])
+    >>> float(categorical_cross_entropy(true_labels, pred_probs))
+    0.567395975254385
+    >>> true_labels = np.array([[1, 0], [0, 1]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
+    >>> categorical_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input arrays must have the same shape.
+    >>> true_labels = np.array([[2, 0, 1], [1, 0, 0]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
+    >>> categorical_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: y_true must be one-hot encoded.
+    >>> true_labels = np.array([[1, 0, 1], [1, 0, 0]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
+    >>> categorical_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: y_true must be one-hot encoded.
+    >>> true_labels = np.array([[1, 0, 0], [0, 1, 0]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.1], [0.2, 0.7, 0.1]])
+    >>> categorical_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Predicted probabilities must sum to approximately 1.
+    """
+    if y_true.shape != y_pred.shape:
+        raise ValueError("Input arrays must have the same shape.")
+
+    if np.any((y_true != 0) & (y_true != 1)) or np.any(y_true.sum(axis=1) != 1):
+        raise ValueError("y_true must be one-hot encoded.")
+
+    if not np.all(np.isclose(np.sum(y_pred, axis=1), 1, rtol=epsilon, atol=epsilon)):
+        raise ValueError("Predicted probabilities must sum to approximately 1.")
+
+    y_pred = np.clip(y_pred, epsilon, 1)  # Clip predictions to avoid log(0)
+    return -np.sum(y_true * np.log(y_pred))
+
+
+def categorical_focal_cross_entropy(
+    y_true: np.ndarray,
+    y_pred: np.ndarray,
+    alpha: np.ndarray = None,
+    gamma: float = 2.0,
+    epsilon: float = 1e-15,
+) -> float:
+    """
+    Calculate the mean categorical focal cross-entropy (CFCE) loss between true
+    labels and predicted probabilities for multi-class classification.
+
+    CFCE loss is a generalization of binary focal cross-entropy for multi-class
+    classification. It addresses class imbalance by focusing on hard examples.
+
+    CFCE = -Σ alpha * (1 - y_pred)**gamma * y_true * log(y_pred)
+
+    Reference: [Lin et al., 2018](https://arxiv.org/pdf/1708.02002.pdf)
+
+    Parameters:
+    - y_true: True labels in one-hot encoded form.
+    - y_pred: Predicted probabilities for each class.
+    - alpha: Array of weighting factors for each class.
+    - gamma: Focusing parameter for modulating the loss (default: 2.0).
+    - epsilon: Small constant to avoid numerical instability.
+
+    Returns:
+    - The mean categorical focal cross-entropy loss.
+
+    >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]])
+    >>> alpha = np.array([0.6, 0.2, 0.7])
+    >>> float(categorical_focal_cross_entropy(true_labels, pred_probs, alpha))
+    0.0025966118981496423
+
+    >>> true_labels = np.array([[0, 1, 0], [0, 0, 1]])
+    >>> pred_probs = np.array([[0.05, 0.95, 0], [0.1, 0.8, 0.1]])
+    >>> alpha = np.array([0.25, 0.25, 0.25])
+    >>> float(categorical_focal_cross_entropy(true_labels, pred_probs, alpha))
+    0.23315276982014324
+
+    >>> true_labels = np.array([[1, 0], [0, 1]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
+    >>> categorical_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input arrays must have the same shape.
+
+    >>> true_labels = np.array([[2, 0, 1], [1, 0, 0]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
+    >>> categorical_focal_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: y_true must be one-hot encoded.
+
+    >>> true_labels = np.array([[1, 0, 1], [1, 0, 0]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
+    >>> categorical_focal_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: y_true must be one-hot encoded.
+
+    >>> true_labels = np.array([[1, 0, 0], [0, 1, 0]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.1], [0.2, 0.7, 0.1]])
+    >>> categorical_focal_cross_entropy(true_labels, pred_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Predicted probabilities must sum to approximately 1.
+
+    >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]])
+    >>> alpha = np.array([0.6, 0.2])
+    >>> categorical_focal_cross_entropy(true_labels, pred_probs, alpha)
+    Traceback (most recent call last):
+        ...
+    ValueError: Length of alpha must match the number of classes.
+    """
+    if y_true.shape != y_pred.shape:
+        raise ValueError("Shape of y_true and y_pred must be the same.")
+
+    if alpha is None:
+        alpha = np.ones(y_true.shape[1])
+
+    if np.any((y_true != 0) & (y_true != 1)) or np.any(y_true.sum(axis=1) != 1):
+        raise ValueError("y_true must be one-hot encoded.")
+
+    if len(alpha) != y_true.shape[1]:
+        raise ValueError("Length of alpha must match the number of classes.")
+
+    if not np.all(np.isclose(np.sum(y_pred, axis=1), 1, rtol=epsilon, atol=epsilon)):
+        raise ValueError("Predicted probabilities must sum to approximately 1.")
+
+    # Clip predicted probabilities to avoid log(0)
+    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
+
+    # Calculate loss for each class and sum across classes
+    cfce_loss = -np.sum(
+        alpha * np.power(1 - y_pred, gamma) * y_true * np.log(y_pred), axis=1
+    )
+
+    return np.mean(cfce_loss)
+
+
+def hinge_loss(y_true: np.ndarray, y_pred: np.ndarray) -> float:
+    """
+    Calculate the mean hinge loss for between true labels and predicted probabilities
+    for training support vector machines (SVMs).
+
+    Hinge loss = max(0, 1 - true * pred)
+
+    Reference: https://en.wikipedia.org/wiki/Hinge_loss
+
+    Args:
+    - y_true: actual values (ground truth) encoded as -1 or 1
+    - y_pred: predicted values
+
+    >>> true_labels = np.array([-1, 1, 1, -1, 1])
+    >>> pred = np.array([-4, -0.3, 0.7, 5, 10])
+    >>> float(hinge_loss(true_labels, pred))
+    1.52
+    >>> true_labels = np.array([-1, 1, 1, -1, 1, 1])
+    >>> pred = np.array([-4, -0.3, 0.7, 5, 10])
+    >>> hinge_loss(true_labels, pred)
+    Traceback (most recent call last):
+    ...
+    ValueError: Length of predicted and actual array must be same.
+    >>> true_labels = np.array([-1, 1, 10, -1, 1])
+    >>> pred = np.array([-4, -0.3, 0.7, 5, 10])
+    >>> hinge_loss(true_labels, pred)
+    Traceback (most recent call last):
+    ...
+    ValueError: y_true can have values -1 or 1 only.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Length of predicted and actual array must be same.")
+
+    if np.any((y_true != -1) & (y_true != 1)):
+        raise ValueError("y_true can have values -1 or 1 only.")
+
+    hinge_losses = np.maximum(0, 1.0 - (y_true * y_pred))
+    return np.mean(hinge_losses)
+
+
+def huber_loss(y_true: np.ndarray, y_pred: np.ndarray, delta: float) -> float:
+    """
+    Calculate the mean Huber loss between the given ground truth and predicted values.
+
+    The Huber loss describes the penalty incurred by an estimation procedure, and it
+    serves as a measure of accuracy for regression models.
+
+    Huber loss =
+        0.5 * (y_true - y_pred)^2                   if |y_true - y_pred| <= delta
+        delta * |y_true - y_pred| - 0.5 * delta^2   otherwise
+
+    Reference: https://en.wikipedia.org/wiki/Huber_loss
+
+    Parameters:
+    - y_true: The true values (ground truth)
+    - y_pred: The predicted values
+
+    >>> true_values = np.array([0.9, 10.0, 2.0, 1.0, 5.2])
+    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
+    >>> bool(np.isclose(huber_loss(true_values, predicted_values, 1.0), 2.102))
+    True
+    >>> true_labels = np.array([11.0, 21.0, 3.32, 4.0, 5.0])
+    >>> predicted_probs = np.array([8.3, 20.8, 2.9, 11.2, 5.0])
+    >>> bool(np.isclose(huber_loss(true_labels, predicted_probs, 1.0), 1.80164))
+    True
+    >>> true_labels = np.array([11.0, 21.0, 3.32, 4.0])
+    >>> predicted_probs = np.array([8.3, 20.8, 2.9, 11.2, 5.0])
+    >>> huber_loss(true_labels, predicted_probs, 1.0)
+    Traceback (most recent call last):
+    ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+
+    huber_mse = 0.5 * (y_true - y_pred) ** 2
+    huber_mae = delta * (np.abs(y_true - y_pred) - 0.5 * delta)
+    return np.where(np.abs(y_true - y_pred) <= delta, huber_mse, huber_mae).mean()
+
+
+def mean_squared_error(y_true: np.ndarray, y_pred: np.ndarray) -> float:
+    """
+    Calculate the mean squared error (MSE) between ground truth and predicted values.
+
+    MSE measures the squared difference between true values and predicted values, and it
+    serves as a measure of accuracy for regression models.
+
+    MSE = (1/n) * Σ(y_true - y_pred)^2
+
+    Reference: https://en.wikipedia.org/wiki/Mean_squared_error
+
+    Parameters:
+    - y_true: The true values (ground truth)
+    - y_pred: The predicted values
+
+    >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
+    >>> bool(np.isclose(mean_squared_error(true_values, predicted_values), 0.028))
+    True
+    >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
+    >>> mean_squared_error(true_labels, predicted_probs)
+    Traceback (most recent call last):
+    ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+
+    squared_errors = (y_true - y_pred) ** 2
+    return np.mean(squared_errors)
+
+
+def mean_absolute_error(y_true: np.ndarray, y_pred: np.ndarray) -> float:
+    """
+    Calculates the Mean Absolute Error (MAE) between ground truth (observed)
+        and predicted values.
+
+    MAE measures the absolute difference between true values and predicted values.
+
+    Equation:
+    MAE = (1/n) * Σ(abs(y_true - y_pred))
+
+    Reference: https://en.wikipedia.org/wiki/Mean_absolute_error
+
+    Parameters:
+    - y_true: The true values (ground truth)
+    - y_pred: The predicted values
+
+    >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
+    >>> bool(np.isclose(mean_absolute_error(true_values, predicted_values), 0.16))
+    True
+    >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
+    >>> bool(np.isclose(mean_absolute_error(true_values, predicted_values), 2.16))
+    False
+    >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 5.2])
+    >>> mean_absolute_error(true_labels, predicted_probs)
+    Traceback (most recent call last):
+    ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+
+    return np.mean(abs(y_true - y_pred))
+
+
+def mean_squared_logarithmic_error(y_true: np.ndarray, y_pred: np.ndarray) -> float:
+    """
+    Calculate the mean squared logarithmic error (MSLE) between ground truth and
+    predicted values.
+
+    MSLE measures the squared logarithmic difference between true values and predicted
+    values for regression models. It's particularly useful for dealing with skewed or
+    large-value data, and it's often used when the relative differences between
+    predicted and true values are more important than absolute differences.
+
+    MSLE = (1/n) * Σ(log(1 + y_true) - log(1 + y_pred))^2
+
+    Reference: https://insideaiml.com/blog/MeanSquared-Logarithmic-Error-Loss-1035
+
+    Parameters:
+    - y_true: The true values (ground truth)
+    - y_pred: The predicted values
+
+    >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
+    >>> float(mean_squared_logarithmic_error(true_values, predicted_values))
+    0.0030860877925181344
+    >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
+    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
+    >>> mean_squared_logarithmic_error(true_labels, predicted_probs)
+    Traceback (most recent call last):
+    ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+
+    squared_logarithmic_errors = (np.log1p(y_true) - np.log1p(y_pred)) ** 2
+    return np.mean(squared_logarithmic_errors)
+
+
+def mean_absolute_percentage_error(
+    y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15
+) -> float:
+    """
+    Calculate the Mean Absolute Percentage Error between y_true and y_pred.
+
+    Mean Absolute Percentage Error calculates the average of the absolute
+    percentage differences between the predicted and true values.
+
+    Formula = (Σ|y_true[i]-Y_pred[i]/y_true[i]|)/n
+
+    Source: https://stephenallwright.com/good-mape-score/
+
+    Parameters:
+    y_true (np.ndarray): Numpy array containing true/target values.
+    y_pred (np.ndarray): Numpy array containing predicted values.
+
+    Returns:
+    float: The Mean Absolute Percentage error between y_true and y_pred.
+
+    Examples:
+    >>> y_true = np.array([10, 20, 30, 40])
+    >>> y_pred = np.array([12, 18, 33, 45])
+    >>> float(mean_absolute_percentage_error(y_true, y_pred))
+    0.13125
+
+    >>> y_true = np.array([1, 2, 3, 4])
+    >>> y_pred = np.array([2, 3, 4, 5])
+    >>> float(mean_absolute_percentage_error(y_true, y_pred))
+    0.5208333333333333
+
+    >>> y_true = np.array([34, 37, 44, 47, 48, 48, 46, 43, 32, 27, 26, 24])
+    >>> y_pred = np.array([37, 40, 46, 44, 46, 50, 45, 44, 34, 30, 22, 23])
+    >>> float(mean_absolute_percentage_error(y_true, y_pred))
+    0.064671076436071
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("The length of the two arrays should be the same.")
+
+    y_true = np.where(y_true == 0, epsilon, y_true)
+    absolute_percentage_diff = np.abs((y_true - y_pred) / y_true)
+
+    return np.mean(absolute_percentage_diff)
+
+
+def perplexity_loss(
+    y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-7
+) -> float:
+    """
+    Calculate the perplexity for the y_true and y_pred.
+
+    Compute the Perplexity which useful in predicting language model
+    accuracy in Natural Language Processing (NLP.)
+    Perplexity is measure of how certain the model in its predictions.
+
+    Perplexity Loss = exp(-1/N (Σ ln(p(x)))
+
+    Reference:
+    https://en.wikipedia.org/wiki/Perplexity
+
+    Args:
+        y_true: Actual label encoded sentences of shape (batch_size, sentence_length)
+        y_pred: Predicted sentences of shape (batch_size, sentence_length, vocab_size)
+        epsilon: Small floating point number to avoid getting inf for log(0)
+
+    Returns:
+        Perplexity loss between y_true and y_pred.
+
+    >>> y_true = np.array([[1, 4], [2, 3]])
+    >>> y_pred = np.array(
+    ...    [[[0.28, 0.19, 0.21 , 0.15, 0.15],
+    ...      [0.24, 0.19, 0.09, 0.18, 0.27]],
+    ...      [[0.03, 0.26, 0.21, 0.18, 0.30],
+    ...       [0.28, 0.10, 0.33, 0.15, 0.12]]]
+    ... )
+    >>> float(perplexity_loss(y_true, y_pred))
+    5.0247347775367945
+    >>> y_true = np.array([[1, 4], [2, 3]])
+    >>> y_pred = np.array(
+    ...    [[[0.28, 0.19, 0.21 , 0.15, 0.15],
+    ...      [0.24, 0.19, 0.09, 0.18, 0.27],
+    ...      [0.30, 0.10, 0.20, 0.15, 0.25]],
+    ...      [[0.03, 0.26, 0.21, 0.18, 0.30],
+    ...       [0.28, 0.10, 0.33, 0.15, 0.12],
+    ...       [0.30, 0.10, 0.20, 0.15, 0.25]],]
+    ... )
+    >>> perplexity_loss(y_true, y_pred)
+    Traceback (most recent call last):
+    ...
+    ValueError: Sentence length of y_true and y_pred must be equal.
+    >>> y_true = np.array([[1, 4], [2, 11]])
+    >>> y_pred = np.array(
+    ...    [[[0.28, 0.19, 0.21 , 0.15, 0.15],
+    ...      [0.24, 0.19, 0.09, 0.18, 0.27]],
+    ...      [[0.03, 0.26, 0.21, 0.18, 0.30],
+    ...       [0.28, 0.10, 0.33, 0.15, 0.12]]]
+    ... )
+    >>> perplexity_loss(y_true, y_pred)
+    Traceback (most recent call last):
+    ...
+    ValueError: Label value must not be greater than vocabulary size.
+    >>> y_true = np.array([[1, 4]])
+    >>> y_pred = np.array(
+    ...    [[[0.28, 0.19, 0.21 , 0.15, 0.15],
+    ...      [0.24, 0.19, 0.09, 0.18, 0.27]],
+    ...      [[0.03, 0.26, 0.21, 0.18, 0.30],
+    ...       [0.28, 0.10, 0.33, 0.15, 0.12]]]
+    ... )
+    >>> perplexity_loss(y_true, y_pred)
+    Traceback (most recent call last):
+    ...
+    ValueError: Batch size of y_true and y_pred must be equal.
+    """
+
+    vocab_size = y_pred.shape[2]
+
+    if y_true.shape[0] != y_pred.shape[0]:
+        raise ValueError("Batch size of y_true and y_pred must be equal.")
+    if y_true.shape[1] != y_pred.shape[1]:
+        raise ValueError("Sentence length of y_true and y_pred must be equal.")
+    if np.max(y_true) > vocab_size:
+        raise ValueError("Label value must not be greater than vocabulary size.")
+
+    # Matrix to select prediction value only for true class
+    filter_matrix = np.array(
+        [[list(np.eye(vocab_size)[word]) for word in sentence] for sentence in y_true]
+    )
+
+    # Getting the matrix containing prediction for only true class
+    true_class_pred = np.sum(y_pred * filter_matrix, axis=2).clip(epsilon, 1)
+
+    # Calculating perplexity for each sentence
+    perp_losses = np.exp(np.negative(np.mean(np.log(true_class_pred), axis=1)))
+
+    return np.mean(perp_losses)
+
+
+def smooth_l1_loss(y_true: np.ndarray, y_pred: np.ndarray, beta: float = 1.0) -> float:
+    """
+    Calculate the Smooth L1 Loss between y_true and y_pred.
+
+    The Smooth L1 Loss is less sensitive to outliers than the L2 Loss and is often used
+    in regression problems, such as object detection.
+
+    Smooth L1 Loss =
+        0.5 * (x - y)^2 / beta, if |x - y| < beta
+        |x - y| - 0.5 * beta, otherwise
+
+    Reference:
+    https://pytorch.org/docs/stable/generated/torch.nn.SmoothL1Loss.html
+
+    Args:
+        y_true: Array of true values.
+        y_pred: Array of predicted values.
+        beta: Specifies the threshold at which to change between L1 and L2 loss.
+
+    Returns:
+        The calculated Smooth L1 Loss between y_true and y_pred.
+
+    Raises:
+        ValueError: If the length of the two arrays is not the same.
+
+    >>> y_true = np.array([3, 5, 2, 7])
+    >>> y_pred = np.array([2.9, 4.8, 2.1, 7.2])
+    >>> float(smooth_l1_loss(y_true, y_pred, 1.0))
+    0.012500000000000022
+
+    >>> y_true = np.array([2, 4, 6])
+    >>> y_pred = np.array([1, 5, 7])
+    >>> float(smooth_l1_loss(y_true, y_pred, 1.0))
+    0.5
+
+    >>> y_true = np.array([1, 3, 5, 7])
+    >>> y_pred = np.array([1, 3, 5, 7])
+    >>> float(smooth_l1_loss(y_true, y_pred, 1.0))
+    0.0
+
+    >>> y_true = np.array([1, 3, 5])
+    >>> y_pred = np.array([1, 3, 5, 7])
+    >>> smooth_l1_loss(y_true, y_pred, 1.0)
+    Traceback (most recent call last):
+    ...
+    ValueError: The length of the two arrays should be the same.
+    """
+
+    if len(y_true) != len(y_pred):
+        raise ValueError("The length of the two arrays should be the same.")
+
+    diff = np.abs(y_true - y_pred)
+    loss = np.where(diff < beta, 0.5 * diff**2 / beta, diff - 0.5 * beta)
+    return np.mean(loss)
+
+
+def kullback_leibler_divergence(y_true: np.ndarray, y_pred: np.ndarray) -> float:
+    """
+    Calculate the Kullback-Leibler divergence (KL divergence) loss between true labels
+    and predicted probabilities.
+
+    KL divergence loss quantifies dissimilarity between true labels and predicted
+    probabilities. It's often used in training generative models.
+
+    KL = Σ(y_true * ln(y_true / y_pred))
+
+    Reference: https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence
+
+    Parameters:
+    - y_true: True class probabilities
+    - y_pred: Predicted class probabilities
+
+    >>> true_labels = np.array([0.2, 0.3, 0.5])
+    >>> predicted_probs = np.array([0.3, 0.3, 0.4])
+    >>> float(kullback_leibler_divergence(true_labels, predicted_probs))
+    0.030478754035472025
+    >>> true_labels = np.array([0.2, 0.3, 0.5])
+    >>> predicted_probs = np.array([0.3, 0.3, 0.4, 0.5])
+    >>> kullback_leibler_divergence(true_labels, predicted_probs)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input arrays must have the same length.
+    """
+    if len(y_true) != len(y_pred):
+        raise ValueError("Input arrays must have the same length.")
+
+    kl_loss = y_true * np.log(y_true / y_pred)
+    return np.sum(kl_loss)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/machine_learning/loss_functions/binary_cross_entropy.py b/machine_learning/loss_functions/binary_cross_entropy.py
deleted file mode 100644
index 4ebca7f21757..000000000000
--- a/machine_learning/loss_functions/binary_cross_entropy.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""
-Binary Cross-Entropy (BCE) Loss Function
-
-Description:
-Quantifies dissimilarity between true labels (0 or 1) and predicted probabilities.
-It's widely used in binary classification tasks.
-
-Formula:
-BCE = -Σ(y_true * log(y_pred) + (1 - y_true) * log(1 - y_pred))
-
-Source:
-[Wikipedia - Cross entropy](https://en.wikipedia.org/wiki/Cross_entropy)
-"""
-
-import numpy as np
-
-
-def binary_cross_entropy(
-    y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15
-) -> float:
-    """
-    Calculate the BCE Loss between true labels and predicted probabilities.
-
-    Parameters:
-    - y_true: True binary labels (0 or 1).
-    - y_pred: Predicted probabilities for class 1.
-    - epsilon: Small constant to avoid numerical instability.
-
-    Returns:
-    - bce_loss: Binary Cross-Entropy Loss.
-
-    Example Usage:
-    >>> true_labels = np.array([0, 1, 1, 0, 1])
-    >>> predicted_probs = np.array([0.2, 0.7, 0.9, 0.3, 0.8])
-    >>> binary_cross_entropy(true_labels, predicted_probs)
-    0.2529995012327421
-    >>> true_labels = np.array([0, 1, 1, 0, 1])
-    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
-    >>> binary_cross_entropy(true_labels, predicted_probs)
-    Traceback (most recent call last):
-        ...
-    ValueError: Input arrays must have the same length.
-    """
-    if len(y_true) != len(y_pred):
-        raise ValueError("Input arrays must have the same length.")
-    # Clip predicted probabilities to avoid log(0) and log(1)
-    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
-
-    # Calculate binary cross-entropy loss
-    bce_loss = -(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
-
-    # Take the mean over all samples
-    return np.mean(bce_loss)
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/machine_learning/loss_functions/categorical_cross_entropy.py b/machine_learning/loss_functions/categorical_cross_entropy.py
deleted file mode 100644
index 68f98902b473..000000000000
--- a/machine_learning/loss_functions/categorical_cross_entropy.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""
-Categorical Cross-Entropy Loss
-
-This function calculates the Categorical Cross-Entropy Loss between true class
-labels and predicted class probabilities.
-
-Formula:
-Categorical Cross-Entropy Loss = -Σ(y_true * ln(y_pred))
-
-Resources:
-- [Wikipedia - Cross entropy](https://en.wikipedia.org/wiki/Cross_entropy)
-"""
-
-import numpy as np
-
-
-def categorical_cross_entropy(
-    y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15
-) -> float:
-    """
-    Calculate Categorical Cross-Entropy Loss between true class labels and
-    predicted class probabilities.
-
-    Parameters:
-    - y_true: True class labels (one-hot encoded) as a NumPy array.
-    - y_pred: Predicted class probabilities as a NumPy array.
-    - epsilon: Small constant to avoid numerical instability.
-
-    Returns:
-    - ce_loss: Categorical Cross-Entropy Loss as a floating-point number.
-
-    Example:
-    >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
-    >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]])
-    >>> categorical_cross_entropy(true_labels, pred_probs)
-    0.567395975254385
-
-    >>> y_true = np.array([[1, 0], [0, 1]])
-    >>> y_pred = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
-    >>> categorical_cross_entropy(y_true, y_pred)
-    Traceback (most recent call last):
-        ...
-    ValueError: Input arrays must have the same shape.
-
-    >>> y_true = np.array([[2, 0, 1], [1, 0, 0]])
-    >>> y_pred = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
-    >>> categorical_cross_entropy(y_true, y_pred)
-    Traceback (most recent call last):
-        ...
-    ValueError: y_true must be one-hot encoded.
-
-    >>> y_true = np.array([[1, 0, 1], [1, 0, 0]])
-    >>> y_pred = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]])
-    >>> categorical_cross_entropy(y_true, y_pred)
-    Traceback (most recent call last):
-        ...
-    ValueError: y_true must be one-hot encoded.
-
-    >>> y_true = np.array([[1, 0, 0], [0, 1, 0]])
-    >>> y_pred = np.array([[0.9, 0.1, 0.1], [0.2, 0.7, 0.1]])
-    >>> categorical_cross_entropy(y_true, y_pred)
-    Traceback (most recent call last):
-        ...
-    ValueError: Predicted probabilities must sum to approximately 1.
-    """
-    if y_true.shape != y_pred.shape:
-        raise ValueError("Input arrays must have the same shape.")
-
-    if np.any((y_true != 0) & (y_true != 1)) or np.any(y_true.sum(axis=1) != 1):
-        raise ValueError("y_true must be one-hot encoded.")
-
-    if not np.all(np.isclose(np.sum(y_pred, axis=1), 1, rtol=epsilon, atol=epsilon)):
-        raise ValueError("Predicted probabilities must sum to approximately 1.")
-
-    # Clip predicted probabilities to avoid log(0)
-    y_pred = np.clip(y_pred, epsilon, 1)
-
-    # Calculate categorical cross-entropy loss
-    return -np.sum(y_true * np.log(y_pred))
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/machine_learning/loss_functions/hinge_loss.py b/machine_learning/loss_functions/hinge_loss.py
deleted file mode 100644
index 5480a8cd62ee..000000000000
--- a/machine_learning/loss_functions/hinge_loss.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""
-Hinge Loss
-
-Description:
-Compute the Hinge loss used for training SVM (Support Vector Machine).
-
-Formula:
-loss = max(0, 1 - true * pred)
-
-Reference: https://en.wikipedia.org/wiki/Hinge_loss
-
-Author: Poojan Smart
-Email: smrtpoojan@gmail.com
-"""
-
-import numpy as np
-
-
-def hinge_loss(y_true: np.ndarray, y_pred: np.ndarray) -> float:
-    """
-    Calculate the mean hinge loss for y_true and y_pred for binary classification.
-
-    Args:
-        y_true: Array of actual values (ground truth) encoded as -1 and 1.
-        y_pred: Array of predicted values.
-
-    Returns:
-        The hinge loss between y_true and y_pred.
-
-    Examples:
-    >>> y_true = np.array([-1, 1, 1, -1, 1])
-    >>> pred = np.array([-4, -0.3, 0.7, 5, 10])
-    >>> hinge_loss(y_true, pred)
-    1.52
-    >>> y_true = np.array([-1, 1, 1, -1, 1, 1])
-    >>> pred = np.array([-4, -0.3, 0.7, 5, 10])
-    >>> hinge_loss(y_true, pred)
-    Traceback (most recent call last):
-    ...
-    ValueError: Length of predicted and actual array must be same.
-    >>> y_true = np.array([-1, 1, 10, -1, 1])
-    >>> pred = np.array([-4, -0.3, 0.7, 5, 10])
-    >>> hinge_loss(y_true, pred)
-    Traceback (most recent call last):
-    ...
-    ValueError: y_true can have values -1 or 1 only.
-    """
-
-    if len(y_true) != len(y_pred):
-        raise ValueError("Length of predicted and actual array must be same.")
-
-    # Raise value error when y_true (encoded labels) have any other values
-    # than -1 and 1
-    if np.any((y_true != -1) & (y_true != 1)):
-        raise ValueError("y_true can have values -1 or 1 only.")
-
-    hinge_losses = np.maximum(0, 1.0 - (y_true * y_pred))
-    return np.mean(hinge_losses)
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/machine_learning/loss_functions/huber_loss.py b/machine_learning/loss_functions/huber_loss.py
deleted file mode 100644
index 202e013f2928..000000000000
--- a/machine_learning/loss_functions/huber_loss.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""
-Huber Loss Function
-
-Description:
-Huber loss function describes the penalty incurred by an estimation procedure.
-It serves as a measure of the model's accuracy in regression tasks.
-
-Formula:
-Huber Loss = if |y_true - y_pred| <= delta then 0.5 * (y_true - y_pred)^2
-             else delta * |y_true - y_pred| - 0.5 * delta^2
-
-Source:
-[Wikipedia - Huber Loss](https://en.wikipedia.org/wiki/Huber_loss)
-"""
-
-import numpy as np
-
-
-def huber_loss(y_true: np.ndarray, y_pred: np.ndarray, delta: float) -> float:
-    """
-    Calculate the mean of Huber Loss.
-
-    Parameters:
-    - y_true: The true values (ground truth).
-    - y_pred: The predicted values.
-
-    Returns:
-    - huber_loss: The mean of Huber Loss between y_true and y_pred.
-
-    Example usage:
-    >>> true_values = np.array([0.9, 10.0, 2.0, 1.0, 5.2])
-    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
-    >>> np.isclose(huber_loss(true_values, predicted_values, 1.0), 2.102)
-    True
-    >>> true_labels = np.array([11.0, 21.0, 3.32, 4.0, 5.0])
-    >>> predicted_probs = np.array([8.3, 20.8, 2.9, 11.2, 5.0])
-    >>> np.isclose(huber_loss(true_labels, predicted_probs, 1.0), 1.80164)
-    True
-    """
-
-    if len(y_true) != len(y_pred):
-        raise ValueError("Input arrays must have the same length.")
-
-    huber_mse = 0.5 * (y_true - y_pred) ** 2
-    huber_mae = delta * (np.abs(y_true - y_pred) - 0.5 * delta)
-    return np.where(np.abs(y_true - y_pred) <= delta, huber_mse, huber_mae).mean()
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/machine_learning/loss_functions/mean_squared_error.py b/machine_learning/loss_functions/mean_squared_error.py
deleted file mode 100644
index d2b0e1e158ba..000000000000
--- a/machine_learning/loss_functions/mean_squared_error.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-Mean Squared Error (MSE) Loss Function
-
-Description:
-MSE measures the mean squared difference between true values and predicted values.
-It serves as a measure of the model's accuracy in regression tasks.
-
-Formula:
-MSE = (1/n) * Σ(y_true - y_pred)^2
-
-Source:
-[Wikipedia - Mean squared error](https://en.wikipedia.org/wiki/Mean_squared_error)
-"""
-
-import numpy as np
-
-
-def mean_squared_error(y_true: np.ndarray, y_pred: np.ndarray) -> float:
-    """
-    Calculate the Mean Squared Error (MSE) between two arrays.
-
-    Parameters:
-    - y_true: The true values (ground truth).
-    - y_pred: The predicted values.
-
-    Returns:
-    - mse: The Mean Squared Error between y_true and y_pred.
-
-    Example usage:
-    >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
-    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
-    >>> mean_squared_error(true_values, predicted_values)
-    0.028000000000000032
-    >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
-    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
-    >>> mean_squared_error(true_labels, predicted_probs)
-    Traceback (most recent call last):
-        ...
-    ValueError: Input arrays must have the same length.
-    """
-    if len(y_true) != len(y_pred):
-        raise ValueError("Input arrays must have the same length.")
-
-    squared_errors = (y_true - y_pred) ** 2
-    return np.mean(squared_errors)
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/machine_learning/loss_functions/mean_squared_logarithmic_error.py b/machine_learning/loss_functions/mean_squared_logarithmic_error.py
deleted file mode 100644
index 935ebff37a51..000000000000
--- a/machine_learning/loss_functions/mean_squared_logarithmic_error.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""
-Mean Squared Logarithmic Error (MSLE) Loss Function
-
-Description:
-MSLE measures the mean squared logarithmic difference between
-true values and predicted values, particularly useful when
-dealing with regression problems involving skewed or large-value
-targets. It is often used when the relative differences between
-predicted and true values are more important than absolute
-differences.
-
-Formula:
-MSLE = (1/n) * Σ(log(1 + y_true) - log(1 + y_pred))^2
-
-Source:
-(https://insideaiml.com/blog/MeanSquared-Logarithmic-Error-Loss-1035)
-"""
-
-import numpy as np
-
-
-def mean_squared_logarithmic_error(y_true: np.ndarray, y_pred: np.ndarray) -> float:
-    """
-    Calculate the Mean Squared Logarithmic Error (MSLE) between two arrays.
-
-    Parameters:
-    - y_true: The true values (ground truth).
-    - y_pred: The predicted values.
-
-    Returns:
-    - msle: The Mean Squared Logarithmic Error between y_true and y_pred.
-
-    Example usage:
-    >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
-    >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2])
-    >>> mean_squared_logarithmic_error(true_values, predicted_values)
-    0.0030860877925181344
-    >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
-    >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2])
-    >>> mean_squared_logarithmic_error(true_labels, predicted_probs)
-    Traceback (most recent call last):
-        ...
-    ValueError: Input arrays must have the same length.
-    """
-    if len(y_true) != len(y_pred):
-        raise ValueError("Input arrays must have the same length.")
-
-    squared_logarithmic_errors = (np.log1p(y_true) - np.log1p(y_pred)) ** 2
-    return np.mean(squared_logarithmic_errors)
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/machine_learning/lstm/lstm_prediction.py.DISABLED.txt b/machine_learning/lstm/lstm_prediction.py
similarity index 76%
rename from machine_learning/lstm/lstm_prediction.py.DISABLED.txt
rename to machine_learning/lstm/lstm_prediction.py
index 16530e935ea7..81ac5f01d3d6 100644
--- a/machine_learning/lstm/lstm_prediction.py.DISABLED.txt
+++ b/machine_learning/lstm/lstm_prediction.py
@@ -1,14 +1,15 @@
 """
-    Create a Long Short Term Memory (LSTM) network model
-    An LSTM is a type of Recurrent Neural Network (RNN) as discussed at:
-    * https://colah.github.io/posts/2015-08-Understanding-LSTMs
-    * https://en.wikipedia.org/wiki/Long_short-term_memory
+Create a Long Short Term Memory (LSTM) network model
+An LSTM is a type of Recurrent Neural Network (RNN) as discussed at:
+* https://colah.github.io/posts/2015-08-Understanding-LSTMs
+* https://en.wikipedia.org/wiki/Long_short-term_memory
 """
+
 import numpy as np
 import pandas as pd
+from keras.layers import LSTM, Dense
+from keras.models import Sequential
 from sklearn.preprocessing import MinMaxScaler
-from tensorflow.keras.layers import LSTM, Dense
-from tensorflow.keras.models import Sequential
 
 if __name__ == "__main__":
     """
@@ -17,11 +18,11 @@
     make sure you set the price column on line number 21.  Here we
     use a dataset which have the price on 3rd column.
     """
-    df = pd.read_csv("sample_data.csv", header=None)
-    len_data = df.shape[:1][0]
+    sample_data = pd.read_csv("sample_data.csv", header=None)
+    len_data = sample_data.shape[:1][0]
     # If you're using some other dataset input the target column
-    actual_data = df.iloc[:, 1:2]
-    actual_data = actual_data.values.reshape(len_data, 1)
+    actual_data = sample_data.iloc[:, 1:2]
+    actual_data = actual_data.to_numpy().reshape(len_data, 1)
     actual_data = MinMaxScaler().fit_transform(actual_data)
     look_back = 10
     forward_days = 5
diff --git a/machine_learning/mfcc.py b/machine_learning/mfcc.py
index 7ce8ceb50ff2..dcc3151d5a1a 100644
--- a/machine_learning/mfcc.py
+++ b/machine_learning/mfcc.py
@@ -57,7 +57,6 @@
 Author: Amir Lavasani
 """
 
-
 import logging
 
 import numpy as np
@@ -163,9 +162,9 @@ def normalize(audio: np.ndarray) -> np.ndarray:
     Examples:
     >>> audio = np.array([1, 2, 3, 4, 5])
     >>> normalized_audio = normalize(audio)
-    >>> np.max(normalized_audio)
+    >>> float(np.max(normalized_audio))
     1.0
-    >>> np.min(normalized_audio)
+    >>> float(np.min(normalized_audio))
     0.2
     """
     # Divide the entire audio signal by the maximum absolute value
@@ -230,7 +229,8 @@ def calculate_fft(audio_windowed: np.ndarray, ftt_size: int = 1024) -> np.ndarra
     Examples:
     >>> audio_windowed = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
     >>> audio_fft = calculate_fft(audio_windowed, ftt_size=4)
-    >>> np.allclose(audio_fft[0], np.array([6.0+0.j, -1.5+0.8660254j, -1.5-0.8660254j]))
+    >>> bool(np.allclose(audio_fft[0], np.array([6.0+0.j, -1.5+0.8660254j,
+    ...     -1.5-0.8660254j])))
     True
     """
     # Transpose the audio data to have time in rows and channels in columns
@@ -282,7 +282,7 @@ def freq_to_mel(freq: float) -> float:
         The frequency in mel scale.
 
     Examples:
-    >>> round(freq_to_mel(1000), 2)
+    >>> float(round(freq_to_mel(1000), 2))
     999.99
     """
     # Use the formula to convert frequency to the mel scale
@@ -322,7 +322,7 @@ def mel_spaced_filterbank(
         Mel-spaced filter bank.
 
     Examples:
-    >>> round(mel_spaced_filterbank(8000, 10, 1024)[0][1], 10)
+    >>> float(round(mel_spaced_filterbank(8000, 10, 1024)[0][1], 10))
     0.0004603981
     """
     freq_min = 0
@@ -439,7 +439,7 @@ def discrete_cosine_transform(dct_filter_num: int, filter_num: int) -> np.ndarra
         The DCT basis matrix.
 
     Examples:
-    >>> round(discrete_cosine_transform(3, 5)[0][0], 5)
+    >>> float(round(discrete_cosine_transform(3, 5)[0][0], 5))
     0.44721
     """
     basis = np.empty((dct_filter_num, filter_num))
diff --git a/machine_learning/multilayer_perceptron_classifier.py b/machine_learning/multilayer_perceptron_classifier.py
index e99a4131e972..40f998c7dfa2 100644
--- a/machine_learning/multilayer_perceptron_classifier.py
+++ b/machine_learning/multilayer_perceptron_classifier.py
@@ -17,7 +17,7 @@
 
 def wrapper(y):
     """
-    >>> wrapper(Y)
+    >>> [int(x) for x in wrapper(Y)]
     [0, 0, 1]
     """
     return list(y)
diff --git a/machine_learning/polynomial_regression.py b/machine_learning/polynomial_regression.py
index 5bafea96f41e..212f40bea197 100644
--- a/machine_learning/polynomial_regression.py
+++ b/machine_learning/polynomial_regression.py
@@ -11,7 +11,7 @@
 
 β = (XᵀX)⁻¹Xᵀy = X⁺y
 
-where X is the design matrix, y is the response vector, and X⁺ denotes the Moore–Penrose
+where X is the design matrix, y is the response vector, and X⁺ denotes the Moore-Penrose
 pseudoinverse of X. In the case of polynomial regression, the design matrix is
 
     |1  x₁  x₁² ⋯ x₁ᵐ|
@@ -106,7 +106,7 @@ def fit(self, x_train: np.ndarray, y_train: np.ndarray) -> None:
 
         β = (XᵀX)⁻¹Xᵀy = X⁺y
 
-        where X⁺ denotes the Moore–Penrose pseudoinverse of the design matrix X. This
+        where X⁺ denotes the Moore-Penrose pseudoinverse of the design matrix X. This
         function computes X⁺ using singular value decomposition (SVD).
 
         References:
@@ -146,7 +146,7 @@ def fit(self, x_train: np.ndarray, y_train: np.ndarray) -> None:
                 "Design matrix is not full rank, can't compute coefficients"
             )
 
-        # np.linalg.pinv() computes the Moore–Penrose pseudoinverse using SVD
+        # np.linalg.pinv() computes the Moore-Penrose pseudoinverse using SVD
         self.params = np.linalg.pinv(X) @ y_train
 
     def predict(self, data: np.ndarray) -> np.ndarray:
diff --git a/machine_learning/principle_component_analysis.py b/machine_learning/principle_component_analysis.py
new file mode 100644
index 000000000000..46ccdb968494
--- /dev/null
+++ b/machine_learning/principle_component_analysis.py
@@ -0,0 +1,85 @@
+"""
+Principal Component Analysis (PCA) is a dimensionality reduction technique
+used in machine learning. It transforms high-dimensional data into a lower-dimensional
+representation while retaining as much variance as possible.
+
+This implementation follows best practices, including:
+- Standardizing the dataset.
+- Computing principal components using Singular Value Decomposition (SVD).
+- Returning transformed data and explained variance ratio.
+"""
+
+import doctest
+
+import numpy as np
+from sklearn.datasets import load_iris
+from sklearn.decomposition import PCA
+from sklearn.preprocessing import StandardScaler
+
+
+def collect_dataset() -> tuple[np.ndarray, np.ndarray]:
+    """
+    Collects the dataset (Iris dataset) and returns feature matrix and target values.
+
+    :return: Tuple containing feature matrix (X) and target labels (y)
+
+    Example:
+    >>> X, y = collect_dataset()
+    >>> X.shape
+    (150, 4)
+    >>> y.shape
+    (150,)
+    """
+    data = load_iris()
+    return np.array(data.data), np.array(data.target)
+
+
+def apply_pca(data_x: np.ndarray, n_components: int) -> tuple[np.ndarray, np.ndarray]:
+    """
+    Applies Principal Component Analysis (PCA) to reduce dimensionality.
+
+    :param data_x: Original dataset (features)
+    :param n_components: Number of principal components to retain
+    :return: Tuple containing transformed dataset and explained variance ratio
+
+    Example:
+    >>> X, _ = collect_dataset()
+    >>> transformed_X, variance = apply_pca(X, 2)
+    >>> transformed_X.shape
+    (150, 2)
+    >>> len(variance) == 2
+    True
+    """
+    # Standardizing the dataset
+    scaler = StandardScaler()
+    data_x_scaled = scaler.fit_transform(data_x)
+
+    # Applying PCA
+    pca = PCA(n_components=n_components)
+    principal_components = pca.fit_transform(data_x_scaled)
+
+    return principal_components, pca.explained_variance_ratio_
+
+
+def main() -> None:
+    """
+    Driver function to execute PCA and display results.
+    """
+    data_x, data_y = collect_dataset()
+
+    # Number of principal components to retain
+    n_components = 2
+
+    # Apply PCA
+    transformed_data, variance_ratio = apply_pca(data_x, n_components)
+
+    print("Transformed Dataset (First 5 rows):")
+    print(transformed_data[:5])
+
+    print("\nExplained Variance Ratio:")
+    print(variance_ratio)
+
+
+if __name__ == "__main__":
+    doctest.testmod()
+    main()
diff --git a/machine_learning/scoring_functions.py b/machine_learning/scoring_functions.py
index 08b969a95c3b..f6b685f4f98a 100644
--- a/machine_learning/scoring_functions.py
+++ b/machine_learning/scoring_functions.py
@@ -20,11 +20,11 @@ def mae(predict, actual):
     """
     Examples(rounded for precision):
     >>> actual = [1,2,3];predict = [1,4,3]
-    >>> np.around(mae(predict,actual),decimals = 2)
+    >>> float(np.around(mae(predict,actual),decimals = 2))
     0.67
 
     >>> actual = [1,1,1];predict = [1,1,1]
-    >>> mae(predict,actual)
+    >>> float(mae(predict,actual))
     0.0
     """
     predict = np.array(predict)
@@ -41,11 +41,11 @@ def mse(predict, actual):
     """
     Examples(rounded for precision):
     >>> actual = [1,2,3];predict = [1,4,3]
-    >>> np.around(mse(predict,actual),decimals = 2)
+    >>> float(np.around(mse(predict,actual),decimals = 2))
     1.33
 
     >>> actual = [1,1,1];predict = [1,1,1]
-    >>> mse(predict,actual)
+    >>> float(mse(predict,actual))
     0.0
     """
     predict = np.array(predict)
@@ -63,11 +63,11 @@ def rmse(predict, actual):
     """
     Examples(rounded for precision):
     >>> actual = [1,2,3];predict = [1,4,3]
-    >>> np.around(rmse(predict,actual),decimals = 2)
+    >>> float(np.around(rmse(predict,actual),decimals = 2))
     1.15
 
     >>> actual = [1,1,1];predict = [1,1,1]
-    >>> rmse(predict,actual)
+    >>> float(rmse(predict,actual))
     0.0
     """
     predict = np.array(predict)
@@ -84,12 +84,10 @@ def rmse(predict, actual):
 def rmsle(predict, actual):
     """
     Examples(rounded for precision):
-    >>> actual = [10,10,30];predict = [10,2,30]
-    >>> np.around(rmsle(predict,actual),decimals = 2)
+    >>> float(np.around(rmsle(predict=[10, 2, 30], actual=[10, 10, 30]), decimals=2))
     0.75
 
-    >>> actual = [1,1,1];predict = [1,1,1]
-    >>> rmsle(predict,actual)
+    >>> float(rmsle(predict=[1, 1, 1], actual=[1, 1, 1]))
     0.0
     """
     predict = np.array(predict)
@@ -117,12 +115,12 @@ def mbd(predict, actual):
 
     Here the model overpredicts
     >>> actual = [1,2,3];predict = [2,3,4]
-    >>> np.around(mbd(predict,actual),decimals = 2)
+    >>> float(np.around(mbd(predict,actual),decimals = 2))
     50.0
 
     Here the model underpredicts
     >>> actual = [1,2,3];predict = [0,1,1]
-    >>> np.around(mbd(predict,actual),decimals = 2)
+    >>> float(np.around(mbd(predict,actual),decimals = 2))
     -66.67
     """
     predict = np.array(predict)
diff --git a/machine_learning/self_organizing_map.py b/machine_learning/self_organizing_map.py
index 32fdf1d2b41d..fb9d0074e791 100644
--- a/machine_learning/self_organizing_map.py
+++ b/machine_learning/self_organizing_map.py
@@ -1,6 +1,7 @@
 """
 https://en.wikipedia.org/wiki/Self-organizing_map
 """
+
 import math
 
 
diff --git a/machine_learning/sequential_minimum_optimization.py b/machine_learning/sequential_minimum_optimization.py
index 9e2304859f8d..625fc28fe60c 100644
--- a/machine_learning/sequential_minimum_optimization.py
+++ b/machine_learning/sequential_minimum_optimization.py
@@ -1,11 +1,9 @@
 """
-    Implementation of sequential minimal optimization (SMO) for support vector machines
-    (SVM).
+Sequential minimal optimization (SMO) for support vector machines (SVM)
 
-    Sequential minimal optimization (SMO) is an algorithm for solving the quadratic
-    programming (QP) problem that arises during the training of support vector
-    machines.
-    It was invented by John Platt in 1998.
+Sequential minimal optimization (SMO) is an algorithm for solving the quadratic
+programming (QP) problem that arises during the training of SVMs. It was invented by
+John Platt in 1998.
 
 Input:
     0: type: numpy.ndarray.
@@ -30,7 +28,6 @@
     https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-98-14.pdf
 """
 
-
 import os
 import sys
 import urllib.request
@@ -125,8 +122,7 @@ def fit(self):
             b_old = self._b
             self._b = b
 
-            # 4:  update error value,here we only calculate those non-bound samples'
-            #     error
+            # 4: update error, here we only calculate the error for non-bound samples
             self._unbound = [i for i in self._all_samples if self._is_unbound(i)]
             for s in self.unbound:
                 if s in (i1, i2):
@@ -137,7 +133,7 @@ def fit(self):
                     + (self._b - b_old)
                 )
 
-            # if i1 or i2 is non-bound,update there error value to zero
+            # if i1 or i2 is non-bound, update their error value to zero
             if self._is_unbound(i1):
                 self._error[i1] = 0
             if self._is_unbound(i2):
@@ -162,7 +158,7 @@ def predict(self, test_samples, classify=True):
                 results.append(result)
         return np.array(results)
 
-    # Check if alpha violate KKT condition
+    # Check if alpha violates the KKT condition
     def _check_obey_kkt(self, index):
         alphas = self.alphas
         tol = self._tol
@@ -173,20 +169,19 @@ def _check_obey_kkt(self, index):
 
     # Get value calculated from kernel function
     def _k(self, i1, i2):
-        # for test samples,use Kernel function
+        # for test samples, use kernel function
         if isinstance(i2, np.ndarray):
             return self.Kernel(self.samples[i1], i2)
-        # for train samples,Kernel values have been saved in matrix
+        # for training samples, kernel values have been saved in matrix
         else:
             return self._K_matrix[i1, i2]
 
-    # Get sample's error
+    # Get error for sample
     def _e(self, index):
         """
         Two cases:
-            1:Sample[index] is non-bound,Fetch error from list: _error
-            2:sample[index] is bound,Use predicted value deduct true value: g(xi) - yi
-
+            1: Sample[index] is non-bound, fetch error from list: _error
+            2: sample[index] is bound, use predicted value minus true value: g(xi) - yi
         """
         # get from error data
         if self._is_unbound(index):
@@ -197,7 +192,7 @@ def _e(self, index):
             yi = self.tags[index]
             return gx - yi
 
-    # Calculate Kernel matrix of all possible i1,i2 ,saving time
+    # Calculate kernel matrix of all possible i1, i2, saving time
     def _calculate_k_matrix(self):
         k_matrix = np.zeros([self.length, self.length])
         for i in self._all_samples:
@@ -207,7 +202,7 @@ def _calculate_k_matrix(self):
                 )
         return k_matrix
 
-    # Predict test sample's tag
+    # Predict tag for test sample
     def _predict(self, sample):
         k = self._k
         predicted_value = (
@@ -223,30 +218,31 @@ def _predict(self, sample):
 
     # Choose alpha1 and alpha2
     def _choose_alphas(self):
-        locis = yield from self._choose_a1()
-        if not locis:
+        loci = yield from self._choose_a1()
+        if not loci:
             return None
-        return locis
+        return loci
 
     def _choose_a1(self):
         """
-        Choose first alpha ;steps:
-           1:First loop over all sample
-           2:Second loop over all non-bound samples till all non-bound samples does not
-               voilate kkt condition.
-           3:Repeat this two process endlessly,till all samples does not voilate kkt
-               condition samples after first loop.
+        Choose first alpha
+        Steps:
+            1: First loop over all samples
+            2: Second loop over all non-bound samples until no non-bound samples violate
+               the KKT condition.
+            3: Repeat these two processes until no samples violate the KKT condition
+               after the first loop.
         """
         while True:
             all_not_obey = True
             # all sample
-            print("scanning all sample!")
+            print("Scanning all samples!")
             for i1 in [i for i in self._all_samples if self._check_obey_kkt(i)]:
                 all_not_obey = False
                 yield from self._choose_a2(i1)
 
             # non-bound sample
-            print("scanning non-bound sample!")
+            print("Scanning non-bound samples!")
             while True:
                 not_obey = True
                 for i1 in [
@@ -257,20 +253,21 @@ def _choose_a1(self):
                     not_obey = False
                     yield from self._choose_a2(i1)
                 if not_obey:
-                    print("all non-bound samples fit the KKT condition!")
+                    print("All non-bound samples satisfy the KKT condition!")
                     break
             if all_not_obey:
-                print("all samples fit the KKT condition! Optimization done!")
+                print("All samples satisfy the KKT condition!")
                 break
         return False
 
     def _choose_a2(self, i1):
         """
-        Choose the second alpha by using heuristic algorithm ;steps:
-           1: Choose alpha2 which gets the maximum step size (|E1 - E2|).
-           2: Start in a random point,loop over all non-bound samples till alpha1 and
+        Choose the second alpha using a heuristic algorithm
+        Steps:
+            1: Choose alpha2 that maximizes the step size (|E1 - E2|).
+            2: Start in a random point, loop over all non-bound samples till alpha1 and
                alpha2 are optimized.
-           3: Start in a random point,loop over all samples till alpha1 and alpha2 are
+            3: Start in a random point, loop over all samples till alpha1 and alpha2 are
                optimized.
         """
         self._unbound = [i for i in self._all_samples if self._is_unbound(i)]
@@ -290,12 +287,13 @@ def _choose_a2(self, i1):
             if cmd is None:
                 return
 
-        for i2 in np.roll(self.unbound, np.random.choice(self.length)):
+        rng = np.random.default_rng()
+        for i2 in np.roll(self.unbound, rng.choice(self.length)):
             cmd = yield i1, i2
             if cmd is None:
                 return
 
-        for i2 in np.roll(self._all_samples, np.random.choice(self.length)):
+        for i2 in np.roll(self._all_samples, rng.choice(self.length)):
             cmd = yield i1, i2
             if cmd is None:
                 return
@@ -306,12 +304,12 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2):
         if i1 == i2:
             return None, None
 
-        # calculate L and H  which bound the new alpha2
+        # calculate L and H which bound the new alpha2
         s = y1 * y2
         if s == -1:
-            l, h = max(0.0, a2 - a1), min(self._c, self._c + a2 - a1)
+            l, h = max(0.0, a2 - a1), min(self._c, self._c + a2 - a1)  # noqa: E741
         else:
-            l, h = max(0.0, a2 + a1 - self._c), min(self._c, a2 + a1)
+            l, h = max(0.0, a2 + a1 - self._c), min(self._c, a2 + a1)  # noqa: E741
         if l == h:
             return None, None
 
@@ -320,7 +318,7 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2):
         k22 = k(i2, i2)
         k12 = k(i1, i2)
 
-        # select the new alpha2 which could get the minimal objectives
+        # select the new alpha2 which could achieve the minimal objectives
         if (eta := k11 + k22 - 2.0 * k12) > 0.0:
             a2_new_unc = a2 + (y2 * (e1 - e2)) / eta
             # a2_new has a boundary
@@ -335,7 +333,7 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2):
             l1 = a1 + s * (a2 - l)
             h1 = a1 + s * (a2 - h)
 
-            # way 1
+            # Method 1
             f1 = y1 * (e1 + b) - a1 * k(i1, i1) - s * a2 * k(i1, i2)
             f2 = y2 * (e2 + b) - a2 * k(i2, i2) - s * a1 * k(i1, i2)
             ol = (
@@ -353,9 +351,8 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2):
                 + s * h * h1 * k(i1, i2)
             )
             """
-            # way 2
-            Use objective function check which alpha2 new could get the minimal
-            objectives
+            Method 2: Use objective function to check which alpha2_new could achieve the
+            minimal objectives
             """
             if ol < (oh - self._eps):
                 a2_new = l
@@ -375,7 +372,7 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2):
 
         return a1_new, a2_new
 
-    # Normalise data using min_max way
+    # Normalize data using min-max method
     def _norm(self, data):
         if self._init:
             self._min = np.min(data, axis=0)
@@ -424,7 +421,7 @@ def _rbf(self, v1, v2):
 
     def _check(self):
         if self._kernel == self._rbf and self.gamma < 0:
-            raise ValueError("gamma value must greater than 0")
+            raise ValueError("gamma value must be non-negative")
 
     def _get_kernel(self, kernel_name):
         maps = {"linear": self._linear, "poly": self._polynomial, "rbf": self._rbf}
@@ -444,26 +441,30 @@ def call_func(*args, **kwargs):
         start_time = time.time()
         func(*args, **kwargs)
         end_time = time.time()
-        print(f"smo algorithm cost {end_time - start_time} seconds")
+        print(f"SMO algorithm cost {end_time - start_time} seconds")
 
     return call_func
 
 
 @count_time
-def test_cancel_data():
-    print("Hello!\nStart test svm by smo algorithm!")
+def test_cancer_data():
+    print("Hello!\nStart test SVM using the SMO algorithm!")
     # 0: download dataset and load into pandas' dataframe
-    if not os.path.exists(r"cancel_data.csv"):
+    if not os.path.exists(r"cancer_data.csv"):
         request = urllib.request.Request(  # noqa: S310
             CANCER_DATASET_URL,
             headers={"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)"},
         )
         response = urllib.request.urlopen(request)  # noqa: S310
         content = response.read().decode("utf-8")
-        with open(r"cancel_data.csv", "w") as f:
+        with open(r"cancer_data.csv", "w") as f:
             f.write(content)
 
-    data = pd.read_csv(r"cancel_data.csv", header=None)
+    data = pd.read_csv(
+        "cancer_data.csv",
+        header=None,
+        dtype={0: str},  # Assuming the first column contains string data
+    )
 
     # 1: pre-processing data
     del data[data.columns.tolist()[0]]
@@ -475,14 +476,14 @@ def test_cancel_data():
     train_data, test_data = samples[:328, :], samples[328:, :]
     test_tags, test_samples = test_data[:, 0], test_data[:, 1:]
 
-    # 3: choose kernel function,and set initial alphas to zero(optional)
-    mykernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5)
+    # 3: choose kernel function, and set initial alphas to zero (optional)
+    my_kernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5)
     al = np.zeros(train_data.shape[0])
 
     # 4: calculating best alphas using SMO algorithm and predict test_data samples
     mysvm = SmoSVM(
         train=train_data,
-        kernel_func=mykernel,
+        kernel_func=my_kernel,
         alpha_list=al,
         cost=0.4,
         b=0.0,
@@ -497,30 +498,30 @@ def test_cancel_data():
     for i in range(test_tags.shape[0]):
         if test_tags[i] == predict[i]:
             score += 1
-    print(f"\nall: {test_num}\nright: {score}\nfalse: {test_num - score}")
+    print(f"\nAll: {test_num}\nCorrect: {score}\nIncorrect: {test_num - score}")
     print(f"Rough Accuracy: {score / test_tags.shape[0]}")
 
 
 def test_demonstration():
     # change stdout
-    print("\nStart plot,please wait!!!")
+    print("\nStarting plot, please wait!")
     sys.stdout = open(os.devnull, "w")
 
     ax1 = plt.subplot2grid((2, 2), (0, 0))
     ax2 = plt.subplot2grid((2, 2), (0, 1))
     ax3 = plt.subplot2grid((2, 2), (1, 0))
     ax4 = plt.subplot2grid((2, 2), (1, 1))
-    ax1.set_title("linear svm,cost:0.1")
+    ax1.set_title("Linear SVM, cost = 0.1")
     test_linear_kernel(ax1, cost=0.1)
-    ax2.set_title("linear svm,cost:500")
+    ax2.set_title("Linear SVM, cost = 500")
     test_linear_kernel(ax2, cost=500)
-    ax3.set_title("rbf kernel svm,cost:0.1")
+    ax3.set_title("RBF kernel SVM, cost = 0.1")
     test_rbf_kernel(ax3, cost=0.1)
-    ax4.set_title("rbf kernel svm,cost:500")
+    ax4.set_title("RBF kernel SVM, cost = 500")
     test_rbf_kernel(ax4, cost=500)
 
     sys.stdout = sys.__stdout__
-    print("Plot done!!!")
+    print("Plot done!")
 
 
 def test_linear_kernel(ax, cost):
@@ -531,10 +532,10 @@ def test_linear_kernel(ax, cost):
     scaler = StandardScaler()
     train_x_scaled = scaler.fit_transform(train_x, train_y)
     train_data = np.hstack((train_y.reshape(500, 1), train_x_scaled))
-    mykernel = Kernel(kernel="linear", degree=5, coef0=1, gamma=0.5)
+    my_kernel = Kernel(kernel="linear", degree=5, coef0=1, gamma=0.5)
     mysvm = SmoSVM(
         train=train_data,
-        kernel_func=mykernel,
+        kernel_func=my_kernel,
         cost=cost,
         tolerance=0.001,
         auto_norm=False,
@@ -551,10 +552,10 @@ def test_rbf_kernel(ax, cost):
     scaler = StandardScaler()
     train_x_scaled = scaler.fit_transform(train_x, train_y)
     train_data = np.hstack((train_y.reshape(500, 1), train_x_scaled))
-    mykernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5)
+    my_kernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5)
     mysvm = SmoSVM(
         train=train_data,
-        kernel_func=mykernel,
+        kernel_func=my_kernel,
         cost=cost,
         tolerance=0.001,
         auto_norm=False,
@@ -567,11 +568,11 @@ def plot_partition_boundary(
     model, train_data, ax, resolution=100, colors=("b", "k", "r")
 ):
     """
-    We can not get the optimum w of our kernel svm model which is different from linear
-    svm.  For this reason, we generate randomly distributed points with high desity and
-    prediced values of these points are calculated by using our trained model. Then we
-    could use this prediced values to draw contour map.
-    And this contour map can represent svm's partition boundary.
+    We cannot get the optimal w of our kernel SVM model, which is different from a
+    linear SVM.  For this reason, we generate randomly distributed points with high
+    density, and predicted values of these points are calculated using our trained
+    model. Then we could use this predicted values to draw contour map, and this contour
+    map represents the SVM's partition boundary.
     """
     train_data_x = train_data[:, 1]
     train_data_y = train_data[:, 2]
@@ -589,7 +590,7 @@ def plot_partition_boundary(
     ax.contour(
         xrange,
         yrange,
-        np.mat(grid).T,
+        np.asmatrix(grid).T,
         levels=(-1, 0, 1),
         linestyles=("--", "-", "--"),
         linewidths=(1, 1, 1),
@@ -616,6 +617,6 @@ def plot_partition_boundary(
 
 
 if __name__ == "__main__":
-    test_cancel_data()
+    test_cancer_data()
     test_demonstration()
     plt.show()
diff --git a/machine_learning/similarity_search.py b/machine_learning/similarity_search.py
index 7a23ec463c8f..c8a573796882 100644
--- a/machine_learning/similarity_search.py
+++ b/machine_learning/similarity_search.py
@@ -7,6 +7,7 @@
     1. the nearest vector
     2. distance between the vector and the nearest vector (float)
 """
+
 from __future__ import annotations
 
 import math
@@ -152,7 +153,7 @@ def cosine_similarity(input_a: np.ndarray, input_b: np.ndarray) -> float:
     >>> cosine_similarity(np.array([1, 2]), np.array([6, 32]))
     0.9615239476408232
     """
-    return np.dot(input_a, input_b) / (norm(input_a) * norm(input_b))
+    return float(np.dot(input_a, input_b) / (norm(input_a) * norm(input_b)))
 
 
 if __name__ == "__main__":
diff --git a/machine_learning/support_vector_machines.py b/machine_learning/support_vector_machines.py
index 24046115ebc4..d17c9044a3e9 100644
--- a/machine_learning/support_vector_machines.py
+++ b/machine_learning/support_vector_machines.py
@@ -14,11 +14,11 @@ def norm_squared(vector: ndarray) -> float:
     Returns:
         float: squared second norm of vector
 
-    >>> norm_squared([1, 2])
+    >>> int(norm_squared([1, 2]))
     5
-    >>> norm_squared(np.asarray([1, 2]))
+    >>> int(norm_squared(np.asarray([1, 2])))
     5
-    >>> norm_squared([0, 0])
+    >>> int(norm_squared([0, 0]))
     0
     """
     return np.dot(vector, vector)
diff --git a/maths/allocation_number.py b/maths/allocation_number.py
index d419e74d01ff..52f1ac4bdb23 100644
--- a/maths/allocation_number.py
+++ b/maths/allocation_number.py
@@ -5,6 +5,7 @@
     for i in allocation_list:
         requests.get(url,headers={'Range':f'bytes={i}'})
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/area.py b/maths/area.py
index ea7216c8fe3f..31a654206977 100644
--- a/maths/area.py
+++ b/maths/area.py
@@ -2,6 +2,7 @@
 Find the area of various geometric shapes
 Wikipedia reference: https://en.wikipedia.org/wiki/Area
 """
+
 from math import pi, sqrt, tan
 
 
diff --git a/maths/area_under_curve.py b/maths/area_under_curve.py
index 0da6546b2e36..10aec768fa09 100644
--- a/maths/area_under_curve.py
+++ b/maths/area_under_curve.py
@@ -1,6 +1,7 @@
 """
 Approximates the area under the curve using the trapezoidal rule
 """
+
 from __future__ import annotations
 
 from collections.abc import Callable
diff --git a/maths/basic_maths.py b/maths/basic_maths.py
index c9e3d00fa23b..833f31c18b9e 100644
--- a/maths/basic_maths.py
+++ b/maths/basic_maths.py
@@ -1,4 +1,5 @@
 """Implementation of Basic Math in Python."""
+
 import math
 
 
diff --git a/maths/binary_exp_mod.py b/maths/binary_exp_mod.py
deleted file mode 100644
index 8893182a3496..000000000000
--- a/maths/binary_exp_mod.py
+++ /dev/null
@@ -1,28 +0,0 @@
-def bin_exp_mod(a: int, n: int, b: int) -> int:
-    """
-    >>> bin_exp_mod(3, 4, 5)
-    1
-    >>> bin_exp_mod(7, 13, 10)
-    7
-    """
-    # mod b
-    assert b != 0, "This cannot accept modulo that is == 0"
-    if n == 0:
-        return 1
-
-    if n % 2 == 1:
-        return (bin_exp_mod(a, n - 1, b) * a) % b
-
-    r = bin_exp_mod(a, n // 2, b)
-    return (r * r) % b
-
-
-if __name__ == "__main__":
-    try:
-        BASE = int(input("Enter Base : ").strip())
-        POWER = int(input("Enter Power : ").strip())
-        MODULO = int(input("Enter Modulo : ").strip())
-    except ValueError:
-        print("Invalid literal for integer")
-
-    print(bin_exp_mod(BASE, POWER, MODULO))
diff --git a/maths/binary_exponentiation.py b/maths/binary_exponentiation.py
index f613767f547e..51ce86d26c41 100644
--- a/maths/binary_exponentiation.py
+++ b/maths/binary_exponentiation.py
@@ -1,48 +1,196 @@
-"""Binary Exponentiation."""
+"""
+Binary Exponentiation
 
-# Author : Junth Basnet
-# Time Complexity : O(logn)
+This is a method to find a^b in O(log b) time complexity and is one of the most commonly
+used methods of exponentiation. The method is also useful for modular exponentiation,
+when the solution to (a^b) % c is required.
 
+To calculate a^b:
+- If b is even, then a^b = (a * a)^(b / 2)
+- If b is odd, then a^b = a * a^(b - 1)
+Repeat until b = 1 or b = 0
 
-def binary_exponentiation(a: int, n: int) -> int:
+For modular exponentiation, we use the fact that (a * b) % c = ((a % c) * (b % c)) % c
+"""
+
+
+def binary_exp_recursive(base: float, exponent: int) -> float:
     """
-    Compute a number raised by some quantity
-    >>> binary_exponentiation(-1, 3)
+    Computes a^b recursively, where a is the base and b is the exponent
+
+    >>> binary_exp_recursive(3, 5)
+    243
+    >>> binary_exp_recursive(11, 13)
+    34522712143931
+    >>> binary_exp_recursive(-1, 3)
     -1
-    >>> binary_exponentiation(-1, 4)
+    >>> binary_exp_recursive(0, 5)
+    0
+    >>> binary_exp_recursive(3, 1)
+    3
+    >>> binary_exp_recursive(3, 0)
     1
-    >>> binary_exponentiation(2, 2)
-    4
-    >>> binary_exponentiation(3, 5)
+    >>> binary_exp_recursive(1.5, 4)
+    5.0625
+    >>> binary_exp_recursive(3, -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: Exponent must be a non-negative integer
+    """
+    if exponent < 0:
+        raise ValueError("Exponent must be a non-negative integer")
+
+    if exponent == 0:
+        return 1
+
+    if exponent % 2 == 1:
+        return binary_exp_recursive(base, exponent - 1) * base
+
+    b = binary_exp_recursive(base, exponent // 2)
+    return b * b
+
+
+def binary_exp_iterative(base: float, exponent: int) -> float:
+    """
+    Computes a^b iteratively, where a is the base and b is the exponent
+
+    >>> binary_exp_iterative(3, 5)
     243
-    >>> binary_exponentiation(10, 3)
-    1000
-    >>> binary_exponentiation(5e3, 1)
-    5000.0
-    >>> binary_exponentiation(-5e3, 1)
-    -5000.0
-    """
-    if n == 0:
+    >>> binary_exp_iterative(11, 13)
+    34522712143931
+    >>> binary_exp_iterative(-1, 3)
+    -1
+    >>> binary_exp_iterative(0, 5)
+    0
+    >>> binary_exp_iterative(3, 1)
+    3
+    >>> binary_exp_iterative(3, 0)
+    1
+    >>> binary_exp_iterative(1.5, 4)
+    5.0625
+    >>> binary_exp_iterative(3, -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: Exponent must be a non-negative integer
+    """
+    if exponent < 0:
+        raise ValueError("Exponent must be a non-negative integer")
+
+    res: int | float = 1
+    while exponent > 0:
+        if exponent & 1:
+            res *= base
+
+        base *= base
+        exponent >>= 1
+
+    return res
+
+
+def binary_exp_mod_recursive(base: float, exponent: int, modulus: int) -> float:
+    """
+    Computes a^b % c recursively, where a is the base, b is the exponent, and c is the
+    modulus
+
+    >>> binary_exp_mod_recursive(3, 4, 5)
+    1
+    >>> binary_exp_mod_recursive(11, 13, 7)
+    4
+    >>> binary_exp_mod_recursive(1.5, 4, 3)
+    2.0625
+    >>> binary_exp_mod_recursive(7, -1, 10)
+    Traceback (most recent call last):
+        ...
+    ValueError: Exponent must be a non-negative integer
+    >>> binary_exp_mod_recursive(7, 13, 0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Modulus must be a positive integer
+    """
+    if exponent < 0:
+        raise ValueError("Exponent must be a non-negative integer")
+    if modulus <= 0:
+        raise ValueError("Modulus must be a positive integer")
+
+    if exponent == 0:
         return 1
 
-    elif n % 2 == 1:
-        return binary_exponentiation(a, n - 1) * a
+    if exponent % 2 == 1:
+        return (binary_exp_mod_recursive(base, exponent - 1, modulus) * base) % modulus
 
-    else:
-        b = binary_exponentiation(a, n // 2)
-        return b * b
+    r = binary_exp_mod_recursive(base, exponent // 2, modulus)
+    return (r * r) % modulus
 
 
-if __name__ == "__main__":
-    import doctest
+def binary_exp_mod_iterative(base: float, exponent: int, modulus: int) -> float:
+    """
+    Computes a^b % c iteratively, where a is the base, b is the exponent, and c is the
+    modulus
 
-    doctest.testmod()
+    >>> binary_exp_mod_iterative(3, 4, 5)
+    1
+    >>> binary_exp_mod_iterative(11, 13, 7)
+    4
+    >>> binary_exp_mod_iterative(1.5, 4, 3)
+    2.0625
+    >>> binary_exp_mod_iterative(7, -1, 10)
+    Traceback (most recent call last):
+        ...
+    ValueError: Exponent must be a non-negative integer
+    >>> binary_exp_mod_iterative(7, 13, 0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Modulus must be a positive integer
+    """
+    if exponent < 0:
+        raise ValueError("Exponent must be a non-negative integer")
+    if modulus <= 0:
+        raise ValueError("Modulus must be a positive integer")
+
+    res: int | float = 1
+    while exponent > 0:
+        if exponent & 1:
+            res = ((res % modulus) * (base % modulus)) % modulus
+
+        base *= base
+        exponent >>= 1
+
+    return res
+
+
+if __name__ == "__main__":
+    from timeit import timeit
 
-    try:
-        BASE = int(float(input("Enter Base : ").strip()))
-        POWER = int(input("Enter Power : ").strip())
-    except ValueError:
-        print("Invalid literal for integer")
+    a = 1269380576
+    b = 374
+    c = 34
 
-    RESULT = binary_exponentiation(BASE, POWER)
-    print(f"{BASE}^({POWER}) : {RESULT}")
+    runs = 100_000
+    print(
+        timeit(
+            f"binary_exp_recursive({a}, {b})",
+            setup="from __main__ import binary_exp_recursive",
+            number=runs,
+        )
+    )
+    print(
+        timeit(
+            f"binary_exp_iterative({a}, {b})",
+            setup="from __main__ import binary_exp_iterative",
+            number=runs,
+        )
+    )
+    print(
+        timeit(
+            f"binary_exp_mod_recursive({a}, {b}, {c})",
+            setup="from __main__ import binary_exp_mod_recursive",
+            number=runs,
+        )
+    )
+    print(
+        timeit(
+            f"binary_exp_mod_iterative({a}, {b}, {c})",
+            setup="from __main__ import binary_exp_mod_iterative",
+            number=runs,
+        )
+    )
diff --git a/maths/binary_exponentiation_2.py b/maths/binary_exponentiation_2.py
deleted file mode 100644
index edb6b66b2594..000000000000
--- a/maths/binary_exponentiation_2.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""
-Binary Exponentiation
-This is a method to find a^b in O(log b) time complexity
-This is one of the most commonly used methods of exponentiation
-It's also useful when the solution to (a^b) % c is required because a, b, c may be
-over the computer's calculation limits
-
-Let's say you need to calculate a ^ b
-- RULE 1 : a ^ b = (a*a) ^ (b/2) ---- example : 4 ^ 4 = (4*4) ^ (4/2) = 16 ^ 2
-- RULE 2 : IF b is odd, then a ^ b = a * (a ^ (b - 1)), where b - 1 is even
-Once b is even, repeat the process until b = 1 or b = 0, because a^1 = a and a^0 = 1
-
-For modular exponentiation, we use the fact that (a*b) % c = ((a%c) * (b%c)) % c
-Now apply RULE 1 or 2 as required
-
-@author chinmoy159
-"""
-
-
-def b_expo(a: int, b: int) -> int:
-    """
-    >>> b_expo(2, 10)
-    1024
-    >>> b_expo(9, 0)
-    1
-    >>> b_expo(0, 12)
-    0
-    >>> b_expo(4, 12)
-    16777216
-    """
-    res = 1
-    while b > 0:
-        if b & 1:
-            res *= a
-
-        a *= a
-        b >>= 1
-
-    return res
-
-
-def b_expo_mod(a: int, b: int, c: int) -> int:
-    """
-    >>> b_expo_mod(2, 10, 1000000007)
-    1024
-    >>> b_expo_mod(11, 13, 19)
-    11
-    >>> b_expo_mod(0, 19, 20)
-    0
-    >>> b_expo_mod(15, 5, 4)
-    3
-    """
-    res = 1
-    while b > 0:
-        if b & 1:
-            res = ((res % c) * (a % c)) % c
-
-        a *= a
-        b >>= 1
-
-    return res
diff --git a/maths/binomial_coefficient.py b/maths/binomial_coefficient.py
index 6d5b46cb5861..24c54326e305 100644
--- a/maths/binomial_coefficient.py
+++ b/maths/binomial_coefficient.py
@@ -1,10 +1,48 @@
 def binomial_coefficient(n: int, r: int) -> int:
     """
-    Find binomial coefficient using pascals triangle.
+    Find binomial coefficient using Pascal's triangle.
+
+    Calculate C(n, r) using Pascal's triangle.
+
+    :param n: The total number of items.
+    :param r: The number of items to choose.
+    :return: The binomial coefficient C(n, r).
 
     >>> binomial_coefficient(10, 5)
     252
+    >>> binomial_coefficient(10, 0)
+    1
+    >>> binomial_coefficient(0, 10)
+    1
+    >>> binomial_coefficient(10, 10)
+    1
+    >>> binomial_coefficient(5, 2)
+    10
+    >>> binomial_coefficient(5, 6)
+    0
+    >>> binomial_coefficient(3, 5)
+    0
+    >>> binomial_coefficient(-2, 3)
+    Traceback (most recent call last):
+        ...
+    ValueError: n and r must be non-negative integers
+    >>> binomial_coefficient(5, -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: n and r must be non-negative integers
+    >>> binomial_coefficient(10.1, 5)
+    Traceback (most recent call last):
+        ...
+    TypeError: 'float' object cannot be interpreted as an integer
+    >>> binomial_coefficient(10, 5.1)
+    Traceback (most recent call last):
+        ...
+    TypeError: 'float' object cannot be interpreted as an integer
     """
+    if n < 0 or r < 0:
+        raise ValueError("n and r must be non-negative integers")
+    if 0 in (n, r):
+        return 1
     c = [0 for i in range(r + 1)]
     # nc0 = 1
     c[0] = 1
@@ -17,4 +55,8 @@ def binomial_coefficient(n: int, r: int) -> int:
     return c[r]
 
 
-print(binomial_coefficient(n=10, r=5))
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    print(binomial_coefficient(n=10, r=5))
diff --git a/maths/binomial_distribution.py b/maths/binomial_distribution.py
index 5b56f2d59244..eabcaea0d1b2 100644
--- a/maths/binomial_distribution.py
+++ b/maths/binomial_distribution.py
@@ -1,5 +1,6 @@
 """For more information about the Binomial Distribution -
-    https://en.wikipedia.org/wiki/Binomial_distribution"""
+https://en.wikipedia.org/wiki/Binomial_distribution"""
+
 from math import factorial
 
 
diff --git a/maths/chinese_remainder_theorem.py b/maths/chinese_remainder_theorem.py
index d3e75e77922a..18af63d106e8 100644
--- a/maths/chinese_remainder_theorem.py
+++ b/maths/chinese_remainder_theorem.py
@@ -11,6 +11,7 @@
 1. Use extended euclid algorithm to find x,y such that a*x + b*y = 1
 2. Take n = ra*by + rb*ax
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/chudnovsky_algorithm.py b/maths/chudnovsky_algorithm.py
index aaee7462822e..d122bf0756f7 100644
--- a/maths/chudnovsky_algorithm.py
+++ b/maths/chudnovsky_algorithm.py
@@ -5,7 +5,7 @@
 def pi(precision: int) -> str:
     """
     The Chudnovsky algorithm is a fast method for calculating the digits of PI,
-    based on Ramanujan’s PI formulae.
+    based on Ramanujan's PI formulae.
 
     https://en.wikipedia.org/wiki/Chudnovsky_algorithm
 
diff --git a/maths/collatz_sequence.py b/maths/collatz_sequence.py
index b47017146a1e..b00dca8d70b7 100644
--- a/maths/collatz_sequence.py
+++ b/maths/collatz_sequence.py
@@ -17,7 +17,7 @@
 from collections.abc import Generator
 
 
-def collatz_sequence(n: int) -> Generator[int, None, None]:
+def collatz_sequence(n: int) -> Generator[int]:
     """
     Generate the Collatz sequence starting at n.
     >>> tuple(collatz_sequence(2.1))
diff --git a/maths/continued_fraction.py b/maths/continued_fraction.py
index 04ff0b6ff0d2..2c38bf88b1e9 100644
--- a/maths/continued_fraction.py
+++ b/maths/continued_fraction.py
@@ -4,7 +4,6 @@
 https://en.wikipedia.org/wiki/Continued_fraction
 """
 
-
 from fractions import Fraction
 from math import floor
 
diff --git a/maths/decimal_to_fraction.py b/maths/decimal_to_fraction.py
index 2aa8e3c3dfd6..be42b9fb3b5a 100644
--- a/maths/decimal_to_fraction.py
+++ b/maths/decimal_to_fraction.py
@@ -16,6 +16,20 @@ def decimal_to_fraction(decimal: float | str) -> tuple[int, int]:
     >>> decimal_to_fraction("78td")
     Traceback (most recent call last):
     ValueError: Please enter a valid number
+    >>> decimal_to_fraction(0)
+    (0, 1)
+    >>> decimal_to_fraction(-2.5)
+    (-5, 2)
+    >>> decimal_to_fraction(0.125)
+    (1, 8)
+    >>> decimal_to_fraction(1000000.25)
+    (4000001, 4)
+    >>> decimal_to_fraction(1.3333)
+    (13333, 10000)
+    >>> decimal_to_fraction("1.23e2")
+    (123, 1)
+    >>> decimal_to_fraction("0.500")
+    (1, 2)
     """
     try:
         decimal = float(decimal)
@@ -34,8 +48,8 @@ def decimal_to_fraction(decimal: float | str) -> tuple[int, int]:
             if remainder == 0:
                 break
             dividend, divisor = divisor, remainder
-        numerator, denominator = numerator / divisor, denominator / divisor
-        return int(numerator), int(denominator)
+        numerator, denominator = numerator // divisor, denominator // divisor
+        return numerator, denominator
 
 
 if __name__ == "__main__":
diff --git a/maths/dual_number_automatic_differentiation.py b/maths/dual_number_automatic_differentiation.py
index f98997c8be4d..09aeb17a4aea 100644
--- a/maths/dual_number_automatic_differentiation.py
+++ b/maths/dual_number_automatic_differentiation.py
@@ -17,10 +17,8 @@ def __init__(self, real, rank):
             self.duals = rank
 
     def __repr__(self):
-        return (
-            f"{self.real}+"
-            f"{'+'.join(str(dual)+'E'+str(n+1)for n,dual in enumerate(self.duals))}"
-        )
+        s = "+".join(f"{dual}E{n}" for n, dual in enumerate(self.duals, 1))
+        return f"{self.real}+{s}"
 
     def reduce(self):
         cur = self.duals.copy()
diff --git a/maths/entropy.py b/maths/entropy.py
index 23753d884484..b816f1d193f7 100644
--- a/maths/entropy.py
+++ b/maths/entropy.py
@@ -4,6 +4,7 @@
 Implementation of entropy of information
 https://en.wikipedia.org/wiki/Entropy_(information_theory)
 """
+
 from __future__ import annotations
 
 import math
@@ -20,10 +21,10 @@ def calculate_prob(text: str) -> None:
     :return: Prints
     1) Entropy of information based on 1 alphabet
     2) Entropy of information based on couples of 2 alphabet
-    3) print Entropy of H(X n∣Xn−1)
+    3) print Entropy of H(X n|Xn-1)
 
     Text from random books. Also, random quotes.
-    >>> text = ("Behind Winston’s back the voice "
+    >>> text = ("Behind Winston's back the voice "
     ...         "from the telescreen was still "
     ...         "babbling and the overfulfilment")
     >>> calculate_prob(text)
@@ -95,8 +96,8 @@ def analyze_text(text: str) -> tuple[dict, dict]:
     The first dictionary stores the frequency of single character strings.
     The second dictionary stores the frequency of two character strings.
     """
-    single_char_strings = Counter()  # type: ignore
-    two_char_strings = Counter()  # type: ignore
+    single_char_strings = Counter()  # type: ignore[var-annotated]
+    two_char_strings = Counter()  # type: ignore[var-annotated]
     single_char_strings[text[-1]] += 1
 
     # first case when we have space at start.
diff --git a/maths/euclidean_distance.py b/maths/euclidean_distance.py
index 9b29b37b0ce6..aa7f3efc7684 100644
--- a/maths/euclidean_distance.py
+++ b/maths/euclidean_distance.py
@@ -13,13 +13,13 @@ def euclidean_distance(vector_1: Vector, vector_2: Vector) -> VectorOut:
     """
     Calculate the distance between the two endpoints of two vectors.
     A vector is defined as a list, tuple, or numpy 1D array.
-    >>> euclidean_distance((0, 0), (2, 2))
+    >>> float(euclidean_distance((0, 0), (2, 2)))
     2.8284271247461903
-    >>> euclidean_distance(np.array([0, 0, 0]), np.array([2, 2, 2]))
+    >>> float(euclidean_distance(np.array([0, 0, 0]), np.array([2, 2, 2])))
     3.4641016151377544
-    >>> euclidean_distance(np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8]))
+    >>> float(euclidean_distance(np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8])))
     8.0
-    >>> euclidean_distance([1, 2, 3, 4], [5, 6, 7, 8])
+    >>> float(euclidean_distance([1, 2, 3, 4], [5, 6, 7, 8]))
     8.0
     """
     return np.sqrt(np.sum((np.asarray(vector_1) - np.asarray(vector_2)) ** 2))
diff --git a/maths/euler_method.py b/maths/euler_method.py
index 30f193e6daa5..c6adb07e2d3d 100644
--- a/maths/euler_method.py
+++ b/maths/euler_method.py
@@ -26,7 +26,7 @@ def explicit_euler(
     ...     return y
     >>> y0 = 1
     >>> y = explicit_euler(f, y0, 0.0, 0.01, 5)
-    >>> y[-1]
+    >>> float(y[-1])
     144.77277243257308
     """
     n = int(np.ceil((x_end - x0) / step_size))
diff --git a/maths/euler_modified.py b/maths/euler_modified.py
index d02123e1e2fb..bb282e9f0ab9 100644
--- a/maths/euler_modified.py
+++ b/maths/euler_modified.py
@@ -24,13 +24,13 @@ def euler_modified(
     >>> def f1(x, y):
     ...     return -2*x*(y**2)
     >>> y = euler_modified(f1, 1.0, 0.0, 0.2, 1.0)
-    >>> y[-1]
+    >>> float(y[-1])
     0.503338255442106
     >>> import math
     >>> def f2(x, y):
     ...     return -2*y + (x**3)*math.exp(-2*x)
     >>> y = euler_modified(f2, 1.0, 0.0, 0.1, 0.3)
-    >>> y[-1]
+    >>> float(y[-1])
     0.5525976431951775
     """
     n = int(np.ceil((x_end - x0) / step_size))
diff --git a/maths/fast_inverse_sqrt.py b/maths/fast_inverse_sqrt.py
new file mode 100644
index 000000000000..79385bb84877
--- /dev/null
+++ b/maths/fast_inverse_sqrt.py
@@ -0,0 +1,54 @@
+"""
+Fast inverse square root (1/sqrt(x)) using the Quake III algorithm.
+Reference: https://en.wikipedia.org/wiki/Fast_inverse_square_root
+Accuracy: https://en.wikipedia.org/wiki/Fast_inverse_square_root#Accuracy
+"""
+
+import struct
+
+
+def fast_inverse_sqrt(number: float) -> float:
+    """
+    Compute the fast inverse square root of a floating-point number using the famous
+    Quake III algorithm.
+
+    :param float number: Input number for which to calculate the inverse square root.
+    :return float: The fast inverse square root of the input number.
+
+    Example:
+    >>> fast_inverse_sqrt(10)
+    0.3156857923527257
+    >>> fast_inverse_sqrt(4)
+    0.49915357479239103
+    >>> fast_inverse_sqrt(4.1)
+    0.4932849504615651
+    >>> fast_inverse_sqrt(0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input must be a positive number.
+    >>> fast_inverse_sqrt(-1)
+    Traceback (most recent call last):
+        ...
+    ValueError: Input must be a positive number.
+    >>> from math import isclose, sqrt
+    >>> all(isclose(fast_inverse_sqrt(i), 1 / sqrt(i), rel_tol=0.00132)
+    ...     for i in range(50, 60))
+    True
+    """
+    if number <= 0:
+        raise ValueError("Input must be a positive number.")
+    i = struct.unpack(">i", struct.pack(">f", number))[0]
+    i = 0x5F3759DF - (i >> 1)
+    y = struct.unpack(">f", struct.pack(">i", i))[0]
+    return y * (1.5 - 0.5 * number * y * y)
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    # https://en.wikipedia.org/wiki/Fast_inverse_square_root#Accuracy
+    from math import sqrt
+
+    for i in range(5, 101, 5):
+        print(f"{i:>3}: {(1 / sqrt(i)) - fast_inverse_sqrt(i):.5f}")
diff --git a/maths/fibonacci.py b/maths/fibonacci.py
index e810add69dc7..24b2d7ae449e 100644
--- a/maths/fibonacci.py
+++ b/maths/fibonacci.py
@@ -1,4 +1,3 @@
-# fibonacci.py
 """
 Calculates the Fibonacci sequence using iteration, recursion, memoization,
 and a simplified form of Binet's formula
@@ -8,18 +7,21 @@
 
 NOTE 2: the Binet's formula function is much more limited in the size of inputs
 that it can handle due to the size limitations of Python floats
+NOTE 3: the matrix function is the fastest and most memory efficient for large n
 
-RESULTS: (n = 20)
-fib_iterative runtime: 0.0055 ms
-fib_recursive runtime: 6.5627 ms
-fib_memoization runtime: 0.0107 ms
-fib_binet runtime: 0.0174 ms
+
+See benchmark numbers in __main__ for performance comparisons/
+https://en.wikipedia.org/wiki/Fibonacci_number for more information
 """
 
 import functools
+from collections.abc import Iterator
 from math import sqrt
 from time import time
 
+import numpy as np
+from numpy import ndarray
+
 
 def time_func(func, *args, **kwargs):
     """
@@ -35,6 +37,31 @@ def time_func(func, *args, **kwargs):
     return output
 
 
+def fib_iterative_yield(n: int) -> Iterator[int]:
+    """
+    Calculates the first n (1-indexed) Fibonacci numbers using iteration with yield
+    >>> list(fib_iterative_yield(0))
+    [0]
+    >>> tuple(fib_iterative_yield(1))
+    (0, 1)
+    >>> tuple(fib_iterative_yield(5))
+    (0, 1, 1, 2, 3, 5)
+    >>> tuple(fib_iterative_yield(10))
+    (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55)
+    >>> tuple(fib_iterative_yield(-1))
+    Traceback (most recent call last):
+        ...
+    ValueError: n is negative
+    """
+    if n < 0:
+        raise ValueError("n is negative")
+    a, b = 0, 1
+    yield a
+    for _ in range(n):
+        yield b
+        a, b = b, a + b
+
+
 def fib_iterative(n: int) -> list[int]:
     """
     Calculates the first n (0-indexed) Fibonacci numbers using iteration
@@ -49,10 +76,10 @@ def fib_iterative(n: int) -> list[int]:
     >>> fib_iterative(-1)
     Traceback (most recent call last):
         ...
-    Exception: n is negative
+    ValueError: n is negative
     """
     if n < 0:
-        raise Exception("n is negative")
+        raise ValueError("n is negative")
     if n == 0:
         return [0]
     fib = [0, 1]
@@ -75,21 +102,33 @@ def fib_recursive(n: int) -> list[int]:
     >>> fib_iterative(-1)
     Traceback (most recent call last):
         ...
-    Exception: n is negative
+    ValueError: n is negative
     """
 
     def fib_recursive_term(i: int) -> int:
         """
         Calculates the i-th (0-indexed) Fibonacci number using recursion
+        >>> fib_recursive_term(0)
+        0
+        >>> fib_recursive_term(1)
+        1
+        >>> fib_recursive_term(5)
+        5
+        >>> fib_recursive_term(10)
+        55
+        >>> fib_recursive_term(-1)
+        Traceback (most recent call last):
+            ...
+        Exception: n is negative
         """
         if i < 0:
-            raise Exception("n is negative")
+            raise ValueError("n is negative")
         if i < 2:
             return i
         return fib_recursive_term(i - 1) + fib_recursive_term(i - 2)
 
     if n < 0:
-        raise Exception("n is negative")
+        raise ValueError("n is negative")
     return [fib_recursive_term(i) for i in range(n + 1)]
 
 
@@ -107,7 +146,7 @@ def fib_recursive_cached(n: int) -> list[int]:
     >>> fib_iterative(-1)
     Traceback (most recent call last):
         ...
-    Exception: n is negative
+    ValueError: n is negative
     """
 
     @functools.cache
@@ -116,13 +155,13 @@ def fib_recursive_term(i: int) -> int:
         Calculates the i-th (0-indexed) Fibonacci number using recursion
         """
         if i < 0:
-            raise Exception("n is negative")
+            raise ValueError("n is negative")
         if i < 2:
             return i
         return fib_recursive_term(i - 1) + fib_recursive_term(i - 2)
 
     if n < 0:
-        raise Exception("n is negative")
+        raise ValueError("n is negative")
     return [fib_recursive_term(i) for i in range(n + 1)]
 
 
@@ -140,10 +179,10 @@ def fib_memoization(n: int) -> list[int]:
     >>> fib_iterative(-1)
     Traceback (most recent call last):
         ...
-    Exception: n is negative
+    ValueError: n is negative
     """
     if n < 0:
-        raise Exception("n is negative")
+        raise ValueError("n is negative")
     # Cache must be outside recursuive function
     # other it will reset every time it calls itself.
     cache: dict[int, int] = {0: 0, 1: 1, 2: 1}  # Prefilled cache
@@ -181,25 +220,113 @@ def fib_binet(n: int) -> list[int]:
     >>> fib_binet(-1)
     Traceback (most recent call last):
         ...
-    Exception: n is negative
+    ValueError: n is negative
     >>> fib_binet(1475)
     Traceback (most recent call last):
         ...
-    Exception: n is too large
+    ValueError: n is too large
     """
     if n < 0:
-        raise Exception("n is negative")
+        raise ValueError("n is negative")
     if n >= 1475:
-        raise Exception("n is too large")
+        raise ValueError("n is too large")
     sqrt_5 = sqrt(5)
     phi = (1 + sqrt_5) / 2
     return [round(phi**i / sqrt_5) for i in range(n + 1)]
 
 
+def matrix_pow_np(m: ndarray, power: int) -> ndarray:
+    """
+    Raises a matrix to the power of 'power' using binary exponentiation.
+
+    Args:
+        m: Matrix as a numpy array.
+        power: The power to which the matrix is to be raised.
+
+    Returns:
+        The matrix raised to the power.
+
+    Raises:
+        ValueError: If power is negative.
+
+    >>> m = np.array([[1, 1], [1, 0]], dtype=int)
+    >>> matrix_pow_np(m, 0)  # Identity matrix when raised to the power of 0
+    array([[1, 0],
+           [0, 1]])
+
+    >>> matrix_pow_np(m, 1)  # Same matrix when raised to the power of 1
+    array([[1, 1],
+           [1, 0]])
+
+    >>> matrix_pow_np(m, 5)
+    array([[8, 5],
+           [5, 3]])
+
+    >>> matrix_pow_np(m, -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: power is negative
+    """
+    result = np.array([[1, 0], [0, 1]], dtype=int)  # Identity Matrix
+    base = m
+    if power < 0:  # Negative power is not allowed
+        raise ValueError("power is negative")
+    while power:
+        if power % 2 == 1:
+            result = np.dot(result, base)
+        base = np.dot(base, base)
+        power //= 2
+    return result
+
+
+def fib_matrix_np(n: int) -> int:
+    """
+    Calculates the n-th Fibonacci number using matrix exponentiation.
+    https://www.nayuki.io/page/fast-fibonacci-algorithms#:~:text=
+    Summary:%20The%20two%20fast%20Fibonacci%20algorithms%20are%20matrix
+
+    Args:
+        n: Fibonacci sequence index
+
+    Returns:
+        The n-th Fibonacci number.
+
+    Raises:
+        ValueError: If n is negative.
+
+    >>> fib_matrix_np(0)
+    0
+    >>> fib_matrix_np(1)
+    1
+    >>> fib_matrix_np(5)
+    5
+    >>> fib_matrix_np(10)
+    55
+    >>> fib_matrix_np(-1)
+    Traceback (most recent call last):
+        ...
+    ValueError: n is negative
+    """
+    if n < 0:
+        raise ValueError("n is negative")
+    if n == 0:
+        return 0
+
+    m = np.array([[1, 1], [1, 0]], dtype=int)
+    result = matrix_pow_np(m, n - 1)
+    return int(result[0, 0])
+
+
 if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    # Time on an M1 MacBook Pro -- Fastest to slowest
     num = 30
-    time_func(fib_iterative, num)
-    time_func(fib_recursive, num)  # Around 3s runtime
-    time_func(fib_recursive_cached, num)  # Around 0ms runtime
-    time_func(fib_memoization, num)
-    time_func(fib_binet, num)
+    time_func(fib_iterative_yield, num)  # 0.0012 ms
+    time_func(fib_iterative, num)  # 0.0031 ms
+    time_func(fib_binet, num)  # 0.0062 ms
+    time_func(fib_memoization, num)  # 0.0100 ms
+    time_func(fib_recursive_cached, num)  # 0.0153 ms
+    time_func(fib_recursive, num)  # 257.0910 ms
+    time_func(fib_matrix_np, num)  # 0.0000 ms
diff --git a/maths/find_max.py b/maths/find_max.py
index 729a80ab421c..4765d300634e 100644
--- a/maths/find_max.py
+++ b/maths/find_max.py
@@ -20,7 +20,7 @@ def find_max_iterative(nums: list[int | float]) -> int | float:
         raise ValueError("find_max_iterative() arg is an empty sequence")
     max_num = nums[0]
     for x in nums:
-        if x > max_num:
+        if x > max_num:  # noqa: PLR1730
             max_num = x
     return max_num
 
diff --git a/maths/gamma.py b/maths/gamma.py
index 822bbc74456f..e328cd8b22b7 100644
--- a/maths/gamma.py
+++ b/maths/gamma.py
@@ -8,6 +8,7 @@
 the non-positive integers
 Python's Standard Library math.gamma() function overflows around gamma(171.624).
 """
+
 import math
 
 from numpy import inf
diff --git a/maths/gaussian.py b/maths/gaussian.py
index 51ebc2e25849..b1e62ea77fe2 100644
--- a/maths/gaussian.py
+++ b/maths/gaussian.py
@@ -1,21 +1,22 @@
 """
 Reference: https://en.wikipedia.org/wiki/Gaussian_function
 """
+
 from numpy import exp, pi, sqrt
 
 
-def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int:
+def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> float:
     """
-    >>> gaussian(1)
+    >>> float(gaussian(1))
     0.24197072451914337
 
-    >>> gaussian(24)
+    >>> float(gaussian(24))
     3.342714441794458e-126
 
-    >>> gaussian(1, 4, 2)
+    >>> float(gaussian(1, 4, 2))
     0.06475879783294587
 
-    >>> gaussian(1, 5, 3)
+    >>> float(gaussian(1, 5, 3))
     0.05467002489199788
 
     Supports NumPy Arrays
@@ -28,7 +29,7 @@ def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int:
            5.05227108e-15, 1.02797736e-18, 7.69459863e-23, 2.11881925e-27,
            2.14638374e-32, 7.99882776e-38, 1.09660656e-43])
 
-    >>> gaussian(15)
+    >>> float(gaussian(15))
     5.530709549844416e-50
 
     >>> gaussian([1,2, 'string'])
@@ -46,10 +47,10 @@ def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int:
         ...
     OverflowError: (34, 'Result too large')
 
-    >>> gaussian(10**-326)
+    >>> float(gaussian(10**-326))
     0.3989422804014327
 
-    >>> gaussian(2523, mu=234234, sigma=3425)
+    >>> float(gaussian(2523, mu=234234, sigma=3425))
     0.0
     """
     return 1 / sqrt(2 * pi * sigma**2) * exp(-((x - mu) ** 2) / (2 * sigma**2))
diff --git a/maths/geometric_mean.py b/maths/geometric_mean.py
new file mode 100644
index 000000000000..240d519ad398
--- /dev/null
+++ b/maths/geometric_mean.py
@@ -0,0 +1,55 @@
+"""
+The Geometric Mean of n numbers is defined as the n-th root of the product
+of those numbers. It is used to measure the central tendency of the numbers.
+https://en.wikipedia.org/wiki/Geometric_mean
+"""
+
+
+def compute_geometric_mean(*args: int) -> float:
+    """
+    Return the geometric mean of the argument numbers.
+    >>> compute_geometric_mean(2,8)
+    4.0
+    >>> compute_geometric_mean('a', 4)
+    Traceback (most recent call last):
+        ...
+    TypeError: Not a Number
+    >>> compute_geometric_mean(5, 125)
+    25.0
+    >>> compute_geometric_mean(1, 0)
+    0.0
+    >>> compute_geometric_mean(1, 5, 25, 5)
+    5.0
+    >>> compute_geometric_mean(2, -2)
+    Traceback (most recent call last):
+        ...
+    ArithmeticError: Cannot Compute Geometric Mean for these numbers.
+    >>> compute_geometric_mean(-5, 25, 1)
+    -5.0
+    """
+    product = 1
+    for number in args:
+        if not isinstance(number, int) and not isinstance(number, float):
+            raise TypeError("Not a Number")
+        product *= number
+    # Cannot calculate the even root for negative product.
+    # Frequently they are restricted to being positive.
+    if product < 0 and len(args) % 2 == 0:
+        raise ArithmeticError("Cannot Compute Geometric Mean for these numbers.")
+    mean = abs(product) ** (1 / len(args))
+    # Since python calculates complex roots for negative products with odd roots.
+    if product < 0:
+        mean = -mean
+    # Since it does floating point arithmetic, it gives 64**(1/3) as 3.99999996
+    possible_mean = float(round(mean))
+    # To check if the rounded number is actually the mean.
+    if possible_mean ** len(args) == product:
+        mean = possible_mean
+    return mean
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod(name="compute_geometric_mean")
+    print(compute_geometric_mean(-3, -27))
diff --git a/maths/integer_square_root.py b/maths/integer_square_root.py
new file mode 100644
index 000000000000..27e874a43c79
--- /dev/null
+++ b/maths/integer_square_root.py
@@ -0,0 +1,73 @@
+"""
+Integer Square Root Algorithm -- An efficient method to calculate the square root of a
+non-negative integer 'num' rounded down to the nearest integer. It uses a binary search
+approach to find the integer square root without using any built-in exponent functions
+or operators.
+* https://en.wikipedia.org/wiki/Integer_square_root
+* https://docs.python.org/3/library/math.html#math.isqrt
+Note:
+    - This algorithm is designed for non-negative integers only.
+    - The result is rounded down to the nearest integer.
+    - The algorithm has a time complexity of O(log(x)).
+    - Original algorithm idea based on binary search.
+"""
+
+
+def integer_square_root(num: int) -> int:
+    """
+    Returns the integer square root of a non-negative integer num.
+    Args:
+        num: A non-negative integer.
+    Returns:
+        The integer square root of num.
+    Raises:
+        ValueError: If num is not an integer or is negative.
+    >>> [integer_square_root(i) for i in range(18)]
+    [0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4]
+    >>> integer_square_root(625)
+    25
+    >>> integer_square_root(2_147_483_647)
+    46340
+    >>> from math import isqrt
+    >>> all(integer_square_root(i) == isqrt(i) for i in range(20))
+    True
+    >>> integer_square_root(-1)
+    Traceback (most recent call last):
+        ...
+    ValueError: num must be non-negative integer
+    >>> integer_square_root(1.5)
+    Traceback (most recent call last):
+        ...
+    ValueError: num must be non-negative integer
+    >>> integer_square_root("0")
+    Traceback (most recent call last):
+        ...
+    ValueError: num must be non-negative integer
+    """
+    if not isinstance(num, int) or num < 0:
+        raise ValueError("num must be non-negative integer")
+
+    if num < 2:
+        return num
+
+    left_bound = 0
+    right_bound = num // 2
+
+    while left_bound <= right_bound:
+        mid = left_bound + (right_bound - left_bound) // 2
+        mid_squared = mid * mid
+        if mid_squared == num:
+            return mid
+
+        if mid_squared < num:
+            left_bound = mid + 1
+        else:
+            right_bound = mid - 1
+
+    return right_bound
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/maths/interquartile_range.py b/maths/interquartile_range.py
index d4d72e73ef49..e91a651647d4 100644
--- a/maths/interquartile_range.py
+++ b/maths/interquartile_range.py
@@ -7,6 +7,7 @@
 Script inspired by this Wikipedia article:
 https://en.wikipedia.org/wiki/Interquartile_range
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/is_ip_v4_address_valid.py b/maths/is_ip_v4_address_valid.py
index 0ae8e021ead1..305afabffed3 100644
--- a/maths/is_ip_v4_address_valid.py
+++ b/maths/is_ip_v4_address_valid.py
@@ -1,13 +1,15 @@
 """
+wiki: https://en.wikipedia.org/wiki/IPv4
+
 Is IP v4 address valid?
 A valid IP address must be four octets in the form of A.B.C.D,
-where A,B,C and D are numbers from 0-254
-for example: 192.168.23.1, 172.254.254.254 are valid IP address
-             192.168.255.0, 255.192.3.121 are invalid IP address
+where A, B, C and D are numbers from 0-255
+for example: 192.168.23.1, 172.255.255.255 are valid IP address
+             192.168.256.0, 256.192.3.121 are invalid IP address
 """
 
 
-def is_ip_v4_address_valid(ip_v4_address: str) -> bool:
+def is_ip_v4_address_valid(ip: str) -> bool:
     """
     print "Valid IP address" If IP is valid.
     or
@@ -16,13 +18,13 @@ def is_ip_v4_address_valid(ip_v4_address: str) -> bool:
     >>> is_ip_v4_address_valid("192.168.0.23")
     True
 
-    >>> is_ip_v4_address_valid("192.255.15.8")
+    >>> is_ip_v4_address_valid("192.256.15.8")
     False
 
     >>> is_ip_v4_address_valid("172.100.0.8")
     True
 
-    >>> is_ip_v4_address_valid("254.255.0.255")
+    >>> is_ip_v4_address_valid("255.256.0.256")
     False
 
     >>> is_ip_v4_address_valid("1.2.33333333.4")
@@ -45,12 +47,29 @@ def is_ip_v4_address_valid(ip_v4_address: str) -> bool:
 
     >>> is_ip_v4_address_valid("1.2.3.")
     False
+
+    >>> is_ip_v4_address_valid("1.2.3.05")
+    False
     """
-    octets = [int(i) for i in ip_v4_address.split(".") if i.isdigit()]
-    return len(octets) == 4 and all(0 <= int(octet) <= 254 for octet in octets)
+    octets = ip.split(".")
+    if len(octets) != 4:
+        return False
+
+    for octet in octets:
+        if not octet.isdigit():
+            return False
+
+        number = int(octet)
+        if len(str(number)) != len(octet):
+            return False
+
+        if not 0 <= number <= 255:
+            return False
+
+    return True
 
 
 if __name__ == "__main__":
     ip = input().strip()
     valid_or_invalid = "valid" if is_ip_v4_address_valid(ip) else "invalid"
-    print(f"{ip} is a {valid_or_invalid} IP v4 address.")
+    print(f"{ip} is a {valid_or_invalid} IPv4 address.")
diff --git a/maths/is_square_free.py b/maths/is_square_free.py
index 08c70dc32c38..a336c37e8dbc 100644
--- a/maths/is_square_free.py
+++ b/maths/is_square_free.py
@@ -3,6 +3,7 @@
 psf/black : True
 ruff : True
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/josephus_problem.py b/maths/josephus_problem.py
new file mode 100644
index 000000000000..271292ba1d9f
--- /dev/null
+++ b/maths/josephus_problem.py
@@ -0,0 +1,130 @@
+"""
+The Josephus problem is a famous theoretical problem related to a certain
+counting-out game. This module provides functions to solve the Josephus problem
+for num_people and a step_size.
+
+The Josephus problem is defined as follows:
+- num_people are standing in a circle.
+- Starting with a specified person, you count around the circle,
+  skipping a fixed number of people (step_size).
+- The person at which you stop counting is eliminated from the circle.
+- The counting continues until only one person remains.
+
+For more information about the Josephus problem, refer to:
+https://en.wikipedia.org/wiki/Josephus_problem
+"""
+
+
+def josephus_recursive(num_people: int, step_size: int) -> int:
+    """
+    Solve the Josephus problem for num_people and a step_size recursively.
+
+    Args:
+        num_people: A positive integer representing the number of people.
+        step_size: A positive integer representing the step size for elimination.
+
+    Returns:
+        The position of the last person remaining.
+
+    Raises:
+        ValueError: If num_people or step_size is not a positive integer.
+
+    Examples:
+        >>> josephus_recursive(7, 3)
+        3
+        >>> josephus_recursive(10, 2)
+        4
+        >>> josephus_recursive(0, 2)
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+        >>> josephus_recursive(1.9, 2)
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+        >>> josephus_recursive(-2, 2)
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+        >>> josephus_recursive(7, 0)
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+        >>> josephus_recursive(7, -2)
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+        >>> josephus_recursive(1_000, 0.01)
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+        >>> josephus_recursive("cat", "dog")
+        Traceback (most recent call last):
+            ...
+        ValueError: num_people or step_size is not a positive integer.
+    """
+    if (
+        not isinstance(num_people, int)
+        or not isinstance(step_size, int)
+        or num_people <= 0
+        or step_size <= 0
+    ):
+        raise ValueError("num_people or step_size is not a positive integer.")
+
+    if num_people == 1:
+        return 0
+
+    return (josephus_recursive(num_people - 1, step_size) + step_size) % num_people
+
+
+def find_winner(num_people: int, step_size: int) -> int:
+    """
+    Find the winner of the Josephus problem for num_people and a step_size.
+
+    Args:
+        num_people (int): Number of people.
+        step_size (int): Step size for elimination.
+
+    Returns:
+        int: The position of the last person remaining (1-based index).
+
+    Examples:
+        >>> find_winner(7, 3)
+        4
+        >>> find_winner(10, 2)
+        5
+    """
+    return josephus_recursive(num_people, step_size) + 1
+
+
+def josephus_iterative(num_people: int, step_size: int) -> int:
+    """
+    Solve the Josephus problem for num_people and a step_size iteratively.
+
+    Args:
+        num_people (int): The number of people in the circle.
+        step_size (int): The number of steps to take before eliminating someone.
+
+    Returns:
+        int: The position of the last person standing.
+
+    Examples:
+        >>> josephus_iterative(5, 2)
+        3
+        >>> josephus_iterative(7, 3)
+        4
+    """
+    circle = list(range(1, num_people + 1))
+    current = 0
+
+    while len(circle) > 1:
+        current = (current + step_size - 1) % len(circle)
+        circle.pop(current)
+
+    return circle[0]
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/maths/karatsuba.py b/maths/karatsuba.py
index 3d29e31d2107..0e063fb44b83 100644
--- a/maths/karatsuba.py
+++ b/maths/karatsuba.py
@@ -1,4 +1,4 @@
-""" Multiply two numbers using Karatsuba algorithm """
+"""Multiply two numbers using Karatsuba algorithm"""
 
 
 def karatsuba(a: int, b: int) -> int:
diff --git a/maths/largest_of_very_large_numbers.py b/maths/largest_of_very_large_numbers.py
index 7e7fea004958..edee50371e02 100644
--- a/maths/largest_of_very_large_numbers.py
+++ b/maths/largest_of_very_large_numbers.py
@@ -4,14 +4,26 @@
 
 
 def res(x, y):
+    """
+    Reduces large number to a more manageable number
+    >>> res(5, 7)
+    4.892790030352132
+    >>> res(0, 5)
+    0
+    >>> res(3, 0)
+    1
+    >>> res(-1, 5)
+    Traceback (most recent call last):
+    ...
+    ValueError: math domain error
+    """
     if 0 not in (x, y):
         # We use the relation x^y = y*log10(x), where 10 is the base.
         return y * math.log10(x)
-    else:
-        if x == 0:  # 0 raised to any number is 0
-            return 0
-        elif y == 0:
-            return 1  # any number raised to 0 is 1
+    elif x == 0:  # 0 raised to any number is 0
+        return 0
+    elif y == 0:
+        return 1  # any number raised to 0 is 1
     raise AssertionError("This should never happen")
 
 
diff --git a/maths/lucas_lehmer_primality_test.py b/maths/lucas_lehmer_primality_test.py
index 0a5621aacd79..af5c81133044 100644
--- a/maths/lucas_lehmer_primality_test.py
+++ b/maths/lucas_lehmer_primality_test.py
@@ -1,13 +1,13 @@
 """
-        In mathematics, the Lucas–Lehmer test (LLT) is a primality test for Mersenne
-        numbers.  https://en.wikipedia.org/wiki/Lucas%E2%80%93Lehmer_primality_test
+In mathematics, the Lucas-Lehmer test (LLT) is a primality test for Mersenne
+numbers.  https://en.wikipedia.org/wiki/Lucas%E2%80%93Lehmer_primality_test
 
-        A Mersenne number is a number that is one less than a power of two.
-        That is M_p = 2^p - 1
-        https://en.wikipedia.org/wiki/Mersenne_prime
+A Mersenne number is a number that is one less than a power of two.
+That is M_p = 2^p - 1
+https://en.wikipedia.org/wiki/Mersenne_prime
 
-        The Lucas–Lehmer test is the primality test used by the
-        Great Internet Mersenne Prime Search (GIMPS) to locate large primes.
+The Lucas-Lehmer test is the primality test used by the
+Great Internet Mersenne Prime Search (GIMPS) to locate large primes.
 """
 
 
diff --git a/maths/maclaurin_series.py b/maths/maclaurin_series.py
index d5c3c3ab958b..6ec5551a5e6e 100644
--- a/maths/maclaurin_series.py
+++ b/maths/maclaurin_series.py
@@ -1,6 +1,7 @@
 """
 https://en.wikipedia.org/wiki/Taylor_series#Trigonometric_functions
 """
+
 from math import factorial, pi
 
 
diff --git a/maths/matrix_exponentiation.py b/maths/matrix_exponentiation.py
index 7c37151c87ca..7cdac9d34674 100644
--- a/maths/matrix_exponentiation.py
+++ b/maths/matrix_exponentiation.py
@@ -39,6 +39,21 @@ def modular_exponentiation(a, b):
 
 
 def fibonacci_with_matrix_exponentiation(n, f1, f2):
+    """
+    Returns the nth number of the Fibonacci sequence that
+    starts with f1 and f2
+    Uses the matrix exponentiation
+    >>> fibonacci_with_matrix_exponentiation(1, 5, 6)
+    5
+    >>> fibonacci_with_matrix_exponentiation(2, 10, 11)
+    11
+    >>> fibonacci_with_matrix_exponentiation(13, 0, 1)
+    144
+    >>> fibonacci_with_matrix_exponentiation(10, 5, 9)
+    411
+    >>> fibonacci_with_matrix_exponentiation(9, 2, 3)
+    89
+    """
     # Trivial Cases
     if n == 1:
         return f1
@@ -50,21 +65,34 @@ def fibonacci_with_matrix_exponentiation(n, f1, f2):
 
 
 def simple_fibonacci(n, f1, f2):
+    """
+    Returns the nth number of the Fibonacci sequence that
+    starts with f1 and f2
+    Uses the definition
+    >>> simple_fibonacci(1, 5, 6)
+    5
+    >>> simple_fibonacci(2, 10, 11)
+    11
+    >>> simple_fibonacci(13, 0, 1)
+    144
+    >>> simple_fibonacci(10, 5, 9)
+    411
+    >>> simple_fibonacci(9, 2, 3)
+    89
+    """
     # Trivial Cases
     if n == 1:
         return f1
     elif n == 2:
         return f2
 
-    fn_1 = f1
-    fn_2 = f2
     n -= 2
 
     while n > 0:
-        fn_1, fn_2 = fn_1 + fn_2, fn_1
+        f2, f1 = f1 + f2, f2
         n -= 1
 
-    return fn_1
+    return f2
 
 
 def matrix_exponentiation_time():
diff --git a/maths/max_sum_sliding_window.py b/maths/max_sum_sliding_window.py
index c6f9b4ed0ad7..c7492978a6c9 100644
--- a/maths/max_sum_sliding_window.py
+++ b/maths/max_sum_sliding_window.py
@@ -6,6 +6,7 @@
 called 'Window sliding technique' where the nested loops can be converted to a single
 loop to reduce time complexity.
 """
+
 from __future__ import annotations
 
 
@@ -42,4 +43,6 @@ def max_sum_in_array(array: list[int], k: int) -> int:
     testmod()
     array = [randint(-1000, 1000) for i in range(100)]
     k = randint(0, 110)
-    print(f"The maximum sum of {k} consecutive elements is {max_sum_in_array(array,k)}")
+    print(
+        f"The maximum sum of {k} consecutive elements is {max_sum_in_array(array, k)}"
+    )
diff --git a/maths/median_of_two_arrays.py b/maths/median_of_two_arrays.py
deleted file mode 100644
index 55aa587a9c4b..000000000000
--- a/maths/median_of_two_arrays.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from __future__ import annotations
-
-
-def median_of_two_arrays(nums1: list[float], nums2: list[float]) -> float:
-    """
-    >>> median_of_two_arrays([1, 2], [3])
-    2
-    >>> median_of_two_arrays([0, -1.1], [2.5, 1])
-    0.5
-    >>> median_of_two_arrays([], [2.5, 1])
-    1.75
-    >>> median_of_two_arrays([], [0])
-    0
-    >>> median_of_two_arrays([], [])
-    Traceback (most recent call last):
-      ...
-    IndexError: list index out of range
-    """
-    all_numbers = sorted(nums1 + nums2)
-    div, mod = divmod(len(all_numbers), 2)
-    if mod == 1:
-        return all_numbers[div]
-    else:
-        return (all_numbers[div] + all_numbers[div - 1]) / 2
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
-    array_1 = [float(x) for x in input("Enter the elements of first array: ").split()]
-    array_2 = [float(x) for x in input("Enter the elements of second array: ").split()]
-    print(f"The median of two arrays is: {median_of_two_arrays(array_1, array_2)}")
diff --git a/maths/minkowski_distance.py b/maths/minkowski_distance.py
index 3237124e8d36..99f02e31e417 100644
--- a/maths/minkowski_distance.py
+++ b/maths/minkowski_distance.py
@@ -19,7 +19,7 @@ def minkowski_distance(
     >>> minkowski_distance([1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], 2)
     8.0
     >>> import numpy as np
-    >>> np.isclose(5.0, minkowski_distance([5.0], [0.0], 3))
+    >>> bool(np.isclose(5.0, minkowski_distance([5.0], [0.0], 3)))
     True
     >>> minkowski_distance([1.0], [2.0], -1)
     Traceback (most recent call last):
diff --git a/maths/modular_division.py b/maths/modular_division.py
index 260d5683705d..2f8f4479b27d 100644
--- a/maths/modular_division.py
+++ b/maths/modular_division.py
@@ -9,7 +9,7 @@ def modular_division(a: int, b: int, n: int) -> int:
     GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor )
 
     Given three integers a, b, and n, such that gcd(a,n)=1 and n>1, the algorithm should
-    return an integer x such that 0≤x≤n−1, and  b/a=x(modn) (that is, b=ax(modn)).
+    return an integer x such that 0≤x≤n-1, and  b/a=x(modn) (that is, b=ax(modn)).
 
     Theorem:
     a has a multiplicative inverse modulo n iff gcd(a,n) = 1
diff --git a/maths/modular_exponential.py b/maths/modular_exponential.py
index 42987dbf3a24..a27e29ebc02a 100644
--- a/maths/modular_exponential.py
+++ b/maths/modular_exponential.py
@@ -1,8 +1,8 @@
 """
-    Modular Exponential.
-    Modular exponentiation is a type of exponentiation performed over a modulus.
-    For more explanation, please check
-    https://en.wikipedia.org/wiki/Modular_exponentiation
+Modular Exponential.
+Modular exponentiation is a type of exponentiation performed over a modulus.
+For more explanation, please check
+https://en.wikipedia.org/wiki/Modular_exponentiation
 """
 
 """Calculate Modular Exponential."""
diff --git a/maths/monte_carlo.py b/maths/monte_carlo.py
index 474f1f65deb4..d174a0b188a2 100644
--- a/maths/monte_carlo.py
+++ b/maths/monte_carlo.py
@@ -1,6 +1,7 @@
 """
 @author: MatteoRaso
 """
+
 from collections.abc import Callable
 from math import pi, sqrt
 from random import uniform
diff --git a/maths/newton_raphson.py b/maths/newton_raphson.py
deleted file mode 100644
index f6b227b5c9c1..000000000000
--- a/maths/newton_raphson.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""
-Author: P Shreyas Shetty
-Implementation of Newton-Raphson method for solving equations of kind
-f(x) = 0. It is an iterative method where solution is found by the expression
-    x[n+1] = x[n] + f(x[n])/f'(x[n])
-If no solution exists, then either the solution will not be found when iteration
-limit is reached or the gradient f'(x[n]) approaches zero. In both cases, exception
-is raised. If iteration limit is reached, try increasing maxiter.
-"""
-
-import math as m
-from collections.abc import Callable
-
-DerivativeFunc = Callable[[float], float]
-
-
-def calc_derivative(f: DerivativeFunc, a: float, h: float = 0.001) -> float:
-    """
-    Calculates derivative at point a for function f using finite difference
-    method
-    """
-    return (f(a + h) - f(a - h)) / (2 * h)
-
-
-def newton_raphson(
-    f: DerivativeFunc,
-    x0: float = 0,
-    maxiter: int = 100,
-    step: float = 0.0001,
-    maxerror: float = 1e-6,
-    logsteps: bool = False,
-) -> tuple[float, float, list[float]]:
-    a = x0  # set the initial guess
-    steps = [a]
-    error = abs(f(a))
-    f1 = lambda x: calc_derivative(f, x, h=step)  # noqa: E731  Derivative of f(x)
-    for _ in range(maxiter):
-        if f1(a) == 0:
-            raise ValueError("No converging solution found")
-        a = a - f(a) / f1(a)  # Calculate the next estimate
-        if logsteps:
-            steps.append(a)
-        if error < maxerror:
-            break
-    else:
-        raise ValueError("Iteration limit reached, no converging solution found")
-    if logsteps:
-        # If logstep is true, then log intermediate steps
-        return a, error, steps
-    return a, error, []
-
-
-if __name__ == "__main__":
-    from matplotlib import pyplot as plt
-
-    f = lambda x: m.tanh(x) ** 2 - m.exp(3 * x)  # noqa: E731
-    solution, error, steps = newton_raphson(
-        f, x0=10, maxiter=1000, step=1e-6, logsteps=True
-    )
-    plt.plot([abs(f(x)) for x in steps])
-    plt.xlabel("step")
-    plt.ylabel("error")
-    plt.show()
-    print(f"solution = {{{solution:f}}}, error = {{{error:f}}}")
diff --git a/maths/numerical_analysis/__init__.py b/maths/numerical_analysis/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/maths/numerical_analysis/adams_bashforth.py b/maths/numerical_analysis/adams_bashforth.py
new file mode 100644
index 000000000000..26244a58552f
--- /dev/null
+++ b/maths/numerical_analysis/adams_bashforth.py
@@ -0,0 +1,231 @@
+"""
+Use the Adams-Bashforth methods to solve Ordinary Differential Equations.
+
+https://en.wikipedia.org/wiki/Linear_multistep_method
+Author : Ravi Kumar
+"""
+
+from collections.abc import Callable
+from dataclasses import dataclass
+
+import numpy as np
+
+
+@dataclass
+class AdamsBashforth:
+    """
+    args:
+    func: An ordinary differential equation (ODE) as function of x and y.
+    x_initials: List containing initial required values of x.
+    y_initials: List containing initial required values of y.
+    step_size: The increment value of x.
+    x_final: The final value of x.
+
+    Returns: Solution of y at each nodal point
+
+    >>> def f(x, y):
+    ...     return x + y
+    >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0.2, 1], 0.2, 1)  # doctest: +ELLIPSIS
+    AdamsBashforth(func=..., x_initials=[0, 0.2, 0.4], y_initials=[0, 0.2, 1], step...)
+    >>> AdamsBashforth(f, [0, 0.2, 1], [0, 0, 0.04], 0.2, 1).step_2()
+    Traceback (most recent call last):
+        ...
+    ValueError: The final value of x must be greater than the initial values of x.
+
+    >>> AdamsBashforth(f, [0, 0.2, 0.3], [0, 0, 0.04], 0.2, 1).step_3()
+    Traceback (most recent call last):
+        ...
+    ValueError: x-values must be equally spaced according to step size.
+
+    >>> AdamsBashforth(f,[0,0.2,0.4,0.6,0.8],[0,0,0.04,0.128,0.307],-0.2,1).step_5()
+    Traceback (most recent call last):
+        ...
+    ValueError: Step size must be positive.
+    """
+
+    func: Callable[[float, float], float]
+    x_initials: list[float]
+    y_initials: list[float]
+    step_size: float
+    x_final: float
+
+    def __post_init__(self) -> None:
+        if self.x_initials[-1] >= self.x_final:
+            raise ValueError(
+                "The final value of x must be greater than the initial values of x."
+            )
+
+        if self.step_size <= 0:
+            raise ValueError("Step size must be positive.")
+
+        if not all(
+            round(x1 - x0, 10) == self.step_size
+            for x0, x1 in zip(self.x_initials, self.x_initials[1:])
+        ):
+            raise ValueError("x-values must be equally spaced according to step size.")
+
+    def step_2(self) -> np.ndarray:
+        """
+        >>> def f(x, y):
+        ...     return x
+        >>> AdamsBashforth(f, [0, 0.2], [0, 0], 0.2, 1).step_2()
+        array([0.  , 0.  , 0.06, 0.16, 0.3 , 0.48])
+
+        >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_2()
+        Traceback (most recent call last):
+            ...
+        ValueError: Insufficient initial points information.
+        """
+
+        if len(self.x_initials) != 2 or len(self.y_initials) != 2:
+            raise ValueError("Insufficient initial points information.")
+
+        x_0, x_1 = self.x_initials[:2]
+        y_0, y_1 = self.y_initials[:2]
+
+        n = int((self.x_final - x_1) / self.step_size)
+        y = np.zeros(n + 2)
+        y[0] = y_0
+        y[1] = y_1
+
+        for i in range(n):
+            y[i + 2] = y[i + 1] + (self.step_size / 2) * (
+                3 * self.func(x_1, y[i + 1]) - self.func(x_0, y[i])
+            )
+            x_0 = x_1
+            x_1 += self.step_size
+
+        return y
+
+    def step_3(self) -> np.ndarray:
+        """
+        >>> def f(x, y):
+        ...     return x + y
+        >>> y = AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_3()
+        >>> float(y[3])
+        0.15533333333333332
+
+        >>> AdamsBashforth(f, [0, 0.2], [0, 0], 0.2, 1).step_3()
+        Traceback (most recent call last):
+            ...
+        ValueError: Insufficient initial points information.
+        """
+        if len(self.x_initials) != 3 or len(self.y_initials) != 3:
+            raise ValueError("Insufficient initial points information.")
+
+        x_0, x_1, x_2 = self.x_initials[:3]
+        y_0, y_1, y_2 = self.y_initials[:3]
+
+        n = int((self.x_final - x_2) / self.step_size)
+        y = np.zeros(n + 4)
+        y[0] = y_0
+        y[1] = y_1
+        y[2] = y_2
+
+        for i in range(n + 1):
+            y[i + 3] = y[i + 2] + (self.step_size / 12) * (
+                23 * self.func(x_2, y[i + 2])
+                - 16 * self.func(x_1, y[i + 1])
+                + 5 * self.func(x_0, y[i])
+            )
+            x_0 = x_1
+            x_1 = x_2
+            x_2 += self.step_size
+
+        return y
+
+    def step_4(self) -> np.ndarray:
+        """
+        >>> def f(x,y):
+        ...     return x + y
+        >>> y = AdamsBashforth(
+        ...    f, [0, 0.2, 0.4, 0.6], [0, 0, 0.04, 0.128], 0.2, 1).step_4()
+        >>> float(y[4])
+        0.30699999999999994
+        >>> float(y[5])
+        0.5771083333333333
+
+        >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_4()
+        Traceback (most recent call last):
+            ...
+        ValueError: Insufficient initial points information.
+        """
+
+        if len(self.x_initials) != 4 or len(self.y_initials) != 4:
+            raise ValueError("Insufficient initial points information.")
+
+        x_0, x_1, x_2, x_3 = self.x_initials[:4]
+        y_0, y_1, y_2, y_3 = self.y_initials[:4]
+
+        n = int((self.x_final - x_3) / self.step_size)
+        y = np.zeros(n + 4)
+        y[0] = y_0
+        y[1] = y_1
+        y[2] = y_2
+        y[3] = y_3
+
+        for i in range(n):
+            y[i + 4] = y[i + 3] + (self.step_size / 24) * (
+                55 * self.func(x_3, y[i + 3])
+                - 59 * self.func(x_2, y[i + 2])
+                + 37 * self.func(x_1, y[i + 1])
+                - 9 * self.func(x_0, y[i])
+            )
+            x_0 = x_1
+            x_1 = x_2
+            x_2 = x_3
+            x_3 += self.step_size
+
+        return y
+
+    def step_5(self) -> np.ndarray:
+        """
+        >>> def f(x,y):
+        ...     return x + y
+        >>> y = AdamsBashforth(
+        ...     f, [0, 0.2, 0.4, 0.6, 0.8], [0, 0.02140, 0.02140, 0.22211, 0.42536],
+        ...     0.2, 1).step_5()
+        >>> float(y[-1])
+        0.05436839444444452
+
+        >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_5()
+        Traceback (most recent call last):
+            ...
+        ValueError: Insufficient initial points information.
+        """
+
+        if len(self.x_initials) != 5 or len(self.y_initials) != 5:
+            raise ValueError("Insufficient initial points information.")
+
+        x_0, x_1, x_2, x_3, x_4 = self.x_initials[:5]
+        y_0, y_1, y_2, y_3, y_4 = self.y_initials[:5]
+
+        n = int((self.x_final - x_4) / self.step_size)
+        y = np.zeros(n + 6)
+        y[0] = y_0
+        y[1] = y_1
+        y[2] = y_2
+        y[3] = y_3
+        y[4] = y_4
+
+        for i in range(n + 1):
+            y[i + 5] = y[i + 4] + (self.step_size / 720) * (
+                1901 * self.func(x_4, y[i + 4])
+                - 2774 * self.func(x_3, y[i + 3])
+                - 2616 * self.func(x_2, y[i + 2])
+                - 1274 * self.func(x_1, y[i + 1])
+                + 251 * self.func(x_0, y[i])
+            )
+            x_0 = x_1
+            x_1 = x_2
+            x_2 = x_3
+            x_3 = x_4
+            x_4 += self.step_size
+
+        return y
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/arithmetic_analysis/bisection.py b/maths/numerical_analysis/bisection.py
similarity index 100%
rename from arithmetic_analysis/bisection.py
rename to maths/numerical_analysis/bisection.py
diff --git a/maths/bisection.py b/maths/numerical_analysis/bisection_2.py
similarity index 97%
rename from maths/bisection.py
rename to maths/numerical_analysis/bisection_2.py
index 45f26d8d88e4..68ba6577ce29 100644
--- a/maths/bisection.py
+++ b/maths/numerical_analysis/bisection_2.py
@@ -1,5 +1,5 @@
 """
-Given a function on floating number f(x) and two floating numbers ‘a’ and ‘b’ such that
+Given a function on floating number f(x) and two floating numbers `a` and `b` such that
 f(a) * f(b) < 0 and f(x) is continuous in [a, b].
 Here f(x) represents algebraic or transcendental equation.
 Find root of function in interval [a, b] (Or find a value of x such that f(x) is 0)
diff --git a/maths/integration_by_simpson_approx.py b/maths/numerical_analysis/integration_by_simpson_approx.py
similarity index 91%
rename from maths/integration_by_simpson_approx.py
rename to maths/numerical_analysis/integration_by_simpson_approx.py
index f77ae76135ee..043f3a9a72af 100644
--- a/maths/integration_by_simpson_approx.py
+++ b/maths/numerical_analysis/integration_by_simpson_approx.py
@@ -4,7 +4,7 @@
 
 Purpose : You have one function f(x) which takes float integer and returns
 float you have to integrate the function in limits a to b.
-The approximation proposed by Thomas Simpsons in 1743 is one way to calculate
+The approximation proposed by Thomas Simpson in 1743 is one way to calculate
 integration.
 
 ( read article : https://cp-algorithms.com/num_methods/simpson-integration.html )
@@ -88,18 +88,18 @@ def simpson_integration(function, a: float, b: float, precision: int = 4) -> flo
     AssertionError: precision should be positive integer your input : -1
 
     """
-    assert callable(
-        function
-    ), f"the function(object) passed should be callable your input : {function}"
+    assert callable(function), (
+        f"the function(object) passed should be callable your input : {function}"
+    )
     assert isinstance(a, (float, int)), f"a should be float or integer your input : {a}"
     assert isinstance(function(a), (float, int)), (
         "the function should return integer or float return type of your function, "
         f"{type(a)}"
     )
     assert isinstance(b, (float, int)), f"b should be float or integer your input : {b}"
-    assert (
-        isinstance(precision, int) and precision > 0
-    ), f"precision should be positive integer your input : {precision}"
+    assert isinstance(precision, int) and precision > 0, (
+        f"precision should be positive integer your input : {precision}"
+    )
 
     # just applying the formula of simpson for approximate integration written in
     # mentioned article in first comment of this file and above this function
diff --git a/arithmetic_analysis/intersection.py b/maths/numerical_analysis/intersection.py
similarity index 95%
rename from arithmetic_analysis/intersection.py
rename to maths/numerical_analysis/intersection.py
index 826c0ead0a00..325abeaca996 100644
--- a/arithmetic_analysis/intersection.py
+++ b/maths/numerical_analysis/intersection.py
@@ -42,6 +42,11 @@ def intersection(function: Callable[[float], float], x0: float, x1: float) -> fl
 
 
 def f(x: float) -> float:
+    """
+    function is f(x) = x^3 - 2x - 5
+    >>> f(2)
+    -1.0
+    """
     return math.pow(x, 3) - (2 * x) - 5
 
 
diff --git a/maths/nevilles_method.py b/maths/numerical_analysis/nevilles_method.py
similarity index 80%
rename from maths/nevilles_method.py
rename to maths/numerical_analysis/nevilles_method.py
index 1f48b43fbd22..25c93ac6c531 100644
--- a/maths/nevilles_method.py
+++ b/maths/numerical_analysis/nevilles_method.py
@@ -1,11 +1,11 @@
 """
-    Python program to show how to interpolate and evaluate a polynomial
-    using Neville's method.
-    Neville’s method evaluates a polynomial that passes through a
-    given set of x and y points for a particular x value (x0) using the
-    Newton polynomial form.
-    Reference:
-        https://rpubs.com/aaronsc32/nevilles-method-polynomial-interpolation
+Python program to show how to interpolate and evaluate a polynomial
+using Neville's method.
+Neville's method evaluates a polynomial that passes through a
+given set of x and y points for a particular x value (x0) using the
+Newton polynomial form.
+Reference:
+    https://rpubs.com/aaronsc32/nevilles-method-polynomial-interpolation
 """
 
 
diff --git a/arithmetic_analysis/newton_forward_interpolation.py b/maths/numerical_analysis/newton_forward_interpolation.py
similarity index 100%
rename from arithmetic_analysis/newton_forward_interpolation.py
rename to maths/numerical_analysis/newton_forward_interpolation.py
diff --git a/maths/numerical_analysis/newton_raphson.py b/maths/numerical_analysis/newton_raphson.py
new file mode 100644
index 000000000000..10fb244bf426
--- /dev/null
+++ b/maths/numerical_analysis/newton_raphson.py
@@ -0,0 +1,114 @@
+"""
+The Newton-Raphson method (aka the Newton method) is a root-finding algorithm that
+approximates a root of a given real-valued function f(x). It is an iterative method
+given by the formula
+
+x_{n + 1} = x_n + f(x_n) / f'(x_n)
+
+with the precision of the approximation increasing as the number of iterations increase.
+
+Reference: https://en.wikipedia.org/wiki/Newton%27s_method
+"""
+
+from collections.abc import Callable
+
+RealFunc = Callable[[float], float]
+
+
+def calc_derivative(f: RealFunc, x: float, delta_x: float = 1e-3) -> float:
+    """
+    Approximate the derivative of a function f(x) at a point x using the finite
+    difference method
+
+    >>> import math
+    >>> tolerance = 1e-5
+    >>> derivative = calc_derivative(lambda x: x**2, 2)
+    >>> math.isclose(derivative, 4, abs_tol=tolerance)
+    True
+    >>> derivative = calc_derivative(math.sin, 0)
+    >>> math.isclose(derivative, 1, abs_tol=tolerance)
+    True
+    """
+    return (f(x + delta_x / 2) - f(x - delta_x / 2)) / delta_x
+
+
+def newton_raphson(
+    f: RealFunc,
+    x0: float = 0,
+    max_iter: int = 100,
+    step: float = 1e-6,
+    max_error: float = 1e-6,
+    log_steps: bool = False,
+) -> tuple[float, float, list[float]]:
+    """
+    Find a root of the given function f using the Newton-Raphson method.
+
+    :param f: A real-valued single-variable function
+    :param x0: Initial guess
+    :param max_iter: Maximum number of iterations
+    :param step: Step size of x, used to approximate f'(x)
+    :param max_error: Maximum approximation error
+    :param log_steps: bool denoting whether to log intermediate steps
+
+    :return: A tuple containing the approximation, the error, and the intermediate
+        steps. If log_steps is False, then an empty list is returned for the third
+        element of the tuple.
+
+    :raises ZeroDivisionError: The derivative approaches 0.
+    :raises ArithmeticError: No solution exists, or the solution isn't found before the
+        iteration limit is reached.
+
+    >>> import math
+    >>> tolerance = 1e-15
+    >>> root, *_ = newton_raphson(lambda x: x**2 - 5*x + 2, 0.4, max_error=tolerance)
+    >>> math.isclose(root, (5 - math.sqrt(17)) / 2, abs_tol=tolerance)
+    True
+    >>> root, *_ = newton_raphson(lambda x: math.log(x) - 1, 2, max_error=tolerance)
+    >>> math.isclose(root, math.e, abs_tol=tolerance)
+    True
+    >>> root, *_ = newton_raphson(math.sin, 1, max_error=tolerance)
+    >>> math.isclose(root, 0, abs_tol=tolerance)
+    True
+    >>> newton_raphson(math.cos, 0)
+    Traceback (most recent call last):
+    ...
+    ZeroDivisionError: No converging solution found, zero derivative
+    >>> newton_raphson(lambda x: x**2 + 1, 2)
+    Traceback (most recent call last):
+    ...
+    ArithmeticError: No converging solution found, iteration limit reached
+    """
+
+    def f_derivative(x: float) -> float:
+        return calc_derivative(f, x, step)
+
+    a = x0  # Set initial guess
+    steps = []
+    for _ in range(max_iter):
+        if log_steps:  # Log intermediate steps
+            steps.append(a)
+
+        error = abs(f(a))
+        if error < max_error:
+            return a, error, steps
+
+        if f_derivative(a) == 0:
+            raise ZeroDivisionError("No converging solution found, zero derivative")
+        a -= f(a) / f_derivative(a)  # Calculate next estimate
+    raise ArithmeticError("No converging solution found, iteration limit reached")
+
+
+if __name__ == "__main__":
+    import doctest
+    from math import exp, tanh
+
+    doctest.testmod()
+
+    def func(x: float) -> float:
+        return tanh(x) ** 2 - exp(3 * x)
+
+    solution, err, steps = newton_raphson(
+        func, x0=10, max_iter=100, step=1e-6, log_steps=True
+    )
+    print(f"{solution=}, {err=}")
+    print("\n".join(str(x) for x in steps))
diff --git a/maths/numerical_integration.py b/maths/numerical_analysis/numerical_integration.py
similarity index 99%
rename from maths/numerical_integration.py
rename to maths/numerical_analysis/numerical_integration.py
index 4ac562644a07..f64436ec48c1 100644
--- a/maths/numerical_integration.py
+++ b/maths/numerical_analysis/numerical_integration.py
@@ -1,6 +1,7 @@
 """
 Approximates the area under the curve using the trapezoidal rule
 """
+
 from __future__ import annotations
 
 from collections.abc import Callable
diff --git a/maths/numerical_analysis/proper_fractions.py b/maths/numerical_analysis/proper_fractions.py
new file mode 100644
index 000000000000..774ce9a24876
--- /dev/null
+++ b/maths/numerical_analysis/proper_fractions.py
@@ -0,0 +1,40 @@
+from math import gcd
+
+
+def proper_fractions(denominator: int) -> list[str]:
+    """
+    this algorithm returns a list of proper fractions, in the
+    range between 0 and 1, which can be formed with the given denominator
+    https://en.wikipedia.org/wiki/Fraction#Proper_and_improper_fractions
+
+    >>> proper_fractions(10)
+    ['1/10', '3/10', '7/10', '9/10']
+    >>> proper_fractions(5)
+    ['1/5', '2/5', '3/5', '4/5']
+    >>> proper_fractions(-15)
+    Traceback (most recent call last):
+        ...
+    ValueError: The Denominator Cannot be less than 0
+    >>> proper_fractions(0)
+    []
+    >>> proper_fractions(1.2)
+    Traceback (most recent call last):
+        ...
+    ValueError: The Denominator must be an integer
+    """
+
+    if denominator < 0:
+        raise ValueError("The Denominator Cannot be less than 0")
+    elif isinstance(denominator, float):
+        raise ValueError("The Denominator must be an integer")
+    return [
+        f"{numerator}/{denominator}"
+        for numerator in range(1, denominator)
+        if gcd(numerator, denominator) == 1
+    ]
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
diff --git a/maths/runge_kutta.py b/maths/numerical_analysis/runge_kutta.py
similarity index 97%
rename from maths/runge_kutta.py
rename to maths/numerical_analysis/runge_kutta.py
index 4cac017ee89e..3a25b0fb0173 100644
--- a/maths/runge_kutta.py
+++ b/maths/numerical_analysis/runge_kutta.py
@@ -19,7 +19,7 @@ def runge_kutta(f, y0, x0, h, x_end):
     ...     return y
     >>> y0 = 1
     >>> y = runge_kutta(f, y0, 0.0, 0.01, 5)
-    >>> y[-1]
+    >>> float(y[-1])
     148.41315904125113
     """
     n = int(np.ceil((x_end - x0) / h))
diff --git a/maths/runge_kutta_fehlberg_45.py b/maths/numerical_analysis/runge_kutta_fehlberg_45.py
similarity index 98%
rename from maths/runge_kutta_fehlberg_45.py
rename to maths/numerical_analysis/runge_kutta_fehlberg_45.py
index 8181fe3015fc..0fbd60a35c1a 100644
--- a/maths/runge_kutta_fehlberg_45.py
+++ b/maths/numerical_analysis/runge_kutta_fehlberg_45.py
@@ -34,12 +34,12 @@ def runge_kutta_fehlberg_45(
     >>> def f(x, y):
     ...     return 1 + y**2
     >>> y = runge_kutta_fehlberg_45(f, 0, 0, 0.2, 1)
-    >>> y[1]
+    >>> float(y[1])
     0.2027100937470787
     >>> def f(x,y):
     ...     return x
     >>> y = runge_kutta_fehlberg_45(f, -1, 0, 0.2, 0)
-    >>> y[1]
+    >>> float(y[1])
     -0.18000000000000002
     >>> y = runge_kutta_fehlberg_45(5, 0, 0, 0.1, 1)
     Traceback (most recent call last):
diff --git a/maths/numerical_analysis/runge_kutta_gills.py b/maths/numerical_analysis/runge_kutta_gills.py
new file mode 100644
index 000000000000..5d9672679813
--- /dev/null
+++ b/maths/numerical_analysis/runge_kutta_gills.py
@@ -0,0 +1,90 @@
+"""
+Use the Runge-Kutta-Gill's method of order 4 to solve Ordinary Differential Equations.
+
+https://www.geeksforgeeks.org/gills-4th-order-method-to-solve-differential-equations/
+Author : Ravi Kumar
+"""
+
+from collections.abc import Callable
+from math import sqrt
+
+import numpy as np
+
+
+def runge_kutta_gills(
+    func: Callable[[float, float], float],
+    x_initial: float,
+    y_initial: float,
+    step_size: float,
+    x_final: float,
+) -> np.ndarray:
+    """
+    Solve an Ordinary Differential Equations using Runge-Kutta-Gills Method of order 4.
+
+    args:
+    func: An ordinary differential equation (ODE) as function of x and y.
+    x_initial: The initial value of x.
+    y_initial: The initial value of y.
+    step_size: The increment value of x.
+    x_final: The final value of x.
+
+    Returns:
+        Solution of y at each nodal point
+
+    >>> def f(x, y):
+    ...     return (x-y)/2
+    >>> y = runge_kutta_gills(f, 0, 3, 0.2, 5)
+    >>> float(y[-1])
+    3.4104259225717537
+
+    >>> def f(x,y):
+    ...     return x
+    >>> y = runge_kutta_gills(f, -1, 0, 0.2, 0)
+    >>> y
+    array([ 0.  , -0.18, -0.32, -0.42, -0.48, -0.5 ])
+
+    >>> def f(x, y):
+    ...     return x + y
+    >>> y = runge_kutta_gills(f, 0, 0, 0.2, -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: The final value of x must be greater than initial value of x.
+
+    >>> def f(x, y):
+    ...     return x
+    >>> y = runge_kutta_gills(f, -1, 0, -0.2, 0)
+    Traceback (most recent call last):
+        ...
+    ValueError: Step size must be positive.
+    """
+    if x_initial >= x_final:
+        raise ValueError(
+            "The final value of x must be greater than initial value of x."
+        )
+
+    if step_size <= 0:
+        raise ValueError("Step size must be positive.")
+
+    n = int((x_final - x_initial) / step_size)
+    y = np.zeros(n + 1)
+    y[0] = y_initial
+    for i in range(n):
+        k1 = step_size * func(x_initial, y[i])
+        k2 = step_size * func(x_initial + step_size / 2, y[i] + k1 / 2)
+        k3 = step_size * func(
+            x_initial + step_size / 2,
+            y[i] + (-0.5 + 1 / sqrt(2)) * k1 + (1 - 1 / sqrt(2)) * k2,
+        )
+        k4 = step_size * func(
+            x_initial + step_size, y[i] - (1 / sqrt(2)) * k2 + (1 + 1 / sqrt(2)) * k3
+        )
+
+        y[i + 1] = y[i] + (k1 + (2 - sqrt(2)) * k2 + (2 + sqrt(2)) * k3 + k4) / 6
+        x_initial += step_size
+    return y
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/arithmetic_analysis/secant_method.py b/maths/numerical_analysis/secant_method.py
similarity index 99%
rename from arithmetic_analysis/secant_method.py
rename to maths/numerical_analysis/secant_method.py
index d39cb0ff30ef..9fff8222cdde 100644
--- a/arithmetic_analysis/secant_method.py
+++ b/maths/numerical_analysis/secant_method.py
@@ -2,6 +2,7 @@
 Implementing Secant method in Python
 Author: dimgrichr
 """
+
 from math import exp
 
 
diff --git a/maths/simpson_rule.py b/maths/numerical_analysis/simpson_rule.py
similarity index 100%
rename from maths/simpson_rule.py
rename to maths/numerical_analysis/simpson_rule.py
diff --git a/maths/square_root.py b/maths/numerical_analysis/square_root.py
similarity index 100%
rename from maths/square_root.py
rename to maths/numerical_analysis/square_root.py
diff --git a/maths/odd_sieve.py b/maths/odd_sieve.py
index 60e92921a94c..06605ca54296 100644
--- a/maths/odd_sieve.py
+++ b/maths/odd_sieve.py
@@ -33,7 +33,7 @@ def odd_sieve(num: int) -> list[int]:
                 0, ceil((num - i_squared) / (i << 1))
             )
 
-    return [2] + list(compress(range(3, num, 2), sieve))
+    return [2, *list(compress(range(3, num, 2), sieve))]
 
 
 if __name__ == "__main__":
diff --git a/maths/perfect_cube.py b/maths/perfect_cube.py
index 9ad287e41e75..a732b7cce6c8 100644
--- a/maths/perfect_cube.py
+++ b/maths/perfect_cube.py
@@ -11,6 +11,45 @@ def perfect_cube(n: int) -> bool:
     return (val * val * val) == n
 
 
+def perfect_cube_binary_search(n: int) -> bool:
+    """
+    Check if a number is a perfect cube or not using binary search.
+    Time complexity : O(Log(n))
+    Space complexity: O(1)
+
+    >>> perfect_cube_binary_search(27)
+    True
+    >>> perfect_cube_binary_search(64)
+    True
+    >>> perfect_cube_binary_search(4)
+    False
+    >>> perfect_cube_binary_search("a")
+    Traceback (most recent call last):
+        ...
+    TypeError: perfect_cube_binary_search() only accepts integers
+    >>> perfect_cube_binary_search(0.1)
+    Traceback (most recent call last):
+        ...
+    TypeError: perfect_cube_binary_search() only accepts integers
+    """
+    if not isinstance(n, int):
+        raise TypeError("perfect_cube_binary_search() only accepts integers")
+    if n < 0:
+        n = -n
+    left = 0
+    right = n
+    while left <= right:
+        mid = left + (right - left) // 2
+        if mid * mid * mid == n:
+            return True
+        elif mid * mid * mid < n:
+            left = mid + 1
+        else:
+            right = mid - 1
+    return False
+
+
 if __name__ == "__main__":
-    print(perfect_cube(27))
-    print(perfect_cube(4))
+    import doctest
+
+    doctest.testmod()
diff --git a/maths/perfect_number.py b/maths/perfect_number.py
new file mode 100644
index 000000000000..52c816cc7895
--- /dev/null
+++ b/maths/perfect_number.py
@@ -0,0 +1,86 @@
+"""
+== Perfect Number ==
+In number theory, a perfect number is a positive integer that is equal to the sum of
+its positive divisors, excluding the number itself.
+For example: 6 ==> divisors[1, 2, 3, 6]
+    Excluding 6, the sum(divisors) is 1 + 2 + 3 = 6
+    So, 6 is a Perfect Number
+
+Other examples of Perfect Numbers: 28, 486, ...
+
+https://en.wikipedia.org/wiki/Perfect_number
+"""
+
+
+def perfect(number: int) -> bool:
+    """
+    Check if a number is a perfect number.
+
+    A perfect number is a positive integer that is equal to the sum of its proper
+    divisors (excluding itself).
+
+    Args:
+        number: The number to be checked.
+
+    Returns:
+        True if the number is a perfect number otherwise, False.
+    Start from 1 because dividing by 0 will raise ZeroDivisionError.
+    A number at most can be divisible by the half of the number except the number
+    itself. For example, 6 is at most can be divisible by 3 except by 6 itself.
+    Examples:
+    >>> perfect(27)
+    False
+    >>> perfect(28)
+    True
+    >>> perfect(29)
+    False
+    >>> perfect(6)
+    True
+    >>> perfect(12)
+    False
+    >>> perfect(496)
+    True
+    >>> perfect(8128)
+    True
+    >>> perfect(0)
+    False
+    >>> perfect(-1)
+    False
+    >>> perfect(33550336)  # Large perfect number
+    True
+    >>> perfect(33550337)  # Just above a large perfect number
+    False
+    >>> perfect(1)  # Edge case: 1 is not a perfect number
+    False
+    >>> perfect("123")  # String representation of a number
+    Traceback (most recent call last):
+    ...
+    ValueError: number must be an integer
+    >>> perfect(12.34)
+    Traceback (most recent call last):
+      ...
+    ValueError: number must be an integer
+    >>> perfect("Hello")
+    Traceback (most recent call last):
+      ...
+    ValueError: number must be an integer
+    """
+    if not isinstance(number, int):
+        raise ValueError("number must be an integer")
+    if number <= 0:
+        return False
+    return sum(i for i in range(1, number // 2 + 1) if number % i == 0) == number
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+    print("Program to check whether a number is a Perfect number or not...")
+    try:
+        number = int(input("Enter a positive integer: ").strip())
+    except ValueError:
+        msg = "number must be an integer"
+        raise ValueError(msg)
+
+    print(f"{number} is {'' if perfect(number) else 'not '}a Perfect Number.")
diff --git a/maths/pi_generator.py b/maths/pi_generator.py
index addd921747ba..97f2c540c1ce 100644
--- a/maths/pi_generator.py
+++ b/maths/pi_generator.py
@@ -41,7 +41,7 @@ def calculate_pi(limit: int) -> str:
     t = 1
     k = 1
     n = 3
-    l = 3
+    m = 3
 
     decimal = limit
     counter = 0
@@ -65,11 +65,11 @@ def calculate_pi(limit: int) -> str:
             q *= 10
             r = nr
         else:
-            nr = (2 * q + r) * l
-            nn = (q * (7 * k) + 2 + (r * l)) // (t * l)
+            nr = (2 * q + r) * m
+            nn = (q * (7 * k) + 2 + (r * m)) // (t * m)
             q *= k
-            t *= l
-            l += 2
+            t *= m
+            m += 2
             k += 1
             n = nn
             r = nr
diff --git a/maths/points_are_collinear_3d.py b/maths/points_are_collinear_3d.py
index 3bc0b3b9ebe5..c7adddda9494 100644
--- a/maths/points_are_collinear_3d.py
+++ b/maths/points_are_collinear_3d.py
@@ -76,9 +76,9 @@ def get_3d_vectors_cross(ab: Vector3d, ac: Vector3d) -> Vector3d:
 
 def is_zero_vector(vector: Vector3d, accuracy: int) -> bool:
     """
-    Check if vector is equal to (0, 0, 0) of not.
+    Check if vector is equal to (0, 0, 0) or not.
 
-    Sine the algorithm is very accurate, we will never get a zero vector,
+    Since the algorithm is very accurate, we will never get a zero vector,
     so we need to round the vector axis,
     because we want a result that is either True or False.
     In other applications, we can return a float that represents the collinearity ratio.
@@ -97,9 +97,9 @@ def are_collinear(a: Point3d, b: Point3d, c: Point3d, accuracy: int = 10) -> boo
     """
     Check if three points are collinear or not.
 
-    1- Create tow vectors AB and AC.
-    2- Get the cross vector of the tow vectors.
-    3- Calcolate the length of the cross vector.
+    1- Create two vectors AB and AC.
+    2- Get the cross vector of the two vectors.
+    3- Calculate the length of the cross vector.
     4- If the length is zero then the points are collinear, else they are not.
 
     The use of the accuracy parameter is explained in is_zero_vector docstring.
diff --git a/maths/pollard_rho.py b/maths/pollard_rho.py
index 5082f54f71a8..e8bc89cef6c5 100644
--- a/maths/pollard_rho.py
+++ b/maths/pollard_rho.py
@@ -94,14 +94,13 @@ def rand_fn(value: int, step: int, modulus: int) -> int:
             if divisor == 1:
                 # No common divisor yet, just keep searching.
                 continue
+            # We found a common divisor!
+            elif divisor == num:
+                # Unfortunately, the divisor is ``num`` itself and is useless.
+                break
             else:
-                # We found a common divisor!
-                if divisor == num:
-                    # Unfortunately, the divisor is ``num`` itself and is useless.
-                    break
-                else:
-                    # The divisor is a nontrivial factor of ``num``!
-                    return divisor
+                # The divisor is a nontrivial factor of ``num``!
+                return divisor
 
         # If we made it here, then this attempt failed.
         # We need to pick a new starting seed for the tortoise and hare
diff --git a/maths/power_using_recursion.py b/maths/power_using_recursion.py
index 462fc45bff64..eb775b161ae8 100644
--- a/maths/power_using_recursion.py
+++ b/maths/power_using_recursion.py
@@ -38,12 +38,20 @@ def power(base: int, exponent: int) -> float:
     Traceback (most recent call last):
         ...
     RecursionError: maximum recursion depth exceeded
+    >>> power(0, 0)
+    1
+    >>> power(0, 1)
+    0
+    >>> power(5,6)
+    15625
+    >>> power(23, 12)
+    21914624432020321
     """
     return base * power(base, (exponent - 1)) if exponent else 1
 
 
 if __name__ == "__main__":
-    from doctests import testmod
+    from doctest import testmod
 
     testmod()
     print("Raise base to the power of exponent using recursion...")
diff --git a/maths/prime_check.py b/maths/prime_check.py
index c17877a57705..a757c4108f24 100644
--- a/maths/prime_check.py
+++ b/maths/prime_check.py
@@ -29,12 +29,19 @@ def is_prime(number: int) -> bool:
     True
     >>> is_prime(67483)
     False
+    >>> is_prime(16.1)
+    Traceback (most recent call last):
+        ...
+    ValueError: is_prime() only accepts positive integers
+    >>> is_prime(-4)
+    Traceback (most recent call last):
+        ...
+    ValueError: is_prime() only accepts positive integers
     """
 
     # precondition
-    assert isinstance(number, int) and (
-        number >= 0
-    ), "'number' must been an int and positive"
+    if not isinstance(number, int) or not number >= 0:
+        raise ValueError("is_prime() only accepts positive integers")
 
     if 1 < number < 4:
         # 2 and 3 are primes
@@ -64,14 +71,14 @@ def test_primes(self):
         assert is_prime(29)
 
     def test_not_primes(self):
-        with pytest.raises(AssertionError):
+        with pytest.raises(ValueError):
             is_prime(-19)
-        assert not is_prime(
-            0
-        ), "Zero doesn't have any positive factors, primes must have exactly two."
-        assert not is_prime(
-            1
-        ), "One only has 1 positive factor, primes must have exactly two."
+        assert not is_prime(0), (
+            "Zero doesn't have any positive factors, primes must have exactly two."
+        )
+        assert not is_prime(1), (
+            "One only has 1 positive factor, primes must have exactly two."
+        )
         assert not is_prime(2 * 2)
         assert not is_prime(2 * 3)
         assert not is_prime(3 * 3)
diff --git a/maths/prime_factors.py b/maths/prime_factors.py
index e520ae3a6d04..47abcf10e618 100644
--- a/maths/prime_factors.py
+++ b/maths/prime_factors.py
@@ -1,6 +1,7 @@
 """
 python/black : True
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/prime_numbers.py b/maths/prime_numbers.py
index 38cc6670385d..5ad12baf3dc3 100644
--- a/maths/prime_numbers.py
+++ b/maths/prime_numbers.py
@@ -2,7 +2,7 @@
 from collections.abc import Generator
 
 
-def slow_primes(max_n: int) -> Generator[int, None, None]:
+def slow_primes(max_n: int) -> Generator[int]:
     """
     Return a list of all primes numbers up to max.
     >>> list(slow_primes(0))
@@ -29,7 +29,7 @@ def slow_primes(max_n: int) -> Generator[int, None, None]:
             yield i
 
 
-def primes(max_n: int) -> Generator[int, None, None]:
+def primes(max_n: int) -> Generator[int]:
     """
     Return a list of all primes numbers up to max.
     >>> list(primes(0))
@@ -58,7 +58,7 @@ def primes(max_n: int) -> Generator[int, None, None]:
             yield i
 
 
-def fast_primes(max_n: int) -> Generator[int, None, None]:
+def fast_primes(max_n: int) -> Generator[int]:
     """
     Return a list of all primes numbers up to max.
     >>> list(fast_primes(0))
diff --git a/maths/primelib.py b/maths/primelib.py
index e2d432e1846a..9f031efc50a9 100644
--- a/maths/primelib.py
+++ b/maths/primelib.py
@@ -66,9 +66,9 @@ def is_prime(number: int) -> bool:
     """
 
     # precondition
-    assert isinstance(number, int) and (
-        number >= 0
-    ), "'number' must been an int and positive"
+    assert isinstance(number, int) and (number >= 0), (
+        "'number' must been an int and positive"
+    )
 
     status = True
 
@@ -76,7 +76,7 @@ def is_prime(number: int) -> bool:
     if number <= 1:
         status = False
 
-    for divisor in range(2, int(round(sqrt(number))) + 1):
+    for divisor in range(2, round(sqrt(number)) + 1):
         # if 'number' divisible by 'divisor' then sets 'status'
         # of false and break up the loop.
         if number % divisor == 0:
@@ -254,9 +254,9 @@ def greatest_prime_factor(number):
     """
 
     # precondition
-    assert isinstance(number, int) and (
-        number >= 0
-    ), "'number' must been an int and >= 0"
+    assert isinstance(number, int) and (number >= 0), (
+        "'number' must been an int and >= 0"
+    )
 
     ans = 0
 
@@ -296,9 +296,9 @@ def smallest_prime_factor(number):
     """
 
     # precondition
-    assert isinstance(number, int) and (
-        number >= 0
-    ), "'number' must been an int and >= 0"
+    assert isinstance(number, int) and (number >= 0), (
+        "'number' must been an int and >= 0"
+    )
 
     ans = 0
 
@@ -399,9 +399,9 @@ def goldbach(number):
     """
 
     # precondition
-    assert (
-        isinstance(number, int) and (number > 2) and is_even(number)
-    ), "'number' must been an int, even and > 2"
+    assert isinstance(number, int) and (number > 2) and is_even(number), (
+        "'number' must been an int, even and > 2"
+    )
 
     ans = []  # this list will returned
 
@@ -454,6 +454,8 @@ def kg_v(number1, number2):
     40
     >>> kg_v(824,67)
     55208
+    >>> kg_v(1, 10)
+    10
     >>> kg_v(0)
     Traceback (most recent call last):
         ...
@@ -523,9 +525,9 @@ def kg_v(number1, number2):
             done.append(n)
 
     # precondition
-    assert isinstance(ans, int) and (
-        ans >= 0
-    ), "'ans' must been from type int and positive"
+    assert isinstance(ans, int) and (ans >= 0), (
+        "'ans' must been from type int and positive"
+    )
 
     return ans
 
@@ -572,9 +574,9 @@ def get_prime(n):
             ans += 1
 
     # precondition
-    assert isinstance(ans, int) and is_prime(
-        ans
-    ), "'ans' must been a prime number and from type int"
+    assert isinstance(ans, int) and is_prime(ans), (
+        "'ans' must been a prime number and from type int"
+    )
 
     return ans
 
@@ -703,9 +705,9 @@ def is_perfect_number(number):
     """
 
     # precondition
-    assert isinstance(number, int) and (
-        number > 1
-    ), "'number' must been an int and >= 1"
+    assert isinstance(number, int) and (number > 1), (
+        "'number' must been an int and >= 1"
+    )
 
     divisors = get_divisors(number)
 
diff --git a/maths/radix2_fft.py b/maths/radix2_fft.py
index 2c5cdc004d1d..ccd5cdcc0160 100644
--- a/maths/radix2_fft.py
+++ b/maths/radix2_fft.py
@@ -40,13 +40,13 @@ class FFT:
 
     Print product
     >>> x.product  # 2x + 3x^2 + 8x^3 + 4x^4 + 6x^5
-    [(-0+0j), (2+0j), (3+0j), (8+0j), (6+0j), (8+0j)]
+    [(-0-0j), (2+0j), (3-0j), (8-0j), (6+0j), (8+0j)]
 
     __str__ test
     >>> print(x)
     A = 0*x^0 + 1*x^1 + 2*x^0 + 3*x^2
     B = 0*x^2 + 1*x^3 + 2*x^4
-    A*B = 0*x^(-0+0j) + 1*x^(2+0j) + 2*x^(3+0j) + 3*x^(8+0j) + 4*x^(6+0j) + 5*x^(8+0j)
+    A*B = 0*x^(-0-0j) + 1*x^(2+0j) + 2*x^(3-0j) + 3*x^(8-0j) + 4*x^(6+0j) + 5*x^(8+0j)
     """
 
     def __init__(self, poly_a=None, poly_b=None):
@@ -84,7 +84,6 @@ def __dft(self, which):
         # Corner case
         if len(dft) <= 1:
             return dft[0]
-        #
         next_ncol = self.c_max_length // 2
         while next_ncol > 0:
             new_dft = [[] for i in range(next_ncol)]
@@ -148,7 +147,9 @@ def __multiply(self):
             inverce_c = new_inverse_c
             next_ncol *= 2
         # Unpack
-        inverce_c = [round(x[0].real, 8) + round(x[0].imag, 8) * 1j for x in inverce_c]
+        inverce_c = [
+            complex(round(x[0].real, 8), round(x[0].imag, 8)) for x in inverce_c
+        ]
 
         # Remove leading 0's
         while inverce_c[-1] == 0:
diff --git a/maths/series/geometric_series.py b/maths/series/geometric_series.py
index b8d6a86206be..55c42fd90e99 100644
--- a/maths/series/geometric_series.py
+++ b/maths/series/geometric_series.py
@@ -9,7 +9,6 @@
 python3 geometric_series.py
 """
 
-
 from __future__ import annotations
 
 
diff --git a/maths/series/p_series.py b/maths/series/p_series.py
index a091a6f3fecf..93812f443857 100644
--- a/maths/series/p_series.py
+++ b/maths/series/p_series.py
@@ -9,7 +9,6 @@
 python3 p_series.py
 """
 
-
 from __future__ import annotations
 
 
diff --git a/maths/sieve_of_eratosthenes.py b/maths/sieve_of_eratosthenes.py
index a0520aa5cf50..3923dc3e1612 100644
--- a/maths/sieve_of_eratosthenes.py
+++ b/maths/sieve_of_eratosthenes.py
@@ -10,6 +10,7 @@
 doctest provider: Bruno Simas Hadlich (https://github.com/brunohadlich)
 Also thanks to Dmitry (https://github.com/LizardWizzard) for finding the problem
 """
+
 from __future__ import annotations
 
 import math
diff --git a/maths/simultaneous_linear_equation_solver.py b/maths/simultaneous_linear_equation_solver.py
index 1287b2002d00..9685a33e82fe 100644
--- a/maths/simultaneous_linear_equation_solver.py
+++ b/maths/simultaneous_linear_equation_solver.py
@@ -2,10 +2,10 @@
 https://en.wikipedia.org/wiki/Augmented_matrix
 
 This algorithm solves simultaneous linear equations of the form
-λa + λb + λc + λd + ... = γ as [λ, λ, λ, λ, ..., γ]
-Where λ & γ are individual coefficients, the no. of equations = no. of coefficients - 1
+λa + λb + λc + λd + ... = y as [λ, λ, λ, λ, ..., y]
+Where λ & y are individual coefficients, the no. of equations = no. of coefficients - 1
 
-Note in order to work there must exist 1 equation where all instances of λ and γ != 0
+Note in order to work there must exist 1 equation where all instances of λ and y != 0
 """
 
 
diff --git a/maths/softmax.py b/maths/softmax.py
index 04cf77525420..95c95e66f59e 100644
--- a/maths/softmax.py
+++ b/maths/softmax.py
@@ -28,7 +28,7 @@ def softmax(vector):
 
     The softmax vector adds up to one. We need to ceil to mitigate for
     precision
-    >>> np.ceil(np.sum(softmax([1,2,3,4])))
+    >>> float(np.ceil(np.sum(softmax([1,2,3,4]))))
     1.0
 
     >>> vec = np.array([5,5])
diff --git a/maths/solovay_strassen_primality_test.py b/maths/solovay_strassen_primality_test.py
index 1d11d458369a..b2d905b07bed 100644
--- a/maths/solovay_strassen_primality_test.py
+++ b/maths/solovay_strassen_primality_test.py
@@ -9,7 +9,6 @@
 https://en.wikipedia.org/wiki/Solovay%E2%80%93Strassen_primality_test
 """
 
-
 import random
 
 
diff --git a/maths/spearman_rank_correlation_coefficient.py b/maths/spearman_rank_correlation_coefficient.py
new file mode 100644
index 000000000000..32ff6b9e3d71
--- /dev/null
+++ b/maths/spearman_rank_correlation_coefficient.py
@@ -0,0 +1,82 @@
+from collections.abc import Sequence
+
+
+def assign_ranks(data: Sequence[float]) -> list[int]:
+    """
+    Assigns ranks to elements in the array.
+
+    :param data: List of floats.
+    :return: List of ints representing the ranks.
+
+    Example:
+    >>> assign_ranks([3.2, 1.5, 4.0, 2.7, 5.1])
+    [3, 1, 4, 2, 5]
+
+    >>> assign_ranks([10.5, 8.1, 12.4, 9.3, 11.0])
+    [3, 1, 5, 2, 4]
+    """
+    ranked_data = sorted((value, index) for index, value in enumerate(data))
+    ranks = [0] * len(data)
+
+    for position, (_, index) in enumerate(ranked_data):
+        ranks[index] = position + 1
+
+    return ranks
+
+
+def calculate_spearman_rank_correlation(
+    variable_1: Sequence[float], variable_2: Sequence[float]
+) -> float:
+    """
+    Calculates Spearman's rank correlation coefficient.
+
+    :param variable_1: List of floats representing the first variable.
+    :param variable_2: List of floats representing the second variable.
+    :return: Spearman's rank correlation coefficient.
+
+    Example Usage:
+
+    >>> x = [1, 2, 3, 4, 5]
+    >>> y = [5, 4, 3, 2, 1]
+    >>> calculate_spearman_rank_correlation(x, y)
+    -1.0
+
+    >>> x = [1, 2, 3, 4, 5]
+    >>> y = [2, 4, 6, 8, 10]
+    >>> calculate_spearman_rank_correlation(x, y)
+    1.0
+
+    >>> x = [1, 2, 3, 4, 5]
+    >>> y = [5, 1, 2, 9, 5]
+    >>> calculate_spearman_rank_correlation(x, y)
+    0.6
+    """
+    n = len(variable_1)
+    rank_var1 = assign_ranks(variable_1)
+    rank_var2 = assign_ranks(variable_2)
+
+    # Calculate differences of ranks
+    d = [rx - ry for rx, ry in zip(rank_var1, rank_var2)]
+
+    # Calculate the sum of squared differences
+    d_squared = sum(di**2 for di in d)
+
+    # Calculate the Spearman's rank correlation coefficient
+    rho = 1 - (6 * d_squared) / (n * (n**2 - 1))
+
+    return rho
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    # Example usage:
+    print(
+        f"{calculate_spearman_rank_correlation([1, 2, 3, 4, 5], [2, 4, 6, 8, 10]) = }"
+    )
+
+    print(f"{calculate_spearman_rank_correlation([1, 2, 3, 4, 5], [5, 4, 3, 2, 1]) = }")
+
+    print(f"{calculate_spearman_rank_correlation([1, 2, 3, 4, 5], [5, 1, 2, 9, 5]) = }")
diff --git a/maths/special_numbers/__init__.py b/maths/special_numbers/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/maths/special_numbers/armstrong_numbers.py b/maths/special_numbers/armstrong_numbers.py
index b037aacb16c3..a3cb69b814de 100644
--- a/maths/special_numbers/armstrong_numbers.py
+++ b/maths/special_numbers/armstrong_numbers.py
@@ -8,6 +8,7 @@
 
 On-Line Encyclopedia of Integer Sequences entry: https://oeis.org/A005188
 """
+
 PASSING = (1, 153, 370, 371, 1634, 24678051, 115132219018763992565095597973971522401)
 FAILING: tuple = (-153, -1, 0, 1.2, 200, "A", [], {}, None)
 
@@ -42,9 +43,9 @@ def armstrong_number(n: int) -> bool:
 def pluperfect_number(n: int) -> bool:
     """Return True if n is a pluperfect number or False if it is not
 
-    >>> all(armstrong_number(n) for n in PASSING)
+    >>> all(pluperfect_number(n) for n in PASSING)
     True
-    >>> any(armstrong_number(n) for n in FAILING)
+    >>> any(pluperfect_number(n) for n in FAILING)
     False
     """
     if not isinstance(n, int) or n < 1:
@@ -69,9 +70,9 @@ def pluperfect_number(n: int) -> bool:
 def narcissistic_number(n: int) -> bool:
     """Return True if n is a narcissistic number or False if it is not.
 
-    >>> all(armstrong_number(n) for n in PASSING)
+    >>> all(narcissistic_number(n) for n in PASSING)
     True
-    >>> any(armstrong_number(n) for n in FAILING)
+    >>> any(narcissistic_number(n) for n in FAILING)
     False
     """
     if not isinstance(n, int) or n < 1:
diff --git a/maths/special_numbers/bell_numbers.py b/maths/special_numbers/bell_numbers.py
index 660ec6e6aa09..d573e7a3962d 100644
--- a/maths/special_numbers/bell_numbers.py
+++ b/maths/special_numbers/bell_numbers.py
@@ -21,6 +21,10 @@ def bell_numbers(max_set_length: int) -> list[int]:
         list: A list of Bell numbers for sets of lengths from 0 to max_set_length.
 
     Examples:
+    >>> bell_numbers(-2)
+    Traceback (most recent call last):
+        ...
+    ValueError: max_set_length must be non-negative
     >>> bell_numbers(0)
     [1]
     >>> bell_numbers(1)
@@ -61,8 +65,7 @@ def _binomial_coefficient(total_elements: int, elements_to_choose: int) -> int:
     if elements_to_choose in {0, total_elements}:
         return 1
 
-    if elements_to_choose > total_elements - elements_to_choose:
-        elements_to_choose = total_elements - elements_to_choose
+    elements_to_choose = min(elements_to_choose, total_elements - elements_to_choose)
 
     coefficient = 1
     for i in range(elements_to_choose):
diff --git a/maths/special_numbers/hamming_numbers.py b/maths/special_numbers/hamming_numbers.py
index 4575119c8a95..a473cc93883b 100644
--- a/maths/special_numbers/hamming_numbers.py
+++ b/maths/special_numbers/hamming_numbers.py
@@ -13,6 +13,10 @@ def hamming(n_element: int) -> list:
     :param n_element: The number of elements on the list
     :return: The nth element of the list
 
+    >>> hamming(-5)
+    Traceback (most recent call last):
+        ...
+    ValueError: n_element should be a positive number
     >>> hamming(5)
     [1, 2, 3, 4, 5]
     >>> hamming(10)
@@ -22,7 +26,7 @@ def hamming(n_element: int) -> list:
     """
     n_element = int(n_element)
     if n_element < 1:
-        my_error = ValueError("a should be a positive number")
+        my_error = ValueError("n_element should be a positive number")
         raise my_error
 
     hamming_list = [1]
diff --git a/maths/special_numbers/happy_number.py b/maths/special_numbers/happy_number.py
new file mode 100644
index 000000000000..eac3167e304b
--- /dev/null
+++ b/maths/special_numbers/happy_number.py
@@ -0,0 +1,48 @@
+def is_happy_number(number: int) -> bool:
+    """
+    A happy number is a number which eventually reaches 1 when replaced by the sum of
+    the square of each digit.
+
+    :param number: The number to check for happiness.
+    :return: True if the number is a happy number, False otherwise.
+
+    >>> is_happy_number(19)
+    True
+    >>> is_happy_number(2)
+    False
+    >>> is_happy_number(23)
+    True
+    >>> is_happy_number(1)
+    True
+    >>> is_happy_number(0)
+    Traceback (most recent call last):
+        ...
+    ValueError: number=0 must be a positive integer
+    >>> is_happy_number(-19)
+    Traceback (most recent call last):
+        ...
+    ValueError: number=-19 must be a positive integer
+    >>> is_happy_number(19.1)
+    Traceback (most recent call last):
+        ...
+    ValueError: number=19.1 must be a positive integer
+    >>> is_happy_number("happy")
+    Traceback (most recent call last):
+        ...
+    ValueError: number='happy' must be a positive integer
+    """
+    if not isinstance(number, int) or number <= 0:
+        msg = f"{number=} must be a positive integer"
+        raise ValueError(msg)
+
+    seen = set()
+    while number != 1 and number not in seen:
+        seen.add(number)
+        number = sum(int(digit) ** 2 for digit in str(number))
+    return number == 1
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/maths/special_numbers/harshad_numbers.py b/maths/special_numbers/harshad_numbers.py
index 61667adfa127..417120bd840e 100644
--- a/maths/special_numbers/harshad_numbers.py
+++ b/maths/special_numbers/harshad_numbers.py
@@ -11,6 +11,8 @@ def int_to_base(number: int, base: int) -> str:
     Where 'base' ranges from 2 to 36.
 
     Examples:
+    >>> int_to_base(0, 21)
+    '0'
     >>> int_to_base(23, 2)
     '10111'
     >>> int_to_base(58, 5)
@@ -26,6 +28,10 @@ def int_to_base(number: int, base: int) -> str:
     Traceback (most recent call last):
         ...
     ValueError: 'base' must be between 2 and 36 inclusive
+    >>> int_to_base(-99, 16)
+    Traceback (most recent call last):
+        ...
+    ValueError: number must be a positive integer
     """
 
     if base < 2 or base > 36:
@@ -101,6 +107,8 @@ def harshad_numbers_in_base(limit: int, base: int) -> list[str]:
     Traceback (most recent call last):
         ...
     ValueError: 'base' must be between 2 and 36 inclusive
+    >>> harshad_numbers_in_base(-12, 6)
+    []
     """
 
     if base < 2 or base > 36:
diff --git a/maths/special_numbers/perfect_number.py b/maths/special_numbers/perfect_number.py
index 148e988fb4c5..a022dc677638 100644
--- a/maths/special_numbers/perfect_number.py
+++ b/maths/special_numbers/perfect_number.py
@@ -14,21 +14,66 @@
 
 def perfect(number: int) -> bool:
     """
+    Check if a number is a perfect number.
+
+    A perfect number is a positive integer that is equal to the sum of its proper
+    divisors (excluding itself).
+
+    Args:
+        number: The number to be checked.
+
+    Returns:
+        True if the number is a perfect number, False otherwise.
+
+    Start from 1 because dividing by 0 will raise ZeroDivisionError.
+    A number at most can be divisible by the half of the number except the number
+    itself. For example, 6 is at most can be divisible by 3 except by 6 itself.
+
+    Examples:
     >>> perfect(27)
     False
     >>> perfect(28)
     True
     >>> perfect(29)
     False
-
-    Start from 1 because dividing by 0 will raise ZeroDivisionError.
-    A number at most can be divisible by the half of the number except the number
-    itself.  For example, 6 is at most can be divisible by 3 except by 6 itself.
+    >>> perfect(6)
+    True
+    >>> perfect(12)
+    False
+    >>> perfect(496)
+    True
+    >>> perfect(8128)
+    True
+    >>> perfect(0)
+    False
+    >>> perfect(-1)
+    False
+    >>> perfect(12.34)
+    Traceback (most recent call last):
+      ...
+    ValueError: number must be an integer
+    >>> perfect("Hello")
+    Traceback (most recent call last):
+      ...
+    ValueError: number must be an integer
     """
+    if not isinstance(number, int):
+        raise ValueError("number must be an integer")
+    if number <= 0:
+        return False
     return sum(i for i in range(1, number // 2 + 1) if number % i == 0) == number
 
 
 if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
     print("Program to check whether a number is a Perfect number or not...")
-    number = int(input("Enter number: ").strip())
+    try:
+        number = int(input("Enter a positive integer: ").strip())
+    except ValueError:
+        msg = "number must be an integer"
+        print(msg)
+        raise ValueError(msg)
+
     print(f"{number} is {'' if perfect(number) else 'not '}a Perfect Number.")
diff --git a/maths/special_numbers/triangular_numbers.py b/maths/special_numbers/triangular_numbers.py
new file mode 100644
index 000000000000..5be89e6108b2
--- /dev/null
+++ b/maths/special_numbers/triangular_numbers.py
@@ -0,0 +1,43 @@
+"""
+A triangular number or triangle number counts objects arranged in an
+equilateral triangle. This module provides a function to generate n'th
+triangular number.
+
+For more information about triangular numbers, refer to:
+https://en.wikipedia.org/wiki/Triangular_number
+"""
+
+
+def triangular_number(position: int) -> int:
+    """
+    Generate the triangular number at the specified position.
+
+    Args:
+        position (int): The position of the triangular number to generate.
+
+    Returns:
+        int: The triangular number at the specified position.
+
+    Raises:
+        ValueError: If `position` is negative.
+
+    Examples:
+    >>> triangular_number(1)
+    1
+    >>> triangular_number(3)
+    6
+    >>> triangular_number(-1)
+    Traceback (most recent call last):
+        ...
+    ValueError: param `position` must be non-negative
+    """
+    if position < 0:
+        raise ValueError("param `position` must be non-negative")
+
+    return position * (position + 1) // 2
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/maths/special_numbers/weird_number.py b/maths/special_numbers/weird_number.py
index 2834a9fee31e..5c9240d0ea4e 100644
--- a/maths/special_numbers/weird_number.py
+++ b/maths/special_numbers/weird_number.py
@@ -3,6 +3,7 @@
 
 Fun fact: The set of weird numbers has positive asymptotic density.
 """
+
 from math import sqrt
 
 
diff --git a/maths/tanh.py b/maths/tanh.py
index 38a369d9118d..011d6f17e22b 100644
--- a/maths/tanh.py
+++ b/maths/tanh.py
@@ -9,6 +9,7 @@
 Script inspired from its corresponding Wikipedia article
 https://en.wikipedia.org/wiki/Activation_function
 """
+
 import numpy as np
 
 
diff --git a/maths/test_factorial.py b/maths/test_factorial.py
new file mode 100644
index 000000000000..d80d88add745
--- /dev/null
+++ b/maths/test_factorial.py
@@ -0,0 +1,37 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "pytest",
+# ]
+# ///
+
+import pytest
+
+from maths.factorial import factorial, factorial_recursive
+
+
+@pytest.mark.parametrize("function", [factorial, factorial_recursive])
+def test_zero(function):
+    assert function(0) == 1
+
+
+@pytest.mark.parametrize("function", [factorial, factorial_recursive])
+def test_positive_integers(function):
+    assert function(1) == 1
+    assert function(5) == 120
+    assert function(7) == 5040
+
+
+@pytest.mark.parametrize("function", [factorial, factorial_recursive])
+def test_large_number(function):
+    assert function(10) == 3628800
+
+
+@pytest.mark.parametrize("function", [factorial, factorial_recursive])
+def test_negative_number(function):
+    with pytest.raises(ValueError):
+        function(-3)
+
+
+if __name__ == "__main__":
+    pytest.main(["-v", __file__])
diff --git a/maths/trapezoidal_rule.py b/maths/trapezoidal_rule.py
index 9a4ddc8af66b..21b10b239b5f 100644
--- a/maths/trapezoidal_rule.py
+++ b/maths/trapezoidal_rule.py
@@ -1,17 +1,26 @@
 """
 Numerical integration or quadrature for a smooth function f with known values at x_i
+"""
 
-This method is the classical approach of suming 'Equally Spaced Abscissas'
-
-method 1:
-"extended trapezoidal rule"
 
-"""
+def trapezoidal_rule(boundary, steps):
+    """
+    Implements the extended trapezoidal rule for numerical integration.
+    The function f(x) is provided below.
 
+    :param boundary: List containing the lower and upper bounds of integration [a, b]
+    :param steps: The number of steps (intervals) used in the approximation
+    :return: The numerical approximation of the integral
 
-def method_1(boundary, steps):
-    # "extended trapezoidal rule"
-    # int(f) = dx/2 * (f1 + 2f2 + ... + fn)
+    >>> abs(trapezoidal_rule([0, 1], 10) - 0.33333) < 0.01
+    True
+    >>> abs(trapezoidal_rule([0, 1], 100) - 0.33333) < 0.01
+    True
+    >>> abs(trapezoidal_rule([0, 2], 1000) - 2.66667) < 0.01
+    True
+    >>> abs(trapezoidal_rule([1, 2], 1000) - 2.33333) < 0.01
+    True
+    """
     h = (boundary[1] - boundary[0]) / steps
     a = boundary[0]
     b = boundary[1]
@@ -19,32 +28,78 @@ def method_1(boundary, steps):
     y = 0.0
     y += (h / 2.0) * f(a)
     for i in x_i:
-        # print(i)
         y += h * f(i)
     y += (h / 2.0) * f(b)
     return y
 
 
 def make_points(a, b, h):
+    """
+    Generates points between a and b with step size h for trapezoidal integration.
+
+    :param a: The lower bound of integration
+    :param b: The upper bound of integration
+    :param h: The step size
+    :yield: The next x-value in the range (a, b)
+
+    >>> list(make_points(0, 1, 0.1))    # doctest: +NORMALIZE_WHITESPACE
+    [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, \
+    0.8999999999999999]
+    >>> list(make_points(0, 10, 2.5))
+    [2.5, 5.0, 7.5]
+    >>> list(make_points(0, 10, 2))
+    [2, 4, 6, 8]
+    >>> list(make_points(1, 21, 5))
+    [6, 11, 16]
+    >>> list(make_points(1, 5, 2))
+    [3]
+    >>> list(make_points(1, 4, 3))
+    []
+    """
     x = a + h
-    while x < (b - h):
+    while x <= (b - h):
         yield x
-        x = x + h
+        x += h
 
 
-def f(x):  # enter your function here
-    y = (x - 0) * (x - 0)
-    return y
+def f(x):
+    """
+    This is the function to integrate, f(x) = (x - 0)^2 = x^2.
+
+    :param x: The input value
+    :return: The value of f(x)
+
+    >>> f(0)
+    0
+    >>> f(1)
+    1
+    >>> f(0.5)
+    0.25
+    """
+    return x**2
 
 
 def main():
-    a = 0.0  # Lower bound of integration
-    b = 1.0  # Upper bound of integration
-    steps = 10.0  # define number of steps or resolution
-    boundary = [a, b]  # define boundary of integration
-    y = method_1(boundary, steps)
+    """
+    Main function to test the trapezoidal rule.
+    :a: Lower bound of integration
+    :b: Upper bound of integration
+    :steps: define number of steps or resolution
+    :boundary: define boundary of integration
+
+    >>> main()
+    y = 0.3349999999999999
+    """
+    a = 0.0
+    b = 1.0
+    steps = 10.0
+    boundary = [a, b]
+    y = trapezoidal_rule(boundary, steps)
     print(f"y = {y}")
 
 
 if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
     main()
diff --git a/maths/triplet_sum.py b/maths/triplet_sum.py
index af77ed145bce..e74f67daad47 100644
--- a/maths/triplet_sum.py
+++ b/maths/triplet_sum.py
@@ -3,6 +3,7 @@
 we are required to find a triplet from the array such that it's sum is equal to
 the target.
 """
+
 from __future__ import annotations
 
 from itertools import permutations
diff --git a/maths/two_pointer.py b/maths/two_pointer.py
index d0fb0fc9c2f1..8a6d8eb7aff0 100644
--- a/maths/two_pointer.py
+++ b/maths/two_pointer.py
@@ -17,6 +17,7 @@
 
 [1]: https://github.com/TheAlgorithms/Python/blob/master/other/two_sum.py
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/two_sum.py b/maths/two_sum.py
index 12ad332d6c4e..58c933a5078a 100644
--- a/maths/two_sum.py
+++ b/maths/two_sum.py
@@ -11,6 +11,7 @@
 Because nums[0] + nums[1] = 2 + 7 = 9,
 return [0, 1].
 """
+
 from __future__ import annotations
 
 
diff --git a/maths/volume.py b/maths/volume.py
index b4df4e475783..08bdf72b013b 100644
--- a/maths/volume.py
+++ b/maths/volume.py
@@ -1,16 +1,19 @@
 """
 Find the volume of various shapes.
+
 * https://en.wikipedia.org/wiki/Volume
 * https://en.wikipedia.org/wiki/Spherical_cap
 """
+
 from __future__ import annotations
 
-from math import pi, pow
+from math import pi, pow  # noqa: A004
 
 
 def vol_cube(side_length: float) -> float:
     """
     Calculate the Volume of a Cube.
+
     >>> vol_cube(1)
     1.0
     >>> vol_cube(3)
@@ -32,6 +35,7 @@ def vol_cube(side_length: float) -> float:
 def vol_spherical_cap(height: float, radius: float) -> float:
     """
     Calculate the volume of the spherical cap.
+
     >>> vol_spherical_cap(1, 2)
     5.235987755982988
     >>> vol_spherical_cap(1.6, 2.6)
@@ -56,20 +60,29 @@ def vol_spherical_cap(height: float, radius: float) -> float:
 def vol_spheres_intersect(
     radius_1: float, radius_2: float, centers_distance: float
 ) -> float:
-    """
+    r"""
     Calculate the volume of the intersection of two spheres.
+
     The intersection is composed by two spherical caps and therefore its volume is the
-    sum of the volumes of the spherical caps. First, it calculates the heights (h1, h2)
-    of the spherical caps, then the two volumes and it returns the sum.
+    sum of the volumes of the spherical caps.
+    First, it calculates the heights :math:`(h_1, h_2)` of the spherical caps,
+    then the two volumes and it returns the sum.
     The height formulas are
-    h1 = (radius_1 - radius_2 + centers_distance)
-       * (radius_1 + radius_2 - centers_distance)
-       / (2 * centers_distance)
-    h2 = (radius_2 - radius_1 + centers_distance)
-       * (radius_2 + radius_1 - centers_distance)
-       / (2 * centers_distance)
-    if centers_distance is 0 then it returns the volume of the smallers sphere
-    :return vol_spherical_cap(h1, radius_2) + vol_spherical_cap(h2, radius_1)
+
+    .. math::
+        h_1 = \frac{(radius_1 - radius_2 + centers\_distance)
+                    \cdot (radius_1 + radius_2 - centers\_distance)}
+                   {2 \cdot centers\_distance}
+
+        h_2 = \frac{(radius_2 - radius_1 + centers\_distance)
+                    \cdot (radius_2 + radius_1 - centers\_distance)}
+                   {2 \cdot centers\_distance}
+
+    if `centers_distance` is 0 then it returns the volume of the smallers sphere
+
+    :return: ``vol_spherical_cap`` (:math:`h_1`, :math:`radius_2`)
+             + ``vol_spherical_cap`` (:math:`h_2`, :math:`radius_1`)
+
     >>> vol_spheres_intersect(2, 2, 1)
     21.205750411731103
     >>> vol_spheres_intersect(2.6, 2.6, 1.6)
@@ -111,14 +124,18 @@ def vol_spheres_intersect(
 def vol_spheres_union(
     radius_1: float, radius_2: float, centers_distance: float
 ) -> float:
-    """
+    r"""
     Calculate the volume of the union of two spheres that possibly intersect.
-    It is the sum of sphere A and sphere B minus their intersection.
-    First, it calculates the volumes (v1, v2) of the spheres,
-    then the volume of the intersection (i) and it returns the sum v1+v2-i.
-    If centers_distance is 0 then it returns the volume of the larger sphere
-    :return vol_sphere(radius_1) + vol_sphere(radius_2)
-                - vol_spheres_intersect(radius_1, radius_2, centers_distance)
+
+    It is the sum of sphere :math:`A` and sphere :math:`B` minus their intersection.
+    First, it calculates the volumes :math:`(v_1, v_2)` of the spheres,
+    then the volume of the intersection :math:`i` and
+    it returns the sum :math:`v_1 + v_2 - i`.
+    If `centers_distance` is 0 then it returns the volume of the larger sphere
+
+    :return: ``vol_sphere`` (:math:`radius_1`) + ``vol_sphere`` (:math:`radius_2`)
+             - ``vol_spheres_intersect``
+             (:math:`radius_1`, :math:`radius_2`, :math:`centers\_distance`)
 
     >>> vol_spheres_union(2, 2, 1)
     45.814892864851146
@@ -156,7 +173,9 @@ def vol_spheres_union(
 def vol_cuboid(width: float, height: float, length: float) -> float:
     """
     Calculate the Volume of a Cuboid.
-    :return multiple of width, length and height
+
+    :return: multiple of `width`, `length` and `height`
+
     >>> vol_cuboid(1, 1, 1)
     1.0
     >>> vol_cuboid(1, 2, 3)
@@ -184,10 +203,12 @@ def vol_cuboid(width: float, height: float, length: float) -> float:
 
 
 def vol_cone(area_of_base: float, height: float) -> float:
-    """
-    Calculate the Volume of a Cone.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Cone
-    :return (1/3) * area_of_base * height
+    r"""
+    | Calculate the Volume of a Cone.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Cone
+
+    :return: :math:`\frac{1}{3} \cdot area\_of\_base \cdot height`
+
     >>> vol_cone(10, 3)
     10.0
     >>> vol_cone(1, 1)
@@ -211,10 +232,12 @@ def vol_cone(area_of_base: float, height: float) -> float:
 
 
 def vol_right_circ_cone(radius: float, height: float) -> float:
-    """
-    Calculate the Volume of a Right Circular Cone.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Cone
-    :return (1/3) * pi * radius^2 * height
+    r"""
+    | Calculate the Volume of a Right Circular Cone.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Cone
+
+    :return: :math:`\frac{1}{3} \cdot \pi \cdot radius^2 \cdot height`
+
     >>> vol_right_circ_cone(2, 3)
     12.566370614359172
     >>> vol_right_circ_cone(0, 0)
@@ -236,10 +259,12 @@ def vol_right_circ_cone(radius: float, height: float) -> float:
 
 
 def vol_prism(area_of_base: float, height: float) -> float:
-    """
-    Calculate the Volume of a Prism.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Prism_(geometry)
-    :return V = Bh
+    r"""
+    | Calculate the Volume of a Prism.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Prism_(geometry)
+
+    :return: :math:`V = B \cdot h`
+
     >>> vol_prism(10, 2)
     20.0
     >>> vol_prism(11, 1)
@@ -263,10 +288,12 @@ def vol_prism(area_of_base: float, height: float) -> float:
 
 
 def vol_pyramid(area_of_base: float, height: float) -> float:
-    """
-    Calculate the Volume of a Pyramid.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Pyramid_(geometry)
-    :return  (1/3) * Bh
+    r"""
+    | Calculate the Volume of a Pyramid.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Pyramid_(geometry)
+
+    :return: :math:`\frac{1}{3} \cdot B \cdot h`
+
     >>> vol_pyramid(10, 3)
     10.0
     >>> vol_pyramid(1.5, 3)
@@ -290,10 +317,12 @@ def vol_pyramid(area_of_base: float, height: float) -> float:
 
 
 def vol_sphere(radius: float) -> float:
-    """
-    Calculate the Volume of a Sphere.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Sphere
-    :return (4/3) * pi * r^3
+    r"""
+    | Calculate the Volume of a Sphere.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Sphere
+
+    :return: :math:`\frac{4}{3} \cdot \pi \cdot r^3`
+
     >>> vol_sphere(5)
     523.5987755982989
     >>> vol_sphere(1)
@@ -314,10 +343,13 @@ def vol_sphere(radius: float) -> float:
 
 
 def vol_hemisphere(radius: float) -> float:
-    """Calculate the volume of a hemisphere
-    Wikipedia reference: https://en.wikipedia.org/wiki/Hemisphere
-    Other references: https://www.cuemath.com/geometry/hemisphere
-    :return 2/3 * pi * radius^3
+    r"""
+    | Calculate the volume of a hemisphere
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Hemisphere
+    | Other references: https://www.cuemath.com/geometry/hemisphere
+
+    :return: :math:`\frac{2}{3} \cdot \pi \cdot radius^3`
+
     >>> vol_hemisphere(1)
     2.0943951023931953
     >>> vol_hemisphere(7)
@@ -338,9 +370,12 @@ def vol_hemisphere(radius: float) -> float:
 
 
 def vol_circular_cylinder(radius: float, height: float) -> float:
-    """Calculate the Volume of a Circular Cylinder.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Cylinder
-    :return pi * radius^2 * height
+    r"""
+    | Calculate the Volume of a Circular Cylinder.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Cylinder
+
+    :return: :math:`\pi \cdot radius^2 \cdot height`
+
     >>> vol_circular_cylinder(1, 1)
     3.141592653589793
     >>> vol_circular_cylinder(4, 3)
@@ -367,7 +402,9 @@ def vol_circular_cylinder(radius: float, height: float) -> float:
 def vol_hollow_circular_cylinder(
     inner_radius: float, outer_radius: float, height: float
 ) -> float:
-    """Calculate the Volume of a Hollow Circular Cylinder.
+    """
+    Calculate the Volume of a Hollow Circular Cylinder.
+
     >>> vol_hollow_circular_cylinder(1, 2, 3)
     28.274333882308138
     >>> vol_hollow_circular_cylinder(1.6, 2.6, 3.6)
@@ -404,8 +441,9 @@ def vol_hollow_circular_cylinder(
 
 
 def vol_conical_frustum(height: float, radius_1: float, radius_2: float) -> float:
-    """Calculate the Volume of a Conical Frustum.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Frustum
+    """
+    | Calculate the Volume of a Conical Frustum.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Frustum
 
     >>> vol_conical_frustum(45, 7, 28)
     48490.482608158454
@@ -442,9 +480,12 @@ def vol_conical_frustum(height: float, radius_1: float, radius_2: float) -> floa
 
 
 def vol_torus(torus_radius: float, tube_radius: float) -> float:
-    """Calculate the Volume of a Torus.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Torus
-    :return 2pi^2 * torus_radius * tube_radius^2
+    r"""
+    | Calculate the Volume of a Torus.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Torus
+
+    :return: :math:`2 \pi^2 \cdot torus\_radius \cdot tube\_radius^2`
+
     >>> vol_torus(1, 1)
     19.739208802178716
     >>> vol_torus(4, 3)
@@ -470,8 +511,9 @@ def vol_torus(torus_radius: float, tube_radius: float) -> float:
 
 
 def vol_icosahedron(tri_side: float) -> float:
-    """Calculate the Volume of an Icosahedron.
-    Wikipedia reference: https://en.wikipedia.org/wiki/Regular_icosahedron
+    """
+    | Calculate the Volume of an Icosahedron.
+    | Wikipedia reference: https://en.wikipedia.org/wiki/Regular_icosahedron
 
     >>> from math import isclose
     >>> isclose(vol_icosahedron(2.5), 34.088984228514256)
diff --git a/maths/zellers_congruence.py b/maths/zellers_congruence.py
index 483fb000f86b..b958ed3b8659 100644
--- a/maths/zellers_congruence.py
+++ b/maths/zellers_congruence.py
@@ -4,13 +4,14 @@
 
 def zeller(date_input: str) -> str:
     """
-    Zellers Congruence Algorithm
-    Find the day of the week for nearly any Gregorian or Julian calendar date
+    | Zellers Congruence Algorithm
+    | Find the day of the week for nearly any Gregorian or Julian calendar date
 
     >>> zeller('01-31-2010')
     'Your date 01-31-2010, is a Sunday!'
 
-    Validate out of range month
+    Validate out of range month:
+
     >>> zeller('13-31-2010')
     Traceback (most recent call last):
         ...
@@ -21,6 +22,7 @@ def zeller(date_input: str) -> str:
     ValueError: invalid literal for int() with base 10: '.2'
 
     Validate out of range date:
+
     >>> zeller('01-33-2010')
     Traceback (most recent call last):
         ...
@@ -31,30 +33,35 @@ def zeller(date_input: str) -> str:
     ValueError: invalid literal for int() with base 10: '.4'
 
     Validate second separator:
+
     >>> zeller('01-31*2010')
     Traceback (most recent call last):
         ...
     ValueError: Date separator must be '-' or '/'
 
     Validate first separator:
+
     >>> zeller('01^31-2010')
     Traceback (most recent call last):
         ...
     ValueError: Date separator must be '-' or '/'
 
     Validate out of range year:
+
     >>> zeller('01-31-8999')
     Traceback (most recent call last):
         ...
     ValueError: Year out of range. There has to be some sort of limit...right?
 
     Test null input:
+
     >>> zeller()
     Traceback (most recent call last):
         ...
     TypeError: zeller() missing 1 required positional argument: 'date_input'
 
-    Test length of date_input:
+    Test length of `date_input`:
+
     >>> zeller('')
     Traceback (most recent call last):
         ...
diff --git a/matrix/cramers_rule_2x2.py b/matrix/cramers_rule_2x2.py
index 4f52dbe646ad..081035bec002 100644
--- a/matrix/cramers_rule_2x2.py
+++ b/matrix/cramers_rule_2x2.py
@@ -73,12 +73,11 @@ def cramers_rule_2x2(equation1: list[int], equation2: list[int]) -> tuple[float,
             raise ValueError("Infinite solutions. (Consistent system)")
         else:
             raise ValueError("No solution. (Inconsistent system)")
+    elif determinant_x == determinant_y == 0:
+        # Trivial solution (Inconsistent system)
+        return (0.0, 0.0)
     else:
-        if determinant_x == determinant_y == 0:
-            # Trivial solution (Inconsistent system)
-            return (0.0, 0.0)
-        else:
-            x = determinant_x / determinant
-            y = determinant_y / determinant
-            # Non-Trivial Solution (Consistent system)
-            return (x, y)
+        x = determinant_x / determinant
+        y = determinant_y / determinant
+        # Non-Trivial Solution (Consistent system)
+        return (x, y)
diff --git a/matrix/largest_square_area_in_matrix.py b/matrix/largest_square_area_in_matrix.py
index a93369c56bbd..16263fb798f1 100644
--- a/matrix/largest_square_area_in_matrix.py
+++ b/matrix/largest_square_area_in_matrix.py
@@ -31,7 +31,7 @@
 
 Approach:
 We initialize another matrix (dp) with the same dimensions
-as the original one initialized with all 0’s.
+as the original one initialized with all 0's.
 
 dp_array(i,j) represents the side length of the maximum square whose
 bottom right corner is the cell with index (i,j) in the original matrix.
@@ -39,7 +39,7 @@
 Starting from index (0,0), for every 1 found in the original matrix,
 we update the value of the current element as
 
-dp_array(i,j)=dp_array(dp(i−1,j),dp_array(i−1,j−1),dp_array(i,j−1)) + 1.
+dp_array(i,j)=dp_array(dp(i-1,j),dp_array(i-1,j-1),dp_array(i,j-1)) + 1.
 """
 
 
diff --git a/matrix/matrix_based_game.py b/matrix/matrix_based_game.py
new file mode 100644
index 000000000000..6181086c6704
--- /dev/null
+++ b/matrix/matrix_based_game.py
@@ -0,0 +1,284 @@
+"""
+Matrix-Based Game Script
+=========================
+This script implements a matrix-based game where players interact with a grid of
+elements. The primary goals are to:
+- Identify connected elements of the same type from a selected position.
+- Remove those elements, adjust the matrix by simulating gravity, and reorganize empty
+  columns.
+- Calculate and display the score based on the number of elements removed in each move.
+
+Functions:
+-----------
+1. `find_repeat`: Finds all connected elements of the same type.
+2. `increment_score`: Calculates the score for a given move.
+3. `move_x`: Simulates gravity in a column.
+4. `move_y`: Reorganizes the matrix by shifting columns leftward when a column becomes
+    empty.
+5. `play`: Executes a single move, updating the matrix and returning the score.
+
+Input Format:
+--------------
+1. Matrix size (`lines`): Integer specifying the size of the matrix (N x N).
+2. Matrix content (`matrix`): Rows of the matrix, each consisting of characters.
+3. Number of moves (`movs`): Integer indicating the number of moves.
+4. List of moves (`movements`): A comma-separated string of coordinates for each move.
+
+(0,0) position starts from first left column to last right, and below row to up row
+
+
+Example Input:
+---------------
+4
+RRBG
+RBBG
+YYGG
+XYGG
+2
+0 1,1 1
+
+Example (0,0) = X
+
+Output:
+--------
+The script outputs the total score after processing all moves.
+
+Usage:
+-------
+Run the script and provide the required inputs as prompted.
+
+"""
+
+
+def validate_matrix_size(size: int) -> None:
+    """
+    >>> validate_matrix_size(-1)
+    Traceback (most recent call last):
+        ...
+    ValueError: Matrix size must be a positive integer.
+    """
+    if not isinstance(size, int) or size <= 0:
+        raise ValueError("Matrix size must be a positive integer.")
+
+
+def validate_matrix_content(matrix: list[str], size: int) -> None:
+    """
+    Validates that the number of elements in the matrix matches the given size.
+
+    >>> validate_matrix_content(['aaaa', 'aaaa', 'aaaa', 'aaaa'], 3)
+    Traceback (most recent call last):
+        ...
+    ValueError: The matrix dont match with size.
+    >>> validate_matrix_content(['aa%', 'aaa', 'aaa'], 3)
+    Traceback (most recent call last):
+        ...
+    ValueError: Matrix rows can only contain letters and numbers.
+    >>> validate_matrix_content(['aaa', 'aaa', 'aaaa'], 3)
+    Traceback (most recent call last):
+        ...
+    ValueError: Each row in the matrix must have exactly 3 characters.
+    """
+    print(matrix)
+    if len(matrix) != size:
+        raise ValueError("The matrix dont match with size.")
+    for row in matrix:
+        if len(row) != size:
+            msg = f"Each row in the matrix must have exactly {size} characters."
+            raise ValueError(msg)
+        if not all(char.isalnum() for char in row):
+            raise ValueError("Matrix rows can only contain letters and numbers.")
+
+
+def validate_moves(moves: list[tuple[int, int]], size: int) -> None:
+    """
+    >>> validate_moves([(1, 2), (-1, 0)], 3)
+    Traceback (most recent call last):
+        ...
+    ValueError: Move is out of bounds for a matrix.
+    """
+    for move in moves:
+        x, y = move
+        if not (0 <= x < size and 0 <= y < size):
+            raise ValueError("Move is out of bounds for a matrix.")
+
+
+def parse_moves(input_str: str) -> list[tuple[int, int]]:
+    """
+    >>> parse_moves("0 1, 1 1")
+    [(0, 1), (1, 1)]
+    >>> parse_moves("0 1, 1 1, 2")
+    Traceback (most recent call last):
+        ...
+    ValueError: Each move must have exactly two numbers.
+    >>> parse_moves("0 1, 1 1, 2 4 5 6")
+    Traceback (most recent call last):
+        ...
+    ValueError: Each move must have exactly two numbers.
+    """
+    moves = []
+    for pair in input_str.split(","):
+        parts = pair.strip().split()
+        if len(parts) != 2:
+            raise ValueError("Each move must have exactly two numbers.")
+        x, y = map(int, parts)
+        moves.append((x, y))
+    return moves
+
+
+def find_repeat(
+    matrix_g: list[list[str]], row: int, column: int, size: int
+) -> set[tuple[int, int]]:
+    """
+    Finds all connected elements of the same type from a given position.
+
+    >>> find_repeat([['A', 'B', 'A'], ['A', 'B', 'A'], ['A', 'A', 'A']], 0, 0, 3)
+    {(1, 2), (2, 1), (0, 0), (2, 0), (0, 2), (2, 2), (1, 0)}
+    >>> find_repeat([['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-']], 1, 1, 3)
+    set()
+    """
+
+    column = size - 1 - column
+    visited = set()
+    repeated = set()
+
+    if (color := matrix_g[column][row]) != "-":
+
+        def dfs(row_n: int, column_n: int) -> None:
+            if row_n < 0 or row_n >= size or column_n < 0 or column_n >= size:
+                return
+            if (row_n, column_n) in visited:
+                return
+            visited.add((row_n, column_n))
+            if matrix_g[row_n][column_n] == color:
+                repeated.add((row_n, column_n))
+                dfs(row_n - 1, column_n)
+                dfs(row_n + 1, column_n)
+                dfs(row_n, column_n - 1)
+                dfs(row_n, column_n + 1)
+
+        dfs(column, row)
+
+    return repeated
+
+
+def increment_score(count: int) -> int:
+    """
+    Calculates the score for a move based on the number of elements removed.
+
+    >>> increment_score(3)
+    6
+    >>> increment_score(0)
+    0
+    """
+    return int(count * (count + 1) / 2)
+
+
+def move_x(matrix_g: list[list[str]], column: int, size: int) -> list[list[str]]:
+    """
+    Simulates gravity in a specific column.
+
+    >>> move_x([['-', 'A'], ['-', '-'], ['-', 'C']], 1, 2)
+    [['-', '-'], ['-', 'A'], ['-', 'C']]
+    """
+
+    new_list = []
+
+    for row in range(size):
+        if matrix_g[row][column] != "-":
+            new_list.append(matrix_g[row][column])
+        else:
+            new_list.insert(0, matrix_g[row][column])
+    for row in range(size):
+        matrix_g[row][column] = new_list[row]
+    return matrix_g
+
+
+def move_y(matrix_g: list[list[str]], size: int) -> list[list[str]]:
+    """
+    Shifts all columns leftward when an entire column becomes empty.
+
+    >>> move_y([['-', 'A'], ['-', '-'], ['-', 'C']], 2)
+    [['A', '-'], ['-', '-'], ['-', 'C']]
+    """
+
+    empty_columns = []
+
+    for column in range(size - 1, -1, -1):
+        if all(matrix_g[row][column] == "-" for row in range(size)):
+            empty_columns.append(column)
+
+    for column in empty_columns:
+        for col in range(column + 1, size):
+            for row in range(size):
+                matrix_g[row][col - 1] = matrix_g[row][col]
+        for row in range(size):
+            matrix_g[row][-1] = "-"
+
+    return matrix_g
+
+
+def play(
+    matrix_g: list[list[str]], pos_x: int, pos_y: int, size: int
+) -> tuple[list[list[str]], int]:
+    """
+    Processes a single move, updating the matrix and calculating the score.
+
+    >>> play([['R', 'G'], ['R', 'G']], 0, 0, 2)
+    ([['G', '-'], ['G', '-']], 3)
+    """
+
+    same_colors = find_repeat(matrix_g, pos_x, pos_y, size)
+
+    if len(same_colors) != 0:
+        for pos in same_colors:
+            matrix_g[pos[0]][pos[1]] = "-"
+        for column in range(size):
+            matrix_g = move_x(matrix_g, column, size)
+
+        matrix_g = move_y(matrix_g, size)
+
+    return (matrix_g, increment_score(len(same_colors)))
+
+
+def process_game(size: int, matrix: list[str], moves: list[tuple[int, int]]) -> int:
+    """Processes the game logic for the given matrix and moves.
+
+    Args:
+        size (int): Size of the game board.
+        matrix (List[str]): Initial game matrix.
+        moves (List[Tuple[int, int]]): List of moves as (x, y) coordinates.
+
+    Returns:
+        int: The total score obtained.
+    >>> process_game(3, ['aaa', 'bbb', 'ccc'], [(0, 0)])
+    6
+    """
+
+    game_matrix = [list(row) for row in matrix]
+    total_score = 0
+
+    for move in moves:
+        pos_x, pos_y = move
+        game_matrix, score = play(game_matrix, pos_x, pos_y, size)
+        total_score += score
+
+    return total_score
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod(verbose=True)
+    try:
+        size = int(input("Enter the size of the matrix: "))
+        validate_matrix_size(size)
+        print(f"Enter the {size} rows of the matrix:")
+        matrix = [input(f"Row {i + 1}: ") for i in range(size)]
+        validate_matrix_content(matrix, size)
+        moves_input = input("Enter the moves (e.g., '0 0, 1 1'): ")
+        moves = parse_moves(moves_input)
+        validate_moves(moves, size)
+        score = process_game(size, matrix, moves)
+        print(f"Total score: {score}")
+    except ValueError as e:
+        print(f"{e}")
diff --git a/matrix/matrix_class.py b/matrix/matrix_class.py
index a5940a38e836..dee9247282f9 100644
--- a/matrix/matrix_class.py
+++ b/matrix/matrix_class.py
@@ -260,7 +260,7 @@ def add_row(self, row: list[int], position: int | None = None) -> None:
         if position is None:
             self.rows.append(row)
         else:
-            self.rows = self.rows[0:position] + [row] + self.rows[position:]
+            self.rows = [*self.rows[0:position], row, *self.rows[position:]]
 
     def add_column(self, column: list[int], position: int | None = None) -> None:
         type_error = TypeError(
@@ -279,7 +279,7 @@ def add_column(self, column: list[int], position: int | None = None) -> None:
             self.rows = [self.rows[i] + [column[i]] for i in range(self.num_rows)]
         else:
             self.rows = [
-                self.rows[i][0:position] + [column[i]] + self.rows[i][position:]
+                [*self.rows[i][0:position], column[i], *self.rows[i][position:]]
                 for i in range(self.num_rows)
             ]
 
diff --git a/matrix/matrix_equalization.py b/matrix/matrix_equalization.py
new file mode 100644
index 000000000000..e7e76505cf63
--- /dev/null
+++ b/matrix/matrix_equalization.py
@@ -0,0 +1,55 @@
+from sys import maxsize
+
+
+def array_equalization(vector: list[int], step_size: int) -> int:
+    """
+    This algorithm equalizes all elements of the input vector
+    to a common value, by making the minimal number of
+    "updates" under the constraint of a step size (step_size).
+
+    >>> array_equalization([1, 1, 6, 2, 4, 6, 5, 1, 7, 2, 2, 1, 7, 2, 2], 4)
+    4
+    >>> array_equalization([22, 81, 88, 71, 22, 81, 632, 81, 81, 22, 92], 2)
+    5
+    >>> array_equalization([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 5)
+    0
+    >>> array_equalization([22, 22, 22, 33, 33, 33], 2)
+    2
+    >>> array_equalization([1, 2, 3], 0)
+    Traceback (most recent call last):
+    ValueError: Step size must be positive and non-zero.
+    >>> array_equalization([1, 2, 3], -1)
+    Traceback (most recent call last):
+    ValueError: Step size must be positive and non-zero.
+    >>> array_equalization([1, 2, 3], 0.5)
+    Traceback (most recent call last):
+    ValueError: Step size must be an integer.
+    >>> array_equalization([1, 2, 3], maxsize)
+    1
+    """
+    if step_size <= 0:
+        raise ValueError("Step size must be positive and non-zero.")
+    if not isinstance(step_size, int):
+        raise ValueError("Step size must be an integer.")
+
+    unique_elements = set(vector)
+    min_updates = maxsize
+
+    for element in unique_elements:
+        elem_index = 0
+        updates = 0
+        while elem_index < len(vector):
+            if vector[elem_index] != element:
+                updates += 1
+                elem_index += step_size
+            else:
+                elem_index += 1
+        min_updates = min(min_updates, updates)
+
+    return min_updates
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
diff --git a/matrix/matrix_multiplication_recursion.py b/matrix/matrix_multiplication_recursion.py
index 287142480ce7..57c4d80de017 100644
--- a/matrix/matrix_multiplication_recursion.py
+++ b/matrix/matrix_multiplication_recursion.py
@@ -7,6 +7,7 @@
 Perform matrix multiplication using a recursive algorithm.
 https://en.wikipedia.org/wiki/Matrix_multiplication
 """
+
 # type Matrix = list[list[int]]  # psf/black currenttly fails on this line
 Matrix = list[list[int]]
 
diff --git a/matrix/sherman_morrison.py b/matrix/sherman_morrison.py
index 7f10ae706e85..e2a09c1d0070 100644
--- a/matrix/sherman_morrison.py
+++ b/matrix/sherman_morrison.py
@@ -65,7 +65,7 @@ def validate_indices(self, loc: tuple[int, int]) -> bool:
         >>> a.validate_indices((0, 0))
         True
         """
-        if not (isinstance(loc, (list, tuple)) and len(loc) == 2):
+        if not (isinstance(loc, (list, tuple)) and len(loc) == 2):  # noqa: SIM114
             return False
         elif not (0 <= loc[0] < self.row and 0 <= loc[1] < self.column):
             return False
diff --git a/matrix/spiral_print.py b/matrix/spiral_print.py
index 5eef263f7aef..88bde1db594d 100644
--- a/matrix/spiral_print.py
+++ b/matrix/spiral_print.py
@@ -89,7 +89,7 @@ def spiral_traversal(matrix: list[list]) -> list[int]:
     Algorithm:
         Step 1. first pop the 0 index list. (which is [1,2,3,4] and concatenate the
                 output of [step 2])
-        Step 2. Now perform matrix’s Transpose operation (Change rows to column
+        Step 2. Now perform matrix's Transpose operation (Change rows to column
                 and vice versa) and reverse the resultant matrix.
         Step 3. Pass the output of [2nd step], to same recursive function till
                 base case hits.
@@ -116,7 +116,9 @@ def spiral_traversal(matrix: list[list]) -> list[int]:
     [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7] + spiral_traversal([])
     """
     if matrix:
-        return list(matrix.pop(0)) + spiral_traversal(list(zip(*matrix))[::-1])
+        return list(matrix.pop(0)) + spiral_traversal(
+            [list(row) for row in zip(*matrix)][::-1]
+        )
     else:
         return []
 
diff --git a/matrix/tests/test_matrix_operation.py b/matrix/tests/test_matrix_operation.py
index 638f97daa2ed..21ed7e371fd8 100644
--- a/matrix/tests/test_matrix_operation.py
+++ b/matrix/tests/test_matrix_operation.py
@@ -12,7 +12,7 @@
 import sys
 
 import numpy as np
-import pytest  # type: ignore
+import pytest
 
 # Custom/local libraries
 from matrix import matrix_operation as matop
@@ -31,7 +31,7 @@
 logger.addHandler(stream_handler)
 
 
-@pytest.mark.mat_ops()
+@pytest.mark.mat_ops
 @pytest.mark.parametrize(
     ("mat1", "mat2"), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), (mat_f, mat_h)]
 )
@@ -51,7 +51,7 @@ def test_addition(mat1, mat2):
             matop.add(mat1, mat2)
 
 
-@pytest.mark.mat_ops()
+@pytest.mark.mat_ops
 @pytest.mark.parametrize(
     ("mat1", "mat2"), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), (mat_f, mat_h)]
 )
@@ -71,7 +71,7 @@ def test_subtraction(mat1, mat2):
             assert matop.subtract(mat1, mat2)
 
 
-@pytest.mark.mat_ops()
+@pytest.mark.mat_ops
 @pytest.mark.parametrize(
     ("mat1", "mat2"), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), (mat_f, mat_h)]
 )
@@ -93,21 +93,21 @@ def test_multiplication(mat1, mat2):
             assert matop.subtract(mat1, mat2)
 
 
-@pytest.mark.mat_ops()
+@pytest.mark.mat_ops
 def test_scalar_multiply():
     act = (3.5 * np.array(mat_a)).tolist()
     theo = matop.scalar_multiply(mat_a, 3.5)
     assert theo == act
 
 
-@pytest.mark.mat_ops()
+@pytest.mark.mat_ops
 def test_identity():
     act = (np.identity(5)).tolist()
     theo = matop.identity(5)
     assert theo == act
 
 
-@pytest.mark.mat_ops()
+@pytest.mark.mat_ops
 @pytest.mark.parametrize("mat", [mat_a, mat_b, mat_c, mat_d, mat_e, mat_f])
 def test_transpose(mat):
     if (np.array(mat)).shape < (2, 2):
diff --git a/matrix/validate_sudoku_board.py b/matrix/validate_sudoku_board.py
index 0ee7b3df0b83..a7e08d169059 100644
--- a/matrix/validate_sudoku_board.py
+++ b/matrix/validate_sudoku_board.py
@@ -54,6 +54,66 @@ def is_valid_sudoku_board(sudoku_board: list[list[str]]) -> bool:
     ... ,[".",".",".",".","8",".",".","7","9"]
     ... ])
     False
+    >>> is_valid_sudoku_board([
+    ...  ["1","2","3","4","5","6","7","8","9"]
+    ... ,["4","5","6","7","8","9","1","2","3"]
+    ... ,["7","8","9","1","2","3","4","5","6"]
+    ... ,[".",".",".",".",".",".",".",".","."]
+    ... ,[".",".",".",".",".",".",".",".","."]
+    ... ,[".",".",".",".",".",".",".",".","."]
+    ... ,[".",".",".",".",".",".",".",".","."]
+    ... ,[".",".",".",".",".",".",".",".","."]
+    ... ,[".",".",".",".",".",".",".",".","."]
+    ... ])
+    True
+    >>> is_valid_sudoku_board([
+    ...  ["1","2","3",".",".",".",".",".","."]
+    ... ,["4","5","6",".",".",".",".",".","."]
+    ... ,["7","8","9",".",".",".",".",".","."]
+    ... ,[".",".",".","4","5","6",".",".","."]
+    ... ,[".",".",".","7","8","9",".",".","."]
+    ... ,[".",".",".","1","2","3",".",".","."]
+    ... ,[".",".",".",".",".",".","7","8","9"]
+    ... ,[".",".",".",".",".",".","1","2","3"]
+    ... ,[".",".",".",".",".",".","4","5","6"]
+    ... ])
+    True
+    >>> is_valid_sudoku_board([
+    ...  ["1","2","3",".",".",".","5","6","4"]
+    ... ,["4","5","6",".",".",".","8","9","7"]
+    ... ,["7","8","9",".",".",".","2","3","1"]
+    ... ,[".",".",".","4","5","6",".",".","."]
+    ... ,[".",".",".","7","8","9",".",".","."]
+    ... ,[".",".",".","1","2","3",".",".","."]
+    ... ,["3","1","2",".",".",".","7","8","9"]
+    ... ,["6","4","5",".",".",".","1","2","3"]
+    ... ,["9","7","8",".",".",".","4","5","6"]
+    ... ])
+    True
+    >>> is_valid_sudoku_board([
+    ...  ["1","2","3","4","5","6","7","8","9"]
+    ... ,["2",".",".",".",".",".",".",".","8"]
+    ... ,["3",".",".",".",".",".",".",".","7"]
+    ... ,["4",".",".",".",".",".",".",".","6"]
+    ... ,["5",".",".",".",".",".",".",".","5"]
+    ... ,["6",".",".",".",".",".",".",".","4"]
+    ... ,["7",".",".",".",".",".",".",".","3"]
+    ... ,["8",".",".",".",".",".",".",".","2"]
+    ... ,["9","8","7","6","5","4","3","2","1"]
+    ... ])
+    False
+    >>> is_valid_sudoku_board([
+    ...  ["1","2","3","8","9","7","5","6","4"]
+    ... ,["4","5","6","2","3","1","8","9","7"]
+    ... ,["7","8","9","5","6","4","2","3","1"]
+    ... ,["2","3","1","4","5","6","9","7","8"]
+    ... ,["5","6","4","7","8","9","3","1","2"]
+    ... ,["8","9","7","1","2","3","6","4","5"]
+    ... ,["3","1","2","6","4","5","7","8","9"]
+    ... ,["6","4","5","9","7","8","1","2","3"]
+    ... ,["9","7","8","3","1","2","4","5","6"]
+    ... ])
+    True
     >>> is_valid_sudoku_board([["1", "2", "3", "4", "5", "6", "7", "8", "9"]])
     Traceback (most recent call last):
         ...
diff --git a/networking_flow/ford_fulkerson.py b/networking_flow/ford_fulkerson.py
index 716ed508e679..b47d3b68f3d1 100644
--- a/networking_flow/ford_fulkerson.py
+++ b/networking_flow/ford_fulkerson.py
@@ -1,39 +1,96 @@
-# Ford-Fulkerson Algorithm for Maximum Flow Problem
 """
+Ford-Fulkerson Algorithm for Maximum Flow Problem
+* https://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm
+
 Description:
-    (1) Start with initial flow as 0;
-    (2) Choose augmenting path from source to sink and add path to flow;
+    (1) Start with initial flow as 0
+    (2) Choose the augmenting path from source to sink and add the path to flow
 """
 
+graph = [
+    [0, 16, 13, 0, 0, 0],
+    [0, 0, 10, 12, 0, 0],
+    [0, 4, 0, 0, 14, 0],
+    [0, 0, 9, 0, 0, 20],
+    [0, 0, 0, 7, 0, 4],
+    [0, 0, 0, 0, 0, 0],
+]
+
+
+def breadth_first_search(graph: list, source: int, sink: int, parents: list) -> bool:
+    """
+    This function returns True if there is a node that has not iterated.
+
+    Args:
+        graph: Adjacency matrix of graph
+        source: Source
+        sink: Sink
+        parents: Parent list
+
+    Returns:
+        True if there is a node that has not iterated.
+
+    >>> breadth_first_search(graph, 0, 5, [-1, -1, -1, -1, -1, -1])
+    True
+    >>> breadth_first_search(graph, 0, 6, [-1, -1, -1, -1, -1, -1])
+    Traceback (most recent call last):
+        ...
+    IndexError: list index out of range
+    """
+    visited = [False] * len(graph)  # Mark all nodes as not visited
+    queue = []  # breadth-first search queue
 
-def bfs(graph, s, t, parent):
-    # Return True if there is node that has not iterated.
-    visited = [False] * len(graph)
-    queue = []
-    queue.append(s)
-    visited[s] = True
+    # Source node
+    queue.append(source)
+    visited[source] = True
 
     while queue:
-        u = queue.pop(0)
-        for ind in range(len(graph[u])):
-            if visited[ind] is False and graph[u][ind] > 0:
+        u = queue.pop(0)  # Pop the front node
+        # Traverse all adjacent nodes of u
+        for ind, node in enumerate(graph[u]):
+            if visited[ind] is False and node > 0:
                 queue.append(ind)
                 visited[ind] = True
-                parent[ind] = u
+                parents[ind] = u
+    return visited[sink]
 
-    return visited[t]
 
+def ford_fulkerson(graph: list, source: int, sink: int) -> int:
+    """
+    This function returns the maximum flow from source to sink in the given graph.
 
-def ford_fulkerson(graph, source, sink):
-    # This array is filled by BFS and to store path
+    CAUTION: This function changes the given graph.
+
+    Args:
+        graph: Adjacency matrix of graph
+        source: Source
+        sink: Sink
+
+    Returns:
+        Maximum flow
+
+    >>> test_graph = [
+    ...     [0, 16, 13, 0, 0, 0],
+    ...     [0, 0, 10, 12, 0, 0],
+    ...     [0, 4, 0, 0, 14, 0],
+    ...     [0, 0, 9, 0, 0, 20],
+    ...     [0, 0, 0, 7, 0, 4],
+    ...     [0, 0, 0, 0, 0, 0],
+    ... ]
+    >>> ford_fulkerson(test_graph, 0, 5)
+    23
+    """
+    # This array is filled by breadth-first search and to store path
     parent = [-1] * (len(graph))
     max_flow = 0
-    while bfs(graph, source, sink, parent):
-        path_flow = float("Inf")
+
+    # While there is a path from source to sink
+    while breadth_first_search(graph, source, sink, parent):
+        path_flow = int(1e9)  # Infinite value
         s = sink
 
         while s != source:
-            # Find the minimum value in select path
+            # Find the minimum value in the selected path
             path_flow = min(path_flow, graph[parent[s]][s])
             s = parent[s]
 
@@ -45,17 +102,12 @@ def ford_fulkerson(graph, source, sink):
             graph[u][v] -= path_flow
             graph[v][u] += path_flow
             v = parent[v]
+
     return max_flow
 
 
-graph = [
-    [0, 16, 13, 0, 0, 0],
-    [0, 0, 10, 12, 0, 0],
-    [0, 4, 0, 0, 14, 0],
-    [0, 0, 9, 0, 0, 20],
-    [0, 0, 0, 7, 0, 4],
-    [0, 0, 0, 0, 0, 0],
-]
+if __name__ == "__main__":
+    from doctest import testmod
 
-source, sink = 0, 5
-print(ford_fulkerson(graph, source, sink))
+    testmod()
+    print(f"{ford_fulkerson(graph, source=0, sink=5) = }")
diff --git a/neural_network/activation_functions/__init__.py b/neural_network/activation_functions/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/neural_network/activation_functions/binary_step.py b/neural_network/activation_functions/binary_step.py
index 8f8f4d405fd2..d3d774602182 100644
--- a/neural_network/activation_functions/binary_step.py
+++ b/neural_network/activation_functions/binary_step.py
@@ -8,7 +8,6 @@
 https://en.wikipedia.org/wiki/Activation_function
 """
 
-
 import numpy as np
 
 
diff --git a/maths/gaussian_error_linear_unit.py b/neural_network/activation_functions/gaussian_error_linear_unit.py
similarity index 100%
rename from maths/gaussian_error_linear_unit.py
rename to neural_network/activation_functions/gaussian_error_linear_unit.py
diff --git a/neural_network/activation_functions/mish.py b/neural_network/activation_functions/mish.py
index e51655df8a3f..57a91413fe50 100644
--- a/neural_network/activation_functions/mish.py
+++ b/neural_network/activation_functions/mish.py
@@ -7,7 +7,8 @@
 """
 
 import numpy as np
-from softplus import softplus
+
+from .softplus import softplus
 
 
 def mish(vector: np.ndarray) -> np.ndarray:
diff --git a/neural_network/activation_functions/rectified_linear_unit.py b/neural_network/activation_functions/rectified_linear_unit.py
index 458c6bd5c391..2d5cf96fd387 100644
--- a/neural_network/activation_functions/rectified_linear_unit.py
+++ b/neural_network/activation_functions/rectified_linear_unit.py
@@ -9,6 +9,7 @@
 Script inspired from its corresponding Wikipedia article
 https://en.wikipedia.org/wiki/Rectifier_(neural_networks)
 """
+
 from __future__ import annotations
 
 import numpy as np
diff --git a/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py b/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py
index 603ac0b7e120..a053e690ba44 100644
--- a/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py
+++ b/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py
@@ -8,7 +8,6 @@
 https://en.wikipedia.org/wiki/Soboleva_modified_hyperbolic_tangent
 """
 
-
 import numpy as np
 
 
diff --git a/neural_network/back_propagation_neural_network.py b/neural_network/back_propagation_neural_network.py
index bdd096b3f653..182f759c5fc7 100644
--- a/neural_network/back_propagation_neural_network.py
+++ b/neural_network/back_propagation_neural_network.py
@@ -2,10 +2,10 @@
 
 """
 
-A Framework of Back Propagation Neural Network（BP） model
+A Framework of Back Propagation Neural Network (BP) model
 
 Easy to use:
-    * add many layers as you want ！！！
+    * add many layers as you want ! ! !
     * clearly see how the loss decreasing
 Easy to expand:
     * more activation functions
@@ -17,6 +17,7 @@
 Date: 2017.11.23
 
 """
+
 import numpy as np
 from matplotlib import pyplot as plt
 
@@ -50,8 +51,9 @@ def __init__(
         self.is_input_layer = is_input_layer
 
     def initializer(self, back_units):
-        self.weight = np.asmatrix(np.random.normal(0, 0.5, (self.units, back_units)))
-        self.bias = np.asmatrix(np.random.normal(0, 0.5, self.units)).T
+        rng = np.random.default_rng()
+        self.weight = np.asmatrix(rng.normal(0, 0.5, (self.units, back_units)))
+        self.bias = np.asmatrix(rng.normal(0, 0.5, self.units)).T
         if self.activation is None:
             self.activation = sigmoid
 
@@ -173,7 +175,8 @@ def plot_loss(self):
 
 
 def example():
-    x = np.random.randn(10, 10)
+    rng = np.random.default_rng()
+    x = rng.normal(size=(10, 10))
     y = np.asarray(
         [
             [0.8, 0.4],
diff --git a/neural_network/convolution_neural_network.py b/neural_network/convolution_neural_network.py
index f2e88fe7bd88..d4ac360a98de 100644
--- a/neural_network/convolution_neural_network.py
+++ b/neural_network/convolution_neural_network.py
@@ -1,18 +1,19 @@
 """
-     - - - - - -- - - - - - - - - - - - - - - - - - - - - - -
-    Name - - CNN - Convolution Neural Network For Photo Recognizing
-    Goal - - Recognize Handing Writing Word Photo
-    Detail: Total 5 layers neural network
-            * Convolution layer
-            * Pooling layer
-            * Input layer layer of BP
-            * Hidden layer of BP
-            * Output layer of BP
-    Author: Stephen Lee
-    Github: 245885195@qq.com
-    Date: 2017.9.20
-    - - - - - -- - - - - - - - - - - - - - - - - - - - - - -
+ - - - - - -- - - - - - - - - - - - - - - - - - - - - - -
+Name - - CNN - Convolution Neural Network For Photo Recognizing
+Goal - - Recognize Handwriting Word Photo
+Detail: Total 5 layers neural network
+        * Convolution layer
+        * Pooling layer
+        * Input layer layer of BP
+        * Hidden layer of BP
+        * Output layer of BP
+Author: Stephen Lee
+Github: 245885195@qq.com
+Date: 2017.9.20
+- - - - - -- - - - - - - - - - - - - - - - - - - - - - -
 """
+
 import pickle
 
 import numpy as np
@@ -40,15 +41,16 @@ def __init__(
         self.size_pooling1 = size_p1
         self.rate_weight = rate_w
         self.rate_thre = rate_t
+        rng = np.random.default_rng()
         self.w_conv1 = [
-            np.mat(-1 * np.random.rand(self.conv1[0], self.conv1[0]) + 0.5)
+            np.asmatrix(-1 * rng.random((self.conv1[0], self.conv1[0])) + 0.5)
             for i in range(self.conv1[1])
         ]
-        self.wkj = np.mat(-1 * np.random.rand(self.num_bp3, self.num_bp2) + 0.5)
-        self.vji = np.mat(-1 * np.random.rand(self.num_bp2, self.num_bp1) + 0.5)
-        self.thre_conv1 = -2 * np.random.rand(self.conv1[1]) + 1
-        self.thre_bp2 = -2 * np.random.rand(self.num_bp2) + 1
-        self.thre_bp3 = -2 * np.random.rand(self.num_bp3) + 1
+        self.wkj = np.asmatrix(-1 * rng.random((self.num_bp3, self.num_bp2)) + 0.5)
+        self.vji = np.asmatrix(-1 * rng.random((self.num_bp2, self.num_bp1)) + 0.5)
+        self.thre_conv1 = -2 * rng.random(self.conv1[1]) + 1
+        self.thre_bp2 = -2 * rng.random(self.num_bp2) + 1
+        self.thre_bp3 = -2 * rng.random(self.num_bp3) + 1
 
     def save_model(self, save_path):
         # save model dict with pickle
@@ -133,7 +135,7 @@ def convolute(self, data, convs, w_convs, thre_convs, conv_step):
             )
             data_featuremap.append(featuremap)
 
-        # expanding the data slice to One dimenssion
+        # expanding the data slice to one dimension
         focus1_list = []
         for each_focus in data_focus:
             focus1_list.extend(self.Expand_Mat(each_focus))
@@ -302,7 +304,7 @@ def draw_error():
             plt.grid(True, alpha=0.5)
             plt.show()
 
-        print("------------------Training Complished---------------------")
+        print("------------------Training Complete---------------------")
         print((" - - Training epoch: ", rp, f"     - - Mse: {mse:.6f}"))
         if draw_e:
             draw_error()
@@ -351,5 +353,5 @@ def convolution(self, data):
 
 if __name__ == "__main__":
     """
-    I will put the example on other file
+    I will put the example in another file
     """
diff --git a/neural_network/input_data.py.DEPRECATED.txt b/neural_network/input_data.py
similarity index 87%
rename from neural_network/input_data.py.DEPRECATED.txt
rename to neural_network/input_data.py
index a58e64907e45..3a8628f939f8 100644
--- a/neural_network/input_data.py.DEPRECATED.txt
+++ b/neural_network/input_data.py
@@ -17,26 +17,30 @@
 This module and all its submodules are deprecated.
 """
 
-
-import collections
 import gzip
 import os
+import typing
 import urllib
 
-import numpy
+import numpy as np
 from tensorflow.python.framework import dtypes, random_seed
 from tensorflow.python.platform import gfile
 from tensorflow.python.util.deprecation import deprecated
 
-_Datasets = collections.namedtuple("_Datasets", ["train", "validation", "test"])
+
+class _Datasets(typing.NamedTuple):
+    train: "_DataSet"
+    validation: "_DataSet"
+    test: "_DataSet"
+
 
 # CVDF mirror of http://yann.lecun.com/exdb/mnist/
 DEFAULT_SOURCE_URL = "https://storage.googleapis.com/cvdf-datasets/mnist/"
 
 
 def _read32(bytestream):
-    dt = numpy.dtype(numpy.uint32).newbyteorder(">")
-    return numpy.frombuffer(bytestream.read(4), dtype=dt)[0]
+    dt = np.dtype(np.uint32).newbyteorder(">")
+    return np.frombuffer(bytestream.read(4), dtype=dt)[0]
 
 
 @deprecated(None, "Please use tf.data to implement this functionality.")
@@ -57,14 +61,13 @@ def _extract_images(f):
     with gzip.GzipFile(fileobj=f) as bytestream:
         magic = _read32(bytestream)
         if magic != 2051:
-            raise ValueError(
-                "Invalid magic number %d in MNIST image file: %s" % (magic, f.name)
-            )
+            msg = f"Invalid magic number {magic} in MNIST image file: {f.name}"
+            raise ValueError(msg)
         num_images = _read32(bytestream)
         rows = _read32(bytestream)
         cols = _read32(bytestream)
         buf = bytestream.read(rows * cols * num_images)
-        data = numpy.frombuffer(buf, dtype=numpy.uint8)
+        data = np.frombuffer(buf, dtype=np.uint8)
         data = data.reshape(num_images, rows, cols, 1)
         return data
 
@@ -73,8 +76,8 @@ def _extract_images(f):
 def _dense_to_one_hot(labels_dense, num_classes):
     """Convert class labels from scalars to one-hot vectors."""
     num_labels = labels_dense.shape[0]
-    index_offset = numpy.arange(num_labels) * num_classes
-    labels_one_hot = numpy.zeros((num_labels, num_classes))
+    index_offset = np.arange(num_labels) * num_classes
+    labels_one_hot = np.zeros((num_labels, num_classes))
     labels_one_hot.flat[index_offset + labels_dense.ravel()] = 1
     return labels_one_hot
 
@@ -98,12 +101,11 @@ def _extract_labels(f, one_hot=False, num_classes=10):
     with gzip.GzipFile(fileobj=f) as bytestream:
         magic = _read32(bytestream)
         if magic != 2049:
-            raise ValueError(
-                "Invalid magic number %d in MNIST label file: %s" % (magic, f.name)
-            )
+            msg = f"Invalid magic number {magic} in MNIST label file: {f.name}"
+            raise ValueError(msg)
         num_items = _read32(bytestream)
         buf = bytestream.read(num_items)
-        labels = numpy.frombuffer(buf, dtype=numpy.uint8)
+        labels = np.frombuffer(buf, dtype=np.uint8)
         if one_hot:
             return _dense_to_one_hot(labels, num_classes)
         return labels
@@ -149,17 +151,18 @@ def __init__(
         """
         seed1, seed2 = random_seed.get_seed(seed)
         # If op level seed is not set, use whatever graph level seed is returned
-        numpy.random.seed(seed1 if seed is None else seed2)
+        self._rng = np.random.default_rng(seed1 if seed is None else seed2)
         dtype = dtypes.as_dtype(dtype).base_dtype
         if dtype not in (dtypes.uint8, dtypes.float32):
-            raise TypeError("Invalid image dtype %r, expected uint8 or float32" % dtype)
+            msg = f"Invalid image dtype {dtype!r}, expected uint8 or float32"
+            raise TypeError(msg)
         if fake_data:
             self._num_examples = 10000
             self.one_hot = one_hot
         else:
-            assert (
-                images.shape[0] == labels.shape[0]
-            ), f"images.shape: {images.shape} labels.shape: {labels.shape}"
+            assert images.shape[0] == labels.shape[0], (
+                f"images.shape: {images.shape} labels.shape: {labels.shape}"
+            )
             self._num_examples = images.shape[0]
 
             # Convert shape from [num examples, rows, columns, depth]
@@ -171,8 +174,8 @@ def __init__(
                 )
             if dtype == dtypes.float32:
                 # Convert from [0, 255] -> [0.0, 1.0].
-                images = images.astype(numpy.float32)
-                images = numpy.multiply(images, 1.0 / 255.0)
+                images = images.astype(np.float32)
+                images = np.multiply(images, 1.0 / 255.0)
         self._images = images
         self._labels = labels
         self._epochs_completed = 0
@@ -206,8 +209,8 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True):
         start = self._index_in_epoch
         # Shuffle for the first epoch
         if self._epochs_completed == 0 and start == 0 and shuffle:
-            perm0 = numpy.arange(self._num_examples)
-            numpy.random.shuffle(perm0)
+            perm0 = np.arange(self._num_examples)
+            self._rng.shuffle(perm0)
             self._images = self.images[perm0]
             self._labels = self.labels[perm0]
         # Go to the next epoch
@@ -220,8 +223,8 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True):
             labels_rest_part = self._labels[start : self._num_examples]
             # Shuffle the data
             if shuffle:
-                perm = numpy.arange(self._num_examples)
-                numpy.random.shuffle(perm)
+                perm = np.arange(self._num_examples)
+                self._rng.shuffle(perm)
                 self._images = self.images[perm]
                 self._labels = self.labels[perm]
             # Start next epoch
@@ -231,8 +234,8 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True):
             images_new_part = self._images[start:end]
             labels_new_part = self._labels[start:end]
             return (
-                numpy.concatenate((images_rest_part, images_new_part), axis=0),
-                numpy.concatenate((labels_rest_part, labels_new_part), axis=0),
+                np.concatenate((images_rest_part, images_new_part), axis=0),
+                np.concatenate((labels_rest_part, labels_new_part), axis=0),
             )
         else:
             self._index_in_epoch += batch_size
diff --git a/neural_network/perceptron.py b/neural_network/perceptron.py.DISABLED
similarity index 100%
rename from neural_network/perceptron.py
rename to neural_network/perceptron.py.DISABLED
diff --git a/neural_network/simple_neural_network.py b/neural_network/simple_neural_network.py
index f2a3234873b5..8751a38908cf 100644
--- a/neural_network/simple_neural_network.py
+++ b/neural_network/simple_neural_network.py
@@ -28,7 +28,7 @@ def sigmoid_function(value: float, deriv: bool = False) -> float:
 def forward_propagation(expected: int, number_propagations: int) -> float:
     """Return the value found after the forward propagation training.
 
-    >>> res = forward_propagation(32, 10000000)
+    >>> res = forward_propagation(32, 450_000)  # Was 10_000_000
     >>> res > 31 and res < 33
     True
 
diff --git a/neural_network/2_hidden_layers_neural_network.py b/neural_network/two_hidden_layers_neural_network.py
similarity index 79%
rename from neural_network/2_hidden_layers_neural_network.py
rename to neural_network/two_hidden_layers_neural_network.py
index 7b374a93d039..1b7c0beed3ba 100644
--- a/neural_network/2_hidden_layers_neural_network.py
+++ b/neural_network/two_hidden_layers_neural_network.py
@@ -5,11 +5,11 @@
     - https://en.wikipedia.org/wiki/Feedforward_neural_network (Feedforward)
 """
 
-import numpy
+import numpy as np
 
 
 class TwoHiddenLayerNeuralNetwork:
-    def __init__(self, input_array: numpy.ndarray, output_array: numpy.ndarray) -> None:
+    def __init__(self, input_array: np.ndarray, output_array: np.ndarray) -> None:
         """
         This function initializes the TwoHiddenLayerNeuralNetwork class with random
         weights for every layer and initializes predicted output with zeroes.
@@ -28,30 +28,29 @@ def __init__(self, input_array: numpy.ndarray, output_array: numpy.ndarray) -> N
         # Random initial weights are assigned.
         # self.input_array.shape[1] is used to represent number of nodes in input layer.
         # First hidden layer consists of 4 nodes.
-        self.input_layer_and_first_hidden_layer_weights = numpy.random.rand(
-            self.input_array.shape[1], 4
+        rng = np.random.default_rng()
+        self.input_layer_and_first_hidden_layer_weights = rng.random(
+            (self.input_array.shape[1], 4)
         )
 
         # Random initial values for the first hidden layer.
         # First hidden layer has 4 nodes.
         # Second hidden layer has 3 nodes.
-        self.first_hidden_layer_and_second_hidden_layer_weights = numpy.random.rand(
-            4, 3
-        )
+        self.first_hidden_layer_and_second_hidden_layer_weights = rng.random((4, 3))
 
         # Random initial values for the second hidden layer.
         # Second hidden layer has 3 nodes.
         # Output layer has 1 node.
-        self.second_hidden_layer_and_output_layer_weights = numpy.random.rand(3, 1)
+        self.second_hidden_layer_and_output_layer_weights = rng.random((3, 1))
 
         # Real output values provided.
         self.output_array = output_array
 
         # Predicted output values by the neural network.
         # Predicted_output array initially consists of zeroes.
-        self.predicted_output = numpy.zeros(output_array.shape)
+        self.predicted_output = np.zeros(output_array.shape)
 
-    def feedforward(self) -> numpy.ndarray:
+    def feedforward(self) -> np.ndarray:
         """
         The information moves in only one direction i.e. forward from the input nodes,
         through the two hidden nodes and to the output nodes.
@@ -60,24 +59,24 @@ def feedforward(self) -> numpy.ndarray:
         Return layer_between_second_hidden_layer_and_output
             (i.e the last layer of the neural network).
 
-        >>> input_val = numpy.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float)
-        >>> output_val = numpy.array(([0], [0], [0]), dtype=float)
+        >>> input_val = np.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float)
+        >>> output_val = np.array(([0], [0], [0]), dtype=float)
         >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val)
         >>> res = nn.feedforward()
-        >>> array_sum = numpy.sum(res)
-        >>> numpy.isnan(array_sum)
+        >>> array_sum = np.sum(res)
+        >>> bool(np.isnan(array_sum))
         False
         """
         # Layer_between_input_and_first_hidden_layer is the layer connecting the
         # input nodes with the first hidden layer nodes.
         self.layer_between_input_and_first_hidden_layer = sigmoid(
-            numpy.dot(self.input_array, self.input_layer_and_first_hidden_layer_weights)
+            np.dot(self.input_array, self.input_layer_and_first_hidden_layer_weights)
         )
 
         # layer_between_first_hidden_layer_and_second_hidden_layer is the layer
         # connecting the first hidden set of nodes with the second hidden set of nodes.
         self.layer_between_first_hidden_layer_and_second_hidden_layer = sigmoid(
-            numpy.dot(
+            np.dot(
                 self.layer_between_input_and_first_hidden_layer,
                 self.first_hidden_layer_and_second_hidden_layer_weights,
             )
@@ -86,7 +85,7 @@ def feedforward(self) -> numpy.ndarray:
         # layer_between_second_hidden_layer_and_output is the layer connecting
         # second hidden layer with the output node.
         self.layer_between_second_hidden_layer_and_output = sigmoid(
-            numpy.dot(
+            np.dot(
                 self.layer_between_first_hidden_layer_and_second_hidden_layer,
                 self.second_hidden_layer_and_output_layer_weights,
             )
@@ -100,25 +99,25 @@ def back_propagation(self) -> None:
         error rate obtained in the previous epoch (i.e., iteration).
         Updation is done using derivative of sogmoid activation function.
 
-        >>> input_val = numpy.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float)
-        >>> output_val = numpy.array(([0], [0], [0]), dtype=float)
+        >>> input_val = np.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float)
+        >>> output_val = np.array(([0], [0], [0]), dtype=float)
         >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val)
         >>> res = nn.feedforward()
         >>> nn.back_propagation()
         >>> updated_weights = nn.second_hidden_layer_and_output_layer_weights
-        >>> (res == updated_weights).all()
+        >>> bool((res == updated_weights).all())
         False
         """
 
-        updated_second_hidden_layer_and_output_layer_weights = numpy.dot(
+        updated_second_hidden_layer_and_output_layer_weights = np.dot(
             self.layer_between_first_hidden_layer_and_second_hidden_layer.T,
             2
             * (self.output_array - self.predicted_output)
             * sigmoid_derivative(self.predicted_output),
         )
-        updated_first_hidden_layer_and_second_hidden_layer_weights = numpy.dot(
+        updated_first_hidden_layer_and_second_hidden_layer_weights = np.dot(
             self.layer_between_input_and_first_hidden_layer.T,
-            numpy.dot(
+            np.dot(
                 2
                 * (self.output_array - self.predicted_output)
                 * sigmoid_derivative(self.predicted_output),
@@ -128,10 +127,10 @@ def back_propagation(self) -> None:
                 self.layer_between_first_hidden_layer_and_second_hidden_layer
             ),
         )
-        updated_input_layer_and_first_hidden_layer_weights = numpy.dot(
+        updated_input_layer_and_first_hidden_layer_weights = np.dot(
             self.input_array.T,
-            numpy.dot(
-                numpy.dot(
+            np.dot(
+                np.dot(
                     2
                     * (self.output_array - self.predicted_output)
                     * sigmoid_derivative(self.predicted_output),
@@ -155,7 +154,7 @@ def back_propagation(self) -> None:
             updated_second_hidden_layer_and_output_layer_weights
         )
 
-    def train(self, output: numpy.ndarray, iterations: int, give_loss: bool) -> None:
+    def train(self, output: np.ndarray, iterations: int, give_loss: bool) -> None:
         """
         Performs the feedforwarding and back propagation process for the
         given number of iterations.
@@ -166,23 +165,23 @@ def train(self, output: numpy.ndarray, iterations: int, give_loss: bool) -> None
         give_loss : boolean value, If True then prints loss for each iteration,
                     If False then nothing is printed
 
-        >>> input_val = numpy.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float)
-        >>> output_val = numpy.array(([0], [1], [1]), dtype=float)
+        >>> input_val = np.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float)
+        >>> output_val = np.array(([0], [1], [1]), dtype=float)
         >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val)
         >>> first_iteration_weights = nn.feedforward()
         >>> nn.back_propagation()
         >>> updated_weights = nn.second_hidden_layer_and_output_layer_weights
-        >>> (first_iteration_weights == updated_weights).all()
+        >>> bool((first_iteration_weights == updated_weights).all())
         False
         """
         for iteration in range(1, iterations + 1):
             self.output = self.feedforward()
             self.back_propagation()
             if give_loss:
-                loss = numpy.mean(numpy.square(output - self.feedforward()))
+                loss = np.mean(np.square(output - self.feedforward()))
                 print(f"Iteration {iteration} Loss: {loss}")
 
-    def predict(self, input_arr: numpy.ndarray) -> int:
+    def predict(self, input_arr: np.ndarray) -> int:
         """
         Predict's the output for the given input values using
         the trained neural network.
@@ -192,8 +191,8 @@ def predict(self, input_arr: numpy.ndarray) -> int:
         than the threshold value else returns 0,
         as the real output values are in binary.
 
-        >>> input_val = numpy.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float)
-        >>> output_val = numpy.array(([0], [1], [1]), dtype=float)
+        >>> input_val = np.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float)
+        >>> output_val = np.array(([0], [1], [1]), dtype=float)
         >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val)
         >>> nn.train(output_val, 1000, False)
         >>> nn.predict([0, 1, 0]) in (0, 1)
@@ -204,18 +203,18 @@ def predict(self, input_arr: numpy.ndarray) -> int:
         self.array = input_arr
 
         self.layer_between_input_and_first_hidden_layer = sigmoid(
-            numpy.dot(self.array, self.input_layer_and_first_hidden_layer_weights)
+            np.dot(self.array, self.input_layer_and_first_hidden_layer_weights)
         )
 
         self.layer_between_first_hidden_layer_and_second_hidden_layer = sigmoid(
-            numpy.dot(
+            np.dot(
                 self.layer_between_input_and_first_hidden_layer,
                 self.first_hidden_layer_and_second_hidden_layer_weights,
             )
         )
 
         self.layer_between_second_hidden_layer_and_output = sigmoid(
-            numpy.dot(
+            np.dot(
                 self.layer_between_first_hidden_layer_and_second_hidden_layer,
                 self.second_hidden_layer_and_output_layer_weights,
             )
@@ -224,26 +223,26 @@ def predict(self, input_arr: numpy.ndarray) -> int:
         return int((self.layer_between_second_hidden_layer_and_output > 0.6)[0])
 
 
-def sigmoid(value: numpy.ndarray) -> numpy.ndarray:
+def sigmoid(value: np.ndarray) -> np.ndarray:
     """
     Applies sigmoid activation function.
 
     return normalized values
 
-    >>> sigmoid(numpy.array(([1, 0, 2], [1, 0, 0]), dtype=numpy.float64))
+    >>> sigmoid(np.array(([1, 0, 2], [1, 0, 0]), dtype=np.float64))
     array([[0.73105858, 0.5       , 0.88079708],
            [0.73105858, 0.5       , 0.5       ]])
     """
-    return 1 / (1 + numpy.exp(-value))
+    return 1 / (1 + np.exp(-value))
 
 
-def sigmoid_derivative(value: numpy.ndarray) -> numpy.ndarray:
+def sigmoid_derivative(value: np.ndarray) -> np.ndarray:
     """
     Provides the derivative value of the sigmoid function.
 
     returns derivative of the sigmoid value
 
-    >>> sigmoid_derivative(numpy.array(([1, 0, 2], [1, 0, 0]), dtype=numpy.float64))
+    >>> sigmoid_derivative(np.array(([1, 0, 2], [1, 0, 0]), dtype=np.float64))
     array([[ 0.,  0., -2.],
            [ 0.,  0.,  0.]])
     """
@@ -264,7 +263,7 @@ def example() -> int:
     True
     """
     # Input values.
-    test_input = numpy.array(
+    test_input = np.array(
         (
             [0, 0, 0],
             [0, 0, 1],
@@ -275,11 +274,11 @@ def example() -> int:
             [1, 1, 0],
             [1, 1, 1],
         ),
-        dtype=numpy.float64,
+        dtype=np.float64,
     )
 
     # True output values for the given input values.
-    output = numpy.array(([0], [1], [1], [0], [1], [0], [0], [1]), dtype=numpy.float64)
+    output = np.array(([0], [1], [1], [0], [1], [0], [0], [1]), dtype=np.float64)
 
     # Calling neural network class.
     neural_network = TwoHiddenLayerNeuralNetwork(
@@ -290,7 +289,7 @@ def example() -> int:
     # Set give_loss to True if you want to see loss in every iteration.
     neural_network.train(output=output, iterations=10, give_loss=False)
 
-    return neural_network.predict(numpy.array(([1, 1, 1]), dtype=numpy.float64))
+    return neural_network.predict(np.array(([1, 1, 1]), dtype=np.float64))
 
 
 if __name__ == "__main__":
diff --git a/other/dijkstra_bankers_algorithm.py b/other/bankers_algorithm.py
similarity index 92%
rename from other/dijkstra_bankers_algorithm.py
rename to other/bankers_algorithm.py
index be7bceba125d..b1da851fc0f3 100644
--- a/other/dijkstra_bankers_algorithm.py
+++ b/other/bankers_algorithm.py
@@ -10,15 +10,14 @@
 predetermined maximum possible amounts of all resources, and then makes a "s-state"
 check to test for possible deadlock conditions for all other pending activities,
 before deciding whether allocation should be allowed to continue.
-[Source] Wikipedia
-[Credit] Rosetta Code C implementation helped very much.
- (https://rosettacode.org/wiki/Banker%27s_algorithm)
+
+| [Source] Wikipedia
+| [Credit] Rosetta Code C implementation helped very much.
+| (https://rosettacode.org/wiki/Banker%27s_algorithm)
 """
 
 from __future__ import annotations
 
-import time
-
 import numpy as np
 
 test_claim_vector = [8, 5, 9, 7]
@@ -77,7 +76,7 @@ def __available_resources(self) -> list[int]:
     def __need(self) -> list[list[int]]:
         """
         Implement safety checker that calculates the needs by ensuring that
-        max_claim[i][j] - alloc_table[i][j] <= avail[j]
+        ``max_claim[i][j] - alloc_table[i][j] <= avail[j]``
         """
         return [
             list(np.array(self.__maximum_claim_table[i]) - np.array(allocated_resource))
@@ -88,10 +87,14 @@ def __need_index_manager(self) -> dict[int, list[int]]:
         """
         This function builds an index control dictionary to track original ids/indices
         of processes when altered during execution of method "main"
-            Return: {0: [a: int, b: int], 1: [c: int, d: int]}
-        >>> (BankersAlgorithm(test_claim_vector, test_allocated_res_table,
-        ...     test_maximum_claim_table)._BankersAlgorithm__need_index_manager()
-        ...     )  # doctest: +NORMALIZE_WHITESPACE
+
+            :Return: {0: [a: int, b: int], 1: [c: int, d: int]}
+
+        >>> index_control = BankersAlgorithm(
+        ...     test_claim_vector, test_allocated_res_table, test_maximum_claim_table
+        ... )._BankersAlgorithm__need_index_manager()
+        >>> {key: [int(x) for x in value] for key, value
+        ...     in index_control.items()}  # doctest: +NORMALIZE_WHITESPACE
         {0: [1, 2, 0, 3], 1: [0, 1, 3, 1], 2: [1, 1, 0, 2], 3: [1, 3, 2, 0],
          4: [2, 0, 0, 3]}
         """
@@ -100,7 +103,8 @@ def __need_index_manager(self) -> dict[int, list[int]]:
     def main(self, **kwargs) -> None:
         """
         Utilize various methods in this class to simulate the Banker's algorithm
-        Return: None
+            :Return: None
+
         >>> BankersAlgorithm(test_claim_vector, test_allocated_res_table,
         ...    test_maximum_claim_table).main(describe=True)
                  Allocated Resource Table
@@ -216,7 +220,6 @@ def __pretty_data(self):
             "Initial Available Resources:       "
             + " ".join(str(x) for x in self.__available_resources())
         )
-        time.sleep(1)
 
 
 if __name__ == "__main__":
diff --git a/other/davis_putnam_logemann_loveland.py b/other/davis_putnam_logemann_loveland.py
index f5fb103ba528..7d0bcce15a29 100644
--- a/other/davis_putnam_logemann_loveland.py
+++ b/other/davis_putnam_logemann_loveland.py
@@ -1,13 +1,14 @@
 #!/usr/bin/env python3
 
 """
-Davis–Putnam–Logemann–Loveland (DPLL) algorithm is a complete, backtracking-based
+Davis-Putnam-Logemann-Loveland (DPLL) algorithm is a complete, backtracking-based
 search algorithm for deciding the satisfiability of propositional logic formulae in
 conjunctive normal form, i.e, for solving the Conjunctive Normal Form SATisfiability
 (CNF-SAT) problem.
 
 For more information about the algorithm: https://en.wikipedia.org/wiki/DPLL_algorithm
 """
+
 from __future__ import annotations
 
 import random
@@ -16,13 +17,15 @@
 
 class Clause:
     """
-    A clause represented in Conjunctive Normal Form.
-    A clause is a set of literals, either complemented or otherwise.
+    | A clause represented in Conjunctive Normal Form.
+    | A clause is a set of literals, either complemented or otherwise.
+
     For example:
-        {A1, A2, A3'} is the clause (A1 v A2 v A3')
-        {A5', A2', A1} is the clause (A5' v A2' v A1)
+        * {A1, A2, A3'} is the clause (A1 v A2 v A3')
+        * {A5', A2', A1} is the clause (A5' v A2' v A1)
 
     Create model
+
     >>> clause = Clause(["A1", "A2'", "A3"])
     >>> clause.evaluate({"A1": True})
     True
@@ -33,11 +36,12 @@ def __init__(self, literals: list[str]) -> None:
         Represent the literals and an assignment in a clause."
         """
         # Assign all literals to None initially
-        self.literals: dict[str, bool | None] = {literal: None for literal in literals}
+        self.literals: dict[str, bool | None] = dict.fromkeys(literals)
 
     def __str__(self) -> str:
         """
         To print a clause as in Conjunctive Normal Form.
+
         >>> str(Clause(["A1", "A2'", "A3"]))
         "{A1 , A2' , A3}"
         """
@@ -46,6 +50,7 @@ def __str__(self) -> str:
     def __len__(self) -> int:
         """
         To print a clause as in Conjunctive Normal Form.
+
         >>> len(Clause([]))
         0
         >>> len(Clause(["A1", "A2'", "A3"]))
@@ -63,20 +68,21 @@ def assign(self, model: dict[str, bool | None]) -> None:
                 value = model[symbol]
             else:
                 continue
-            if value is not None:
-                # Complement assignment if literal is in complemented form
-                if literal.endswith("'"):
-                    value = not value
+            # Complement assignment if literal is in complemented form
+            if value is not None and literal.endswith("'"):
+                value = not value
             self.literals[literal] = value
 
     def evaluate(self, model: dict[str, bool | None]) -> bool | None:
         """
         Evaluates the clause with the assignments in model.
+
         This has the following steps:
-        1. Return True if both a literal and its complement exist in the clause.
-        2. Return True if a single literal has the assignment True.
-        3. Return None(unable to complete evaluation) if a literal has no assignment.
-        4. Compute disjunction of all values assigned in clause.
+          1. Return ``True`` if both a literal and its complement exist in the clause.
+          2. Return ``True`` if a single literal has the assignment ``True``.
+          3. Return ``None`` (unable to complete evaluation)
+             if a literal has no assignment.
+          4. Compute disjunction of all values assigned in clause.
         """
         for literal in self.literals:
             symbol = literal.rstrip("'") if literal.endswith("'") else literal + "'"
@@ -92,10 +98,10 @@ def evaluate(self, model: dict[str, bool | None]) -> bool | None:
 
 class Formula:
     """
-    A formula represented in Conjunctive Normal Form.
-    A formula is a set of clauses.
-    For example,
-        {{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1))
+    | A formula represented in Conjunctive Normal Form.
+    | A formula is a set of clauses.
+    | For example,
+    |   {{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1))
     """
 
     def __init__(self, clauses: Iterable[Clause]) -> None:
@@ -107,7 +113,8 @@ def __init__(self, clauses: Iterable[Clause]) -> None:
     def __str__(self) -> str:
         """
         To print a formula as in Conjunctive Normal Form.
-        str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]))
+
+        >>> str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]))
         "{{A1 , A2' , A3} , {A5' , A2' , A1}}"
         """
         return "{" + " , ".join(str(clause) for clause in self.clauses) + "}"
@@ -115,8 +122,8 @@ def __str__(self) -> str:
 
 def generate_clause() -> Clause:
     """
-    Randomly generate a clause.
-    All literals have the name Ax, where x is an integer from 1 to 5.
+    | Randomly generate a clause.
+    | All literals have the name Ax, where x is an integer from ``1`` to ``5``.
     """
     literals = []
     no_of_literals = random.randint(1, 5)
@@ -149,11 +156,12 @@ def generate_formula() -> Formula:
 
 def generate_parameters(formula: Formula) -> tuple[list[Clause], list[str]]:
     """
-    Return the clauses and symbols from a formula.
-    A symbol is the uncomplemented form of a literal.
+    | Return the clauses and symbols from a formula.
+    | A symbol is the uncomplemented form of a literal.
+
     For example,
-        Symbol of A3 is A3.
-        Symbol of A5' is A5.
+      * Symbol of A3 is A3.
+      * Symbol of A5' is A5.
 
     >>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])
     >>> clauses, symbols = generate_parameters(formula)
@@ -177,21 +185,20 @@ def find_pure_symbols(
     clauses: list[Clause], symbols: list[str], model: dict[str, bool | None]
 ) -> tuple[list[str], dict[str, bool | None]]:
     """
-    Return pure symbols and their values to satisfy clause.
-    Pure symbols are symbols in a formula that exist only
-    in one form, either complemented or otherwise.
-    For example,
-        { { A4 , A3 , A5' , A1 , A3' } , { A4 } , { A3 } } has
-        pure symbols A4, A5' and A1.
+    | Return pure symbols and their values to satisfy clause.
+    | Pure symbols are symbols in a formula that exist only in one form,
+    | either complemented or otherwise.
+    | For example,
+    |   {{A4 , A3 , A5' , A1 , A3'} , {A4} , {A3}} has pure symbols A4, A5' and A1.
+
     This has the following steps:
-    1. Ignore clauses that have already evaluated to be True.
-    2. Find symbols that occur only in one form in the rest of the clauses.
-    3. Assign value True or False depending on whether the symbols occurs
-    in normal or complemented form respectively.
+      1. Ignore clauses that have already evaluated to be ``True``.
+      2. Find symbols that occur only in one form in the rest of the clauses.
+      3. Assign value ``True`` or ``False`` depending on whether the symbols occurs
+         in normal or complemented form respectively.
 
     >>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])
     >>> clauses, symbols = generate_parameters(formula)
-
     >>> pure_symbols, values = find_pure_symbols(clauses, symbols, {})
     >>> pure_symbols
     ['A1', 'A2', 'A3', 'A5']
@@ -226,24 +233,26 @@ def find_pure_symbols(
 
 
 def find_unit_clauses(
-    clauses: list[Clause], model: dict[str, bool | None]
+    clauses: list[Clause],
+    model: dict[str, bool | None],  # noqa: ARG001
 ) -> tuple[list[str], dict[str, bool | None]]:
     """
     Returns the unit symbols and their values to satisfy clause.
+
     Unit symbols are symbols in a formula that are:
-    - Either the only symbol in a clause
-    - Or all other literals in that clause have been assigned False
+      - Either the only symbol in a clause
+      - Or all other literals in that clause have been assigned ``False``
+
     This has the following steps:
-    1. Find symbols that are the only occurrences in a clause.
-    2. Find symbols in a clause where all other literals are assigned False.
-    3. Assign True or False depending on whether the symbols occurs in
-    normal or complemented form respectively.
+      1. Find symbols that are the only occurrences in a clause.
+      2. Find symbols in a clause where all other literals are assigned ``False``.
+      3. Assign ``True`` or ``False`` depending on whether the symbols occurs in
+         normal or complemented form respectively.
 
     >>> clause1 = Clause(["A4", "A3", "A5'", "A1", "A3'"])
     >>> clause2 = Clause(["A4"])
     >>> clause3 = Clause(["A3"])
     >>> clauses, symbols = generate_parameters(Formula([clause1, clause2, clause3]))
-
     >>> unit_clauses, values = find_unit_clauses(clauses, {})
     >>> unit_clauses
     ['A4', 'A3']
@@ -277,16 +286,16 @@ def dpll_algorithm(
     clauses: list[Clause], symbols: list[str], model: dict[str, bool | None]
 ) -> tuple[bool | None, dict[str, bool | None] | None]:
     """
-    Returns the model if the formula is satisfiable, else None
+    Returns the model if the formula is satisfiable, else ``None``
+
     This has the following steps:
-    1. If every clause in clauses is True, return True.
-    2. If some clause in clauses is False, return False.
-    3. Find pure symbols.
-    4. Find unit symbols.
+      1. If every clause in clauses is ``True``, return ``True``.
+      2. If some clause in clauses is ``False``, return ``False``.
+      3. Find pure symbols.
+      4. Find unit symbols.
 
     >>> formula = Formula([Clause(["A4", "A3", "A5'", "A1", "A3'"]), Clause(["A4"])])
     >>> clauses, symbols = generate_parameters(formula)
-
     >>> soln, model = dpll_algorithm(clauses, symbols, {})
     >>> soln
     True
diff --git a/other/doomsday.py b/other/doomsday.py
index d8fe261156a1..be3b18eeecaa 100644
--- a/other/doomsday.py
+++ b/other/doomsday.py
@@ -46,7 +46,7 @@ def get_week_day(year: int, month: int, day: int) -> str:
     ) % 7
     day_anchor = (
         DOOMSDAY_NOT_LEAP[month - 1]
-        if (year % 4 != 0) or (centurian == 0 and (year % 400) == 0)
+        if year % 4 != 0 or (centurian == 0 and year % 400 != 0)
         else DOOMSDAY_LEAP[month - 1]
     )
     week_day = (dooms_day + day - day_anchor) % 7
diff --git a/other/fischer_yates_shuffle.py b/other/fischer_yates_shuffle.py
index fa2f4dce9db0..5e90b10edd89 100644
--- a/other/fischer_yates_shuffle.py
+++ b/other/fischer_yates_shuffle.py
@@ -1,10 +1,11 @@
 #!/usr/bin/python
 """
-The Fisher–Yates shuffle is an algorithm for generating a random permutation of a
+The Fisher-Yates shuffle is an algorithm for generating a random permutation of a
 finite sequence.
 For more details visit
 wikipedia/Fischer-Yates-Shuffle.
 """
+
 import random
 from typing import Any
 
diff --git a/other/gauss_easter.py b/other/gauss_easter.py
index 4447d4ab86af..8c8c37c92796 100644
--- a/other/gauss_easter.py
+++ b/other/gauss_easter.py
@@ -1,8 +1,9 @@
 """
 https://en.wikipedia.org/wiki/Computus#Gauss'_Easter_algorithm
 """
+
 import math
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
 
 
 def gauss_easter(year: int) -> datetime:
@@ -10,16 +11,16 @@ def gauss_easter(year: int) -> datetime:
     Calculation Gregorian easter date for given year
 
     >>> gauss_easter(2007)
-    datetime.datetime(2007, 4, 8, 0, 0)
+    datetime.datetime(2007, 4, 8, 0, 0, tzinfo=datetime.timezone.utc)
 
     >>> gauss_easter(2008)
-    datetime.datetime(2008, 3, 23, 0, 0)
+    datetime.datetime(2008, 3, 23, 0, 0, tzinfo=datetime.timezone.utc)
 
     >>> gauss_easter(2020)
-    datetime.datetime(2020, 4, 12, 0, 0)
+    datetime.datetime(2020, 4, 12, 0, 0, tzinfo=datetime.timezone.utc)
 
     >>> gauss_easter(2021)
-    datetime.datetime(2021, 4, 4, 0, 0)
+    datetime.datetime(2021, 4, 4, 0, 0, tzinfo=datetime.timezone.utc)
     """
     metonic_cycle = year % 19
     julian_leap_year = year % 4
@@ -44,16 +45,16 @@ def gauss_easter(year: int) -> datetime:
     ) % 7
 
     if days_to_add == 29 and days_from_phm_to_sunday == 6:
-        return datetime(year, 4, 19)
+        return datetime(year, 4, 19, tzinfo=UTC)
     elif days_to_add == 28 and days_from_phm_to_sunday == 6:
-        return datetime(year, 4, 18)
+        return datetime(year, 4, 18, tzinfo=UTC)
     else:
-        return datetime(year, 3, 22) + timedelta(
+        return datetime(year, 3, 22, tzinfo=UTC) + timedelta(
             days=int(days_to_add + days_from_phm_to_sunday)
         )
 
 
 if __name__ == "__main__":
-    for year in (1994, 2000, 2010, 2021, 2023):
-        tense = "will be" if year > datetime.now().year else "was"
+    for year in (1994, 2000, 2010, 2021, 2023, 2032, 2100):
+        tense = "will be" if year > datetime.now(tz=UTC).year else "was"
         print(f"Easter in {year} {tense} {gauss_easter(year)}")
diff --git a/other/least_recently_used.py b/other/least_recently_used.py
index cb692bb1b1c0..d96960868488 100644
--- a/other/least_recently_used.py
+++ b/other/least_recently_used.py
@@ -2,12 +2,12 @@
 
 import sys
 from collections import deque
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 
 
-class LRUCache(Generic[T]):
+class LRUCache[T]:
     """
     Page Replacement Algorithm, Least Recently Used (LRU) Caching.
 
diff --git a/other/lfu_cache.py b/other/lfu_cache.py
index b68ba3a4605c..6eaacff2966a 100644
--- a/other/lfu_cache.py
+++ b/other/lfu_cache.py
@@ -1,13 +1,13 @@
 from __future__ import annotations
 
 from collections.abc import Callable
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 U = TypeVar("U")
 
 
-class DoubleLinkedListNode(Generic[T, U]):
+class DoubleLinkedListNode[T, U]:
     """
     Double Linked List Node built specifically for LFU Cache
 
@@ -24,12 +24,13 @@ def __init__(self, key: T | None, val: U | None):
         self.prev: DoubleLinkedListNode[T, U] | None = None
 
     def __repr__(self) -> str:
-        return "Node: key: {}, val: {}, freq: {}, has next: {}, has prev: {}".format(
-            self.key, self.val, self.freq, self.next is not None, self.prev is not None
+        return (
+            f"Node: key: {self.key}, val: {self.val}, freq: {self.freq}, "
+            f"has next: {self.next is not None}, has prev: {self.prev is not None}"
         )
 
 
-class DoubleLinkedList(Generic[T, U]):
+class DoubleLinkedList[T, U]:
     """
     Double Linked List built specifically for LFU Cache
 
@@ -160,7 +161,7 @@ def remove(
         return node
 
 
-class LFUCache(Generic[T, U]):
+class LFUCache[T, U]:
     """
     LFU Cache to store a given capacity of data. Can be used as a stand-alone object
     or as a function decorator.
@@ -195,9 +196,6 @@ class LFUCache(Generic[T, U]):
     CacheInfo(hits=196, misses=100, capacity=100, current_size=100)
     """
 
-    # class variable to map the decorator functions to their respective instance
-    decorator_function_to_instance_map: dict[Callable[[T], U], LFUCache[T, U]] = {}
-
     def __init__(self, capacity: int):
         self.list: DoubleLinkedList[T, U] = DoubleLinkedList()
         self.capacity = capacity
@@ -290,18 +288,23 @@ def decorator(
         """
 
         def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]:
+            # variable to map the decorator functions to their respective instance
+            decorator_function_to_instance_map: dict[
+                Callable[[T], U], LFUCache[T, U]
+            ] = {}
+
             def cache_decorator_wrapper(*args: T) -> U:
-                if func not in cls.decorator_function_to_instance_map:
-                    cls.decorator_function_to_instance_map[func] = LFUCache(size)
+                if func not in decorator_function_to_instance_map:
+                    decorator_function_to_instance_map[func] = LFUCache(size)
 
-                result = cls.decorator_function_to_instance_map[func].get(args[0])
+                result = decorator_function_to_instance_map[func].get(args[0])
                 if result is None:
                     result = func(*args)
-                    cls.decorator_function_to_instance_map[func].put(args[0], result)
+                    decorator_function_to_instance_map[func].put(args[0], result)
                 return result
 
             def cache_info() -> LFUCache[T, U]:
-                return cls.decorator_function_to_instance_map[func]
+                return decorator_function_to_instance_map[func]
 
             setattr(cache_decorator_wrapper, "cache_info", cache_info)  # noqa: B010
 
diff --git a/other/lru_cache.py b/other/lru_cache.py
index 1e5eeac45b4e..058b03b021bc 100644
--- a/other/lru_cache.py
+++ b/other/lru_cache.py
@@ -1,13 +1,13 @@
 from __future__ import annotations
 
 from collections.abc import Callable
-from typing import Generic, TypeVar
+from typing import TypeVar
 
 T = TypeVar("T")
 U = TypeVar("U")
 
 
-class DoubleLinkedListNode(Generic[T, U]):
+class DoubleLinkedListNode[T, U]:
     """
     Double Linked List Node built specifically for LRU Cache
 
@@ -28,7 +28,7 @@ def __repr__(self) -> str:
         )
 
 
-class DoubleLinkedList(Generic[T, U]):
+class DoubleLinkedList[T, U]:
     """
     Double Linked List built specifically for LRU Cache
 
@@ -143,7 +143,7 @@ def remove(
         return node
 
 
-class LRUCache(Generic[T, U]):
+class LRUCache[T, U]:
     """
     LRU Cache to store a given capacity of data. Can be used as a stand-alone object
     or as a function decorator.
@@ -209,9 +209,6 @@ class LRUCache(Generic[T, U]):
     CacheInfo(hits=194, misses=99, capacity=100, current size=99)
     """
 
-    # class variable to map the decorator functions to their respective instance
-    decorator_function_to_instance_map: dict[Callable[[T], U], LRUCache[T, U]] = {}
-
     def __init__(self, capacity: int):
         self.list: DoubleLinkedList[T, U] = DoubleLinkedList()
         self.capacity = capacity
@@ -308,18 +305,23 @@ def decorator(
         """
 
         def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]:
+            # variable to map the decorator functions to their respective instance
+            decorator_function_to_instance_map: dict[
+                Callable[[T], U], LRUCache[T, U]
+            ] = {}
+
             def cache_decorator_wrapper(*args: T) -> U:
-                if func not in cls.decorator_function_to_instance_map:
-                    cls.decorator_function_to_instance_map[func] = LRUCache(size)
+                if func not in decorator_function_to_instance_map:
+                    decorator_function_to_instance_map[func] = LRUCache(size)
 
-                result = cls.decorator_function_to_instance_map[func].get(args[0])
+                result = decorator_function_to_instance_map[func].get(args[0])
                 if result is None:
                     result = func(*args)
-                    cls.decorator_function_to_instance_map[func].put(args[0], result)
+                    decorator_function_to_instance_map[func].put(args[0], result)
                 return result
 
             def cache_info() -> LRUCache[T, U]:
-                return cls.decorator_function_to_instance_map[func]
+                return decorator_function_to_instance_map[func]
 
             setattr(cache_decorator_wrapper, "cache_info", cache_info)  # noqa: B010
 
diff --git a/other/majority_vote_algorithm.py b/other/majority_vote_algorithm.py
index ab8b386dd2e5..8d3b56707d06 100644
--- a/other/majority_vote_algorithm.py
+++ b/other/majority_vote_algorithm.py
@@ -4,6 +4,7 @@
 We have to solve in O(n) time and O(1) Space.
 URL : https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_majority_vote_algorithm
 """
+
 from collections import Counter
 
 
diff --git a/other/nested_brackets.py b/other/nested_brackets.py
index 19c6dd53c8b2..5760fa29b2fd 100644
--- a/other/nested_brackets.py
+++ b/other/nested_brackets.py
@@ -3,9 +3,9 @@
 brackets are properly nested.  A sequence of brackets s is considered properly nested
 if any of the following conditions are true:
 
-        - s is empty
-        - s has the form (U) or [U] or {U} where U is a properly nested string
-        - s has the form VW where V and W are properly nested strings
+    - s is empty
+    - s has the form (U) or [U] or {U} where U is a properly nested string
+    - s has the form VW where V and W are properly nested strings
 
 For example, the string "()()[()]" is properly nested but "[(()]" is not.
 
@@ -14,31 +14,60 @@
 """
 
 
-def is_balanced(s):
-    stack = []
-    open_brackets = set({"(", "[", "{"})
-    closed_brackets = set({")", "]", "}"})
+def is_balanced(s: str) -> bool:
+    """
+    >>> is_balanced("")
+    True
+    >>> is_balanced("()")
+    True
+    >>> is_balanced("[]")
+    True
+    >>> is_balanced("{}")
+    True
+    >>> is_balanced("()[]{}")
+    True
+    >>> is_balanced("(())")
+    True
+    >>> is_balanced("[[")
+    False
+    >>> is_balanced("([{}])")
+    True
+    >>> is_balanced("(()[)]")
+    False
+    >>> is_balanced("([)]")
+    False
+    >>> is_balanced("[[()]]")
+    True
+    >>> is_balanced("(()(()))")
+    True
+    >>> is_balanced("]")
+    False
+    >>> is_balanced("Life is a bowl of cherries.")
+    True
+    >>> is_balanced("Life is a bowl of che{}ies.")
+    True
+    >>> is_balanced("Life is a bowl of che}{ies.")
+    False
+    """
     open_to_closed = {"{": "}", "[": "]", "(": ")"}
-
-    for i in range(len(s)):
-        if s[i] in open_brackets:
-            stack.append(s[i])
-
-        elif s[i] in closed_brackets and (
-            len(stack) == 0 or (len(stack) > 0 and open_to_closed[stack.pop()] != s[i])
+    stack = []
+    for symbol in s:
+        if symbol in open_to_closed:
+            stack.append(symbol)
+        elif symbol in open_to_closed.values() and (
+            not stack or open_to_closed[stack.pop()] != symbol
         ):
             return False
-
-    return len(stack) == 0
+    return not stack  # stack should be empty
 
 
 def main():
     s = input("Enter sequence of brackets: ")
-    if is_balanced(s):
-        print(s, "is balanced")
-    else:
-        print(s, "is not balanced")
+    print(f"'{s}' is {'' if is_balanced(s) else 'not '}balanced.")
 
 
 if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
     main()
diff --git a/other/password.py b/other/password.py
index 1ce0d52316e6..dff1316c049c 100644
--- a/other/password.py
+++ b/other/password.py
@@ -51,18 +51,6 @@ def random(chars_incl: str, i: int) -> str:
     return "".join(secrets.choice(chars_incl) for _ in range(i))
 
 
-def random_number(chars_incl, i):
-    pass  # Put your code here...
-
-
-def random_letters(chars_incl, i):
-    pass  # Put your code here...
-
-
-def random_characters(chars_incl, i):
-    pass  # Put your code here...
-
-
 def is_strong_password(password: str, min_length: int = 8) -> bool:
     """
     This will check whether a given password is strong or not. The password must be at
diff --git a/other/quine.py b/other/quine.py
index 500a351d38dc..0fc78333fed1 100644
--- a/other/quine.py
+++ b/other/quine.py
@@ -1,5 +1,5 @@
 #!/bin/python3
-# ruff: noqa
+# ruff: noqa: PLC3002
 """
 Quine:
 
@@ -8,4 +8,5 @@
 
 More info on: https://en.wikipedia.org/wiki/Quine_(computing)
 """
+
 print((lambda quine: quine % quine)("print((lambda quine: quine %% quine)(%r))"))
diff --git a/other/scoring_algorithm.py b/other/scoring_algorithm.py
index af04f432e433..0185d7a2e0c0 100644
--- a/other/scoring_algorithm.py
+++ b/other/scoring_algorithm.py
@@ -1,25 +1,26 @@
 """
-developed by: markmelnic
-original repo: https://github.com/markmelnic/Scoring-Algorithm
+| developed by: markmelnic
+| original repo: https://github.com/markmelnic/Scoring-Algorithm
 
 Analyse data using a range based percentual proximity algorithm
 and calculate the linear maximum likelihood estimation.
 The basic principle is that all values supplied will be broken
-down to a range from 0 to 1 and each column's score will be added
+down to a range from ``0`` to ``1`` and each column's score will be added
 up to get the total score.
 
-==========
 Example for data of vehicles
-price|mileage|registration_year
-20k  |60k    |2012
-22k  |50k    |2011
-23k  |90k    |2015
-16k  |210k   |2010
+::
+
+    price|mileage|registration_year
+    20k  |60k    |2012
+    22k  |50k    |2011
+    23k  |90k    |2015
+    16k  |210k   |2010
 
 We want the vehicle with the lowest price,
 lowest mileage but newest registration year.
 Thus the weights for each column are as follows:
-[0, 0, 1]
+``[0, 0, 1]``
 """
 
 
@@ -97,10 +98,11 @@ def procentual_proximity(
     source_data: list[list[float]], weights: list[int]
 ) -> list[list[float]]:
     """
-    weights - int list
-    possible values - 0 / 1
-    0 if lower values have higher weight in the data set
-    1 if higher values have higher weight in the data set
+    | `weights` - ``int`` list
+    | possible values - ``0`` / ``1``
+
+        * ``0`` if lower values have higher weight in the data set
+        * ``1`` if higher values have higher weight in the data set
 
     >>> procentual_proximity([[20, 60, 2012],[23, 90, 2015],[22, 50, 2011]], [0, 0, 1])
     [[20, 60, 2012, 2.0], [23, 90, 2015, 1.0], [22, 50, 2011, 1.3333333333333335]]
diff --git a/other/sdes.py b/other/sdes.py
index 31105984b9bb..42186f453a3d 100644
--- a/other/sdes.py
+++ b/other/sdes.py
@@ -44,11 +44,11 @@ def function(expansion, s0, s1, key, message):
     right = message[4:]
     temp = apply_table(right, expansion)
     temp = xor(temp, key)
-    l = apply_sbox(s0, temp[:4])  # noqa: E741
-    r = apply_sbox(s1, temp[4:])
-    l = "0" * (2 - len(l)) + l  # noqa: E741
-    r = "0" * (2 - len(r)) + r
-    temp = apply_table(l + r, p4_table)
+    left_bin_str = apply_sbox(s0, temp[:4])
+    right_bin_str = apply_sbox(s1, temp[4:])
+    left_bin_str = "0" * (2 - len(left_bin_str)) + left_bin_str
+    right_bin_str = "0" * (2 - len(right_bin_str)) + right_bin_str
+    temp = apply_table(left_bin_str + right_bin_str, p4_table)
     temp = xor(left, temp)
     return temp + right
 
diff --git a/other/word_search.py b/other/word_search.py
index a4796e220c7c..9e8acadbd9a4 100644
--- a/other/word_search.py
+++ b/other/word_search.py
@@ -5,7 +5,6 @@
 @ https://en.wikipedia.org/wiki/Word_search
 """
 
-
 from random import choice, randint, shuffle
 
 # The words to display on the word search -
diff --git a/physics/archimedes_principle_of_buoyant_force.py b/physics/archimedes_principle_of_buoyant_force.py
index 5f569837220f..38f1a0a83832 100644
--- a/physics/archimedes_principle_of_buoyant_force.py
+++ b/physics/archimedes_principle_of_buoyant_force.py
@@ -3,12 +3,11 @@
 fluid.  This principle was discovered by the Greek mathematician Archimedes.
 
 Equation for calculating buoyant force:
-Fb = ρ * V * g
+Fb = p * V * g
 
 https://en.wikipedia.org/wiki/Archimedes%27_principle
 """
 
-
 # Acceleration Constant on Earth (unit m/s^2)
 g = 9.80665  # Also available in scipy.constants.g
 
diff --git a/physics/basic_orbital_capture.py b/physics/basic_orbital_capture.py
index eeb45e60240c..eb1fdd9d6420 100644
--- a/physics/basic_orbital_capture.py
+++ b/physics/basic_orbital_capture.py
@@ -1,22 +1,21 @@
-from math import pow, sqrt
-
-from scipy.constants import G, c, pi
-
 """
 These two functions will return the radii of impact for a target object
-of mass M and radius R as well as it's effective cross sectional area σ(sigma).
-That is to say any projectile with velocity v passing within σ, will impact the
+of mass M and radius R as well as it's effective cross sectional area sigma.
+That is to say any projectile with velocity v passing within sigma, will impact the
 target object with mass M. The derivation of which is given at the bottom
 of this file.
 
 The derivation shows that a projectile does not need to aim directly at the target
 body in order to hit it, as  R_capture>R_target. Astronomers refer to the effective
-cross section for capture as σ=π*R_capture**2.
+cross section for capture as sigma=π*R_capture**2.
 
 This algorithm does not account for an N-body problem.
-
 """
 
+from math import pow, sqrt  # noqa: A004
+
+from scipy.constants import G, c, pi
+
 
 def capture_radii(
     target_body_radius: float, target_body_mass: float, projectile_velocity: float
diff --git a/physics/center_of_mass.py b/physics/center_of_mass.py
new file mode 100644
index 000000000000..7a20e71be801
--- /dev/null
+++ b/physics/center_of_mass.py
@@ -0,0 +1,110 @@
+"""
+Calculating the center of mass for a discrete system of particles, given their
+positions and masses.
+
+Description:
+
+In physics, the center of mass of a distribution of mass in space (sometimes referred
+to as the barycenter or balance point) is the unique point at any given time where the
+weighted relative position of the distributed mass sums to zero. This is the point to
+which a force may be applied to cause a linear acceleration without an angular
+acceleration.
+
+Calculations in mechanics are often simplified when formulated with respect to the
+center of mass. It is a hypothetical point where the entire mass of an object may be
+assumed to be concentrated to visualize its motion. In other words, the center of mass
+is the particle equivalent of a given object for the application of Newton's laws of
+motion.
+
+In the case of a system of particles P_i, i = 1, ..., n , each with mass m_i that are
+located in space with coordinates r_i, i = 1, ..., n , the coordinates R of the center
+of mass corresponds to:
+
+R = (Σ(mi * ri) / Σ(mi))
+
+Reference: https://en.wikipedia.org/wiki/Center_of_mass
+"""
+
+from collections import namedtuple
+
+Particle = namedtuple("Particle", "x y z mass")  # noqa: PYI024
+Coord3D = namedtuple("Coord3D", "x y z")  # noqa: PYI024
+
+
+def center_of_mass(particles: list[Particle]) -> Coord3D:
+    """
+    Input Parameters
+    ----------------
+    particles: list(Particle):
+    A list of particles where each particle is a tuple with it's (x, y, z) position and
+    it's mass.
+
+    Returns
+    -------
+    Coord3D:
+    A tuple with the coordinates of the center of mass (Xcm, Ycm, Zcm) rounded to two
+    decimal places.
+
+    Examples
+    --------
+    >>> center_of_mass([
+    ...     Particle(1.5, 4, 3.4, 4),
+    ...     Particle(5, 6.8, 7, 8.1),
+    ...     Particle(9.4, 10.1, 11.6, 12)
+    ... ])
+    Coord3D(x=6.61, y=7.98, z=8.69)
+
+    >>> center_of_mass([
+    ...     Particle(1, 2, 3, 4),
+    ...     Particle(5, 6, 7, 8),
+    ...     Particle(9, 10, 11, 12)
+    ... ])
+    Coord3D(x=6.33, y=7.33, z=8.33)
+
+    >>> center_of_mass([
+    ...     Particle(1, 2, 3, -4),
+    ...     Particle(5, 6, 7, 8),
+    ...     Particle(9, 10, 11, 12)
+    ... ])
+    Traceback (most recent call last):
+        ...
+    ValueError: Mass of all particles must be greater than 0
+
+    >>> center_of_mass([
+    ...     Particle(1, 2, 3, 0),
+    ...     Particle(5, 6, 7, 8),
+    ...     Particle(9, 10, 11, 12)
+    ... ])
+    Traceback (most recent call last):
+        ...
+    ValueError: Mass of all particles must be greater than 0
+
+    >>> center_of_mass([])
+    Traceback (most recent call last):
+        ...
+    ValueError: No particles provided
+    """
+    if not particles:
+        raise ValueError("No particles provided")
+
+    if any(particle.mass <= 0 for particle in particles):
+        raise ValueError("Mass of all particles must be greater than 0")
+
+    total_mass = sum(particle.mass for particle in particles)
+
+    center_of_mass_x = round(
+        sum(particle.x * particle.mass for particle in particles) / total_mass, 2
+    )
+    center_of_mass_y = round(
+        sum(particle.y * particle.mass for particle in particles) / total_mass, 2
+    )
+    center_of_mass_z = round(
+        sum(particle.z * particle.mass for particle in particles) / total_mass, 2
+    )
+    return Coord3D(center_of_mass_x, center_of_mass_y, center_of_mass_z)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/physics/centripetal_force.py b/physics/centripetal_force.py
index 04069d256468..a4c624582475 100644
--- a/physics/centripetal_force.py
+++ b/physics/centripetal_force.py
@@ -6,7 +6,7 @@
 The unit of centripetal force is newton.
 
 The centripetal force is always directed perpendicular to the
-direction of the object’s displacement. Using Newton’s second
+direction of the object's displacement. Using Newton's second
 law of motion, it is found that the centripetal force of an object
 moving in a circular path always acts towards the centre of the circle.
 The Centripetal Force Formula is given as the product of mass (in kg)
diff --git a/physics/doppler_frequency.py b/physics/doppler_frequency.py
new file mode 100644
index 000000000000..2a761c72d9b8
--- /dev/null
+++ b/physics/doppler_frequency.py
@@ -0,0 +1,104 @@
+"""
+Doppler's effect
+
+The Doppler effect (also Doppler shift) is the change in the frequency of a wave in
+relation to an observer who is moving relative to the source of the wave.  The Doppler
+effect is named after the physicist Christian Doppler.  A common example of Doppler
+shift is the change of pitch heard when a vehicle sounding a horn approaches and
+recedes from an observer.
+
+The reason for the Doppler effect is that when the source of the waves is moving
+towards the observer, each successive wave crest is emitted from a position closer to
+the observer than the crest of the previous wave.  Therefore, each wave takes slightly
+less time to reach the observer than the previous wave. Hence, the time between the
+arrivals of successive wave crests at the observer is reduced, causing an increase in
+the frequency.  Similarly, if the source of waves is moving away from the observer,
+each wave is emitted from a position farther from the observer than the previous wave,
+so the arrival time between successive waves is increased, reducing the frequency.
+
+If the source of waves is stationary but the observer is moving with respect to the
+source, the transmission velocity of the waves changes (ie the rate at which the
+observer receives waves) even if the wavelength and frequency emitted from the source
+remain constant.
+
+These results are all summarized by the Doppler formula:
+
+    f = (f0 * (v + v0)) / (v - vs)
+
+where:
+    f: frequency of the wave
+    f0: frequency of the wave when the source is stationary
+    v: velocity of the wave in the medium
+    v0: velocity of the observer, positive if the observer is moving towards the source
+    vs: velocity of the source, positive if the source is moving towards the observer
+
+Doppler's effect has many applications in physics and engineering, such as radar,
+astronomy, medical imaging, and seismology.
+
+References:
+https://en.wikipedia.org/wiki/Doppler_effect
+
+Now, we will implement a function that calculates the frequency of a wave as a function
+of the frequency of the wave when the source is stationary, the velocity of the wave
+in the medium, the velocity of the observer and the velocity of the source.
+"""
+
+
+def doppler_effect(
+    org_freq: float, wave_vel: float, obs_vel: float, src_vel: float
+) -> float:
+    """
+    Input Parameters:
+    -----------------
+    org_freq: frequency of the wave when the source is stationary
+    wave_vel: velocity of the wave in the medium
+    obs_vel: velocity of the observer, +ve if the observer is moving towards the source
+    src_vel: velocity of the source, +ve if the source is moving towards the observer
+
+    Returns:
+    --------
+    f: frequency of the wave as perceived by the observer
+
+    Docstring Tests:
+    >>> doppler_effect(100, 330, 10, 0)  # observer moving towards the source
+    103.03030303030303
+    >>> doppler_effect(100, 330, -10, 0)  # observer moving away from the source
+    96.96969696969697
+    >>> doppler_effect(100, 330, 0, 10)  # source moving towards the observer
+    103.125
+    >>> doppler_effect(100, 330, 0, -10)  # source moving away from the observer
+    97.05882352941177
+    >>> doppler_effect(100, 330, 10, 10)  # source & observer moving towards each other
+    106.25
+    >>> doppler_effect(100, 330, -10, -10)  # source and observer moving away
+    94.11764705882354
+    >>> doppler_effect(100, 330, 10, 330)  # source moving at same speed as the wave
+    Traceback (most recent call last):
+        ...
+    ZeroDivisionError: Division by zero implies vs=v and observer in front of the source
+    >>> doppler_effect(100, 330, 10, 340)  # source moving faster than the wave
+    Traceback (most recent call last):
+        ...
+    ValueError: Non-positive frequency implies vs>v or v0>v (in the opposite direction)
+    >>> doppler_effect(100, 330, -340, 10)  # observer moving faster than the wave
+    Traceback (most recent call last):
+        ...
+    ValueError: Non-positive frequency implies vs>v or v0>v (in the opposite direction)
+    """
+
+    if wave_vel == src_vel:
+        raise ZeroDivisionError(
+            "Division by zero implies vs=v and observer in front of the source"
+        )
+    doppler_freq = (org_freq * (wave_vel + obs_vel)) / (wave_vel - src_vel)
+    if doppler_freq <= 0:
+        raise ValueError(
+            "Non-positive frequency implies vs>v or v0>v (in the opposite direction)"
+        )
+    return doppler_freq
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/physics/escape_velocity.py b/physics/escape_velocity.py
new file mode 100644
index 000000000000..e54ed5e50798
--- /dev/null
+++ b/physics/escape_velocity.py
@@ -0,0 +1,67 @@
+import math
+
+
+def escape_velocity(mass: float, radius: float) -> float:
+    """
+    Calculates the escape velocity needed to break free from a celestial body's
+    gravitational field.
+
+    The formula used is:
+        v = sqrt(2 * G * M / R)
+
+    where:
+        v = escape velocity (m/s)
+        G = gravitational constant (6.67430 * 10^-11 m^3 kg^-1 s^-2)
+        M = mass of the celestial body (kg)
+        R = radius from the center of mass (m)
+
+    Source:
+        https://en.wikipedia.org/wiki/Escape_velocity
+
+    Args:
+        mass (float): Mass of the celestial body in kilograms.
+        radius (float): Radius from the center of mass in meters.
+
+    Returns:
+        float: Escape velocity in meters per second, rounded to 3 decimal places.
+
+    Examples:
+        >>> escape_velocity(mass=5.972e24, radius=6.371e6)  # Earth
+        11185.978
+        >>> escape_velocity(mass=7.348e22, radius=1.737e6)  # Moon
+        2376.307
+        >>> escape_velocity(mass=1.898e27, radius=6.9911e7)  # Jupiter
+        60199.545
+        >>> escape_velocity(mass=0, radius=1.0)
+        0.0
+        >>> escape_velocity(mass=1.0, radius=0)
+        Traceback (most recent call last):
+            ...
+        ZeroDivisionError: Radius cannot be zero.
+    """
+    gravitational_constant = 6.67430e-11  # m^3 kg^-1 s^-2
+
+    if radius == 0:
+        raise ZeroDivisionError("Radius cannot be zero.")
+
+    velocity = math.sqrt(2 * gravitational_constant * mass / radius)
+    return round(velocity, 3)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+    print("Calculate escape velocity of a celestial body...\n")
+
+    try:
+        mass = float(input("Enter mass of the celestial body (in kgs): ").strip())
+        radius = float(input("Enter radius from the center of mass (in ms): ").strip())
+
+        velocity = escape_velocity(mass=mass, radius=radius)
+        print(f"Escape velocity is {velocity} m/s")
+
+    except ValueError:
+        print("Invalid input. Please enter valid numeric values.")
+    except ZeroDivisionError as e:
+        print(e)
diff --git a/physics/grahams_law.py b/physics/grahams_law.py
index 6e5d75127e83..c56359280ea4 100644
--- a/physics/grahams_law.py
+++ b/physics/grahams_law.py
@@ -14,7 +14,7 @@
 (Description adapted from https://en.wikipedia.org/wiki/Graham%27s_law)
 """
 
-from math import pow, sqrt
+from math import pow, sqrt  # noqa: A004
 
 
 def validate(*values: float) -> bool:
diff --git a/physics/horizontal_projectile_motion.py b/physics/horizontal_projectile_motion.py
index 80f85a1b7146..32dc5eb36f35 100644
--- a/physics/horizontal_projectile_motion.py
+++ b/physics/horizontal_projectile_motion.py
@@ -1,20 +1,23 @@
 """
 Horizontal Projectile Motion problem in physics.
+
 This algorithm solves a specific problem in which
-the motion starts from the ground as can be seen below:
-      (v = 0)
-               *  *
-           *          *
-        *                *
-      *                    *
-    *                        *
-   *                          *
-GROUND                      GROUND
+the motion starts from the ground as can be seen below::
+
+          (v = 0)
+                   *  *
+               *          *
+            *                *
+          *                    *
+        *                        *
+       *                          *
+    GROUND                      GROUND
+
 For more info: https://en.wikipedia.org/wiki/Projectile_motion
 """
 
 # Importing packages
-from math import radians as angle_to_radians
+from math import radians as deg_to_rad
 from math import sin
 
 # Acceleration Constant on Earth (unit m/s^2)
@@ -28,10 +31,10 @@ def check_args(init_velocity: float, angle: float) -> None:
 
     # Ensure valid instance
     if not isinstance(init_velocity, (int, float)):
-        raise TypeError("Invalid velocity. Should be a positive number.")
+        raise TypeError("Invalid velocity. Should be an integer or float.")
 
     if not isinstance(angle, (int, float)):
-        raise TypeError("Invalid angle. Range is 1-90 degrees.")
+        raise TypeError("Invalid angle. Should be an integer or float.")
 
     # Ensure valid angle
     if angle > 90 or angle < 1:
@@ -43,14 +46,17 @@ def check_args(init_velocity: float, angle: float) -> None:
 
 
 def horizontal_distance(init_velocity: float, angle: float) -> float:
-    """
+    r"""
     Returns the horizontal distance that the object cover
+
     Formula:
-            v_0^2 * sin(2 * alpha)
-            ---------------------
-                   g
-    v_0 - initial velocity
-    alpha - angle
+        .. math::
+            \frac{v_0^2 \cdot \sin(2 \alpha)}{g}
+
+            v_0 - \text{initial velocity}
+
+            \alpha - \text{angle}
+
     >>> horizontal_distance(30, 45)
     91.77
     >>> horizontal_distance(100, 78)
@@ -65,19 +71,22 @@ def horizontal_distance(init_velocity: float, angle: float) -> float:
     ValueError: Invalid angle. Range is 1-90 degrees.
     """
     check_args(init_velocity, angle)
-    radians = angle_to_radians(2 * angle)
+    radians = deg_to_rad(2 * angle)
     return round(init_velocity**2 * sin(radians) / g, 2)
 
 
 def max_height(init_velocity: float, angle: float) -> float:
-    """
+    r"""
     Returns the maximum height that the object reach
+
     Formula:
-            v_0^2 * sin^2(alpha)
-            --------------------
-                   2g
-    v_0 - initial velocity
-    alpha - angle
+        .. math::
+            \frac{v_0^2 \cdot \sin^2 (\alpha)}{2 g}
+
+            v_0 - \text{initial velocity}
+
+            \alpha - \text{angle}
+
     >>> max_height(30, 45)
     22.94
     >>> max_height(100, 78)
@@ -85,26 +94,29 @@ def max_height(init_velocity: float, angle: float) -> float:
     >>> max_height("a", 20)
     Traceback (most recent call last):
         ...
-    TypeError: Invalid velocity. Should be a positive number.
+    TypeError: Invalid velocity. Should be an integer or float.
     >>> horizontal_distance(30, "b")
     Traceback (most recent call last):
         ...
-    TypeError: Invalid angle. Range is 1-90 degrees.
+    TypeError: Invalid angle. Should be an integer or float.
     """
     check_args(init_velocity, angle)
-    radians = angle_to_radians(angle)
+    radians = deg_to_rad(angle)
     return round(init_velocity**2 * sin(radians) ** 2 / (2 * g), 2)
 
 
 def total_time(init_velocity: float, angle: float) -> float:
-    """
+    r"""
     Returns total time of the motion
+
     Formula:
-            2 * v_0 * sin(alpha)
-            --------------------
-                   g
-    v_0 - initial velocity
-    alpha - angle
+        .. math::
+            \frac{2 v_0 \cdot \sin (\alpha)}{g}
+
+            v_0 - \text{initial velocity}
+
+            \alpha - \text{angle}
+
     >>> total_time(30, 45)
     4.33
     >>> total_time(100, 78)
@@ -116,15 +128,17 @@ def total_time(init_velocity: float, angle: float) -> float:
     >>> total_time(30, "b")
     Traceback (most recent call last):
         ...
-    TypeError: Invalid angle. Range is 1-90 degrees.
+    TypeError: Invalid angle. Should be an integer or float.
     """
     check_args(init_velocity, angle)
-    radians = angle_to_radians(angle)
+    radians = deg_to_rad(angle)
     return round(2 * init_velocity * sin(radians) / g, 2)
 
 
 def test_motion() -> None:
     """
+    Test motion
+
     >>> test_motion()
     """
     v0, angle = 25, 20
diff --git a/arithmetic_analysis/image_data/2D_problems.jpg b/physics/image_data/2D_problems.jpg
similarity index 100%
rename from arithmetic_analysis/image_data/2D_problems.jpg
rename to physics/image_data/2D_problems.jpg
diff --git a/arithmetic_analysis/image_data/2D_problems_1.jpg b/physics/image_data/2D_problems_1.jpg
similarity index 100%
rename from arithmetic_analysis/image_data/2D_problems_1.jpg
rename to physics/image_data/2D_problems_1.jpg
diff --git a/physics/image_data/__init__.py b/physics/image_data/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/arithmetic_analysis/in_static_equilibrium.py b/physics/in_static_equilibrium.py
similarity index 95%
rename from arithmetic_analysis/in_static_equilibrium.py
rename to physics/in_static_equilibrium.py
index 7aaecf174a5e..fb5a9b5fff66 100644
--- a/arithmetic_analysis/in_static_equilibrium.py
+++ b/physics/in_static_equilibrium.py
@@ -1,94 +1,95 @@
-"""
-Checks if a system of forces is in static equilibrium.
-"""
-from __future__ import annotations
-
-from numpy import array, cos, cross, float64, radians, sin
-from numpy.typing import NDArray
-
-
-def polar_force(
-    magnitude: float, angle: float, radian_mode: bool = False
-) -> list[float]:
-    """
-    Resolves force along rectangular components.
-    (force, angle) => (force_x, force_y)
-    >>> import math
-    >>> force = polar_force(10, 45)
-    >>> math.isclose(force[0], 7.071067811865477)
-    True
-    >>> math.isclose(force[1], 7.0710678118654755)
-    True
-    >>> force = polar_force(10, 3.14, radian_mode=True)
-    >>> math.isclose(force[0], -9.999987317275396)
-    True
-    >>> math.isclose(force[1], 0.01592652916486828)
-    True
-    """
-    if radian_mode:
-        return [magnitude * cos(angle), magnitude * sin(angle)]
-    return [magnitude * cos(radians(angle)), magnitude * sin(radians(angle))]
-
-
-def in_static_equilibrium(
-    forces: NDArray[float64], location: NDArray[float64], eps: float = 10**-1
-) -> bool:
-    """
-    Check if a system is in equilibrium.
-    It takes two numpy.array objects.
-    forces ==>  [
-                        [force1_x, force1_y],
-                        [force2_x, force2_y],
-                        ....]
-    location ==>  [
-                        [x1, y1],
-                        [x2, y2],
-                        ....]
-    >>> force = array([[1, 1], [-1, 2]])
-    >>> location = array([[1, 0], [10, 0]])
-    >>> in_static_equilibrium(force, location)
-    False
-    """
-    # summation of moments is zero
-    moments: NDArray[float64] = cross(location, forces)
-    sum_moments: float = sum(moments)
-    return abs(sum_moments) < eps
-
-
-if __name__ == "__main__":
-    # Test to check if it works
-    forces = array(
-        [
-            polar_force(718.4, 180 - 30),
-            polar_force(879.54, 45),
-            polar_force(100, -90),
-        ]
-    )
-
-    location: NDArray[float64] = array([[0, 0], [0, 0], [0, 0]])
-
-    assert in_static_equilibrium(forces, location)
-
-    # Problem 1 in image_data/2D_problems.jpg
-    forces = array(
-        [
-            polar_force(30 * 9.81, 15),
-            polar_force(215, 180 - 45),
-            polar_force(264, 90 - 30),
-        ]
-    )
-
-    location = array([[0, 0], [0, 0], [0, 0]])
-
-    assert in_static_equilibrium(forces, location)
-
-    # Problem in image_data/2D_problems_1.jpg
-    forces = array([[0, -2000], [0, -1200], [0, 15600], [0, -12400]])
-
-    location = array([[0, 0], [6, 0], [10, 0], [12, 0]])
-
-    assert in_static_equilibrium(forces, location)
-
-    import doctest
-
-    doctest.testmod()
+"""
+Checks if a system of forces is in static equilibrium.
+"""
+
+from __future__ import annotations
+
+from numpy import array, cos, cross, float64, radians, sin
+from numpy.typing import NDArray
+
+
+def polar_force(
+    magnitude: float, angle: float, radian_mode: bool = False
+) -> list[float]:
+    """
+    Resolves force along rectangular components.
+    (force, angle) => (force_x, force_y)
+    >>> import math
+    >>> force = polar_force(10, 45)
+    >>> math.isclose(force[0], 7.071067811865477)
+    True
+    >>> math.isclose(force[1], 7.0710678118654755)
+    True
+    >>> force = polar_force(10, 3.14, radian_mode=True)
+    >>> math.isclose(force[0], -9.999987317275396)
+    True
+    >>> math.isclose(force[1], 0.01592652916486828)
+    True
+    """
+    if radian_mode:
+        return [magnitude * cos(angle), magnitude * sin(angle)]
+    return [magnitude * cos(radians(angle)), magnitude * sin(radians(angle))]
+
+
+def in_static_equilibrium(
+    forces: NDArray[float64], location: NDArray[float64], eps: float = 10**-1
+) -> bool:
+    """
+    Check if a system is in equilibrium.
+    It takes two numpy.array objects.
+    forces ==>  [
+                        [force1_x, force1_y],
+                        [force2_x, force2_y],
+                        ....]
+    location ==>  [
+                        [x1, y1],
+                        [x2, y2],
+                        ....]
+    >>> force = array([[1, 1], [-1, 2]])
+    >>> location = array([[1, 0], [10, 0]])
+    >>> in_static_equilibrium(force, location)
+    False
+    """
+    # summation of moments is zero
+    moments: NDArray[float64] = cross(location, forces)
+    sum_moments: float = sum(moments)
+    return bool(abs(sum_moments) < eps)
+
+
+if __name__ == "__main__":
+    # Test to check if it works
+    forces = array(
+        [
+            polar_force(718.4, 180 - 30),
+            polar_force(879.54, 45),
+            polar_force(100, -90),
+        ]
+    )
+
+    location: NDArray[float64] = array([[0, 0], [0, 0], [0, 0]])
+
+    assert in_static_equilibrium(forces, location)
+
+    # Problem 1 in image_data/2D_problems.jpg
+    forces = array(
+        [
+            polar_force(30 * 9.81, 15),
+            polar_force(215, 180 - 45),
+            polar_force(264, 90 - 30),
+        ]
+    )
+
+    location = array([[0, 0], [0, 0], [0, 0]])
+
+    assert in_static_equilibrium(forces, location)
+
+    # Problem in image_data/2D_problems_1.jpg
+    forces = array([[0, -2000], [0, -1200], [0, 15600], [0, -12400]])
+
+    location = array([[0, 0], [6, 0], [10, 0], [12, 0]])
+
+    assert in_static_equilibrium(forces, location)
+
+    import doctest
+
+    doctest.testmod()
diff --git a/physics/lens_formulae.py b/physics/lens_formulae.py
new file mode 100644
index 000000000000..162f3a8f3b29
--- /dev/null
+++ b/physics/lens_formulae.py
@@ -0,0 +1,131 @@
+"""
+This module has functions which calculate focal length of lens, distance of
+image from the lens and distance of object from the lens.
+The above is calculated using the lens formula.
+
+In optics, the relationship between the distance of the image (v),
+the distance of the object (u), and
+the focal length (f) of the lens is given by the formula known as the Lens formula.
+The Lens formula is applicable for convex as well as concave lenses. The formula
+is given as follows:
+
+-------------------
+| 1/f = 1/v + 1/u |
+-------------------
+
+Where
+    f = focal length of the lens in meters.
+    v = distance of the image from the lens in meters.
+    u = distance of the object from the lens in meters.
+
+To make our calculations easy few assumptions are made while deriving the formula
+which are important to keep in mind before solving this equation.
+The assumptions are as follows:
+    1. The object O is a point object lying somewhere on the principle axis.
+    2. The lens is thin.
+    3. The aperture of the lens taken must be small.
+    4. The angles of incidence and angle of refraction should be small.
+
+Sign convention is a set of rules to set signs for image distance, object distance,
+focal length, etc
+for mathematical analysis of image formation. According to it:
+    1. Object is always placed to the left of lens.
+    2. All distances are measured from the optical centre of the mirror.
+    3. Distances measured in the direction of the incident ray are positive and
+    the distances measured in the direction opposite
+    to that of the incident rays are negative.
+    4. Distances measured along y-axis above the principal axis are positive and
+    that measured along y-axis below the principal
+    axis are negative.
+
+Note: Sign convention can be reversed and will still give the correct results.
+
+Reference for Sign convention:
+https://www.toppr.com/ask/content/concept/sign-convention-for-lenses-210246/
+
+Reference for assumptions:
+https://testbook.com/physics/derivation-of-lens-maker-formula
+"""
+
+
+def focal_length_of_lens(
+    object_distance_from_lens: float, image_distance_from_lens: float
+) -> float:
+    """
+    Doctests:
+    >>> from math import isclose
+    >>> isclose(focal_length_of_lens(10,4), 6.666666666666667)
+    True
+    >>> from math import isclose
+    >>> isclose(focal_length_of_lens(2.7,5.8), -5.0516129032258075)
+    True
+    >>> focal_length_of_lens(0, 20)  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid inputs. Enter non zero values with respect
+    to the sign convention.
+    """
+
+    if object_distance_from_lens == 0 or image_distance_from_lens == 0:
+        raise ValueError(
+            "Invalid inputs. Enter non zero values with respect to the sign convention."
+        )
+    focal_length = 1 / (
+        (1 / image_distance_from_lens) - (1 / object_distance_from_lens)
+    )
+    return focal_length
+
+
+def object_distance(
+    focal_length_of_lens: float, image_distance_from_lens: float
+) -> float:
+    """
+    Doctests:
+    >>> from math import isclose
+    >>> isclose(object_distance(10,40), -13.333333333333332)
+    True
+
+    >>> from math import isclose
+    >>> isclose(object_distance(6.2,1.5), 1.9787234042553192)
+    True
+
+    >>> object_distance(0, 20)  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid inputs. Enter non zero values with respect
+    to the sign convention.
+    """
+
+    if image_distance_from_lens == 0 or focal_length_of_lens == 0:
+        raise ValueError(
+            "Invalid inputs. Enter non zero values with respect to the sign convention."
+        )
+
+    object_distance = 1 / ((1 / image_distance_from_lens) - (1 / focal_length_of_lens))
+    return object_distance
+
+
+def image_distance(
+    focal_length_of_lens: float, object_distance_from_lens: float
+) -> float:
+    """
+    Doctests:
+    >>> from math import isclose
+    >>> isclose(image_distance(50,40), 22.22222222222222)
+    True
+    >>> from math import isclose
+    >>> isclose(image_distance(5.3,7.9), 3.1719696969696973)
+    True
+
+    >>> object_distance(0, 20)  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+        ...
+    ValueError: Invalid inputs. Enter non zero values with respect
+    to the sign convention.
+    """
+    if object_distance_from_lens == 0 or focal_length_of_lens == 0:
+        raise ValueError(
+            "Invalid inputs. Enter non zero values with respect to the sign convention."
+        )
+    image_distance = 1 / ((1 / object_distance_from_lens) + (1 / focal_length_of_lens))
+    return image_distance
diff --git a/physics/lorentz_transformation_four_vector.py b/physics/lorentz_transformation_four_vector.py
index f4fda4dff8cd..3b0fd83d45df 100644
--- a/physics/lorentz_transformation_four_vector.py
+++ b/physics/lorentz_transformation_four_vector.py
@@ -12,13 +12,13 @@
 with respect to X, then the Lorentz transformation from X to X' is X' = BX,
 where
 
-    | γ  -γβ  0  0|
-B = |-γβ  γ   0  0|
+    | y  -γβ  0  0|
+B = |-γβ  y   0  0|
     | 0   0   1  0|
     | 0   0   0  1|
 
 is the matrix describing the Lorentz boost between X and X',
-γ = 1 / √(1 - v²/c²) is the Lorentz factor, and β = v/c is the velocity as
+y = 1 / √(1 - v²/c²) is the Lorentz factor, and β = v/c is the velocity as
 a fraction of c.
 
 Reference: https://en.wikipedia.org/wiki/Lorentz_transformation
@@ -63,7 +63,7 @@ def beta(velocity: float) -> float:
 
 def gamma(velocity: float) -> float:
     """
-    Calculate the Lorentz factor γ = 1 / √(1 - v²/c²) for a given velocity
+    Calculate the Lorentz factor y = 1 / √(1 - v²/c²) for a given velocity
     >>> gamma(4)
     1.0000000000000002
     >>> gamma(1e5)
@@ -90,12 +90,12 @@ def transformation_matrix(velocity: float) -> np.ndarray:
     """
     Calculate the Lorentz transformation matrix for movement in the x direction:
 
-    | γ  -γβ  0  0|
-    |-γβ  γ   0  0|
+    | y  -γβ  0  0|
+    |-γβ  y   0  0|
     | 0   0   1  0|
     | 0   0   0  1|
 
-    where γ is the Lorentz factor and β is the velocity as a fraction of c
+    where y is the Lorentz factor and β is the velocity as a fraction of c
     >>> transformation_matrix(29979245)
     array([[ 1.00503781, -0.10050378,  0.        ,  0.        ],
            [-0.10050378,  1.00503781,  0.        ,  0.        ],
diff --git a/physics/malus_law.py b/physics/malus_law.py
index ae77d45cf614..374b3423f8ff 100644
--- a/physics/malus_law.py
+++ b/physics/malus_law.py
@@ -31,7 +31,7 @@
 Real polarizers are also not perfect blockers of the polarization orthogonal to
 their polarization axis; the ratio of the transmission of the unwanted component
 to the wanted component is called the extinction ratio, and varies from around
-1:500 for Polaroid to about 1:106 for Glan–Taylor prism polarizers.
+1:500 for Polaroid to about 1:106 for Glan-Taylor prism polarizers.
 
 Reference : "https://en.wikipedia.org/wiki/Polarizer#Malus's_law_and_other_properties"
 """
diff --git a/physics/mass_energy_equivalence.py b/physics/mass_energy_equivalence.py
new file mode 100644
index 000000000000..4a4c7890f4e0
--- /dev/null
+++ b/physics/mass_energy_equivalence.py
@@ -0,0 +1,77 @@
+"""
+Title:
+Finding the energy equivalence of mass and mass equivalence of energy
+by Einstein's equation.
+
+Description:
+Einstein's mass-energy equivalence is a pivotal concept in theoretical physics.
+It asserts that energy (E) and mass (m) are directly related by the speed of
+light in vacuum (c) squared, as described in the equation E = mc². This means that
+mass and energy are interchangeable; a mass increase corresponds to an energy increase,
+and vice versa. This principle has profound implications in nuclear reactions,
+explaining the release of immense energy from minuscule changes in atomic nuclei.
+
+Equations:
+E = mc² and m = E/c², where m is mass, E is Energy, c is speed of light in vacuum.
+
+Reference:
+https://en.wikipedia.org/wiki/Mass%E2%80%93energy_equivalence
+"""
+
+from scipy.constants import c  # speed of light in vacuum (299792458 m/s)
+
+
+def energy_from_mass(mass: float) -> float:
+    """
+    Calculates the Energy equivalence of the Mass using E = mc²
+    in SI units J from Mass in kg.
+
+    mass (float): Mass of body.
+
+    Usage example:
+    >>> energy_from_mass(124.56)
+    1.11948945063458e+19
+    >>> energy_from_mass(320)
+    2.8760165719578165e+19
+    >>> energy_from_mass(0)
+    0.0
+    >>> energy_from_mass(-967.9)
+    Traceback (most recent call last):
+        ...
+    ValueError: Mass can't be negative.
+
+    """
+    if mass < 0:
+        raise ValueError("Mass can't be negative.")
+    return mass * c**2
+
+
+def mass_from_energy(energy: float) -> float:
+    """
+    Calculates the Mass equivalence of the Energy using m = E/c²
+    in SI units kg from Energy in J.
+
+    energy (float): Mass of body.
+
+    Usage example:
+    >>> mass_from_energy(124.56)
+    1.3859169098203872e-15
+    >>> mass_from_energy(320)
+    3.560480179371579e-15
+    >>> mass_from_energy(0)
+    0.0
+    >>> mass_from_energy(-967.9)
+    Traceback (most recent call last):
+        ...
+    ValueError: Energy can't be negative.
+
+    """
+    if energy < 0:
+        raise ValueError("Energy can't be negative.")
+    return energy / c**2
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/physics/n_body_simulation.py b/physics/n_body_simulation.py
index 46330844df61..9bfb6b3c6864 100644
--- a/physics/n_body_simulation.py
+++ b/physics/n_body_simulation.py
@@ -11,7 +11,6 @@
 (See also http://www.shodor.org/refdesk/Resources/Algorithms/EulersMethod/ )
 """
 
-
 from __future__ import annotations
 
 import random
@@ -165,9 +164,7 @@ def update_system(self, delta_time: float) -> None:
 
                     # Calculation of the distance using Pythagoras's theorem
                     # Extra factor due to the softening technique
-                    distance = (dif_x**2 + dif_y**2 + self.softening_factor) ** (
-                        1 / 2
-                    )
+                    distance = (dif_x**2 + dif_y**2 + self.softening_factor) ** (1 / 2)
 
                     # Newton's law of universal gravitation.
                     force_x += (
@@ -242,7 +239,7 @@ def plot(
         ax.add_patch(patch)
 
     # Function called at each step of the animation
-    def update(frame: int) -> list[plt.Circle]:
+    def update(frame: int) -> list[plt.Circle]:  # noqa: ARG001
         update_step(body_system, DELTA_TIME, patches)
         return patches
 
diff --git a/physics/newtons_second_law_of_motion.py b/physics/newtons_second_law_of_motion.py
index 53fab6ce78b9..4149e2494f31 100644
--- a/physics/newtons_second_law_of_motion.py
+++ b/physics/newtons_second_law_of_motion.py
@@ -1,18 +1,22 @@
-"""
-Description :
-Newton's second law of motion pertains to the behavior of objects for which
-all existing forces are not balanced.
-The second law states that the acceleration of an object is dependent upon two variables
-- the net force acting upon the object and the mass of the object.
-The acceleration of an object depends directly
-upon the net force acting upon the object,
-and inversely upon the mass of the object.
-As the force acting upon an object is increased,
-the acceleration of the object is increased.
-As the mass of an object is increased, the acceleration of the object is decreased.
+r"""
+Description:
+    Newton's second law of motion pertains to the behavior of objects for which
+    all existing forces are not balanced.
+    The second law states that the acceleration of an object is dependent upon
+    two variables - the net force acting upon the object and the mass of the object.
+    The acceleration of an object depends directly
+    upon the net force acting upon the object,
+    and inversely upon the mass of the object.
+    As the force acting upon an object is increased,
+    the acceleration of the object is increased.
+    As the mass of an object is increased, the acceleration of the object is decreased.
+
 Source: https://www.physicsclassroom.com/class/newtlaws/Lesson-3/Newton-s-Second-Law
-Formulation: Fnet = m • a
-Diagrammatic Explanation:
+
+Formulation: F_net = m • a
+
+Diagrammatic Explanation::
+
               Forces are unbalanced
                         |
                         |
@@ -26,35 +30,42 @@
                     /        \
                    /          \
                   /            \
-        __________________   ____ ________________
-        |The acceleration |  |The acceleration   |
-        |depends directly |  |depends inversely  |
-        |on the net Force |  |upon the object's  |
-        |_________________|  |mass_______________|
-Units:
-1 Newton = 1 kg X meters / (seconds^2)
+    __________________      ____________________
+   | The acceleration |    | The acceleration   |
+   | depends directly |    | depends inversely  |
+   | on the net force |    | upon the object's  |
+   |                  |    | mass               |
+   |__________________|    |____________________|
+
+Units: 1 Newton = 1 kg • meters/seconds^2
+
 How to use?
-Inputs:
-    ___________________________________________________
-   |Name         | Units                   | Type      |
-   |-------------|-------------------------|-----------|
-   |mass         | (in kgs)                | float     |
-   |-------------|-------------------------|-----------|
-   |acceleration | (in meters/(seconds^2)) | float     |
-   |_____________|_________________________|___________|
-
-Output:
-    ___________________________________________________
-   |Name         | Units                   | Type      |
-   |-------------|-------------------------|-----------|
-   |force        | (in Newtons)            | float     |
-   |_____________|_________________________|___________|
+
+Inputs::
+
+    ______________ _____________________ ___________
+   | Name         | Units               | Type      |
+   |--------------|---------------------|-----------|
+   | mass         | in kgs              | float     |
+   |--------------|---------------------|-----------|
+   | acceleration | in meters/seconds^2 | float     |
+   |______________|_____________________|___________|
+
+Output::
+
+    ______________ _______________________ ___________
+   | Name         | Units                 | Type      |
+   |--------------|-----------------------|-----------|
+   | force        | in Newtons            | float     |
+   |______________|_______________________|___________|
 
 """
 
 
 def newtons_second_law_of_motion(mass: float, acceleration: float) -> float:
     """
+    Calculates force from `mass` and `acceleration`
+
     >>> newtons_second_law_of_motion(10, 10)
     100
     >>> newtons_second_law_of_motion(2.0, 1)
diff --git a/physics/orbital_transfer_work.py b/physics/orbital_transfer_work.py
new file mode 100644
index 000000000000..7fb90fe9be0e
--- /dev/null
+++ b/physics/orbital_transfer_work.py
@@ -0,0 +1,73 @@
+def orbital_transfer_work(
+    mass_central: float, mass_object: float, r_initial: float, r_final: float
+) -> str:
+    """
+    Calculates the work required to move an object from one orbit to another in a
+    gravitational field based on the change in total mechanical energy.
+
+    The formula used is:
+        W = (G * M * m / 2) * (1/r_initial - 1/r_final)
+
+    where:
+        W = work done (Joules)
+        G = gravitational constant (6.67430 * 10^-11 m^3 kg^-1 s^-2)
+        M = mass of the central body (kg)
+        m = mass of the orbiting object (kg)
+        r_initial = initial orbit radius (m)
+        r_final = final orbit radius (m)
+
+    Args:
+        mass_central (float): Mass of the central body (kg)
+        mass_object (float): Mass of the object being moved (kg)
+        r_initial (float): Initial orbital radius (m)
+        r_final (float): Final orbital radius (m)
+
+    Returns:
+        str: Work done in Joules as a string in scientific notation (3 decimals)
+
+    Examples:
+        >>> orbital_transfer_work(5.972e24, 1000, 6.371e6, 7e6)
+        '2.811e+09'
+        >>> orbital_transfer_work(5.972e24, 500, 7e6, 6.371e6)
+        '-1.405e+09'
+        >>> orbital_transfer_work(1.989e30, 1000, 1.5e11, 2.28e11)
+        '1.514e+11'
+    """
+    gravitational_constant = 6.67430e-11
+
+    if r_initial <= 0 or r_final <= 0:
+        raise ValueError("Orbital radii must be greater than zero.")
+
+    work = (gravitational_constant * mass_central * mass_object / 2) * (
+        1 / r_initial - 1 / r_final
+    )
+    return f"{work:.3e}"
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+    print("Orbital transfer work calculator\n")
+
+    try:
+        M = float(input("Enter mass of central body (kg): ").strip())
+        if M <= 0:
+            r1 = float(input("Enter initial orbit radius (m): ").strip())
+        if r1 <= 0:
+            raise ValueError("Initial orbit radius must be greater than zero.")
+
+        r2 = float(input("Enter final orbit radius (m): ").strip())
+        if r2 <= 0:
+            raise ValueError("Final orbit radius must be greater than zero.")
+        m = float(input("Enter mass of orbiting object (kg): ").strip())
+        if m <= 0:
+            raise ValueError("Mass of the orbiting object must be greater than zero.")
+        r1 = float(input("Enter initial orbit radius (m): ").strip())
+        r2 = float(input("Enter final orbit radius (m): ").strip())
+
+        result = orbital_transfer_work(M, m, r1, r2)
+        print(f"Work done in orbital transfer: {result} Joules")
+
+    except ValueError as e:
+        print(f"Input error: {e}")
diff --git a/physics/period_of_pendulum.py b/physics/period_of_pendulum.py
new file mode 100644
index 000000000000..2e3c7bc3ef1e
--- /dev/null
+++ b/physics/period_of_pendulum.py
@@ -0,0 +1,53 @@
+"""
+Title : Computing the time period of a simple pendulum
+
+The simple pendulum is a mechanical system that sways or moves in an
+oscillatory motion. The simple pendulum comprises of a small bob of
+mass m suspended by a thin string of length L and secured to a platform
+at its upper end. Its motion occurs in a vertical plane and is mainly
+driven by gravitational force. The period of the pendulum depends on the
+length of the string and the amplitude (the maximum angle) of oscillation.
+However, the effect of the amplitude can be ignored if the amplitude is
+small. It should be noted that the period does not depend on the mass of
+the bob.
+
+For small amplitudes, the period of a simple pendulum is given by the
+following approximation:
+T ≈ 2π * √(L / g)
+
+where:
+L = length of string from which the bob is hanging (in m)
+g = acceleration due to gravity (approx 9.8 m/s²)
+
+Reference : https://byjus.com/jee/simple-pendulum/
+"""
+
+from math import pi
+
+from scipy.constants import g
+
+
+def period_of_pendulum(length: float) -> float:
+    """
+    >>> period_of_pendulum(1.23)
+    2.2252155506257845
+    >>> period_of_pendulum(2.37)
+    3.0888278441908574
+    >>> period_of_pendulum(5.63)
+    4.76073193364765
+    >>> period_of_pendulum(-12)
+    Traceback (most recent call last):
+        ...
+    ValueError: The length should be non-negative
+    >>> period_of_pendulum(0)
+    0.0
+    """
+    if length < 0:
+        raise ValueError("The length should be non-negative")
+    return 2 * pi * (length / g) ** 0.5
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/physics/rainfall_intensity.py b/physics/rainfall_intensity.py
new file mode 100644
index 000000000000..cee8d50ddc2f
--- /dev/null
+++ b/physics/rainfall_intensity.py
@@ -0,0 +1,143 @@
+"""
+Rainfall Intensity
+==================
+This module contains functions to calculate the intensity of
+a rainfall event for a given duration and return period.
+
+This function uses the Sherman intensity-duration-frequency curve.
+
+References
+----------
+- Aparicio, F. (1997): Fundamentos de Hidrología de Superficie.
+    Balderas, México, Limusa. 303 p.
+- https://en.wikipedia.org/wiki/Intensity-duration-frequency_curve
+"""
+
+
+def rainfall_intensity(
+    coefficient_k: float,
+    coefficient_a: float,
+    coefficient_b: float,
+    coefficient_c: float,
+    return_period: float,
+    duration: float,
+) -> float:
+    """
+    Calculate the intensity of a rainfall event for a given duration and return period.
+    It's based on the Sherman intensity-duration-frequency curve:
+
+    I = k * T^a / (D + b)^c
+
+    where:
+        I = Intensity of the rainfall event [mm/h]
+        k, a, b, c = Coefficients obtained through statistical distribution adjust
+        T = Return period in years
+        D = Rainfall event duration in minutes
+
+    Parameters
+    ----------
+    coefficient_k : float
+        Coefficient obtained through statistical distribution adjust.
+    coefficient_a : float
+        Coefficient obtained through statistical distribution adjust.
+    coefficient_b : float
+        Coefficient obtained through statistical distribution adjust.
+    coefficient_c : float
+        Coefficient obtained through statistical distribution adjust.
+    return_period : float
+        Return period in years.
+    duration : float
+        Rainfall event duration in minutes.
+
+    Returns
+    -------
+    intensity : float
+        Intensity of the rainfall event in mm/h.
+
+    Raises
+    ------
+    ValueError
+        If any of the parameters are not positive.
+
+    Examples
+    --------
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 10, 60)
+    49.83339231138578
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 10, 30)
+    77.36319588106228
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 5, 60)
+    43.382487747633625
+
+    >>> rainfall_intensity(0, 0.2, 11.6, 0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, -0.2, 11.6, 0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0.2, -11.6, 0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, -0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0, 11.6, 0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0.2, 0, 0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, 0, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(0, 0.2, 11.6, 0.81, 10, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 0, 60)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 10, 0)
+    Traceback (most recent call last):
+    ...
+    ValueError: All parameters must be positive.
+
+    """
+    if (
+        coefficient_k <= 0
+        or coefficient_a <= 0
+        or coefficient_b <= 0
+        or coefficient_c <= 0
+        or return_period <= 0
+        or duration <= 0
+    ):
+        raise ValueError("All parameters must be positive.")
+    intensity = (coefficient_k * (return_period**coefficient_a)) / (
+        (duration + coefficient_b) ** coefficient_c
+    )
+    return intensity
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/physics/reynolds_number.py b/physics/reynolds_number.py
index dffe690f8822..c24a9e002855 100644
--- a/physics/reynolds_number.py
+++ b/physics/reynolds_number.py
@@ -8,10 +8,10 @@
 viscous forces.
 
 R = Inertial Forces / Viscous Forces
-R = (ρ * V * D)/μ
+R = (p * V * D)/μ
 
 where :
-ρ = Density of fluid (in Kg/m^3)
+p = Density of fluid (in Kg/m^3)
 D = Diameter of pipe through which fluid flows (in m)
 V = Velocity of flow of the fluid (in m/s)
 μ = Viscosity of the fluid (in Ns/m^2)
diff --git a/physics/rms_speed_of_molecule.py b/physics/rms_speed_of_molecule.py
index 478cee01c7fd..fb23eb8a21cf 100644
--- a/physics/rms_speed_of_molecule.py
+++ b/physics/rms_speed_of_molecule.py
@@ -20,7 +20,6 @@
 alternative method.
 """
 
-
 UNIVERSAL_GAS_CONSTANT = 8.3144598
 
 
diff --git a/physics/speeds_of_gas_molecules.py b/physics/speeds_of_gas_molecules.py
index a50d1c0f6d76..3f50ffa5b45d 100644
--- a/physics/speeds_of_gas_molecules.py
+++ b/physics/speeds_of_gas_molecules.py
@@ -4,43 +4,43 @@
 distribution is a probability distribution that describes the distribution of
 speeds of particles in an ideal gas.
 
-The distribution is given by the following equation:
+The distribution is given by the following equation::
 
         -------------------------------------------------
         | f(v) = (M/2πRT)^(3/2) * 4πv^2 * e^(-Mv^2/2RT) |
         -------------------------------------------------
 
 where:
-    f(v) is the fraction of molecules with a speed v
-    M is the molar mass of the gas in kg/mol
-    R is the gas constant
-    T is the absolute temperature
+    * ``f(v)`` is the fraction of molecules with a speed ``v``
+    * ``M`` is the molar mass of the gas in kg/mol
+    * ``R`` is the gas constant
+    * ``T`` is the absolute temperature
 
 More information about the Maxwell-Boltzmann distribution can be found here:
 https://en.wikipedia.org/wiki/Maxwell%E2%80%93Boltzmann_distribution
 
 The average speed can be calculated by integrating the Maxwell-Boltzmann distribution
-from 0 to infinity and dividing by the total number of molecules. The result is:
+from 0 to infinity and dividing by the total number of molecules. The result is::
 
-        ---------------------
-        | vavg = √(8RT/πM)  |
-        ---------------------
+        ----------------------
+        | v_avg = √(8RT/πM)  |
+        ----------------------
 
 The most probable speed is the speed at which the Maxwell-Boltzmann distribution
 is at its maximum. This can be found by differentiating the Maxwell-Boltzmann
-distribution with respect to v and setting the result equal to zero. The result is:
+distribution with respect to ``v`` and setting the result equal to zero. The result is::
 
-        ---------------------
-        | vmp = √(2RT/M)    |
-        ---------------------
+        ----------------------
+        | v_mp = √(2RT/M)    |
+        ----------------------
 
 The root-mean-square speed is another measure of the average speed
 of the molecules in a gas. It is calculated by taking the square root
-of the average of the squares of the speeds of the molecules. The result is:
+of the average of the squares of the speeds of the molecules. The result is::
 
-        ---------------------
-        | vrms = √(3RT/M)   |
-        ---------------------
+        ----------------------
+        | v_rms = √(3RT/M)   |
+        ----------------------
 
 Here we have defined functions to calculate the average and
 most probable speeds of molecules in a gas given the
@@ -57,10 +57,11 @@ def avg_speed_of_molecule(temperature: float, molar_mass: float) -> float:
     and returns the average speed of a molecule in the gas (in m/s).
 
     Examples:
+
     >>> avg_speed_of_molecule(273, 0.028) # nitrogen at 273 K
-    454.3488755020387
+    454.3488755062257
     >>> avg_speed_of_molecule(300, 0.032) # oxygen at 300 K
-    445.52572733919885
+    445.5257273433045
     >>> avg_speed_of_molecule(-273, 0.028) # invalid temperature
     Traceback (most recent call last):
         ...
@@ -84,10 +85,11 @@ def mps_speed_of_molecule(temperature: float, molar_mass: float) -> float:
     and returns the most probable speed of a molecule in the gas (in m/s).
 
     Examples:
+
     >>> mps_speed_of_molecule(273, 0.028) # nitrogen at 273 K
-    402.65620701908966
+    402.65620702280023
     >>> mps_speed_of_molecule(300, 0.032) # oxygen at 300 K
-    394.836895549922
+    394.8368955535605
     >>> mps_speed_of_molecule(-273, 0.028) # invalid temperature
     Traceback (most recent call last):
         ...
diff --git a/physics/terminal_velocity.py b/physics/terminal_velocity.py
new file mode 100644
index 000000000000..16714bd02671
--- /dev/null
+++ b/physics/terminal_velocity.py
@@ -0,0 +1,60 @@
+"""
+Title : Computing the terminal velocity of an object falling
+        through a fluid.
+
+Terminal velocity is defined as the highest velocity attained by an
+object falling through a fluid. It is observed when the sum of drag force
+and buoyancy is equal to the downward gravity force acting on the
+object. The acceleration of the object is zero as the net force acting on
+the object is zero.
+
+Vt = ((2 * m * g)/(p * A * Cd))^0.5
+
+where :
+Vt = Terminal velocity (in m/s)
+m = Mass of the falling object (in Kg)
+g = Acceleration due to gravity (value taken : imported from scipy)
+p = Density of the fluid through which the object is falling (in Kg/m^3)
+A = Projected area of the object (in m^2)
+Cd = Drag coefficient (dimensionless)
+
+Reference : https://byjus.com/physics/derivation-of-terminal-velocity/
+"""
+
+from scipy.constants import g
+
+
+def terminal_velocity(
+    mass: float, density: float, area: float, drag_coefficient: float
+) -> float:
+    """
+    >>> terminal_velocity(1, 25, 0.6, 0.77)
+    1.3031197996044768
+    >>> terminal_velocity(2, 100, 0.45, 0.23)
+    1.9467947148674276
+    >>> terminal_velocity(5, 50, 0.2, 0.5)
+    4.428690551393267
+    >>> terminal_velocity(-5, 50, -0.2, -2)
+    Traceback (most recent call last):
+        ...
+    ValueError: mass, density, area and the drag coefficient all need to be positive
+    >>> terminal_velocity(3, -20, -1, 2)
+    Traceback (most recent call last):
+        ...
+    ValueError: mass, density, area and the drag coefficient all need to be positive
+    >>> terminal_velocity(-2, -1, -0.44, -1)
+    Traceback (most recent call last):
+        ...
+    ValueError: mass, density, area and the drag coefficient all need to be positive
+    """
+    if mass <= 0 or density <= 0 or area <= 0 or drag_coefficient <= 0:
+        raise ValueError(
+            "mass, density, area and the drag coefficient all need to be positive"
+        )
+    return ((2 * mass * g) / (density * area * drag_coefficient)) ** 0.5
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/project_euler/problem_002/sol4.py b/project_euler/problem_002/sol4.py
index 70b7d6a80a1d..a13d34fd760e 100644
--- a/project_euler/problem_002/sol4.py
+++ b/project_euler/problem_002/sol4.py
@@ -14,6 +14,7 @@
 References:
     - https://en.wikipedia.org/wiki/Fibonacci_number
 """
+
 import math
 from decimal import Decimal, getcontext
 
@@ -60,7 +61,7 @@ def solution(n: int = 4000000) -> int:
     if n <= 0:
         raise ValueError("Parameter n must be greater than or equal to one.")
     getcontext().prec = 100
-    phi = (Decimal(5) ** Decimal(0.5) + 1) / Decimal(2)
+    phi = (Decimal(5) ** Decimal("0.5") + 1) / Decimal(2)
 
     index = (math.floor(math.log(n * (phi + 2), phi) - 1) // 3) * 3 + 2
     num = Decimal(round(phi ** Decimal(index + 1))) / (phi + 2)
diff --git a/project_euler/problem_003/sol1.py b/project_euler/problem_003/sol1.py
index a7d01bb041ba..d1c0e61cf1a6 100644
--- a/project_euler/problem_003/sol1.py
+++ b/project_euler/problem_003/sol1.py
@@ -10,6 +10,7 @@
 References:
     - https://en.wikipedia.org/wiki/Prime_number#Unique_factorization
 """
+
 import math
 
 
diff --git a/project_euler/problem_004/sol1.py b/project_euler/problem_004/sol1.py
index f237afdd942d..f80a3253e741 100644
--- a/project_euler/problem_004/sol1.py
+++ b/project_euler/problem_004/sol1.py
@@ -4,7 +4,7 @@
 Largest palindrome product
 
 A palindromic number reads the same both ways. The largest palindrome made
-from the product of two 2-digit numbers is 9009 = 91 × 99.
+from the product of two 2-digit numbers is 9009 = 91 x 99.
 
 Find the largest palindrome made from the product of two 3-digit numbers.
 
diff --git a/project_euler/problem_004/sol2.py b/project_euler/problem_004/sol2.py
index abc880966d58..1fa75e7d0c83 100644
--- a/project_euler/problem_004/sol2.py
+++ b/project_euler/problem_004/sol2.py
@@ -4,7 +4,7 @@
 Largest palindrome product
 
 A palindromic number reads the same both ways. The largest palindrome made
-from the product of two 2-digit numbers is 9009 = 91 × 99.
+from the product of two 2-digit numbers is 9009 = 91 x 99.
 
 Find the largest palindrome made from the product of two 3-digit numbers.
 
diff --git a/project_euler/problem_006/sol3.py b/project_euler/problem_006/sol3.py
index 529f233c9f8e..16445258c2b7 100644
--- a/project_euler/problem_006/sol3.py
+++ b/project_euler/problem_006/sol3.py
@@ -15,6 +15,7 @@
 Find the difference between the sum of the squares of the first one
 hundred natural numbers and the square of the sum.
 """
+
 import math
 
 
diff --git a/project_euler/problem_007/sol2.py b/project_euler/problem_007/sol2.py
index 75d351889ea8..fd99453c1100 100644
--- a/project_euler/problem_007/sol2.py
+++ b/project_euler/problem_007/sol2.py
@@ -11,6 +11,7 @@
 References:
     - https://en.wikipedia.org/wiki/Prime_number
 """
+
 import math
 
 
diff --git a/project_euler/problem_007/sol3.py b/project_euler/problem_007/sol3.py
index 774260db99a0..39db51a93427 100644
--- a/project_euler/problem_007/sol3.py
+++ b/project_euler/problem_007/sol3.py
@@ -11,6 +11,7 @@
 References:
     - https://en.wikipedia.org/wiki/Prime_number
 """
+
 import itertools
 import math
 
diff --git a/project_euler/problem_008/sol1.py b/project_euler/problem_008/sol1.py
index 69dd1b4736c1..a38b2045f996 100644
--- a/project_euler/problem_008/sol1.py
+++ b/project_euler/problem_008/sol1.py
@@ -4,7 +4,7 @@
 Largest product in a series
 
 The four adjacent digits in the 1000-digit number that have the greatest
-product are 9 × 9 × 8 × 9 = 5832.
+product are 9 x 9 x 8 x 9 = 5832.
 
     73167176531330624919225119674426574742355349194934
     96983520312774506326239578318016984801869478851843
@@ -75,8 +75,7 @@ def solution(n: str = N) -> int:
         product = 1
         for j in range(13):
             product *= int(n[i + j])
-        if product > largest_product:
-            largest_product = product
+        largest_product = max(largest_product, product)
     return largest_product
 
 
diff --git a/project_euler/problem_008/sol2.py b/project_euler/problem_008/sol2.py
index 889c3a3143c2..e48231e4023b 100644
--- a/project_euler/problem_008/sol2.py
+++ b/project_euler/problem_008/sol2.py
@@ -4,7 +4,7 @@
 Largest product in a series
 
 The four adjacent digits in the 1000-digit number that have the greatest
-product are 9 × 9 × 8 × 9 = 5832.
+product are 9 x 9 x 8 x 9 = 5832.
 
     73167176531330624919225119674426574742355349194934
     96983520312774506326239578318016984801869478851843
@@ -30,6 +30,7 @@
 Find the thirteen adjacent digits in the 1000-digit number that have the
 greatest product. What is the value of this product?
 """
+
 from functools import reduce
 
 N = (
diff --git a/project_euler/problem_008/sol3.py b/project_euler/problem_008/sol3.py
index c6081aa05e2c..0d319b9684dd 100644
--- a/project_euler/problem_008/sol3.py
+++ b/project_euler/problem_008/sol3.py
@@ -4,7 +4,7 @@
 Largest product in a series
 
 The four adjacent digits in the 1000-digit number that have the greatest
-product are 9 × 9 × 8 × 9 = 5832.
+product are 9 x 9 x 8 x 9 = 5832.
 
     73167176531330624919225119674426574742355349194934
     96983520312774506326239578318016984801869478851843
@@ -30,6 +30,7 @@
 Find the thirteen adjacent digits in the 1000-digit number that have the
 greatest product. What is the value of this product?
 """
+
 import sys
 
 N = (
diff --git a/project_euler/problem_009/sol2.py b/project_euler/problem_009/sol2.py
index 722ad522ee45..443a529571cc 100644
--- a/project_euler/problem_009/sol2.py
+++ b/project_euler/problem_009/sol2.py
@@ -39,8 +39,7 @@ def solution(n: int = 1000) -> int:
         c = n - a - b
         if c * c == (a * a + b * b):
             candidate = a * b * c
-            if candidate >= product:
-                product = candidate
+            product = max(product, candidate)
     return product
 
 
diff --git a/project_euler/problem_010/sol2.py b/project_euler/problem_010/sol2.py
index 245cca1d1720..1a1fc0f33cb3 100644
--- a/project_euler/problem_010/sol2.py
+++ b/project_euler/problem_010/sol2.py
@@ -10,6 +10,7 @@
 References:
     - https://en.wikipedia.org/wiki/Prime_number
 """
+
 import math
 from collections.abc import Iterator
 from itertools import takewhile
diff --git a/project_euler/problem_011/sol1.py b/project_euler/problem_011/sol1.py
index ad45f0983a7c..3d3e864f927b 100644
--- a/project_euler/problem_011/sol1.py
+++ b/project_euler/problem_011/sol1.py
@@ -63,8 +63,7 @@ def largest_product(grid):
             max_product = max(
                 vert_product, horz_product, lr_diag_product, rl_diag_product
             )
-            if max_product > largest:
-                largest = max_product
+            largest = max(largest, max_product)
 
     return largest
 
diff --git a/project_euler/problem_011/sol2.py b/project_euler/problem_011/sol2.py
index 9ea0db991aaf..7637deafc3cb 100644
--- a/project_euler/problem_011/sol2.py
+++ b/project_euler/problem_011/sol2.py
@@ -35,39 +35,45 @@ def solution():
     70600674
     """
     with open(os.path.dirname(__file__) + "/grid.txt") as f:
-        l = []  # noqa: E741
+        grid = []
         for _ in range(20):
-            l.append([int(x) for x in f.readline().split()])
+            grid.append([int(x) for x in f.readline().split()])
 
         maximum = 0
 
         # right
         for i in range(20):
             for j in range(17):
-                temp = l[i][j] * l[i][j + 1] * l[i][j + 2] * l[i][j + 3]
-                if temp > maximum:
-                    maximum = temp
+                temp = grid[i][j] * grid[i][j + 1] * grid[i][j + 2] * grid[i][j + 3]
+                maximum = max(maximum, temp)
 
         # down
         for i in range(17):
             for j in range(20):
-                temp = l[i][j] * l[i + 1][j] * l[i + 2][j] * l[i + 3][j]
-                if temp > maximum:
-                    maximum = temp
+                temp = grid[i][j] * grid[i + 1][j] * grid[i + 2][j] * grid[i + 3][j]
+                maximum = max(maximum, temp)
 
         # diagonal 1
         for i in range(17):
             for j in range(17):
-                temp = l[i][j] * l[i + 1][j + 1] * l[i + 2][j + 2] * l[i + 3][j + 3]
-                if temp > maximum:
-                    maximum = temp
+                temp = (
+                    grid[i][j]
+                    * grid[i + 1][j + 1]
+                    * grid[i + 2][j + 2]
+                    * grid[i + 3][j + 3]
+                )
+                maximum = max(maximum, temp)
 
         # diagonal 2
         for i in range(17):
             for j in range(3, 20):
-                temp = l[i][j] * l[i + 1][j - 1] * l[i + 2][j - 2] * l[i + 3][j - 3]
-                if temp > maximum:
-                    maximum = temp
+                temp = (
+                    grid[i][j]
+                    * grid[i + 1][j - 1]
+                    * grid[i + 2][j - 2]
+                    * grid[i + 3][j - 3]
+                )
+                maximum = max(maximum, temp)
         return maximum
 
 
diff --git a/project_euler/problem_013/sol1.py b/project_euler/problem_013/sol1.py
index 7a414a9379e0..87d0e0a60e9b 100644
--- a/project_euler/problem_013/sol1.py
+++ b/project_euler/problem_013/sol1.py
@@ -5,6 +5,7 @@
 Work out the first ten digits of the sum of the following one-hundred 50-digit
 numbers.
 """
+
 import os
 
 
diff --git a/project_euler/problem_014/sol2.py b/project_euler/problem_014/sol2.py
index 2448e652ce5b..797b0f9886fe 100644
--- a/project_euler/problem_014/sol2.py
+++ b/project_euler/problem_014/sol2.py
@@ -25,6 +25,7 @@
 
 Which starting number, under one million, produces the longest chain?
 """
+
 from __future__ import annotations
 
 COLLATZ_SEQUENCE_LENGTHS = {1: 1}
diff --git a/project_euler/problem_015/sol1.py b/project_euler/problem_015/sol1.py
index fb2020d6179f..3c9dae1aed77 100644
--- a/project_euler/problem_015/sol1.py
+++ b/project_euler/problem_015/sol1.py
@@ -1,10 +1,11 @@
 """
 Problem 15: https://projecteuler.net/problem=15
 
-Starting in the top left corner of a 2×2 grid, and only being able to move to
+Starting in the top left corner of a 2x2 grid, and only being able to move to
 the right and down, there are exactly 6 routes to the bottom right corner.
-How many such routes are there through a 20×20 grid?
+How many such routes are there through a 20x20 grid?
 """
+
 from math import factorial
 
 
diff --git a/project_euler/problem_018/solution.py b/project_euler/problem_018/solution.py
index 70306148bb9e..cbe8743be15f 100644
--- a/project_euler/problem_018/solution.py
+++ b/project_euler/problem_018/solution.py
@@ -27,6 +27,7 @@
 63 66 04 68 89 53 67 30 73 16 69 87 40 31
 04 62 98 27 23 09 70 98 73 93 38 53 60 04 23
 """
+
 import os
 
 
diff --git a/project_euler/problem_019/sol1.py b/project_euler/problem_019/sol1.py
index 0e38137d4f01..656f104c390d 100644
--- a/project_euler/problem_019/sol1.py
+++ b/project_euler/problem_019/sol1.py
@@ -46,10 +46,9 @@ def solution():
             elif day > 29 and month == 2:
                 month += 1
                 day = day - 29
-        else:
-            if day > days_per_month[month - 1]:
-                month += 1
-                day = day - days_per_month[month - 2]
+        elif day > days_per_month[month - 1]:
+            month += 1
+            day = day - days_per_month[month - 2]
 
         if month > 12:
             year += 1
diff --git a/project_euler/problem_020/sol1.py b/project_euler/problem_020/sol1.py
index b472024e54c0..1439bdca38e6 100644
--- a/project_euler/problem_020/sol1.py
+++ b/project_euler/problem_020/sol1.py
@@ -1,9 +1,9 @@
 """
 Problem 20: https://projecteuler.net/problem=20
 
-n! means n × (n − 1) × ... × 3 × 2 × 1
+n! means n x (n - 1) x ... x 3 x 2 x 1
 
-For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800,
+For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800,
 and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27.
 
 Find the sum of the digits in the number 100!
diff --git a/project_euler/problem_020/sol2.py b/project_euler/problem_020/sol2.py
index 676e96e7836a..61684cd5ef6d 100644
--- a/project_euler/problem_020/sol2.py
+++ b/project_euler/problem_020/sol2.py
@@ -1,13 +1,14 @@
 """
 Problem 20: https://projecteuler.net/problem=20
 
-n! means n × (n − 1) × ... × 3 × 2 × 1
+n! means n x (n - 1) x ... x 3 x 2 x 1
 
-For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800,
+For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800,
 and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27.
 
 Find the sum of the digits in the number 100!
 """
+
 from math import factorial
 
 
diff --git a/project_euler/problem_020/sol3.py b/project_euler/problem_020/sol3.py
index 4f28ac5fcfde..8984def9c34e 100644
--- a/project_euler/problem_020/sol3.py
+++ b/project_euler/problem_020/sol3.py
@@ -1,13 +1,14 @@
 """
 Problem 20: https://projecteuler.net/problem=20
 
-n! means n × (n − 1) × ... × 3 × 2 × 1
+n! means n x (n - 1) x ... x 3 x 2 x 1
 
-For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800,
+For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800,
 and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27.
 
 Find the sum of the digits in the number 100!
 """
+
 from math import factorial
 
 
diff --git a/project_euler/problem_020/sol4.py b/project_euler/problem_020/sol4.py
index b32ce309dfa6..511ac81e176b 100644
--- a/project_euler/problem_020/sol4.py
+++ b/project_euler/problem_020/sol4.py
@@ -1,9 +1,9 @@
 """
 Problem 20: https://projecteuler.net/problem=20
 
-n! means n × (n − 1) × ... × 3 × 2 × 1
+n! means n x (n - 1) x ... x 3 x 2 x 1
 
-For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800,
+For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800,
 and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27.
 
 Find the sum of the digits in the number 100!
diff --git a/project_euler/problem_021/sol1.py b/project_euler/problem_021/sol1.py
index 353510ae8f94..f6dbfa8864db 100644
--- a/project_euler/problem_021/sol1.py
+++ b/project_euler/problem_021/sol1.py
@@ -13,6 +13,7 @@
 
 Evaluate the sum of all the amicable numbers under 10000.
 """
+
 from math import sqrt
 
 
diff --git a/project_euler/problem_022/sol1.py b/project_euler/problem_022/sol1.py
index 982906245e87..c4af5dfa81df 100644
--- a/project_euler/problem_022/sol1.py
+++ b/project_euler/problem_022/sol1.py
@@ -10,10 +10,11 @@
 
 For example, when the list is sorted into alphabetical order, COLIN, which is
 worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. So, COLIN would
-obtain a score of 938 × 53 = 49714.
+obtain a score of 938 x 53 = 49714.
 
 What is the total of all the name scores in the file?
 """
+
 import os
 
 
diff --git a/project_euler/problem_022/sol2.py b/project_euler/problem_022/sol2.py
index 5ae41c84686e..9c22b6bba0cc 100644
--- a/project_euler/problem_022/sol2.py
+++ b/project_euler/problem_022/sol2.py
@@ -10,10 +10,11 @@
 
 For example, when the list is sorted into alphabetical order, COLIN, which is
 worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. So, COLIN would
-obtain a score of 938 × 53 = 49714.
+obtain a score of 938 x 53 = 49714.
 
 What is the total of all the name scores in the file?
 """
+
 import os
 
 
diff --git a/project_euler/problem_024/sol1.py b/project_euler/problem_024/sol1.py
index 1c6378b38260..3fb1bd4ec582 100644
--- a/project_euler/problem_024/sol1.py
+++ b/project_euler/problem_024/sol1.py
@@ -9,6 +9,7 @@
 What is the millionth lexicographic permutation of the digits 0, 1, 2, 3, 4, 5,
 6, 7, 8 and 9?
 """
+
 from itertools import permutations
 
 
diff --git a/project_euler/problem_025/sol1.py b/project_euler/problem_025/sol1.py
index 803464b5d786..b3bbb56d20be 100644
--- a/project_euler/problem_025/sol1.py
+++ b/project_euler/problem_025/sol1.py
@@ -1,7 +1,7 @@
 """
 The Fibonacci sequence is defined by the recurrence relation:
 
-    Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1.
+    Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1.
 
 Hence the first 12 terms will be:
 
diff --git a/project_euler/problem_025/sol2.py b/project_euler/problem_025/sol2.py
index 6f49e89fb465..4094b6251d50 100644
--- a/project_euler/problem_025/sol2.py
+++ b/project_euler/problem_025/sol2.py
@@ -1,7 +1,7 @@
 """
 The Fibonacci sequence is defined by the recurrence relation:
 
-    Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1.
+    Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1.
 
 Hence the first 12 terms will be:
 
@@ -23,10 +23,11 @@
 What is the index of the first term in the Fibonacci sequence to contain 1000
 digits?
 """
+
 from collections.abc import Generator
 
 
-def fibonacci_generator() -> Generator[int, None, None]:
+def fibonacci_generator() -> Generator[int]:
     """
     A generator that produces numbers in the Fibonacci sequence
 
diff --git a/project_euler/problem_025/sol3.py b/project_euler/problem_025/sol3.py
index 0b9f3a0c84ef..e33b159ac65c 100644
--- a/project_euler/problem_025/sol3.py
+++ b/project_euler/problem_025/sol3.py
@@ -1,7 +1,7 @@
 """
 The Fibonacci sequence is defined by the recurrence relation:
 
-    Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1.
+    Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1.
 
 Hence the first 12 terms will be:
 
diff --git a/project_euler/problem_027/sol1.py b/project_euler/problem_027/sol1.py
index c93e2b4fa251..48755ec19763 100644
--- a/project_euler/problem_027/sol1.py
+++ b/project_euler/problem_027/sol1.py
@@ -9,12 +9,12 @@
 It turns out that the formula will produce 40 primes for the consecutive values
 n = 0 to 39. However, when n = 40, 402 + 40 + 41 = 40(40 + 1) + 41 is divisible
 by 41, and certainly when n = 41, 412 + 41 + 41 is clearly divisible by 41.
-The incredible formula  n2 − 79n + 1601 was discovered, which produces 80 primes
-for the consecutive values n = 0 to 79. The product of the coefficients, −79 and
-1601, is −126479.
+The incredible formula  n2 - 79n + 1601 was discovered, which produces 80 primes
+for the consecutive values n = 0 to 79. The product of the coefficients, -79 and
+1601, is -126479.
 Considering quadratics of the form:
 n² + an + b, where |a| &lt; 1000 and |b| &lt; 1000
-where |n| is the modulus/absolute value of ne.g. |11| = 11 and |−4| = 4
+where |n| is the modulus/absolute value of ne.g. |11| = 11 and |-4| = 4
 Find the product of the coefficients, a and b, for the quadratic expression that
 produces the maximum number of primes for consecutive values of n, starting with
 n = 0.
diff --git a/project_euler/problem_028/sol1.py b/project_euler/problem_028/sol1.py
index 1ea5d4fcafd4..0a4648af36c4 100644
--- a/project_euler/problem_028/sol1.py
+++ b/project_euler/problem_028/sol1.py
@@ -37,7 +37,7 @@ def solution(n: int = 1001) -> int:
     """
     total = 1
 
-    for i in range(1, int(ceil(n / 2.0))):
+    for i in range(1, ceil(n / 2.0)):
         odd = 2 * i + 1
         even = 2 * i
         total = total + 4 * odd**2 - 6 * even
diff --git a/project_euler/problem_030/sol1.py b/project_euler/problem_030/sol1.py
index 2c6b4e4e85d5..7d83e314523f 100644
--- a/project_euler/problem_030/sol1.py
+++ b/project_euler/problem_030/sol1.py
@@ -1,4 +1,4 @@
-""" Problem Statement (Digit Fifth Powers): https://projecteuler.net/problem=30
+"""Problem Statement (Digit Fifth Powers): https://projecteuler.net/problem=30
 
 Surprisingly there are only three numbers that can be written as the sum of fourth
 powers of their digits:
@@ -21,7 +21,6 @@
 and hence a number between 1000 and 1000000
 """
 
-
 DIGITS_FIFTH_POWER = {str(digit): digit**5 for digit in range(10)}
 
 
diff --git a/project_euler/problem_031/sol1.py b/project_euler/problem_031/sol1.py
index ba40cf383175..4c9c533eecb7 100644
--- a/project_euler/problem_031/sol1.py
+++ b/project_euler/problem_031/sol1.py
@@ -2,14 +2,14 @@
 Coin sums
 Problem 31: https://projecteuler.net/problem=31
 
-In England the currency is made up of pound, £, and pence, p, and there are
+In England the currency is made up of pound, f, and pence, p, and there are
 eight coins in general circulation:
 
-1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p).
-It is possible to make £2 in the following way:
+1p, 2p, 5p, 10p, 20p, 50p, f1 (100p) and f2 (200p).
+It is possible to make f2 in the following way:
 
-1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p
-How many different ways can £2 be made using any number of coins?
+1xf1 + 1x50p + 2x20p + 1x5p + 1x2p + 3x1p
+How many different ways can f2 be made using any number of coins?
 """
 
 
diff --git a/project_euler/problem_031/sol2.py b/project_euler/problem_031/sol2.py
index f9e4dc384bff..574f8d4107a1 100644
--- a/project_euler/problem_031/sol2.py
+++ b/project_euler/problem_031/sol2.py
@@ -3,17 +3,17 @@
 
 Coin sums
 
-In England the currency is made up of pound, £, and pence, p, and there are
+In England the currency is made up of pound, f, and pence, p, and there are
 eight coins in general circulation:
 
-1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p).
-It is possible to make £2 in the following way:
+1p, 2p, 5p, 10p, 20p, 50p, f1 (100p) and f2 (200p).
+It is possible to make f2 in the following way:
 
-1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p
-How many different ways can £2 be made using any number of coins?
+1xf1 + 1x50p + 2x20p + 1x5p + 1x2p + 3x1p
+How many different ways can f2 be made using any number of coins?
 
 Hint:
-    > There are 100 pence in a pound (£1 = 100p)
+    > There are 100 pence in a pound (f1 = 100p)
     > There are coins(in pence) are available: 1, 2, 5, 10, 20, 50, 100 and 200.
     > how many different ways you can combine these values to create 200 pence.
 
diff --git a/project_euler/problem_032/sol32.py b/project_euler/problem_032/sol32.py
index c4d11e86c877..c0ca2ce10791 100644
--- a/project_euler/problem_032/sol32.py
+++ b/project_euler/problem_032/sol32.py
@@ -3,7 +3,7 @@
 digits 1 to n exactly once; for example, the 5-digit number, 15234, is 1 through
 5 pandigital.
 
-The product 7254 is unusual, as the identity, 39 × 186 = 7254, containing
+The product 7254 is unusual, as the identity, 39 x 186 = 7254, containing
 multiplicand, multiplier, and product is 1 through 9 pandigital.
 
 Find the sum of all products whose multiplicand/multiplier/product identity can
@@ -12,6 +12,7 @@
 HINT: Some products can be obtained in more than one way so be sure to only
 include it once in your sum.
 """
+
 import itertools
 
 
diff --git a/project_euler/problem_033/sol1.py b/project_euler/problem_033/sol1.py
index 32be424b6a7b..71790d34fbed 100644
--- a/project_euler/problem_033/sol1.py
+++ b/project_euler/problem_033/sol1.py
@@ -14,6 +14,7 @@
 If the product of these four fractions is given in its lowest common
 terms, find the value of the denominator.
 """
+
 from __future__ import annotations
 
 from fractions import Fraction
@@ -43,9 +44,13 @@ def fraction_list(digit_len: int) -> list[str]:
     last_digit = int("1" + "0" * digit_len)
     for num in range(den, last_digit):
         while den <= 99:
-            if (num != den) and (num % 10 == den // 10) and (den % 10 != 0):
-                if is_digit_cancelling(num, den):
-                    solutions.append(f"{num}/{den}")
+            if (
+                (num != den)
+                and (num % 10 == den // 10)
+                and (den % 10 != 0)
+                and is_digit_cancelling(num, den)
+            ):
+                solutions.append(f"{num}/{den}")
             den += 1
         num += 1
         den = 10
diff --git a/project_euler/problem_034/__init__.py b/project_euler/problem_034/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_034/__init__.py
+++ b/project_euler/problem_034/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_035/__init__.py b/project_euler/problem_035/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_035/__init__.py
+++ b/project_euler/problem_035/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_035/sol1.py b/project_euler/problem_035/sol1.py
index 644c992ed8a5..cf9f6821d798 100644
--- a/project_euler/problem_035/sol1.py
+++ b/project_euler/problem_035/sol1.py
@@ -15,6 +15,7 @@
 we will rule out the numbers which contain an even digit. After this we will
 generate each circular combination of the number and check if all are prime.
 """
+
 from __future__ import annotations
 
 sieve = [True] * 1000001
diff --git a/project_euler/problem_036/sol1.py b/project_euler/problem_036/sol1.py
index 1d27356ec51e..3865b2a39ea9 100644
--- a/project_euler/problem_036/sol1.py
+++ b/project_euler/problem_036/sol1.py
@@ -14,6 +14,7 @@
 (Please note that the palindromic number, in either base, may not include
 leading zeros.)
 """
+
 from __future__ import annotations
 
 
diff --git a/project_euler/problem_037/__init__.py b/project_euler/problem_037/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_037/__init__.py
+++ b/project_euler/problem_037/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_037/sol1.py b/project_euler/problem_037/sol1.py
index ef7686cbcb96..c66eb9fb1735 100644
--- a/project_euler/problem_037/sol1.py
+++ b/project_euler/problem_037/sol1.py
@@ -85,10 +85,10 @@ def validate(n: int) -> bool:
     >>> validate(3797)
     True
     """
-    if len(str(n)) > 3:
-        if not is_prime(int(str(n)[-3:])) or not is_prime(int(str(n)[:3])):
-            return False
-    return True
+    return not (
+        len(str(n)) > 3
+        and (not is_prime(int(str(n)[-3:])) or not is_prime(int(str(n)[:3])))
+    )
 
 
 def compute_truncated_primes(count: int = 11) -> list[int]:
diff --git a/project_euler/problem_038/sol1.py b/project_euler/problem_038/sol1.py
index e4a6d09f8f7d..382892723b7d 100644
--- a/project_euler/problem_038/sol1.py
+++ b/project_euler/problem_038/sol1.py
@@ -3,9 +3,9 @@
 
 Take the number 192 and multiply it by each of 1, 2, and 3:
 
-192 × 1 = 192
-192 × 2 = 384
-192 × 3 = 576
+192 x 1 = 192
+192 x 2 = 384
+192 x 3 = 576
 
 By concatenating each product we get the 1 to 9 pandigital, 192384576. We will call
 192384576 the concatenated product of 192 and (1,2,3)
@@ -37,6 +37,7 @@
 =>  100 <= a < 334, candidate = a * 10^6 + 2a * 10^3 + 3a
                               = 1002003 * a
 """
+
 from __future__ import annotations
 
 
diff --git a/project_euler/problem_039/__init__.py b/project_euler/problem_039/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_039/__init__.py
+++ b/project_euler/problem_039/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_040/sol1.py b/project_euler/problem_040/sol1.py
index 69be377723a5..721bd063c28a 100644
--- a/project_euler/problem_040/sol1.py
+++ b/project_euler/problem_040/sol1.py
@@ -11,7 +11,7 @@
 If dn represents the nth digit of the fractional part, find the value of the
 following expression.
 
-d1 × d10 × d100 × d1000 × d10000 × d100000 × d1000000
+d1 x d10 x d100 x d1000 x d10000 x d100000 x d1000000
 """
 
 
diff --git a/project_euler/problem_041/__init__.py b/project_euler/problem_041/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_041/__init__.py
+++ b/project_euler/problem_041/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_041/sol1.py b/project_euler/problem_041/sol1.py
index 2ef0120684c3..0c37f5469a6c 100644
--- a/project_euler/problem_041/sol1.py
+++ b/project_euler/problem_041/sol1.py
@@ -10,6 +10,7 @@
 So we will check only 7 digit pandigital numbers to obtain the largest possible
 pandigital prime.
 """
+
 from __future__ import annotations
 
 import math
diff --git a/project_euler/problem_042/solution42.py b/project_euler/problem_042/solution42.py
index f8a54e40eaab..f678bcdef710 100644
--- a/project_euler/problem_042/solution42.py
+++ b/project_euler/problem_042/solution42.py
@@ -13,6 +13,7 @@
 containing nearly two-thousand common English words, how many are triangle
 words?
 """
+
 import os
 
 # Precomputes a list of the 100 first triangular numbers
diff --git a/project_euler/problem_043/__init__.py b/project_euler/problem_043/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_043/__init__.py
+++ b/project_euler/problem_043/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_043/sol1.py b/project_euler/problem_043/sol1.py
index c533f40da9c9..f3a2c71edc4e 100644
--- a/project_euler/problem_043/sol1.py
+++ b/project_euler/problem_043/sol1.py
@@ -18,7 +18,6 @@
 Find the sum of all 0 to 9 pandigital numbers with this property.
 """
 
-
 from itertools import permutations
 
 
diff --git a/project_euler/problem_044/__init__.py b/project_euler/problem_044/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_044/__init__.py
+++ b/project_euler/problem_044/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_044/sol1.py b/project_euler/problem_044/sol1.py
index 3b75b6a56a8e..2613563a4bf1 100644
--- a/project_euler/problem_044/sol1.py
+++ b/project_euler/problem_044/sol1.py
@@ -1,14 +1,14 @@
 """
 Problem 44: https://projecteuler.net/problem=44
 
-Pentagonal numbers are generated by the formula, Pn=n(3n−1)/2. The first ten
+Pentagonal numbers are generated by the formula, Pn=n(3n-1)/2. The first ten
 pentagonal numbers are:
 1, 5, 12, 22, 35, 51, 70, 92, 117, 145, ...
 It can be seen that P4 + P7 = 22 + 70 = 92 = P8. However, their difference,
-70 − 22 = 48, is not pentagonal.
+70 - 22 = 48, is not pentagonal.
 
 Find the pair of pentagonal numbers, Pj and Pk, for which their sum and difference
-are pentagonal and D = |Pk − Pj| is minimised; what is the value of D?
+are pentagonal and D = |Pk - Pj| is minimised; what is the value of D?
 """
 
 
diff --git a/project_euler/problem_045/__init__.py b/project_euler/problem_045/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_045/__init__.py
+++ b/project_euler/problem_045/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_045/sol1.py b/project_euler/problem_045/sol1.py
index d921b2802c2d..8d016de6e542 100644
--- a/project_euler/problem_045/sol1.py
+++ b/project_euler/problem_045/sol1.py
@@ -3,8 +3,8 @@
 
 Triangle, pentagonal, and hexagonal numbers are generated by the following formulae:
 Triangle	 	T(n) = (n * (n + 1)) / 2	 	1, 3, 6, 10, 15, ...
-Pentagonal	 	P(n) = (n * (3 * n − 1)) / 2	 	1, 5, 12, 22, 35, ...
-Hexagonal	 	H(n) = n * (2 * n − 1)	 	1, 6, 15, 28, 45, ...
+Pentagonal	 	P(n) = (n * (3 * n - 1)) / 2	 	1, 5, 12, 22, 35, ...
+Hexagonal	 	H(n) = n * (2 * n - 1)	 	1, 6, 15, 28, 45, ...
 It can be verified that T(285) = P(165) = H(143) = 40755.
 
 Find the next triangle number that is also pentagonal and hexagonal.
diff --git a/project_euler/problem_046/__init__.py b/project_euler/problem_046/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_046/__init__.py
+++ b/project_euler/problem_046/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_046/sol1.py b/project_euler/problem_046/sol1.py
index 07dd9bbf84c8..f27f658e63e5 100644
--- a/project_euler/problem_046/sol1.py
+++ b/project_euler/problem_046/sol1.py
@@ -4,12 +4,12 @@
 It was proposed by Christian Goldbach that every odd composite number can be
 written as the sum of a prime and twice a square.
 
-9 = 7 + 2 × 12
-15 = 7 + 2 × 22
-21 = 3 + 2 × 32
-25 = 7 + 2 × 32
-27 = 19 + 2 × 22
-33 = 31 + 2 × 12
+9 = 7 + 2 x 12
+15 = 7 + 2 x 22
+21 = 3 + 2 x 32
+25 = 7 + 2 x 32
+27 = 19 + 2 x 22
+33 = 31 + 2 x 12
 
 It turns out that the conjecture was false.
 
diff --git a/project_euler/problem_047/sol1.py b/project_euler/problem_047/sol1.py
index 1287e0d9e107..d174de27dcd0 100644
--- a/project_euler/problem_047/sol1.py
+++ b/project_euler/problem_047/sol1.py
@@ -5,14 +5,14 @@
 
 The first two consecutive numbers to have two distinct prime factors are:
 
-14 = 2 × 7
-15 = 3 × 5
+14 = 2 x 7
+15 = 3 x 5
 
 The first three consecutive numbers to have three distinct prime factors are:
 
-644 = 2² × 7 × 23
-645 = 3 × 5 × 43
-646 = 2 × 17 × 19.
+644 = 2² x 7 x 23
+645 = 3 x 5 x 43
+646 = 2 x 17 x 19.
 
 Find the first four consecutive integers to have four distinct prime factors each.
 What is the first of these numbers?
@@ -24,7 +24,7 @@
 def unique_prime_factors(n: int) -> set:
     """
     Find unique prime factors of an integer.
-    Tests include sorting because only the set really matters,
+    Tests include sorting because only the set matters,
     not the order in which it is produced.
     >>> sorted(set(unique_prime_factors(14)))
     [2, 7]
@@ -58,7 +58,7 @@ def upf_len(num: int) -> int:
 
 def equality(iterable: list) -> bool:
     """
-    Check equality of ALL elements in an interable.
+    Check the equality of ALL elements in an iterable
     >>> equality([1, 2, 3, 4])
     False
     >>> equality([2, 2, 2, 2])
@@ -69,7 +69,7 @@ def equality(iterable: list) -> bool:
     return len(set(iterable)) in (0, 1)
 
 
-def run(n: int) -> list:
+def run(n: int) -> list[int]:
     """
     Runs core process to find problem solution.
     >>> run(3)
@@ -77,7 +77,7 @@ def run(n: int) -> list:
     """
 
     # Incrementor variable for our group list comprehension.
-    # This serves as the first number in each list of values
+    # This is the first number in each list of values
     # to test.
     base = 2
 
@@ -85,7 +85,7 @@ def run(n: int) -> list:
         # Increment each value of a generated range
         group = [base + i for i in range(n)]
 
-        # Run elements through out unique_prime_factors function
+        # Run elements through the unique_prime_factors function
         # Append our target number to the end.
         checker = [upf_len(x) for x in group]
         checker.append(n)
@@ -98,7 +98,7 @@ def run(n: int) -> list:
         base += 1
 
 
-def solution(n: int = 4) -> int:
+def solution(n: int = 4) -> int | None:
     """Return the first value of the first four consecutive integers to have four
     distinct prime factors each.
     >>> solution()
diff --git a/project_euler/problem_050/sol1.py b/project_euler/problem_050/sol1.py
index fc6e6f2b9a5d..0a5f861f0ef0 100644
--- a/project_euler/problem_050/sol1.py
+++ b/project_euler/problem_050/sol1.py
@@ -15,6 +15,7 @@
 Which prime, below one-million, can be written as the sum of the most
 consecutive primes?
 """
+
 from __future__ import annotations
 
 
diff --git a/project_euler/problem_051/sol1.py b/project_euler/problem_051/sol1.py
index 921704bc4455..dc740c8b947d 100644
--- a/project_euler/problem_051/sol1.py
+++ b/project_euler/problem_051/sol1.py
@@ -15,6 +15,7 @@
 Find the smallest prime which, by replacing part of the number (not necessarily
 adjacent digits) with the same digit, is part of an eight prime value family.
 """
+
 from __future__ import annotations
 
 from collections import Counter
diff --git a/project_euler/problem_053/sol1.py b/project_euler/problem_053/sol1.py
index 0692bbe0ebb8..192cbf25e50c 100644
--- a/project_euler/problem_053/sol1.py
+++ b/project_euler/problem_053/sol1.py
@@ -10,12 +10,13 @@
 
 In general,
 
-nCr = n!/(r!(n−r)!),where r ≤ n, n! = n×(n−1)×...×3×2×1, and 0! = 1.
+nCr = n!/(r!(n-r)!),where r ≤ n, n! = nx(n-1)x...x3x2x1, and 0! = 1.
 It is not until n = 23, that a value exceeds one-million: 23C10 = 1144066.
 
 How many, not necessarily distinct, values of nCr, for 1 ≤ n ≤ 100, are greater
 than one-million?
 """
+
 from math import factorial
 
 
diff --git a/project_euler/problem_054/sol1.py b/project_euler/problem_054/sol1.py
index 86dfa5edd2f5..66aa3a0826f5 100644
--- a/project_euler/problem_054/sol1.py
+++ b/project_euler/problem_054/sol1.py
@@ -40,6 +40,7 @@
 https://www.codewars.com/kata/ranking-poker-hands
 https://www.codewars.com/kata/sortable-poker-hands
 """
+
 from __future__ import annotations
 
 import os
diff --git a/project_euler/problem_055/__init__.py b/project_euler/problem_055/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_055/__init__.py
+++ b/project_euler/problem_055/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_056/sol1.py b/project_euler/problem_056/sol1.py
index c772bec58692..828dbd3a8ddf 100644
--- a/project_euler/problem_056/sol1.py
+++ b/project_euler/problem_056/sol1.py
@@ -30,9 +30,7 @@ def solution(a: int = 100, b: int = 100) -> int:
     # RETURN the MAXIMUM from the list of SUMs of the list of INT converted from STR of
     # BASE raised to the POWER
     return max(
-        sum(int(x) for x in str(base**power))
-        for base in range(a)
-        for power in range(b)
+        sum(int(x) for x in str(base**power)) for base in range(a) for power in range(b)
     )
 
 
diff --git a/project_euler/problem_058/__init__.py b/project_euler/problem_058/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_058/__init__.py
+++ b/project_euler/problem_058/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_058/sol1.py b/project_euler/problem_058/sol1.py
index 6a991c58b6b8..1d2f406eafdb 100644
--- a/project_euler/problem_058/sol1.py
+++ b/project_euler/problem_058/sol1.py
@@ -33,6 +33,7 @@
 count of current primes.
 
 """
+
 import math
 
 
diff --git a/project_euler/problem_059/sol1.py b/project_euler/problem_059/sol1.py
index b795dd243b08..65bfd3f0b0fb 100644
--- a/project_euler/problem_059/sol1.py
+++ b/project_euler/problem_059/sol1.py
@@ -25,6 +25,7 @@
 must contain common English words, decrypt the message and find the sum of the ASCII
 values in the original text.
 """
+
 from __future__ import annotations
 
 import string
diff --git a/project_euler/problem_063/__init__.py b/project_euler/problem_063/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_063/__init__.py
+++ b/project_euler/problem_063/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_067/sol1.py b/project_euler/problem_067/sol1.py
index 2b41fedc6784..171ff8c268f6 100644
--- a/project_euler/problem_067/sol1.py
+++ b/project_euler/problem_067/sol1.py
@@ -11,6 +11,7 @@
 'Save Link/Target As...'), a 15K text file containing a triangle with
 one-hundred rows.
 """
+
 import os
 
 
diff --git a/project_euler/problem_067/sol2.py b/project_euler/problem_067/sol2.py
index 2e88a57170a8..4fb093d49956 100644
--- a/project_euler/problem_067/sol2.py
+++ b/project_euler/problem_067/sol2.py
@@ -11,6 +11,7 @@
 'Save Link/Target As...'), a 15K text file containing a triangle with
 one-hundred rows.
 """
+
 import os
 
 
diff --git a/project_euler/problem_070/sol1.py b/project_euler/problem_070/sol1.py
index f1114a280a31..9874b7418559 100644
--- a/project_euler/problem_070/sol1.py
+++ b/project_euler/problem_070/sol1.py
@@ -28,6 +28,7 @@
 Finding totients
 https://en.wikipedia.org/wiki/Euler's_totient_function#Euler's_product_formula
 """
+
 from __future__ import annotations
 
 import numpy as np
diff --git a/project_euler/problem_072/sol1.py b/project_euler/problem_072/sol1.py
index 5a28be564556..f09db0673323 100644
--- a/project_euler/problem_072/sol1.py
+++ b/project_euler/problem_072/sol1.py
@@ -43,7 +43,7 @@ def solution(limit: int = 1_000_000) -> int:
             ind = np.arange(2 * i, limit + 1, i)  # indexes for selection
             phi[ind] -= phi[ind] // i
 
-    return np.sum(phi[2 : limit + 1])
+    return int(np.sum(phi[2 : limit + 1]))
 
 
 if __name__ == "__main__":
diff --git a/project_euler/problem_074/sol1.py b/project_euler/problem_074/sol1.py
index a257d4d94fa8..91440b3fd02b 100644
--- a/project_euler/problem_074/sol1.py
+++ b/project_euler/problem_074/sol1.py
@@ -27,7 +27,6 @@
 non-repeating terms?
 """
 
-
 DIGIT_FACTORIALS = {
     "0": 1,
     "1": 1,
diff --git a/project_euler/problem_074/sol2.py b/project_euler/problem_074/sol2.py
index b54bc023e387..52a996bfa51d 100644
--- a/project_euler/problem_074/sol2.py
+++ b/project_euler/problem_074/sol2.py
@@ -33,6 +33,7 @@
 is greater then the desired one.
 After generating each chain, the length is checked and the counter increases.
 """
+
 from math import factorial
 
 DIGIT_FACTORIAL: dict[str, int] = {str(digit): factorial(digit) for digit in range(10)}
diff --git a/project_euler/problem_077/sol1.py b/project_euler/problem_077/sol1.py
index 6098ea9e50a6..e8f4e979a625 100644
--- a/project_euler/problem_077/sol1.py
+++ b/project_euler/problem_077/sol1.py
@@ -12,6 +12,7 @@
 What is the first value which can be written as the sum of primes in over
 five thousand different ways?
 """
+
 from __future__ import annotations
 
 from functools import lru_cache
diff --git a/project_euler/problem_079/sol1.py b/project_euler/problem_079/sol1.py
index d34adcd243b0..74392e9bd094 100644
--- a/project_euler/problem_079/sol1.py
+++ b/project_euler/problem_079/sol1.py
@@ -13,6 +13,7 @@
 Given that the three characters are always asked for in order, analyse the file
 so as to determine the shortest possible secret passcode of unknown length.
 """
+
 import itertools
 from pathlib import Path
 
diff --git a/project_euler/problem_080/sol1.py b/project_euler/problem_080/sol1.py
index 916998bdd8ad..8cfcbd41b588 100644
--- a/project_euler/problem_080/sol1.py
+++ b/project_euler/problem_080/sol1.py
@@ -6,6 +6,7 @@
 square roots.
 Time: 5 October 2020, 18:30
 """
+
 import decimal
 
 
diff --git a/project_euler/problem_081/sol1.py b/project_euler/problem_081/sol1.py
index aef6106b54df..293027bddd0e 100644
--- a/project_euler/problem_081/sol1.py
+++ b/project_euler/problem_081/sol1.py
@@ -13,6 +13,7 @@
 and down in matrix.txt (https://projecteuler.net/project/resources/p081_matrix.txt),
 a 31K text file containing an 80 by 80 matrix.
 """
+
 import os
 
 
diff --git a/project_euler/problem_085/sol1.py b/project_euler/problem_085/sol1.py
index d0f29796498c..d0b361ee750d 100644
--- a/project_euler/problem_085/sol1.py
+++ b/project_euler/problem_085/sol1.py
@@ -44,6 +44,7 @@
 Reference: https://en.wikipedia.org/wiki/Triangular_number
            https://en.wikipedia.org/wiki/Quadratic_formula
 """
+
 from __future__ import annotations
 
 from math import ceil, floor, sqrt
diff --git a/project_euler/problem_086/sol1.py b/project_euler/problem_086/sol1.py
index 064af215c049..cbd2b648e0ac 100644
--- a/project_euler/problem_086/sol1.py
+++ b/project_euler/problem_086/sol1.py
@@ -66,7 +66,6 @@
 
 """
 
-
 from math import sqrt
 
 
diff --git a/project_euler/problem_089/__init__.py b/project_euler/problem_089/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_089/__init__.py
+++ b/project_euler/problem_089/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_091/sol1.py b/project_euler/problem_091/sol1.py
index 6c9aa3fa6c70..7db98fca0049 100644
--- a/project_euler/problem_091/sol1.py
+++ b/project_euler/problem_091/sol1.py
@@ -11,7 +11,6 @@
 Given that 0 ≤ x1, y1, x2, y2 ≤ 50, how many right triangles can be formed?
 """
 
-
 from itertools import combinations, product
 
 
diff --git a/project_euler/problem_092/sol1.py b/project_euler/problem_092/sol1.py
index 8d3f0c9ddd7b..3e45e82207a7 100644
--- a/project_euler/problem_092/sol1.py
+++ b/project_euler/problem_092/sol1.py
@@ -68,7 +68,7 @@ def chain(number: int) -> bool:
     """
 
     if CHAINS[number - 1] is not None:
-        return CHAINS[number - 1]  # type: ignore
+        return CHAINS[number - 1]  # type: ignore[return-value]
 
     number_chain = chain(next_number(number))
     CHAINS[number - 1] = number_chain
diff --git a/project_euler/problem_095/__init__.py b/project_euler/problem_095/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/project_euler/problem_095/sol1.py b/project_euler/problem_095/sol1.py
new file mode 100644
index 000000000000..82d84c5544de
--- /dev/null
+++ b/project_euler/problem_095/sol1.py
@@ -0,0 +1,164 @@
+"""
+Project Euler Problem 95: https://projecteuler.net/problem=95
+
+Amicable Chains
+
+The proper divisors of a number are all the divisors excluding the number itself.
+For example, the proper divisors of 28 are 1, 2, 4, 7, and 14.
+As the sum of these divisors is equal to 28, we call it a perfect number.
+
+Interestingly the sum of the proper divisors of 220 is 284 and
+the sum of the proper divisors of 284 is 220, forming a chain of two numbers.
+For this reason, 220 and 284 are called an amicable pair.
+
+Perhaps less well known are longer chains.
+For example, starting with 12496, we form a chain of five numbers:
+    12496 -> 14288 -> 15472 -> 14536 -> 14264 (-> 12496 -> ...)
+
+Since this chain returns to its starting point, it is called an amicable chain.
+
+Find the smallest member of the longest amicable chain with
+no element exceeding one million.
+
+Solution is doing the following:
+- Get relevant prime numbers
+- Iterate over product combination of prime numbers to generate all non-prime
+  numbers up to max number, by keeping track of prime factors
+- Calculate the sum of factors for each number
+- Iterate over found some factors to find longest chain
+"""
+
+from math import isqrt
+
+
+def generate_primes(max_num: int) -> list[int]:
+    """
+    Calculates the list of primes up to and including `max_num`.
+
+    >>> generate_primes(6)
+    [2, 3, 5]
+    """
+    are_primes = [True] * (max_num + 1)
+    are_primes[0] = are_primes[1] = False
+    for i in range(2, isqrt(max_num) + 1):
+        if are_primes[i]:
+            for j in range(i * i, max_num + 1, i):
+                are_primes[j] = False
+
+    return [prime for prime, is_prime in enumerate(are_primes) if is_prime]
+
+
+def multiply(
+    chain: list[int],
+    primes: list[int],
+    min_prime_idx: int,
+    prev_num: int,
+    max_num: int,
+    prev_sum: int,
+    primes_degrees: dict[int, int],
+) -> None:
+    """
+    Run over all prime combinations to generate non-prime numbers.
+
+    >>> chain = [0] * 3
+    >>> primes_degrees = {}
+    >>> multiply(
+    ...     chain=chain,
+    ...     primes=[2],
+    ...     min_prime_idx=0,
+    ...     prev_num=1,
+    ...     max_num=2,
+    ...     prev_sum=0,
+    ...     primes_degrees=primes_degrees,
+    ... )
+    >>> chain
+    [0, 0, 1]
+    >>> primes_degrees
+    {2: 1}
+    """
+
+    min_prime = primes[min_prime_idx]
+    num = prev_num * min_prime
+
+    min_prime_degree = primes_degrees.get(min_prime, 0)
+    min_prime_degree += 1
+    primes_degrees[min_prime] = min_prime_degree
+
+    new_sum = prev_sum * min_prime + (prev_sum + prev_num) * (min_prime - 1) // (
+        min_prime**min_prime_degree - 1
+    )
+    chain[num] = new_sum
+
+    for prime_idx in range(min_prime_idx, len(primes)):
+        if primes[prime_idx] * num > max_num:
+            break
+
+        multiply(
+            chain=chain,
+            primes=primes,
+            min_prime_idx=prime_idx,
+            prev_num=num,
+            max_num=max_num,
+            prev_sum=new_sum,
+            primes_degrees=primes_degrees.copy(),
+        )
+
+
+def find_longest_chain(chain: list[int], max_num: int) -> int:
+    """
+    Finds the smallest element of longest chain
+
+    >>> find_longest_chain(chain=[0, 0, 0, 0, 0, 0, 6], max_num=6)
+    6
+    """
+
+    max_len = 0
+    min_elem = 0
+    for start in range(2, len(chain)):
+        visited = {start}
+        elem = chain[start]
+        length = 1
+
+        while elem > 1 and elem <= max_num and elem not in visited:
+            visited.add(elem)
+            elem = chain[elem]
+            length += 1
+
+        if elem == start and length > max_len:
+            max_len = length
+            min_elem = start
+
+    return min_elem
+
+
+def solution(max_num: int = 1000000) -> int:
+    """
+    Runs the calculation for numbers <= `max_num`.
+
+    >>> solution(10)
+    6
+    >>> solution(200000)
+    12496
+    """
+
+    primes = generate_primes(max_num)
+    chain = [0] * (max_num + 1)
+    for prime_idx, prime in enumerate(primes):
+        if prime**2 > max_num:
+            break
+
+        multiply(
+            chain=chain,
+            primes=primes,
+            min_prime_idx=prime_idx,
+            prev_num=1,
+            max_num=max_num,
+            prev_sum=0,
+            primes_degrees={},
+        )
+
+    return find_longest_chain(chain=chain, max_num=max_num)
+
+
+if __name__ == "__main__":
+    print(f"{solution() = }")
diff --git a/project_euler/problem_097/__init__.py b/project_euler/problem_097/__init__.py
index 792d6005489e..e69de29bb2d1 100644
--- a/project_euler/problem_097/__init__.py
+++ b/project_euler/problem_097/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/project_euler/problem_097/sol1.py b/project_euler/problem_097/sol1.py
index 2807e893ded0..a349f3a1dbc9 100644
--- a/project_euler/problem_097/sol1.py
+++ b/project_euler/problem_097/sol1.py
@@ -1,7 +1,7 @@
 """
 The first known prime found to exceed one million digits was discovered in 1999,
-and is a Mersenne prime of the form 2**6972593 − 1; it contains exactly 2,098,960
-digits. Subsequently other Mersenne primes, of the form 2**p − 1, have been found
+and is a Mersenne prime of the form 2**6972593 - 1; it contains exactly 2,098,960
+digits. Subsequently other Mersenne primes, of the form 2**p - 1, have been found
 which contain more digits.
 However, in 2004 there was found a massive non-Mersenne prime which contains
 2,357,207 digits: (28433 * (2 ** 7830457 + 1)).
diff --git a/project_euler/problem_101/sol1.py b/project_euler/problem_101/sol1.py
index d5c503af796a..2d209333cf31 100644
--- a/project_euler/problem_101/sol1.py
+++ b/project_euler/problem_101/sol1.py
@@ -41,6 +41,7 @@
 
 Find the sum of FITs for the BOPs.
 """
+
 from __future__ import annotations
 
 from collections.abc import Callable
diff --git a/project_euler/problem_102/sol1.py b/project_euler/problem_102/sol1.py
index 4f6e6361e3e8..85fe5eac1e22 100644
--- a/project_euler/problem_102/sol1.py
+++ b/project_euler/problem_102/sol1.py
@@ -18,6 +18,7 @@
 NOTE: The first two examples in the file represent the triangles in the
 example given above.
 """
+
 from __future__ import annotations
 
 from pathlib import Path
diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py
index 60fd6fe99adb..a0267faa6a38 100644
--- a/project_euler/problem_104/sol1.py
+++ b/project_euler/problem_104/sol1.py
@@ -3,7 +3,7 @@
 
 The Fibonacci sequence is defined by the recurrence relation:
 
-Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1.
+Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1.
 It turns out that F541, which contains 113 digits, is the first Fibonacci number
 for which the last nine digits are 1-9 pandigital (contain all the digits 1 to 9,
 but not necessarily in order). And F2749, which contains 575 digits, is the first
@@ -15,7 +15,7 @@
 
 import sys
 
-sys.set_int_max_str_digits(0)  # type: ignore
+sys.set_int_max_str_digits(0)
 
 
 def check(number: int) -> bool:
diff --git a/project_euler/problem_107/sol1.py b/project_euler/problem_107/sol1.py
index 4659eac24bd3..79cdd937042e 100644
--- a/project_euler/problem_107/sol1.py
+++ b/project_euler/problem_107/sol1.py
@@ -27,6 +27,7 @@
     We use Prim's algorithm to find a Minimum Spanning Tree.
     Reference: https://en.wikipedia.org/wiki/Prim%27s_algorithm
 """
+
 from __future__ import annotations
 
 import os
@@ -80,10 +81,11 @@ def prims_algorithm(self) -> Graph:
         while len(subgraph.vertices) < len(self.vertices):
             min_weight = max(self.edges.values()) + 1
             for edge, weight in self.edges.items():
-                if (edge[0] in subgraph.vertices) ^ (edge[1] in subgraph.vertices):
-                    if weight < min_weight:
-                        min_edge = edge
-                        min_weight = weight
+                if (edge[0] in subgraph.vertices) ^ (
+                    edge[1] in subgraph.vertices
+                ) and weight < min_weight:
+                    min_edge = edge
+                    min_weight = weight
 
             subgraph.add_edge(min_edge, min_weight)
 
diff --git a/project_euler/problem_120/sol1.py b/project_euler/problem_120/sol1.py
index 0e6821214560..2f403972502f 100644
--- a/project_euler/problem_120/sol1.py
+++ b/project_euler/problem_120/sol1.py
@@ -3,7 +3,7 @@
 
 Description:
 
-Let r be the remainder when (a−1)^n + (a+1)^n is divided by a^2.
+Let r be the remainder when (a-1)^n + (a+1)^n is divided by a^2.
 For example, if a = 7 and n = 3, then r = 42: 6^3 + 8^3 = 728 ≡ 42 mod 49.
 And as n varies, so too will r, but for a = 7 it turns out that r_max = 42.
 For 3 ≤ a ≤ 1000, find ∑ r_max.
diff --git a/project_euler/problem_122/__init__.py b/project_euler/problem_122/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/project_euler/problem_122/sol1.py b/project_euler/problem_122/sol1.py
new file mode 100644
index 000000000000..cd8b1e67708c
--- /dev/null
+++ b/project_euler/problem_122/sol1.py
@@ -0,0 +1,89 @@
+"""
+Project Euler Problem 122: https://projecteuler.net/problem=122
+
+Efficient Exponentiation
+
+The most naive way of computing n^15 requires fourteen multiplications:
+
+                                               n x n x ... x n = n^15.
+
+But using a "binary" method you can compute it in six multiplications:
+
+                                                         n x n = n^2
+                                                     n^2 x n^2 = n^4
+                                                     n^4 x n^4 = n^8
+                                                     n^8 x n^4 = n^12
+                                                    n^12 x n^2 = n^14
+                                                      n^14 x n = n^15
+
+However it is yet possible to compute it in only five multiplications:
+
+                                                                n x n = n^2
+                                                              n^2 x n = n^3
+                                                            n^3 x n^3 = n^6
+                                                            n^6 x n^6 = n^12
+                                                           n^12 x n^3 = n^15
+
+We shall define m(k) to be the minimum number of multiplications to compute n^k;
+for example m(15) = 5.
+
+Find sum_{k = 1}^200 m(k).
+
+It uses the fact that for rather small n, applicable for this problem, the solution
+for each number can be formed by increasing the largest element.
+
+References:
+- https://en.wikipedia.org/wiki/Addition_chain
+"""
+
+
+def solve(nums: list[int], goal: int, depth: int) -> bool:
+    """
+    Checks if nums can have a sum equal to goal, given that length of nums does
+    not exceed depth.
+
+    >>> solve([1], 2, 2)
+    True
+    >>> solve([1], 2, 0)
+    False
+    """
+    if len(nums) > depth:
+        return False
+    for el in nums:
+        if el + nums[-1] == goal:
+            return True
+        nums.append(el + nums[-1])
+        if solve(nums=nums, goal=goal, depth=depth):
+            return True
+        del nums[-1]
+    return False
+
+
+def solution(n: int = 200) -> int:
+    """
+    Calculates sum of smallest number of multiplactions for each number up to
+    and including n.
+
+    >>> solution(1)
+    0
+    >>> solution(2)
+    1
+    >>> solution(14)
+    45
+    >>> solution(15)
+    50
+    """
+    total = 0
+    for i in range(2, n + 1):
+        max_length = 0
+        while True:
+            nums = [1]
+            max_length += 1
+            if solve(nums=nums, goal=i, depth=max_length):
+                break
+        total += max_length
+    return total
+
+
+if __name__ == "__main__":
+    print(f"{solution() = }")
diff --git a/project_euler/problem_123/sol1.py b/project_euler/problem_123/sol1.py
index f74cdd999401..265348d2d4c8 100644
--- a/project_euler/problem_123/sol1.py
+++ b/project_euler/problem_123/sol1.py
@@ -4,7 +4,7 @@
 Name: Prime square remainders
 
 Let pn be the nth prime: 2, 3, 5, 7, 11, ..., and
-let r be the remainder when (pn−1)^n + (pn+1)^n is divided by pn^2.
+let r be the remainder when (pn-1)^n + (pn+1)^n is divided by pn^2.
 
 For example, when n = 3, p3 = 5, and 43 + 63 = 280 ≡ 5 mod 25.
 The least value of n for which the remainder first exceeds 10^9 is 7037.
@@ -37,12 +37,13 @@
     r = 2pn when n is odd
     r = 2   when n is even.
 """
+
 from __future__ import annotations
 
 from collections.abc import Generator
 
 
-def sieve() -> Generator[int, None, None]:
+def sieve() -> Generator[int]:
     """
     Returns a prime number generator using sieve method.
     >>> type(sieve())
diff --git a/project_euler/problem_135/sol1.py b/project_euler/problem_135/sol1.py
index ac91fa4e2b9d..d57ace489191 100644
--- a/project_euler/problem_135/sol1.py
+++ b/project_euler/problem_135/sol1.py
@@ -3,9 +3,9 @@
 
 Given the positive integers, x, y, and z, are consecutive terms of an arithmetic
 progression, the least value of the positive integer, n, for which the equation,
-x2 − y2 − z2 = n, has exactly two solutions is n = 27:
+x2 - y2 - z2 = n, has exactly two solutions is n = 27:
 
-342 − 272 − 202 = 122 − 92 − 62 = 27
+342 - 272 - 202 = 122 - 92 - 62 = 27
 
 It turns out that n = 1155 is the least value which has exactly ten solutions.
 
diff --git a/project_euler/problem_136/__init__.py b/project_euler/problem_136/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/project_euler/problem_136/sol1.py b/project_euler/problem_136/sol1.py
new file mode 100644
index 000000000000..688a9a5d7f24
--- /dev/null
+++ b/project_euler/problem_136/sol1.py
@@ -0,0 +1,63 @@
+"""
+Project Euler Problem 136: https://projecteuler.net/problem=136
+
+Singleton Difference
+
+The positive integers, x, y, and z, are consecutive terms of an arithmetic progression.
+Given that n is a positive integer, the equation, x^2 - y^2 - z^2 = n,
+has exactly one solution when n = 20:
+                              13^2 - 10^2 - 7^2 = 20.
+
+In fact there are twenty-five values of n below one hundred for which
+the equation has a unique solution.
+
+How many values of n less than fifty million have exactly one solution?
+
+By change of variables
+
+x = y + delta
+z = y - delta
+
+The expression can be rewritten:
+
+x^2 - y^2 - z^2 = y * (4 * delta - y) = n
+
+The algorithm loops over delta and y, which is restricted in upper and lower limits,
+to count how many solutions each n has.
+In the end it is counted how many n's have one solution.
+"""
+
+
+def solution(n_limit: int = 50 * 10**6) -> int:
+    """
+    Define n count list and loop over delta, y to get the counts, then check
+    which n has count == 1.
+
+    >>> solution(3)
+    0
+    >>> solution(10)
+    3
+    >>> solution(100)
+    25
+    >>> solution(110)
+    27
+    """
+    n_sol = [0] * n_limit
+
+    for delta in range(1, (n_limit + 1) // 4 + 1):
+        for y in range(4 * delta - 1, delta, -1):
+            n = y * (4 * delta - y)
+            if n >= n_limit:
+                break
+            n_sol[n] += 1
+
+    ans = 0
+    for i in range(n_limit):
+        if n_sol[i] == 1:
+            ans += 1
+
+    return ans
+
+
+if __name__ == "__main__":
+    print(f"{solution() = }")
diff --git a/project_euler/problem_144/sol1.py b/project_euler/problem_144/sol1.py
index b5f103b64ff5..9070455de79f 100644
--- a/project_euler/problem_144/sol1.py
+++ b/project_euler/problem_144/sol1.py
@@ -6,7 +6,7 @@
 The specific white cell we will be considering is an ellipse with the equation
 4x^2 + y^2 = 100
 
-The section corresponding to −0.01 ≤ x ≤ +0.01 at the top is missing, allowing the
+The section corresponding to -0.01 ≤ x ≤ +0.01 at the top is missing, allowing the
 light to enter and exit through the hole.
 ￼￼
 The light beam in this problem starts at the point (0.0,10.1) just outside the white
@@ -20,7 +20,7 @@
 the laser beam and the wall of the white cell; the blue line shows the line tangent to
 the ellipse at the point of incidence of the first bounce.
 
-The slope m of the tangent line at any point (x,y) of the given ellipse is: m = −4x/y
+The slope m of the tangent line at any point (x,y) of the given ellipse is: m = -4x/y
 
 The normal line is perpendicular to this tangent line at the point of incidence.
 
@@ -29,7 +29,6 @@
 How many times does the beam hit the internal surface of the white cell before exiting?
 """
 
-
 from math import isclose, sqrt
 
 
diff --git a/project_euler/problem_145/sol1.py b/project_euler/problem_145/sol1.py
index 71b851178fdb..583bb03a0a90 100644
--- a/project_euler/problem_145/sol1.py
+++ b/project_euler/problem_145/sol1.py
@@ -13,6 +13,7 @@
 
 How many reversible numbers are there below one-billion (10^9)?
 """
+
 EVEN_DIGITS = [0, 2, 4, 6, 8]
 ODD_DIGITS = [1, 3, 5, 7, 9]
 
@@ -109,7 +110,7 @@ def reversible_numbers(
     if (length - 1) % 4 == 0:
         return 0
 
-    return slow_reversible_numbers(length, 0, [0] * length, length)
+    return slow_reversible_numbers(remaining_length, remainder, digits, length)
 
 
 def solution(max_power: int = 9) -> int:
diff --git a/project_euler/problem_164/__init__.py b/project_euler/problem_164/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/project_euler/problem_164/sol1.py b/project_euler/problem_164/sol1.py
new file mode 100644
index 000000000000..5387c89bd757
--- /dev/null
+++ b/project_euler/problem_164/sol1.py
@@ -0,0 +1,65 @@
+"""
+Project Euler Problem 164: https://projecteuler.net/problem=164
+
+Three Consecutive Digital Sum Limit
+
+How many 20 digit numbers n (without any leading zero) exist such that no three
+consecutive digits of n have a sum greater than 9?
+
+Brute-force recursive solution with caching of intermediate results.
+"""
+
+
+def solve(
+    digit: int, prev1: int, prev2: int, sum_max: int, first: bool, cache: dict[str, int]
+) -> int:
+    """
+    Solve for remaining 'digit' digits, with previous 'prev1' digit, and
+    previous-previous 'prev2' digit, total sum of 'sum_max'.
+    Pass around 'cache' to store/reuse intermediate results.
+
+    >>> solve(digit=1, prev1=0, prev2=0, sum_max=9, first=True, cache={})
+    9
+    >>> solve(digit=1, prev1=0, prev2=0, sum_max=9, first=False, cache={})
+    10
+    """
+    if digit == 0:
+        return 1
+
+    cache_str = f"{digit},{prev1},{prev2}"
+    if cache_str in cache:
+        return cache[cache_str]
+
+    comb = 0
+    for curr in range(sum_max - prev1 - prev2 + 1):
+        if first and curr == 0:
+            continue
+
+        comb += solve(
+            digit=digit - 1,
+            prev1=curr,
+            prev2=prev1,
+            sum_max=sum_max,
+            first=False,
+            cache=cache,
+        )
+
+    cache[cache_str] = comb
+    return comb
+
+
+def solution(n_digits: int = 20) -> int:
+    """
+    Solves the problem for n_digits number of digits.
+
+    >>> solution(2)
+    45
+    >>> solution(10)
+    21838806
+    """
+    cache: dict[str, int] = {}
+    return solve(digit=n_digits, prev1=0, prev2=0, sum_max=9, first=True, cache=cache)
+
+
+if __name__ == "__main__":
+    print(f"{solution(10) = }")
diff --git a/project_euler/problem_173/sol1.py b/project_euler/problem_173/sol1.py
index 5416e25462cc..9235d00e1752 100644
--- a/project_euler/problem_173/sol1.py
+++ b/project_euler/problem_173/sol1.py
@@ -11,7 +11,6 @@
 Using up to one million tiles how many different square laminae can be formed?
 """
 
-
 from math import ceil, sqrt
 
 
diff --git a/project_euler/problem_174/sol1.py b/project_euler/problem_174/sol1.py
index cbc0df5a9d65..9a75e8638880 100644
--- a/project_euler/problem_174/sol1.py
+++ b/project_euler/problem_174/sol1.py
@@ -14,7 +14,7 @@
 Let N(n) be the number of t ≤ 1000000 such that t is type L(n); for example,
 N(15) = 832.
 
-What is ∑ N(n) for 1 ≤ n ≤ 10?
+What is sum N(n) for 1 ≤ n ≤ 10?
 """
 
 from collections import defaultdict
@@ -26,6 +26,8 @@ def solution(t_limit: int = 1000000, n_limit: int = 10) -> int:
     Return the sum of N(n) for 1 <= n <= n_limit.
 
     >>> solution(1000,5)
+    222
+    >>> solution(1000,10)
     249
     >>> solution(10000,10)
     2383
@@ -45,7 +47,7 @@ def solution(t_limit: int = 1000000, n_limit: int = 10) -> int:
         for hole_width in range(hole_width_lower_bound, outer_width - 1, 2):
             count[outer_width * outer_width - hole_width * hole_width] += 1
 
-    return sum(1 for n in count.values() if 1 <= n <= 10)
+    return sum(1 for n in count.values() if 1 <= n <= n_limit)
 
 
 if __name__ == "__main__":
diff --git a/project_euler/problem_180/sol1.py b/project_euler/problem_180/sol1.py
index 12e34dcaa76b..72baed42b99e 100644
--- a/project_euler/problem_180/sol1.py
+++ b/project_euler/problem_180/sol1.py
@@ -44,6 +44,7 @@
 Reference:
 https://en.wikipedia.org/wiki/Fermat%27s_Last_Theorem
 """
+
 from __future__ import annotations
 
 from fractions import Fraction
diff --git a/project_euler/problem_190/__init__.py b/project_euler/problem_190/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/project_euler/problem_190/sol1.py b/project_euler/problem_190/sol1.py
new file mode 100644
index 000000000000..b18d45be16b4
--- /dev/null
+++ b/project_euler/problem_190/sol1.py
@@ -0,0 +1,48 @@
+"""
+Project Euler Problem 190: https://projecteuler.net/problem=190
+
+Maximising a Weighted Product
+
+Let S_m = (x_1, x_2, ..., x_m) be the m-tuple of positive real numbers with
+x_1 + x_2 + ... + x_m = m for which P_m = x_1 * x_2^2 * ... * x_m^m is maximised.
+
+For example, it can be verified that |_ P_10 _| = 4112
+(|_ _| is the integer part function).
+
+Find Sum_{m=2}^15 = |_ P_m _|.
+
+Solution:
+- Fix x_1 = m - x_2 - ... - x_m.
+- Calculate partial derivatives of P_m wrt the x_2, ..., x_m. This gives that
+  x_2 = 2 * x_1, x_3 = 3 * x_1, ..., x_m = m * x_1.
+- Calculate partial second order derivatives of P_m wrt the x_2, ..., x_m.
+  By plugging in the values from the previous step, can verify that solution is maximum.
+"""
+
+
+def solution(n: int = 15) -> int:
+    """
+    Calculate sum of |_ P_m _| for m from 2 to n.
+
+    >>> solution(2)
+    1
+    >>> solution(3)
+    2
+    >>> solution(4)
+    4
+    >>> solution(5)
+    10
+    """
+    total = 0
+    for m in range(2, n + 1):
+        x1 = 2 / (m + 1)
+        p = 1.0
+        for i in range(1, m + 1):
+            xi = i * x1
+            p *= xi**i
+        total += int(p)
+    return total
+
+
+if __name__ == "__main__":
+    print(f"{solution() = }")
diff --git a/project_euler/problem_191/sol1.py b/project_euler/problem_191/sol1.py
index 6bff9d54eeca..efb2a5d086ad 100644
--- a/project_euler/problem_191/sol1.py
+++ b/project_euler/problem_191/sol1.py
@@ -25,7 +25,6 @@
       https://projecteuler.net/problem=191
 """
 
-
 cache: dict[tuple[int, int, int], int] = {}
 
 
diff --git a/project_euler/problem_203/sol1.py b/project_euler/problem_203/sol1.py
index da9436246a7c..8ad089ec09aa 100644
--- a/project_euler/problem_203/sol1.py
+++ b/project_euler/problem_203/sol1.py
@@ -27,6 +27,7 @@
 References:
 - https://en.wikipedia.org/wiki/Pascal%27s_triangle
 """
+
 from __future__ import annotations
 
 
diff --git a/project_euler/problem_207/sol1.py b/project_euler/problem_207/sol1.py
index 2b3591f51cfa..c83dc1d4aaef 100644
--- a/project_euler/problem_207/sol1.py
+++ b/project_euler/problem_207/sol1.py
@@ -88,9 +88,11 @@ def solution(max_proportion: float = 1 / 12345) -> int:
             total_partitions += 1
             if check_partition_perfect(partition_candidate):
                 perfect_partitions += 1
-        if perfect_partitions > 0:
-            if perfect_partitions / total_partitions < max_proportion:
-                return int(partition_candidate)
+        if (
+            perfect_partitions > 0
+            and perfect_partitions / total_partitions < max_proportion
+        ):
+            return int(partition_candidate)
         integer += 1
 
 
diff --git a/project_euler/problem_345/__init__.py b/project_euler/problem_345/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/project_euler/problem_345/sol1.py b/project_euler/problem_345/sol1.py
new file mode 100644
index 000000000000..4234458c5ad5
--- /dev/null
+++ b/project_euler/problem_345/sol1.py
@@ -0,0 +1,117 @@
+"""
+Project Euler Problem 345: https://projecteuler.net/problem=345
+
+Matrix Sum
+
+We define the Matrix Sum of a matrix as the maximum possible sum of
+matrix elements such that none of the selected elements share the same row or column.
+
+For example, the Matrix Sum of the matrix below equals
+3315 ( = 863 + 383 + 343 + 959 + 767):
+      7  53 183 439 863
+    497 383 563  79 973
+    287  63 343 169 583
+    627 343 773 959 943
+    767 473 103 699 303
+
+Find the Matrix Sum of:
+      7  53 183 439 863 497 383 563  79 973 287  63 343 169 583
+    627 343 773 959 943 767 473 103 699 303 957 703 583 639 913
+    447 283 463  29  23 487 463 993 119 883 327 493 423 159 743
+    217 623   3 399 853 407 103 983  89 463 290 516 212 462 350
+    960 376 682 962 300 780 486 502 912 800 250 346 172 812 350
+    870 456 192 162 593 473 915  45 989 873 823 965 425 329 803
+    973 965 905 919 133 673 665 235 509 613 673 815 165 992 326
+    322 148 972 962 286 255 941 541 265 323 925 281 601  95 973
+    445 721  11 525 473  65 511 164 138 672  18 428 154 448 848
+    414 456 310 312 798 104 566 520 302 248 694 976 430 392 198
+    184 829 373 181 631 101 969 613 840 740 778 458 284 760 390
+    821 461 843 513  17 901 711 993 293 157 274  94 192 156 574
+     34 124   4 878 450 476 712 914 838 669 875 299 823 329 699
+    815 559 813 459 522 788 168 586 966 232 308 833 251 631 107
+    813 883 451 509 615  77 281 613 459 205 380 274 302  35 805
+
+Brute force solution, with caching intermediate steps to speed up the calculation.
+"""
+
+import numpy as np
+from numpy.typing import NDArray
+
+MATRIX_1 = [
+    "7 53 183 439 863",
+    "497 383 563 79 973",
+    "287 63 343 169 583",
+    "627 343 773 959 943",
+    "767 473 103 699 303",
+]
+
+MATRIX_2 = [
+    "7 53 183 439 863 497 383 563 79 973 287 63 343 169 583",
+    "627 343 773 959 943 767 473 103 699 303 957 703 583 639 913",
+    "447 283 463 29 23 487 463 993 119 883 327 493 423 159 743",
+    "217 623 3 399 853 407 103 983 89 463 290 516 212 462 350",
+    "960 376 682 962 300 780 486 502 912 800 250 346 172 812 350",
+    "870 456 192 162 593 473 915 45 989 873 823 965 425 329 803",
+    "973 965 905 919 133 673 665 235 509 613 673 815 165 992 326",
+    "322 148 972 962 286 255 941 541 265 323 925 281 601 95 973",
+    "445 721 11 525 473 65 511 164 138 672 18 428 154 448 848",
+    "414 456 310 312 798 104 566 520 302 248 694 976 430 392 198",
+    "184 829 373 181 631 101 969 613 840 740 778 458 284 760 390",
+    "821 461 843 513 17 901 711 993 293 157 274 94 192 156 574",
+    "34 124 4 878 450 476 712 914 838 669 875 299 823 329 699",
+    "815 559 813 459 522 788 168 586 966 232 308 833 251 631 107",
+    "813 883 451 509 615 77 281 613 459 205 380 274 302 35 805",
+]
+
+
+def solve(arr: NDArray, row: int, cols: set[int], cache: dict[str, int]) -> int:
+    """
+    Finds the max sum for array `arr` starting with row index `row`, and with columns
+    included in `cols`. `cache` is used for caching intermediate results.
+
+    >>> solve(arr=np.array([[1, 2], [3, 4]]), row=0, cols={0, 1}, cache={})
+    5
+    """
+
+    cache_id = f"{row}, {sorted(cols)}"
+    if cache_id in cache:
+        return cache[cache_id]
+
+    if row == len(arr):
+        return 0
+
+    max_sum = 0
+    for col in cols:
+        new_cols = cols - {col}
+        max_sum = max(
+            max_sum,
+            int(arr[row, col])
+            + solve(arr=arr, row=row + 1, cols=new_cols, cache=cache),
+        )
+    cache[cache_id] = max_sum
+    return max_sum
+
+
+def solution(matrix_str: list[str] = MATRIX_2) -> int:
+    """
+    Takes list of strings `matrix_str` to parse the matrix and calculates the max sum.
+
+    >>> solution(["1 2", "3 4"])
+    5
+    >>> solution(MATRIX_1)
+    3315
+    """
+
+    n = len(matrix_str)
+    arr = np.empty(shape=(n, n), dtype=int)
+    for row, matrix_row_str in enumerate(matrix_str):
+        matrix_row_list_str = matrix_row_str.split()
+        for col, elem_str in enumerate(matrix_row_list_str):
+            arr[row, col] = int(elem_str)
+
+    cache: dict[str, int] = {}
+    return solve(arr=arr, row=0, cols=set(range(n)), cache=cache)
+
+
+if __name__ == "__main__":
+    print(f"{solution() = }")
diff --git a/project_euler/problem_551/sol1.py b/project_euler/problem_551/sol1.py
index 2cd75efbb68d..100e9d41dd31 100644
--- a/project_euler/problem_551/sol1.py
+++ b/project_euler/problem_551/sol1.py
@@ -12,7 +12,6 @@
 Find a(10^15)
 """
 
-
 ks = range(2, 20 + 1)
 base = [10**k for k in range(ks[-1] + 1)]
 memo: dict[int, dict[int, list[list[int]]]] = {}
diff --git a/pyproject.toml b/pyproject.toml
index 9c9262d77748..b680cc0d439e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,79 +1,99 @@
+[project]
+name = "thealgorithms-python"
+version = "0.0.1"
+description = "TheAlgorithms in Python"
+authors = [ { name = "TheAlgorithms Contributors" } ]
+requires-python = ">=3.13"
+classifiers = [
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+  "beautifulsoup4>=4.12.3",
+  "fake-useragent>=1.5.1",
+  "httpx>=0.28.1",
+  "imageio>=2.36.1",
+  "keras>=3.7",
+  "lxml>=5.3",
+  "matplotlib>=3.9.3",
+  "numpy>=2.1.3",
+  "opencv-python>=4.10.0.84",
+  "pandas>=2.2.3",
+  "pillow>=11",
+  "rich>=13.9.4",
+  "scikit-learn>=1.5.2",
+  "sphinx-pyproject>=0.3",
+  "statsmodels>=0.14.4",
+  "sympy>=1.13.3",
+  "tweepy>=4.14",
+  "typing-extensions>=4.12.2",
+  "xgboost>=2.1.3",
+]
+
+[dependency-groups]
+test = [
+  "pytest>=8.3.4",
+  "pytest-cov>=6",
+]
+
+docs = [
+  "myst-parser>=4",
+  "sphinx-autoapi>=3.4",
+  "sphinx-pyproject>=0.3",
+]
+euler-validate = [
+  "httpx>=0.28.1",
+  "numpy>=2.1.3",
+]
+
 [tool.ruff]
-ignore = [    # `ruff rule S101` for a description of that rule
-  "ARG001",   # Unused function argument `amount` -- FIX ME?
-  "B904",     # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME
-  "B905",     # `zip()` without an explicit `strict=` parameter -- FIX ME
-  "DTZ001",   # The use of `datetime.datetime()` without `tzinfo` argument is not allowed -- FIX ME
-  "DTZ005",   # The use of `datetime.datetime.now()` without `tzinfo` argument is not allowed -- FIX ME
-  "E741",     # Ambiguous variable name 'l' -- FIX ME
-  "EM101",    # Exception must not use a string literal, assign to variable first
-  "EXE001",   # Shebang is present but file is not executable" -- FIX ME
-  "G004",     # Logging statement uses f-string
-  "ICN001",   # `matplotlib.pyplot` should be imported as `plt` -- FIX ME
-  "INP001",   # File `x/y/z.py` is part of an implicit namespace package. Add an `__init__.py`. -- FIX ME
-  "N999",     # Invalid module name -- FIX ME
-  "NPY002",   # Replace legacy `np.random.choice` call with `np.random.Generator` -- FIX ME
-  "PGH003",   # Use specific rule codes when ignoring type issues -- FIX ME
-  "PLC1901",  # `{}` can be simplified to `{}` as an empty string is falsey
-  "PLR5501",  # Consider using `elif` instead of `else` -- FIX ME
-  "PLW0120",  # `else` clause on loop without a `break` statement -- FIX ME
-  "PLW060",   # Using global for `{name}` but no assignment is done -- DO NOT FIX
-  "PLW2901",  # PLW2901: Redefined loop variable -- FIX ME
-  "PT011",    # `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception
-  "PT018",    # Assertion should be broken down into multiple parts
-  "RUF00",    # Ambiguous unicode character and other rules
-  "RUF100",   # Unused `noqa` directive -- FIX ME
-  "S101",     # Use of `assert` detected -- DO NOT FIX
-  "S105",     # Possible hardcoded password: 'password'
-  "S113",     # Probable use of requests call without timeout -- FIX ME
-  "S311",     # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME
-  "SIM102",   # Use a single `if` statement instead of nested `if` statements -- FIX ME
-  "SLF001",   # Private member accessed: `_Iterator` -- FIX ME
-  "UP038",    # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX
-]
-select = [  # https://beta.ruff.rs/docs/rules
-  "A",      # flake8-builtins
-  "ARG",    # flake8-unused-arguments
-  "ASYNC",  # flake8-async
-  "B",      # flake8-bugbear
-  "BLE",    # flake8-blind-except
-  "C4",     # flake8-comprehensions
-  "C90",    # McCabe cyclomatic complexity
-  "DJ",     # flake8-django
-  "DTZ",    # flake8-datetimez
-  "E",      # pycodestyle
-  "EM",     # flake8-errmsg
-  "EXE",    # flake8-executable
-  "F",      # Pyflakes
-  "FA",     # flake8-future-annotations
-  "FLY",    # flynt
-  "G",      # flake8-logging-format
-  "I",      # isort
-  "ICN",    # flake8-import-conventions
-  "INP",    # flake8-no-pep420
-  "INT",    # flake8-gettext
-  "ISC",  # flake8-implicit-str-concat
-  "N",      # pep8-naming
-  "NPY",    # NumPy-specific rules
-  "PD",   # pandas-vet
-  "PGH",    # pygrep-hooks
-  "PIE",    # flake8-pie
-  "PL",     # Pylint
-  "PT",   # flake8-pytest-style
-  "PYI",    # flake8-pyi
-  "RSE",    # flake8-raise
-  "RUF",    # Ruff-specific rules
-  "S",      # flake8-bandit
-  "SIM",    # flake8-simplify
-  "SLF",    # flake8-self
-  "T10",    # flake8-debugger
-  "TD",     # flake8-todos
-  "TID",    # flake8-tidy-imports
-  "UP",     # pyupgrade
-  "W",      # pycodestyle
-  "YTT",    # flake8-2020
-  # "ANN",  # flake8-annotations  # FIX ME?
-  # "COM",  # flake8-commas
+target-version = "py313"
+
+output-format = "full"
+lint.select = [
+  # https://beta.ruff.rs/docs/rules
+  "A",     # flake8-builtins
+  "ARG",   # flake8-unused-arguments
+  "ASYNC", # flake8-async
+  "B",     # flake8-bugbear
+  "BLE",   # flake8-blind-except
+  "C4",    # flake8-comprehensions
+  "C90",   # McCabe cyclomatic complexity
+  "DJ",    # flake8-django
+  "DTZ",   # flake8-datetimez
+  "E",     # pycodestyle
+  "EM",    # flake8-errmsg
+  "EXE",   # flake8-executable
+  "F",     # Pyflakes
+  "FA",    # flake8-future-annotations
+  "FLY",   # flynt
+  "G",     # flake8-logging-format
+  "I",     # isort
+  "ICN",   # flake8-import-conventions
+  "INP",   # flake8-no-pep420
+  "INT",   # flake8-gettext
+  "ISC",   # flake8-implicit-str-concat
+  "N",     # pep8-naming
+  "NPY",   # NumPy-specific rules
+  "PD",    # pandas-vet
+  "PGH",   # pygrep-hooks
+  "PIE",   # flake8-pie
+  "PL",    # Pylint
+  "PT",    # flake8-pytest-style
+  "PYI",   # flake8-pyi
+  "RSE",   # flake8-raise
+  "RUF",   # Ruff-specific rules
+  "S",     # flake8-bandit
+  "SIM",   # flake8-simplify
+  "SLF",   # flake8-self
+  "T10",   # flake8-debugger
+  "TD",    # flake8-todos
+  "TID",   # flake8-tidy-imports
+  "UP",    # pyupgrade
+  "W",     # pycodestyle
+  "YTT",   # flake8-2020
+  # "ANN",  # flake8-annotations -- FIX ME?
+  # "COM",  # flake8-commas -- DO NOT FIX
   # "D",    # pydocstyle -- FIX ME?
   # "ERA",  # eradicate -- DO NOT FIX
   # "FBT",  # flake8-boolean-trap  # FIX ME
@@ -84,56 +104,162 @@ select = [  # https://beta.ruff.rs/docs/rules
   # "TCH",  # flake8-type-checking
   # "TRY",  # tryceratops
 ]
-show-source = true
-target-version = "py311"
-
-[tool.ruff.mccabe]   # DO NOT INCREASE THIS VALUE
-max-complexity = 17  # default: 10
+lint.ignore = [
+  # `ruff rule S101` for a description of that rule
+  "B904",    # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME
+  "B905",    # `zip()` without an explicit `strict=` parameter -- FIX ME
+  "EM101",   # Exception must not use a string literal, assign to variable first
+  "EXE001",  # Shebang is present but file is not executable -- DO NOT FIX
+  "G004",    # Logging statement uses f-string
+  "ISC001",  # Conflicts with ruff format -- DO NOT FIX
+  "PLC0415", # import-outside-top-level -- DO NOT FIX
+  "PLC1901", # `{}` can be simplified to `{}` as an empty string is falsey
+  "PLW060",  # Using global for `{name}` but no assignment is done -- DO NOT FIX
+  "PLW1641", # eq-without-hash
+  "PLW2901", # PLW2901: Redefined loop variable -- FIX ME
+  "PT011",   # `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception
+  "PT018",   # Assertion should be broken down into multiple parts
+  "PT028",   # pytest-parameter-with-default-argument
+  "S101",    # Use of `assert` detected -- DO NOT FIX
+  "S311",    # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME
+  "SIM905",  # Consider using a list literal instead of `str.split` -- DO NOT FIX
+  "SLF001",  # Private member accessed: `_Iterator` -- FIX ME
+  "UP038",   # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX
+]
 
-[tool.ruff.per-file-ignores]
-"arithmetic_analysis/newton_raphson.py" = ["PGH001"]
-"audio_filters/show_response.py" = ["ARG002"]
-"data_structures/binary_tree/binary_search_tree_recursive.py" = ["BLE001"]
-"data_structures/binary_tree/treap.py" = ["SIM114"]
-"data_structures/hashing/hash_table.py" = ["ARG002"]
-"data_structures/hashing/quadratic_probing.py" = ["ARG002"]
-"data_structures/hashing/tests/test_hash_map.py" = ["BLE001"]
-"data_structures/heap/max_heap.py" = ["SIM114"]
-"graphs/minimum_spanning_tree_prims.py" = ["SIM114"]
-"hashes/enigma_machine.py" = ["BLE001"]
-"machine_learning/decision_tree.py" = ["SIM114"]
-"machine_learning/linear_discriminant_analysis.py" = ["ARG005"]
-"machine_learning/sequential_minimum_optimization.py" = ["SIM115"]
-"matrix/sherman_morrison.py" = ["SIM103", "SIM114"]
-"other/l*u_cache.py" = ["RUF012"]
-"physics/newtons_second_law_of_motion.py" = ["BLE001"]
-"project_euler/problem_099/sol1.py" = ["SIM115"]
-"sorts/external_sort.py" = ["SIM115"]
+lint.per-file-ignores."data_structures/hashing/tests/test_hash_map.py" = [
+  "BLE001",
+]
+lint.per-file-ignores."hashes/enigma_machine.py" = [
+  "BLE001",
+]
+lint.per-file-ignores."machine_learning/sequential_minimum_optimization.py" = [
+  "SIM115",
+]
+lint.per-file-ignores."matrix/sherman_morrison.py" = [
+  "SIM103",
+]
+lint.per-file-ignores."physics/newtons_second_law_of_motion.py" = [
+  "BLE001",
+]
+lint.per-file-ignores."project_euler/problem_099/sol1.py" = [
+  "SIM115",
+]
+lint.per-file-ignores."sorts/external_sort.py" = [
+  "SIM115",
+]
+lint.mccabe.max-complexity = 17 # default: 10
+lint.pylint.allow-magic-value-types = [
+  "float",
+  "int",
+  "str",
+]
+lint.pylint.max-args = 10 # default: 5
+lint.pylint.max-branches = 20 # default: 12
+lint.pylint.max-returns = 8 # default: 6
+lint.pylint.max-statements = 88 # default: 50
 
-[tool.ruff.pylint]   # DO NOT INCREASE THESE VALUES
-allow-magic-value-types = ["float", "int", "str"]
-max-args = 10        # default: 5
-max-branches = 20    # default: 12
-max-returns = 8      # default: 6
-max-statements = 88  # default: 50
+[tool.codespell]
+ignore-words-list = "3rt,abd,aer,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar"
+skip = "./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt"
 
 [tool.pytest.ini_options]
 markers = [
-    "mat_ops: mark a test as utilizing matrix operations.",
+  "mat_ops: mark a test as utilizing matrix operations.",
 ]
 addopts = [
-    "--durations=10",
-    "--doctest-modules",
-    "--showlocals",
+  "--durations=10",
+  "--doctest-modules",
+  "--showlocals",
 ]
 
 [tool.coverage.report]
 omit = [
   ".env/*",
-  "project_euler/*"
+  "project_euler/*",
 ]
 sort = "Cover"
 
-[tool.codespell]
-ignore-words-list = "3rt,ans,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,zar"
-skip = "./.*,*.json,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt"
+[tool.sphinx-pyproject]
+copyright = "2014, TheAlgorithms"
+autoapi_dirs = [
+  "audio_filters",
+  "backtracking",
+  "bit_manipulation",
+  "blockchain",
+  "boolean_algebra",
+  "cellular_automata",
+  "ciphers",
+  "computer_vision",
+  "conversions",
+  "data_compression",
+  "data_structures",
+  "digital_image_processing",
+  "divide_and_conquer",
+  "dynamic_programming",
+  "electronics",
+  "file_transfer",
+  "financial",
+  "fractals",
+  "fuzzy_logic",
+  "genetic_algorithm",
+  "geodesy",
+  "geometry",
+  "graphics",
+  "graphs",
+  "greedy_methods",
+  "hashes",
+  "knapsack",
+  "linear_algebra",
+  "linear_programming",
+  "machine_learning",
+  "maths",
+  "matrix",
+  "networking_flow",
+  "neural_network",
+  "other",
+  "physics",
+  "project_euler",
+  "quantum",
+  "scheduling",
+  "searches",
+  "sorts",
+  "strings",
+  "web_programming",
+]
+autoapi_member_order = "groupwise"
+# autoapi_python_use_implicit_namespaces = true
+exclude_patterns = [
+  ".*/*",
+  "docs/",
+]
+extensions = [
+  "autoapi.extension",
+  "myst_parser",
+]
+html_static_path = [ "_static" ]
+html_theme = "alabaster"
+myst_enable_extensions = [
+  "amsmath",
+  "attrs_inline",
+  "colon_fence",
+  "deflist",
+  "dollarmath",
+  "fieldlist",
+  "html_admonition",
+  "html_image",
+  # "linkify",
+  "replacements",
+  "smartquotes",
+  "strikethrough",
+  "substitution",
+  "tasklist",
+]
+myst_fence_as_directive = [
+  "include",
+]
+templates_path = [ "_templates" ]
+[tool.sphinx-pyproject.source_suffix]
+".rst" = "restructuredtext"
+# ".txt" = "markdown"
+".md" = "markdown"
diff --git a/requirements.txt b/requirements.txt
index 05d9f1e8c545..66b5d8a6b94e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,23 +1,19 @@
 beautifulsoup4
-fake_useragent
+fake-useragent
+httpx
 imageio
-keras ; python_version < '3.12'
+keras
 lxml
 matplotlib
 numpy
 opencv-python
 pandas
 pillow
-# projectq  # uncomment once quantum/quantum_random.py is fixed
-qiskit ; python_version < '3.12'
-qiskit-aer ; python_version < '3.12'
-requests
 rich
-# scikit-fuzzy  # uncomment once fuzzy_logic/fuzzy_operations.py is fixed
 scikit-learn
+sphinx-pyproject
 statsmodels
 sympy
-tensorflow ; python_version < '3.12'
 tweepy
+typing_extensions
 xgboost
-# yulewalker  # uncomment once audio_filters/equal_loudness_filter.py is fixed
diff --git a/scheduling/highest_response_ratio_next.py b/scheduling/highest_response_ratio_next.py
index 057bd64cc729..f858be2ee44a 100644
--- a/scheduling/highest_response_ratio_next.py
+++ b/scheduling/highest_response_ratio_next.py
@@ -4,6 +4,7 @@
 to mitigate the problem of process starvation.
 https://en.wikipedia.org/wiki/Highest_response_ratio_next
 """
+
 from statistics import mean
 
 import numpy as np
@@ -45,8 +46,7 @@ def calculate_turn_around_time(
         i = 0
         while finished_process[i] == 1:
             i += 1
-        if current_time < arrival_time[i]:
-            current_time = arrival_time[i]
+        current_time = max(current_time, arrival_time[i])
 
         response_ratio = 0
         # Index showing the location of the process being performed
@@ -74,7 +74,10 @@ def calculate_turn_around_time(
 
 
 def calculate_waiting_time(
-    process_name: list, turn_around_time: list, burst_time: list, no_of_process: int
+    process_name: list,  # noqa: ARG001
+    turn_around_time: list,
+    burst_time: list,
+    no_of_process: int,
 ) -> list:
     """
     Calculate the waiting time of each processes.
diff --git a/scheduling/job_sequence_with_deadline.py b/scheduling/job_sequence_with_deadline.py
index fccb49cd88e8..ee1fdbd0e55c 100644
--- a/scheduling/job_sequence_with_deadline.py
+++ b/scheduling/job_sequence_with_deadline.py
@@ -13,6 +13,7 @@
 Time Complexity - O(n log n)
 https://medium.com/@nihardudhat2000/job-sequencing-with-deadline-17ddbb5890b5
 """
+
 from dataclasses import dataclass
 from operator import attrgetter
 
diff --git a/scheduling/job_sequencing_with_deadline.py b/scheduling/job_sequencing_with_deadline.py
index 7b23c0b3575f..13946948492f 100644
--- a/scheduling/job_sequencing_with_deadline.py
+++ b/scheduling/job_sequencing_with_deadline.py
@@ -1,9 +1,8 @@
-def job_sequencing_with_deadlines(num_jobs: int, jobs: list) -> list:
+def job_sequencing_with_deadlines(jobs: list) -> list:
     """
     Function to find the maximum profit by doing jobs in a given time frame
 
     Args:
-        num_jobs [int]: Number of jobs
         jobs [list]: A list of tuples of (job_id, deadline, profit)
 
     Returns:
@@ -11,10 +10,10 @@ def job_sequencing_with_deadlines(num_jobs: int, jobs: list) -> list:
         in a given time frame
 
     Examples:
-    >>> job_sequencing_with_deadlines(4,
+    >>> job_sequencing_with_deadlines(
     ... [(1, 4, 20), (2, 1, 10), (3, 1, 40), (4, 1, 30)])
     [2, 60]
-    >>> job_sequencing_with_deadlines(5,
+    >>> job_sequencing_with_deadlines(
     ... [(1, 2, 100), (2, 1, 19), (3, 2, 27), (4, 1, 25), (5, 1, 15)])
     [2, 127]
     """
diff --git a/scheduling/non_preemptive_shortest_job_first.py b/scheduling/non_preemptive_shortest_job_first.py
index 69c974b0044d..cb7ffd3abd9c 100644
--- a/scheduling/non_preemptive_shortest_job_first.py
+++ b/scheduling/non_preemptive_shortest_job_first.py
@@ -5,7 +5,6 @@
 https://en.wikipedia.org/wiki/Shortest_job_next
 """
 
-
 from __future__ import annotations
 
 from statistics import mean
diff --git a/scheduling/round_robin.py b/scheduling/round_robin.py
index e8d54dd9a553..5f6c7f341baa 100644
--- a/scheduling/round_robin.py
+++ b/scheduling/round_robin.py
@@ -3,6 +3,7 @@
 In Round Robin each process is assigned a fixed time slot in a cyclic way.
 https://en.wikipedia.org/wiki/Round-robin_scheduling
 """
+
 from __future__ import annotations
 
 from statistics import mean
diff --git a/scheduling/shortest_job_first.py b/scheduling/shortest_job_first.py
index 871de8207308..91012ee3ac35 100644
--- a/scheduling/shortest_job_first.py
+++ b/scheduling/shortest_job_first.py
@@ -3,6 +3,7 @@
 Please note arrival time and burst
 Please use spaces to separate times entered.
 """
+
 from __future__ import annotations
 
 import pandas as pd
@@ -36,11 +37,14 @@ def calculate_waitingtime(
     # Process until all processes are completed
     while complete != no_of_processes:
         for j in range(no_of_processes):
-            if arrival_time[j] <= increment_time and remaining_time[j] > 0:
-                if remaining_time[j] < minm:
-                    minm = remaining_time[j]
-                    short = j
-                    check = True
+            if (
+                arrival_time[j] <= increment_time
+                and remaining_time[j] > 0
+                and remaining_time[j] < minm
+            ):
+                minm = remaining_time[j]
+                short = j
+                check = True
 
         if not check:
             increment_time += 1
@@ -62,8 +66,7 @@ def calculate_waitingtime(
             finar = finish_time - arrival_time[short]
             waiting_time[short] = finar - burst_time[short]
 
-            if waiting_time[short] < 0:
-                waiting_time[short] = 0
+            waiting_time[short] = max(waiting_time[short], 0)
 
         # Increment time
         increment_time += 1
diff --git a/scripts/build_directory_md.py b/scripts/build_directory_md.py
index 24bc00cd036f..aa95b95db4b5 100755
--- a/scripts/build_directory_md.py
+++ b/scripts/build_directory_md.py
@@ -6,7 +6,11 @@
 
 def good_file_paths(top_dir: str = ".") -> Iterator[str]:
     for dir_path, dir_names, filenames in os.walk(top_dir):
-        dir_names[:] = [d for d in dir_names if d != "scripts" and d[0] not in "._"]
+        dir_names[:] = [
+            d
+            for d in dir_names
+            if d != "scripts" and d[0] not in "._" and "venv" not in d
+        ]
         for filename in filenames:
             if filename == "__init__.py":
                 continue
diff --git a/scripts/close_pull_requests_with_awaiting_changes.sh b/scripts/close_pull_requests_with_awaiting_changes.sh
new file mode 100755
index 000000000000..55e19c980596
--- /dev/null
+++ b/scripts/close_pull_requests_with_awaiting_changes.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# List all open pull requests
+prs=$(gh pr list --state open --json number,title,labels --limit 500)
+
+# Loop through each pull request
+echo "$prs" | jq -c '.[]' | while read -r pr; do
+  pr_number=$(echo "$pr" | jq -r '.number')
+  pr_title=$(echo "$pr" | jq -r '.title')
+  pr_labels=$(echo "$pr" | jq -r '.labels')
+
+  # Check if the "awaiting changes" label is present
+  awaiting_changes=$(echo "$pr_labels" | jq -r '.[] | select(.name == "awaiting changes")')
+  echo "Checking PR #$pr_number $pr_title ($awaiting_changes) ($pr_labels)"
+
+  # If awaiting_changes, close the pull request
+  if [[ -n "$awaiting_changes" ]]; then
+    echo "Closing PR #$pr_number $pr_title due to awaiting_changes label"
+    gh pr close "$pr_number" --comment "Closing awaiting_changes PRs to prepare for Hacktoberfest"
+    sleep 2
+  fi
+done
diff --git a/scripts/close_pull_requests_with_failing_tests.sh b/scripts/close_pull_requests_with_failing_tests.sh
new file mode 100755
index 000000000000..3ec5960aed27
--- /dev/null
+++ b/scripts/close_pull_requests_with_failing_tests.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# List all open pull requests
+prs=$(gh pr list --state open --json number,title,labels --limit 500)
+
+# Loop through each pull request
+echo "$prs" | jq -c '.[]' | while read -r pr; do
+  pr_number=$(echo "$pr" | jq -r '.number')
+  pr_title=$(echo "$pr" | jq -r '.title')
+  pr_labels=$(echo "$pr" | jq -r '.labels')
+
+  # Check if the "tests are failing" label is present
+  tests_are_failing=$(echo "$pr_labels" | jq -r '.[] | select(.name == "tests are failing")')
+  echo "Checking PR #$pr_number $pr_title ($tests_are_failing) ($pr_labels)"
+
+  # If there are failing tests, close the pull request
+  if [[ -n "$tests_are_failing" ]]; then
+    echo "Closing PR #$pr_number $pr_title due to tests_are_failing label"
+    gh pr close "$pr_number" --comment "Closing tests_are_failing PRs to prepare for Hacktoberfest"
+    sleep 2
+  fi
+done
diff --git a/scripts/close_pull_requests_with_require_descriptive_names.sh b/scripts/close_pull_requests_with_require_descriptive_names.sh
new file mode 100755
index 000000000000..0fc3cec1d247
--- /dev/null
+++ b/scripts/close_pull_requests_with_require_descriptive_names.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# List all open pull requests
+prs=$(gh pr list --state open --json number,title,labels --limit 500)
+
+# Loop through each pull request
+echo "$prs" | jq -c '.[]' | while read -r pr; do
+  pr_number=$(echo "$pr" | jq -r '.number')
+  pr_title=$(echo "$pr" | jq -r '.title')
+  pr_labels=$(echo "$pr" | jq -r '.labels')
+
+  # Check if the "require descriptive names" label is present
+  require_descriptive_names=$(echo "$pr_labels" | jq -r '.[] | select(.name == "require descriptive names")')
+  echo "Checking PR #$pr_number $pr_title ($require_descriptive_names) ($pr_labels)"
+
+  # If there are require_descriptive_names, close the pull request
+  if [[ -n "$require_descriptive_names" ]]; then
+    echo "Closing PR #$pr_number $pr_title due to require_descriptive_names label"
+    gh pr close "$pr_number" --comment "Closing require_descriptive_names PRs to prepare for Hacktoberfest"
+  fi
+done
diff --git a/scripts/close_pull_requests_with_require_tests.sh b/scripts/close_pull_requests_with_require_tests.sh
new file mode 100755
index 000000000000..89a54996b584
--- /dev/null
+++ b/scripts/close_pull_requests_with_require_tests.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# List all open pull requests
+prs=$(gh pr list --state open --json number,title,labels --limit 500)
+
+# Loop through each pull request
+echo "$prs" | jq -c '.[]' | while read -r pr; do
+  pr_number=$(echo "$pr" | jq -r '.number')
+  pr_title=$(echo "$pr" | jq -r '.title')
+  pr_labels=$(echo "$pr" | jq -r '.labels')
+
+  # Check if the "require_tests" label is present
+  require_tests=$(echo "$pr_labels" | jq -r '.[] | select(.name == "require tests")')
+  echo "Checking PR #$pr_number $pr_title ($require_tests) ($pr_labels)"
+
+  # If there require tests, close the pull request
+  if [[ -n "$require_tests" ]]; then
+    echo "Closing PR #$pr_number $pr_title due to require_tests label"
+    gh pr close "$pr_number" --comment "Closing require_tests PRs to prepare for Hacktoberfest"
+    # sleep 2
+  fi
+done
diff --git a/scripts/close_pull_requests_with_require_type_hints.sh b/scripts/close_pull_requests_with_require_type_hints.sh
new file mode 100755
index 000000000000..df5d88289cf0
--- /dev/null
+++ b/scripts/close_pull_requests_with_require_type_hints.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# List all open pull requests
+prs=$(gh pr list --state open --json number,title,labels --limit 500)
+
+# Loop through each pull request
+echo "$prs" | jq -c '.[]' | while read -r pr; do
+  pr_number=$(echo "$pr" | jq -r '.number')
+  pr_title=$(echo "$pr" | jq -r '.title')
+  pr_labels=$(echo "$pr" | jq -r '.labels')
+
+  # Check if the "require type hints" label is present
+  require_type_hints=$(echo "$pr_labels" | jq -r '.[] | select(.name == "require type hints")')
+  echo "Checking PR #$pr_number $pr_title ($require_type_hints) ($pr_labels)"
+
+  # If require_type_hints, close the pull request
+  if [[ -n "$require_type_hints" ]]; then
+    echo "Closing PR #$pr_number $pr_title due to require_type_hints label"
+    gh pr close "$pr_number" --comment "Closing require_type_hints PRs to prepare for Hacktoberfest"
+  fi
+done
diff --git a/scripts/find_git_conflicts.sh b/scripts/find_git_conflicts.sh
new file mode 100755
index 000000000000..8af33fa75279
--- /dev/null
+++ b/scripts/find_git_conflicts.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Replace with your repository (format: owner/repo)
+REPO="TheAlgorithms/Python"
+
+# Fetch open pull requests with conflicts into a variable
+echo "Checking for pull requests with conflicts in $REPO..."
+
+prs=$(gh pr list --repo "$REPO" --state open --json number,title,mergeable --jq '.[] | select(.mergeable == "CONFLICTING") | {number, title}' --limit 500)
+
+# Process each conflicting PR
+echo "$prs" | jq -c '.[]' | while read -r pr; do
+    PR_NUMBER=$(echo "$pr" | jq -r '.number')
+    PR_TITLE=$(echo "$pr" | jq -r '.title')
+    echo "PR #$PR_NUMBER - $PR_TITLE has conflicts."
+done
diff --git a/scripts/validate_filenames.py b/scripts/validate_filenames.py
index ed23f3907114..a7328e099dde 100755
--- a/scripts/validate_filenames.py
+++ b/scripts/validate_filenames.py
@@ -1,36 +1,34 @@
 #!/usr/bin/env python3
+
 import os
 
 try:
     from .build_directory_md import good_file_paths
 except ImportError:
-    from build_directory_md import good_file_paths  # type: ignore
+    from build_directory_md import good_file_paths  # type: ignore[no-redef]
 
 filepaths = list(good_file_paths())
 assert filepaths, "good_file_paths() failed!"
 
-upper_files = [file for file in filepaths if file != file.lower()]
-if upper_files:
+if upper_files := [file for file in filepaths if file != file.lower()]:
     print(f"{len(upper_files)} files contain uppercase characters:")
     print("\n".join(upper_files) + "\n")
 
-space_files = [file for file in filepaths if " " in file]
-if space_files:
+if space_files := [file for file in filepaths if " " in file]:
     print(f"{len(space_files)} files contain space characters:")
     print("\n".join(space_files) + "\n")
 
-hyphen_files = [file for file in filepaths if "-" in file]
-if hyphen_files:
+if hyphen_files := [
+    file for file in filepaths if "-" in file and "/site-packages/" not in file
+]:
     print(f"{len(hyphen_files)} files contain hyphen characters:")
     print("\n".join(hyphen_files) + "\n")
 
-nodir_files = [file for file in filepaths if os.sep not in file]
-if nodir_files:
+if nodir_files := [file for file in filepaths if os.sep not in file]:
     print(f"{len(nodir_files)} files are not in a directory:")
     print("\n".join(nodir_files) + "\n")
 
-bad_files = len(upper_files + space_files + hyphen_files + nodir_files)
-if bad_files:
+if bad_files := len(upper_files + space_files + hyphen_files + nodir_files):
     import sys
 
     sys.exit(bad_files)
diff --git a/scripts/validate_solutions.py b/scripts/validate_solutions.py
index ca4af5261a8f..f426153b5683 100755
--- a/scripts/validate_solutions.py
+++ b/scripts/validate_solutions.py
@@ -1,4 +1,13 @@
 #!/usr/bin/env python3
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+#     "pytest",
+# ]
+# ///
+
 import hashlib
 import importlib.util
 import json
@@ -6,8 +15,8 @@
 import pathlib
 from types import ModuleType
 
+import httpx
 import pytest
-import requests
 
 PROJECT_EULER_DIR_PATH = pathlib.Path.cwd().joinpath("project_euler")
 PROJECT_EULER_ANSWERS_PATH = pathlib.Path.cwd().joinpath(
@@ -21,8 +30,8 @@
 def convert_path_to_module(file_path: pathlib.Path) -> ModuleType:
     """Converts a file path to a Python module"""
     spec = importlib.util.spec_from_file_location(file_path.name, str(file_path))
-    module = importlib.util.module_from_spec(spec)  # type: ignore
-    spec.loader.exec_module(module)  # type: ignore
+    module = importlib.util.module_from_spec(spec)  # type: ignore[arg-type]
+    spec.loader.exec_module(module)  # type: ignore[union-attr]
     return module
 
 
@@ -57,7 +66,7 @@ def added_solution_file_path() -> list[pathlib.Path]:
         "Accept": "application/vnd.github.v3+json",
         "Authorization": "token " + os.environ["GITHUB_TOKEN"],
     }
-    files = requests.get(get_files_url(), headers=headers).json()
+    files = httpx.get(get_files_url(), headers=headers, timeout=10).json()
     for file in files:
         filepath = pathlib.Path.cwd().joinpath(file["filename"])
         if (
@@ -71,10 +80,13 @@ def added_solution_file_path() -> list[pathlib.Path]:
 
 
 def collect_solution_file_paths() -> list[pathlib.Path]:
-    if os.environ.get("CI") and os.environ.get("GITHUB_EVENT_NAME") == "pull_request":
-        # Return only if there are any, otherwise default to all solutions
-        if filepaths := added_solution_file_path():
-            return filepaths
+    # Return only if there are any, otherwise default to all solutions
+    if (
+        os.environ.get("CI")
+        and os.environ.get("GITHUB_EVENT_NAME") == "pull_request"
+        and (filepaths := added_solution_file_path())
+    ):
+        return filepaths
     return all_solution_file_paths()
 
 
@@ -89,8 +101,8 @@ def test_project_euler(solution_path: pathlib.Path) -> None:
     problem_number: str = solution_path.parent.name[8:].zfill(3)
     expected: str = PROBLEM_ANSWERS[problem_number]
     solution_module = convert_path_to_module(solution_path)
-    answer = str(solution_module.solution())  # type: ignore
+    answer = str(solution_module.solution())
     answer = hashlib.sha256(answer.encode()).hexdigest()
-    assert (
-        answer == expected
-    ), f"Expected solution to {problem_number} to have hash {expected}, got {answer}"
+    assert answer == expected, (
+        f"Expected solution to {problem_number} to have hash {expected}, got {answer}"
+    )
diff --git a/searches/binary_search.py b/searches/binary_search.py
index 05dadd4fe965..2e66b672d5b4 100644
--- a/searches/binary_search.py
+++ b/searches/binary_search.py
@@ -1,14 +1,15 @@
 #!/usr/bin/env python3
 
 """
-This is pure Python implementation of binary search algorithms
+Pure Python implementations of binary search algorithms
 
-For doctests run following command:
+For doctests run the following command:
 python3 -m doctest -v binary_search.py
 
 For manual testing run:
 python3 binary_search.py
 """
+
 from __future__ import annotations
 
 import bisect
@@ -34,16 +35,12 @@ def bisect_left(
     Examples:
     >>> bisect_left([0, 5, 7, 10, 15], 0)
     0
-
     >>> bisect_left([0, 5, 7, 10, 15], 6)
     2
-
     >>> bisect_left([0, 5, 7, 10, 15], 20)
     5
-
     >>> bisect_left([0, 5, 7, 10, 15], 15, 1, 3)
     3
-
     >>> bisect_left([0, 5, 7, 10, 15], 6, 2)
     2
     """
@@ -79,16 +76,12 @@ def bisect_right(
     Examples:
     >>> bisect_right([0, 5, 7, 10, 15], 0)
     1
-
     >>> bisect_right([0, 5, 7, 10, 15], 15)
     5
-
     >>> bisect_right([0, 5, 7, 10, 15], 6)
     2
-
     >>> bisect_right([0, 5, 7, 10, 15], 15, 1, 3)
     3
-
     >>> bisect_right([0, 5, 7, 10, 15], 6, 2)
     2
     """
@@ -124,7 +117,6 @@ def insort_left(
     >>> insort_left(sorted_collection, 6)
     >>> sorted_collection
     [0, 5, 6, 7, 10, 15]
-
     >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)]
     >>> item = (5, 5)
     >>> insort_left(sorted_collection, item)
@@ -134,12 +126,10 @@ def insort_left(
     True
     >>> item is sorted_collection[2]
     False
-
     >>> sorted_collection = [0, 5, 7, 10, 15]
     >>> insort_left(sorted_collection, 20)
     >>> sorted_collection
     [0, 5, 7, 10, 15, 20]
-
     >>> sorted_collection = [0, 5, 7, 10, 15]
     >>> insort_left(sorted_collection, 15, 1, 3)
     >>> sorted_collection
@@ -167,7 +157,6 @@ def insort_right(
     >>> insort_right(sorted_collection, 6)
     >>> sorted_collection
     [0, 5, 6, 7, 10, 15]
-
     >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)]
     >>> item = (5, 5)
     >>> insort_right(sorted_collection, item)
@@ -177,12 +166,10 @@ def insort_right(
     False
     >>> item is sorted_collection[2]
     True
-
     >>> sorted_collection = [0, 5, 7, 10, 15]
     >>> insort_right(sorted_collection, 20)
     >>> sorted_collection
     [0, 5, 7, 10, 15, 20]
-
     >>> sorted_collection = [0, 5, 7, 10, 15]
     >>> insort_right(sorted_collection, 15, 1, 3)
     >>> sorted_collection
@@ -191,29 +178,28 @@ def insort_right(
     sorted_collection.insert(bisect_right(sorted_collection, item, lo, hi), item)
 
 
-def binary_search(sorted_collection: list[int], item: int) -> int | None:
-    """Pure implementation of binary search algorithm in Python
+def binary_search(sorted_collection: list[int], item: int) -> int:
+    """Pure implementation of a binary search algorithm in Python
 
-    Be careful collection must be ascending sorted, otherwise result will be
+    Be careful collection must be ascending sorted otherwise, the result will be
     unpredictable
 
     :param sorted_collection: some ascending sorted collection with comparable items
     :param item: item value to search
-    :return: index of found item or None if item is not found
+    :return: index of the found item or -1 if the item is not found
 
     Examples:
     >>> binary_search([0, 5, 7, 10, 15], 0)
     0
-
     >>> binary_search([0, 5, 7, 10, 15], 15)
     4
-
     >>> binary_search([0, 5, 7, 10, 15], 5)
     1
-
     >>> binary_search([0, 5, 7, 10, 15], 6)
-
+    -1
     """
+    if list(sorted_collection) != sorted(sorted_collection):
+        raise ValueError("sorted_collection must be sorted in ascending order")
     left = 0
     right = len(sorted_collection) - 1
 
@@ -226,66 +212,66 @@ def binary_search(sorted_collection: list[int], item: int) -> int | None:
             right = midpoint - 1
         else:
             left = midpoint + 1
-    return None
+    return -1
 
 
-def binary_search_std_lib(sorted_collection: list[int], item: int) -> int | None:
-    """Pure implementation of binary search algorithm in Python using stdlib
+def binary_search_std_lib(sorted_collection: list[int], item: int) -> int:
+    """Pure implementation of a binary search algorithm in Python using stdlib
 
-    Be careful collection must be ascending sorted, otherwise result will be
+    Be careful collection must be ascending sorted otherwise, the result will be
     unpredictable
 
     :param sorted_collection: some ascending sorted collection with comparable items
     :param item: item value to search
-    :return: index of found item or None if item is not found
+    :return: index of the found item or -1 if the item is not found
 
     Examples:
     >>> binary_search_std_lib([0, 5, 7, 10, 15], 0)
     0
-
     >>> binary_search_std_lib([0, 5, 7, 10, 15], 15)
     4
-
     >>> binary_search_std_lib([0, 5, 7, 10, 15], 5)
     1
-
     >>> binary_search_std_lib([0, 5, 7, 10, 15], 6)
-
+    -1
     """
+    if list(sorted_collection) != sorted(sorted_collection):
+        raise ValueError("sorted_collection must be sorted in ascending order")
     index = bisect.bisect_left(sorted_collection, item)
     if index != len(sorted_collection) and sorted_collection[index] == item:
         return index
-    return None
+    return -1
 
 
 def binary_search_by_recursion(
-    sorted_collection: list[int], item: int, left: int, right: int
-) -> int | None:
-    """Pure implementation of binary search algorithm in Python by recursion
+    sorted_collection: list[int], item: int, left: int = 0, right: int = -1
+) -> int:
+    """Pure implementation of a binary search algorithm in Python by recursion
 
-    Be careful collection must be ascending sorted, otherwise result will be
+    Be careful collection must be ascending sorted otherwise, the result will be
     unpredictable
     First recursion should be started with left=0 and right=(len(sorted_collection)-1)
 
     :param sorted_collection: some ascending sorted collection with comparable items
     :param item: item value to search
-    :return: index of found item or None if item is not found
+    :return: index of the found item or -1 if the item is not found
 
     Examples:
     >>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4)
     0
-
     >>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4)
     4
-
     >>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4)
     1
-
     >>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4)
-
+    -1
     """
+    if right < 0:
+        right = len(sorted_collection) - 1
+    if list(sorted_collection) != sorted(sorted_collection):
+        raise ValueError("sorted_collection must be sorted in ascending order")
     if right < left:
-        return None
+        return -1
 
     midpoint = left + (right - left) // 2
 
@@ -297,12 +283,78 @@ def binary_search_by_recursion(
         return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right)
 
 
+def exponential_search(sorted_collection: list[int], item: int) -> int:
+    """Pure implementation of an exponential search algorithm in Python
+    Resources used:
+    https://en.wikipedia.org/wiki/Exponential_search
+
+    Be careful collection must be ascending sorted otherwise, result will be
+    unpredictable
+
+    :param sorted_collection: some ascending sorted collection with comparable items
+    :param item: item value to search
+    :return: index of the found item or -1 if the item is not found
+
+    the order of this algorithm is O(lg I) where I is index position of item if exist
+
+    Examples:
+    >>> exponential_search([0, 5, 7, 10, 15], 0)
+    0
+    >>> exponential_search([0, 5, 7, 10, 15], 15)
+    4
+    >>> exponential_search([0, 5, 7, 10, 15], 5)
+    1
+    >>> exponential_search([0, 5, 7, 10, 15], 6)
+    -1
+    """
+    if list(sorted_collection) != sorted(sorted_collection):
+        raise ValueError("sorted_collection must be sorted in ascending order")
+    bound = 1
+    while bound < len(sorted_collection) and sorted_collection[bound] < item:
+        bound *= 2
+    left = bound // 2
+    right = min(bound, len(sorted_collection) - 1)
+    last_result = binary_search_by_recursion(
+        sorted_collection=sorted_collection, item=item, left=left, right=right
+    )
+    if last_result is None:
+        return -1
+    return last_result
+
+
+searches = (  # Fastest to slowest...
+    binary_search_std_lib,
+    binary_search,
+    exponential_search,
+    binary_search_by_recursion,
+)
+
+
 if __name__ == "__main__":
-    user_input = input("Enter numbers separated by comma:\n").strip()
+    import doctest
+    import timeit
+
+    doctest.testmod()
+    for search in searches:
+        name = f"{search.__name__:>26}"
+        print(f"{name}: {search([0, 5, 7, 10, 15], 10) = }")  # type: ignore[operator]
+
+    print("\nBenchmarks...")
+    setup = "collection = range(1000)"
+    for search in searches:
+        name = search.__name__
+        print(
+            f"{name:>26}:",
+            timeit.timeit(
+                f"{name}(collection, 500)", setup=setup, number=5_000, globals=globals()
+            ),
+        )
+
+    user_input = input("\nEnter numbers separated by comma: ").strip()
     collection = sorted(int(item) for item in user_input.split(","))
-    target = int(input("Enter a single number to be found in the list:\n"))
-    result = binary_search(collection, target)
-    if result is None:
+    target = int(input("Enter a single number to be found in the list: "))
+    result = binary_search(sorted_collection=collection, item=target)
+    if result == -1:
         print(f"{target} was not found in {collection}.")
     else:
-        print(f"{target} was found at position {result} in {collection}.")
+        print(f"{target} was found at position {result} of {collection}.")
diff --git a/searches/binary_tree_traversal.py b/searches/binary_tree_traversal.py
index 6fb841af4294..47af57f7f94d 100644
--- a/searches/binary_tree_traversal.py
+++ b/searches/binary_tree_traversal.py
@@ -1,6 +1,7 @@
 """
 This is pure Python implementation of tree traversal algorithms
 """
+
 from __future__ import annotations
 
 import queue
@@ -35,7 +36,7 @@ def build_tree() -> TreeNode:
         right_node = TreeNode(int(check))
         node_found.right = right_node
         q.put(right_node)
-    raise
+    raise ValueError("Something went wrong")
 
 
 def pre_order(node: TreeNode) -> None:
@@ -163,8 +164,8 @@ def level_order_actual(node: TreeNode) -> None:
             if node_dequeued.right:
                 list_.append(node_dequeued.right)
         print()
-        for node in list_:
-            q.put(node)
+        for inner_node in list_:
+            q.put(inner_node)
 
 
 # iteration version
diff --git a/searches/exponential_search.py b/searches/exponential_search.py
new file mode 100644
index 000000000000..ed09b14e101c
--- /dev/null
+++ b/searches/exponential_search.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+
+"""
+Pure Python implementation of exponential search algorithm
+
+For more information, see the Wikipedia page:
+https://en.wikipedia.org/wiki/Exponential_search
+
+For doctests run the following command:
+python3 -m doctest -v exponential_search.py
+
+For manual testing run:
+python3 exponential_search.py
+"""
+
+from __future__ import annotations
+
+
+def binary_search_by_recursion(
+    sorted_collection: list[int], item: int, left: int = 0, right: int = -1
+) -> int:
+    """Pure implementation of binary search algorithm in Python using recursion
+
+    Be careful: the collection must be ascending sorted otherwise, the result will be
+    unpredictable.
+
+    :param sorted_collection: some ascending sorted collection with comparable items
+    :param item: item value to search
+    :param left: starting index for the search
+    :param right: ending index for the search
+    :return: index of the found item or -1 if the item is not found
+
+    Examples:
+    >>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4)
+    0
+    >>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4)
+    4
+    >>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4)
+    1
+    >>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4)
+    -1
+    """
+    if right < 0:
+        right = len(sorted_collection) - 1
+    if list(sorted_collection) != sorted(sorted_collection):
+        raise ValueError("sorted_collection must be sorted in ascending order")
+    if right < left:
+        return -1
+
+    midpoint = left + (right - left) // 2
+
+    if sorted_collection[midpoint] == item:
+        return midpoint
+    elif sorted_collection[midpoint] > item:
+        return binary_search_by_recursion(sorted_collection, item, left, midpoint - 1)
+    else:
+        return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right)
+
+
+def exponential_search(sorted_collection: list[int], item: int) -> int:
+    """
+    Pure implementation of an exponential search algorithm in Python.
+    For more information, refer to:
+    https://en.wikipedia.org/wiki/Exponential_search
+
+    Be careful: the collection must be ascending sorted, otherwise the result will be
+    unpredictable.
+
+    :param sorted_collection: some ascending sorted collection with comparable items
+    :param item: item value to search
+    :return: index of the found item or -1 if the item is not found
+
+    The time complexity of this algorithm is O(log i) where i is the index of the item.
+
+    Examples:
+    >>> exponential_search([0, 5, 7, 10, 15], 0)
+    0
+    >>> exponential_search([0, 5, 7, 10, 15], 15)
+    4
+    >>> exponential_search([0, 5, 7, 10, 15], 5)
+    1
+    >>> exponential_search([0, 5, 7, 10, 15], 6)
+    -1
+    """
+    if list(sorted_collection) != sorted(sorted_collection):
+        raise ValueError("sorted_collection must be sorted in ascending order")
+
+    if sorted_collection[0] == item:
+        return 0
+
+    bound = 1
+    while bound < len(sorted_collection) and sorted_collection[bound] < item:
+        bound *= 2
+
+    left = bound // 2
+    right = min(bound, len(sorted_collection) - 1)
+    return binary_search_by_recursion(sorted_collection, item, left, right)
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
+
+    # Manual testing
+    user_input = input("Enter numbers separated by commas: ").strip()
+    collection = sorted(int(item) for item in user_input.split(","))
+    target = int(input("Enter a number to search for: "))
+    result = exponential_search(sorted_collection=collection, item=target)
+    if result == -1:
+        print(f"{target} was not found in {collection}.")
+    else:
+        print(f"{target} was found at index {result} in {collection}.")
diff --git a/searches/fibonacci_search.py b/searches/fibonacci_search.py
index 55fc05d39eeb..7b2252a68be2 100644
--- a/searches/fibonacci_search.py
+++ b/searches/fibonacci_search.py
@@ -10,6 +10,7 @@
 For manual testing run:
 python3 fibonacci_search.py
 """
+
 from functools import lru_cache
 
 
@@ -122,8 +123,7 @@ def fibonacci_search(arr: list, val: int) -> int:
         elif val > item_k_1:
             offset += fibonacci(fibb_k - 1)
             fibb_k -= 2
-    else:
-        return -1
+    return -1
 
 
 if __name__ == "__main__":
diff --git a/searches/hill_climbing.py b/searches/hill_climbing.py
index 83a3b8b74e27..689b7e5cca8f 100644
--- a/searches/hill_climbing.py
+++ b/searches/hill_climbing.py
@@ -137,11 +137,10 @@ def hill_climbing(
                 if change > max_change and change > 0:
                     max_change = change
                     next_state = neighbor
-            else:  # finding min
+            elif change < min_change and change < 0:  # finding min
                 # to direction with greatest descent
-                if change < min_change and change < 0:
-                    min_change = change
-                    next_state = neighbor
+                min_change = change
+                next_state = neighbor
         if next_state is not None:
             # we found at least one neighbor which improved the current state
             current_state = next_state
diff --git a/searches/interpolation_search.py b/searches/interpolation_search.py
index 49194c2600a0..cb3e0011d0da 100644
--- a/searches/interpolation_search.py
+++ b/searches/interpolation_search.py
@@ -3,13 +3,41 @@
 """
 
 
-def interpolation_search(sorted_collection, item):
-    """Pure implementation of interpolation search algorithm in Python
-    Be careful collection must be ascending sorted, otherwise result will be
-    unpredictable
-    :param sorted_collection: some ascending sorted collection with comparable items
-    :param item: item value to search
-    :return: index of found item or None if item is not found
+def interpolation_search(sorted_collection: list[int], item: int) -> int | None:
+    """
+    Searches for an item in a sorted collection by interpolation search algorithm.
+
+    Args:
+        sorted_collection: sorted list of integers
+        item: item value to search
+
+    Returns:
+        int: The index of the found item, or None if the item is not found.
+    Examples:
+    >>> interpolation_search([1, 2, 3, 4, 5], 2)
+    1
+    >>> interpolation_search([1, 2, 3, 4, 5], 4)
+    3
+    >>> interpolation_search([1, 2, 3, 4, 5], 6) is None
+    True
+    >>> interpolation_search([], 1) is None
+    True
+    >>> interpolation_search([100], 100)
+    0
+    >>> interpolation_search([1, 2, 3, 4, 5], 0) is None
+    True
+    >>> interpolation_search([1, 2, 3, 4, 5], 7) is None
+    True
+    >>> interpolation_search([1, 2, 3, 4, 5], 2)
+    1
+    >>> interpolation_search([1, 2, 3, 4, 5], 0) is None
+    True
+    >>> interpolation_search([1, 2, 3, 4, 5], 7) is None
+    True
+    >>> interpolation_search([1, 2, 3, 4, 5], 2)
+    1
+    >>> interpolation_search([5, 5, 5, 5, 5], 3) is None
+    True
     """
     left = 0
     right = len(sorted_collection) - 1
@@ -19,8 +47,7 @@ def interpolation_search(sorted_collection, item):
         if sorted_collection[left] == sorted_collection[right]:
             if sorted_collection[left] == item:
                 return left
-            else:
-                return None
+            return None
 
         point = left + ((item - sorted_collection[left]) * (right - left)) // (
             sorted_collection[right] - sorted_collection[left]
@@ -33,37 +60,55 @@ def interpolation_search(sorted_collection, item):
         current_item = sorted_collection[point]
         if current_item == item:
             return point
+        if point < left:
+            right = left
+            left = point
+        elif point > right:
+            left = right
+            right = point
+        elif item < current_item:
+            right = point - 1
         else:
-            if point < left:
-                right = left
-                left = point
-            elif point > right:
-                left = right
-                right = point
-            else:
-                if item < current_item:
-                    right = point - 1
-                else:
-                    left = point + 1
+            left = point + 1
     return None
 
 
-def interpolation_search_by_recursion(sorted_collection, item, left, right):
+def interpolation_search_by_recursion(
+    sorted_collection: list[int], item: int, left: int = 0, right: int | None = None
+) -> int | None:
     """Pure implementation of interpolation search algorithm in Python by recursion
     Be careful collection must be ascending sorted, otherwise result will be
     unpredictable
     First recursion should be started with left=0 and right=(len(sorted_collection)-1)
-    :param sorted_collection: some ascending sorted collection with comparable items
-    :param item: item value to search
-    :return: index of found item or None if item is not found
-    """
 
+    Args:
+        sorted_collection: some sorted collection with comparable items
+        item: item value to search
+        left: left index in collection
+        right: right index in collection
+
+    Returns:
+        index of item in collection or None if item is not present
+
+    Examples:
+    >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 0)
+    0
+    >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 15)
+    4
+    >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 5)
+    1
+    >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 100) is None
+    True
+    >>> interpolation_search_by_recursion([5, 5, 5, 5, 5], 3) is None
+    True
+    """
+    if right is None:
+        right = len(sorted_collection) - 1
     # avoid divided by 0 during interpolation
     if sorted_collection[left] == sorted_collection[right]:
         if sorted_collection[left] == item:
             return left
-        else:
-            return None
+        return None
 
     point = left + ((item - sorted_collection[left]) * (right - left)) // (
         sorted_collection[right] - sorted_collection[left]
@@ -75,65 +120,18 @@ def interpolation_search_by_recursion(sorted_collection, item, left, right):
 
     if sorted_collection[point] == item:
         return point
-    elif point < left:
+    if point < left:
         return interpolation_search_by_recursion(sorted_collection, item, point, left)
-    elif point > right:
+    if point > right:
         return interpolation_search_by_recursion(sorted_collection, item, right, left)
-    else:
-        if sorted_collection[point] > item:
-            return interpolation_search_by_recursion(
-                sorted_collection, item, left, point - 1
-            )
-        else:
-            return interpolation_search_by_recursion(
-                sorted_collection, item, point + 1, right
-            )
-
-
-def __assert_sorted(collection):
-    """Check if collection is ascending sorted, if not - raises :py:class:`ValueError`
-    :param collection: collection
-    :return: True if collection is ascending sorted
-    :raise: :py:class:`ValueError` if collection is not ascending sorted
-    Examples:
-    >>> __assert_sorted([0, 1, 2, 4])
-    True
-    >>> __assert_sorted([10, -1, 5])
-    Traceback (most recent call last):
-        ...
-    ValueError: Collection must be ascending sorted
-    """
-    if collection != sorted(collection):
-        raise ValueError("Collection must be ascending sorted")
-    return True
+    if sorted_collection[point] > item:
+        return interpolation_search_by_recursion(
+            sorted_collection, item, left, point - 1
+        )
+    return interpolation_search_by_recursion(sorted_collection, item, point + 1, right)
 
 
 if __name__ == "__main__":
-    import sys
+    import doctest
 
-    """
-        user_input = input('Enter numbers separated by comma:\n').strip()
-    collection = [int(item) for item in user_input.split(',')]
-    try:
-        __assert_sorted(collection)
-    except ValueError:
-        sys.exit('Sequence must be ascending sorted to apply interpolation search')
-
-    target_input = input('Enter a single number to be found in the list:\n')
-    target = int(target_input)
-        """
-
-    debug = 0
-    if debug == 1:
-        collection = [10, 30, 40, 45, 50, 66, 77, 93]
-        try:
-            __assert_sorted(collection)
-        except ValueError:
-            sys.exit("Sequence must be ascending sorted to apply interpolation search")
-        target = 67
-
-    result = interpolation_search(collection, target)
-    if result is not None:
-        print(f"{target} found at positions: {result}")
-    else:
-        print("Not found")
+    doctest.testmod()
diff --git a/searches/jump_search.py b/searches/jump_search.py
index 3bc3c37809a1..e72d85e8a868 100644
--- a/searches/jump_search.py
+++ b/searches/jump_search.py
@@ -14,8 +14,7 @@
 
 
 class Comparable(Protocol):
-    def __lt__(self, other: Any, /) -> bool:
-        ...
+    def __lt__(self, other: Any, /) -> bool: ...
 
 
 T = TypeVar("T", bound=Comparable)
diff --git a/searches/quick_select.py b/searches/quick_select.py
index 5ede8c4dd07f..f67f939c88c3 100644
--- a/searches/quick_select.py
+++ b/searches/quick_select.py
@@ -4,6 +4,7 @@
 sorted, even if it is not already sorted
 https://en.wikipedia.org/wiki/Quickselect
 """
+
 import random
 
 
@@ -59,3 +60,25 @@ def quick_select(items: list, index: int):
     # must be in larger
     else:
         return quick_select(larger, index - (m + count))
+
+
+def median(items: list):
+    """
+    One common application of Quickselect is finding the median, which is
+    the middle element (or average of the two middle elements) in a sorted dataset.
+    It works efficiently on unsorted lists by partially sorting the data without
+    fully sorting the entire list.
+
+    >>> median([3, 2, 2, 9, 9])
+    3
+
+    >>> median([2, 2, 9, 9, 9, 3])
+    6.0
+    """
+    mid, rem = divmod(len(items), 2)
+    if rem != 0:
+        return quick_select(items=items, index=mid)
+    else:
+        low_mid = quick_select(items=items, index=mid - 1)
+        high_mid = quick_select(items=items, index=mid)
+        return (low_mid + high_mid) / 2
diff --git a/searches/simple_binary_search.py b/searches/simple_binary_search.py
index ff043d7369af..00e83ff9e4a3 100644
--- a/searches/simple_binary_search.py
+++ b/searches/simple_binary_search.py
@@ -7,6 +7,7 @@
 For manual testing run:
 python3 simple_binary_search.py
 """
+
 from __future__ import annotations
 
 
diff --git a/searches/tabu_search.py b/searches/tabu_search.py
index d998ddc55976..fd482a81224c 100644
--- a/searches/tabu_search.py
+++ b/searches/tabu_search.py
@@ -24,6 +24,7 @@
     -s size_of_tabu_search
 e.g. python tabu_search.py -f tabudata2.txt -i 4 -s 3
 """
+
 import argparse
 import copy
 
diff --git a/searches/ternary_search.py b/searches/ternary_search.py
index cb36e72faac6..73e4b1ddc68b 100644
--- a/searches/ternary_search.py
+++ b/searches/ternary_search.py
@@ -6,6 +6,7 @@
 Time Complexity  : O(log3 N)
 Space Complexity : O(1)
 """
+
 from __future__ import annotations
 
 # This is the precision for this function which can be altered.
@@ -105,8 +106,7 @@ def ite_ternary_search(array: list[int], target: int) -> int:
         else:
             left = one_third + 1
             right = two_third - 1
-    else:
-        return -1
+    return -1
 
 
 def rec_ternary_search(left: int, right: int, array: list[int], target: int) -> int:
diff --git a/sorts/bead_sort.py b/sorts/bead_sort.py
index e51173643d81..8ce0619fd573 100644
--- a/sorts/bead_sort.py
+++ b/sorts/bead_sort.py
@@ -31,7 +31,7 @@ def bead_sort(sequence: list) -> list:
     if any(not isinstance(x, int) or x < 0 for x in sequence):
         raise TypeError("Sequence must be list of non-negative integers")
     for _ in range(len(sequence)):
-        for i, (rod_upper, rod_lower) in enumerate(zip(sequence, sequence[1:])):
+        for i, (rod_upper, rod_lower) in enumerate(zip(sequence, sequence[1:])):  # noqa: RUF007
             if rod_upper > rod_lower:
                 sequence[i] -= rod_upper - rod_lower
                 sequence[i + 1] += rod_upper - rod_lower
diff --git a/sorts/binary_insertion_sort.py b/sorts/binary_insertion_sort.py
index 8d41025583b1..50653a99e7ce 100644
--- a/sorts/binary_insertion_sort.py
+++ b/sorts/binary_insertion_sort.py
@@ -12,10 +12,11 @@
 
 
 def binary_insertion_sort(collection: list) -> list:
-    """Pure implementation of the binary insertion sort algorithm in Python
-    :param collection: some mutable ordered collection with heterogeneous
-    comparable items inside
-    :return: the same collection ordered by ascending
+    """
+    Sorts a list using the binary insertion sort algorithm.
+
+    :param collection: A mutable ordered collection with comparable items.
+    :return: The same collection ordered in ascending order.
 
     Examples:
     >>> binary_insertion_sort([0, 4, 1234, 4, 1])
@@ -39,23 +40,27 @@ def binary_insertion_sort(collection: list) -> list:
 
     n = len(collection)
     for i in range(1, n):
-        val = collection[i]
+        value_to_insert = collection[i]
         low = 0
         high = i - 1
 
         while low <= high:
             mid = (low + high) // 2
-            if val < collection[mid]:
+            if value_to_insert < collection[mid]:
                 high = mid - 1
             else:
                 low = mid + 1
         for j in range(i, low, -1):
             collection[j] = collection[j - 1]
-        collection[low] = val
+        collection[low] = value_to_insert
     return collection
 
 
-if __name__ == "__main__":
+if __name__ == "__main":
     user_input = input("Enter numbers separated by a comma:\n").strip()
-    unsorted = [int(item) for item in user_input.split(",")]
-    print(binary_insertion_sort(unsorted))
+    try:
+        unsorted = [int(item) for item in user_input.split(",")]
+    except ValueError:
+        print("Invalid input. Please enter valid integers separated by commas.")
+        raise
+    print(f"{binary_insertion_sort(unsorted) = }")
diff --git a/sorts/bitonic_sort.py b/sorts/bitonic_sort.py
index b65f877a45e3..600f8139603a 100644
--- a/sorts/bitonic_sort.py
+++ b/sorts/bitonic_sort.py
@@ -3,6 +3,7 @@
 
 Note that this program works only when size of input is a power of 2.
 """
+
 from __future__ import annotations
 
 
diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py
index bdf85c70dd35..9ec3d5384f38 100644
--- a/sorts/bubble_sort.py
+++ b/sorts/bubble_sort.py
@@ -85,6 +85,8 @@ def bubble_sort_recursive(collection: list[Any]) -> list[Any]:
     [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7]
     >>> bubble_sort_recursive([1, 3.3, 5, 7.7, 2, 4.4, 6])
     [1, 2, 3.3, 4.4, 5, 6, 7.7]
+    >>> bubble_sort_recursive(['a', 'Z', 'B', 'C', 'A', 'c'])
+    ['A', 'B', 'C', 'Z', 'a', 'c']
     >>> import random
     >>> collection_arg = random.sample(range(-50, 50), 100)
     >>> bubble_sort_recursive(collection_arg) == sorted(collection_arg)
diff --git a/sorts/bucket_sort.py b/sorts/bucket_sort.py
index c016e9e26e73..1c1320a58a7d 100644
--- a/sorts/bucket_sort.py
+++ b/sorts/bucket_sort.py
@@ -27,6 +27,7 @@
 
 Source: https://en.wikipedia.org/wiki/Bucket_sort
 """
+
 from __future__ import annotations
 
 
diff --git a/sorts/cocktail_shaker_sort.py b/sorts/cocktail_shaker_sort.py
index b738ff31d768..de126426d986 100644
--- a/sorts/cocktail_shaker_sort.py
+++ b/sorts/cocktail_shaker_sort.py
@@ -1,40 +1,62 @@
-""" https://en.wikipedia.org/wiki/Cocktail_shaker_sort """
+"""
+An implementation of the cocktail shaker sort algorithm in pure Python.
 
+https://en.wikipedia.org/wiki/Cocktail_shaker_sort
+"""
 
-def cocktail_shaker_sort(unsorted: list) -> list:
+
+def cocktail_shaker_sort(arr: list[int]) -> list[int]:
     """
-    Pure implementation of the cocktail shaker sort algorithm in Python.
+    Sorts a list using the Cocktail Shaker Sort algorithm.
+
+    :param arr: List of elements to be sorted.
+    :return: Sorted list.
+
     >>> cocktail_shaker_sort([4, 5, 2, 1, 2])
     [1, 2, 2, 4, 5]
-
     >>> cocktail_shaker_sort([-4, 5, 0, 1, 2, 11])
     [-4, 0, 1, 2, 5, 11]
-
     >>> cocktail_shaker_sort([0.1, -2.4, 4.4, 2.2])
     [-2.4, 0.1, 2.2, 4.4]
-
     >>> cocktail_shaker_sort([1, 2, 3, 4, 5])
     [1, 2, 3, 4, 5]
-
     >>> cocktail_shaker_sort([-4, -5, -24, -7, -11])
     [-24, -11, -7, -5, -4]
+    >>> cocktail_shaker_sort(["elderberry", "banana", "date", "apple", "cherry"])
+    ['apple', 'banana', 'cherry', 'date', 'elderberry']
+    >>> cocktail_shaker_sort((-4, -5, -24, -7, -11))
+    Traceback (most recent call last):
+        ...
+    TypeError: 'tuple' object does not support item assignment
     """
-    for i in range(len(unsorted) - 1, 0, -1):
+    start, end = 0, len(arr) - 1
+
+    while start < end:
         swapped = False
 
-        for j in range(i, 0, -1):
-            if unsorted[j] < unsorted[j - 1]:
-                unsorted[j], unsorted[j - 1] = unsorted[j - 1], unsorted[j]
+        # Pass from left to right
+        for i in range(start, end):
+            if arr[i] > arr[i + 1]:
+                arr[i], arr[i + 1] = arr[i + 1], arr[i]
                 swapped = True
 
-        for j in range(i):
-            if unsorted[j] > unsorted[j + 1]:
-                unsorted[j], unsorted[j + 1] = unsorted[j + 1], unsorted[j]
+        if not swapped:
+            break
+
+        end -= 1  # Decrease the end pointer after each pass
+
+        # Pass from right to left
+        for i in range(end, start, -1):
+            if arr[i] < arr[i - 1]:
+                arr[i], arr[i - 1] = arr[i - 1], arr[i]
                 swapped = True
 
         if not swapped:
             break
-    return unsorted
+
+        start += 1  # Increase the start pointer after each pass
+
+    return arr
 
 
 if __name__ == "__main__":
diff --git a/sorts/double_sort.py b/sorts/double_sort.py
index a19641d94752..bd5fdca1e63c 100644
--- a/sorts/double_sort.py
+++ b/sorts/double_sort.py
@@ -1,4 +1,7 @@
-def double_sort(lst):
+from typing import Any
+
+
+def double_sort(collection: list[Any]) -> list[Any]:
     """This sorting algorithm sorts an array using the principle of bubble sort,
     but does it both from left to right and right to left.
     Hence, it's called "Double sort"
@@ -14,29 +17,28 @@ def double_sort(lst):
     >>> double_sort([-3, 10, 16, -42, 29]) == sorted([-3, 10, 16, -42, 29])
     True
     """
-    no_of_elements = len(lst)
+    no_of_elements = len(collection)
     for _ in range(
         int(((no_of_elements - 1) / 2) + 1)
     ):  # we don't need to traverse to end of list as
         for j in range(no_of_elements - 1):
-            if (
-                lst[j + 1] < lst[j]
-            ):  # applying bubble sort algorithm from left to right (or forwards)
-                temp = lst[j + 1]
-                lst[j + 1] = lst[j]
-                lst[j] = temp
-            if (
-                lst[no_of_elements - 1 - j] < lst[no_of_elements - 2 - j]
-            ):  # applying bubble sort algorithm from right to left (or backwards)
-                temp = lst[no_of_elements - 1 - j]
-                lst[no_of_elements - 1 - j] = lst[no_of_elements - 2 - j]
-                lst[no_of_elements - 2 - j] = temp
-    return lst
+            # apply the bubble sort algorithm from left to right (or forwards)
+            if collection[j + 1] < collection[j]:
+                collection[j], collection[j + 1] = collection[j + 1], collection[j]
+            # apply the bubble sort algorithm from right to left (or backwards)
+            if collection[no_of_elements - 1 - j] < collection[no_of_elements - 2 - j]:
+                (
+                    collection[no_of_elements - 1 - j],
+                    collection[no_of_elements - 2 - j],
+                ) = (
+                    collection[no_of_elements - 2 - j],
+                    collection[no_of_elements - 1 - j],
+                )
+    return collection
 
 
 if __name__ == "__main__":
-    print("enter the list to be sorted")
-    lst = [int(x) for x in input().split()]  # inputing elements of the list in one line
-    sorted_lst = double_sort(lst)
+    # allow the user to input the elements of the list on one line
+    unsorted = [int(x) for x in input("Enter the list to be sorted: ").split() if x]
     print("the sorted list is")
-    print(sorted_lst)
+    print(f"{double_sort(unsorted) = }")
diff --git a/sorts/dutch_national_flag_sort.py b/sorts/dutch_national_flag_sort.py
index 758e3a887b84..b4f1665cea00 100644
--- a/sorts/dutch_national_flag_sort.py
+++ b/sorts/dutch_national_flag_sort.py
@@ -23,7 +23,6 @@
 python dnf_sort.py
 """
 
-
 # Python program to sort a sequence containing only 0, 1 and 2 in a single pass.
 red = 0  # The first color of the flag.
 white = 1  # The second color of the flag.
diff --git a/sorts/external_sort.py b/sorts/external_sort.py
index e6b0d47f79f5..cfddee4fe7f8 100644
--- a/sorts/external_sort.py
+++ b/sorts/external_sort.py
@@ -61,7 +61,7 @@ def __init__(self, files):
         self.files = files
         self.empty = set()
         self.num_buffers = len(files)
-        self.buffers = {i: None for i in range(self.num_buffers)}
+        self.buffers = dict.fromkeys(range(self.num_buffers))
 
     def get_dict(self):
         return {
@@ -77,10 +77,7 @@ def refresh(self):
                     self.empty.add(i)
                     self.files[i].close()
 
-        if len(self.empty) == self.num_buffers:
-            return False
-
-        return True
+        return len(self.empty) != self.num_buffers
 
     def unshift(self, index):
         value = self.buffers[index]
diff --git a/sorts/heap_sort.py b/sorts/heap_sort.py
index 4dca879bd89c..44ee1d4b39f1 100644
--- a/sorts/heap_sort.py
+++ b/sorts/heap_sort.py
@@ -1,17 +1,22 @@
 """
-This is a pure Python implementation of the heap sort algorithm.
-
-For doctests run following command:
-python -m doctest -v heap_sort.py
-or
-python3 -m doctest -v heap_sort.py
-
-For manual testing run:
-python heap_sort.py
+A pure Python implementation of the heap sort algorithm.
 """
 
 
-def heapify(unsorted, index, heap_size):
+def heapify(unsorted: list[int], index: int, heap_size: int) -> None:
+    """
+    :param unsorted: unsorted list containing integers numbers
+    :param index: index
+    :param heap_size: size of the heap
+    :return: None
+    >>> unsorted = [1, 4, 3, 5, 2]
+    >>> heapify(unsorted, 0, len(unsorted))
+    >>> unsorted
+    [4, 5, 3, 1, 2]
+    >>> heapify(unsorted, 0, len(unsorted))
+    >>> unsorted
+    [5, 4, 3, 1, 2]
+    """
     largest = index
     left_index = 2 * index + 1
     right_index = 2 * index + 2
@@ -22,26 +27,26 @@ def heapify(unsorted, index, heap_size):
         largest = right_index
 
     if largest != index:
-        unsorted[largest], unsorted[index] = unsorted[index], unsorted[largest]
+        unsorted[largest], unsorted[index] = (unsorted[index], unsorted[largest])
         heapify(unsorted, largest, heap_size)
 
 
-def heap_sort(unsorted):
+def heap_sort(unsorted: list[int]) -> list[int]:
     """
-    Pure implementation of the heap sort algorithm in Python
-    :param collection: some mutable ordered collection with heterogeneous
-    comparable items inside
+    A pure Python implementation of the heap sort algorithm
+
+    :param collection: a mutable ordered collection of heterogeneous comparable items
     :return: the same collection ordered by ascending
 
     Examples:
     >>> heap_sort([0, 5, 3, 2, 2])
     [0, 2, 2, 3, 5]
-
     >>> heap_sort([])
     []
-
     >>> heap_sort([-2, -5, -45])
     [-45, -5, -2]
+    >>> heap_sort([3, 7, 9, 28, 123, -5, 8, -30, -200, 0, 4])
+    [-200, -30, -5, 0, 3, 4, 7, 8, 9, 28, 123]
     """
     n = len(unsorted)
     for i in range(n // 2 - 1, -1, -1):
@@ -53,6 +58,10 @@ def heap_sort(unsorted):
 
 
 if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
     user_input = input("Enter numbers separated by a comma:\n").strip()
-    unsorted = [int(item) for item in user_input.split(",")]
-    print(heap_sort(unsorted))
+    if user_input:
+        unsorted = [int(item) for item in user_input.split(",")]
+        print(f"{heap_sort(unsorted) = }")
diff --git a/sorts/insertion_sort.py b/sorts/insertion_sort.py
index f11ddac349a0..2e39be255df7 100644
--- a/sorts/insertion_sort.py
+++ b/sorts/insertion_sort.py
@@ -18,14 +18,13 @@
 
 
 class Comparable(Protocol):
-    def __lt__(self, other: Any, /) -> bool:
-        ...
+    def __lt__(self, other: Any, /) -> bool: ...
 
 
 T = TypeVar("T", bound=Comparable)
 
 
-def insertion_sort(collection: MutableSequence[T]) -> MutableSequence[T]:
+def insertion_sort[T: Comparable](collection: MutableSequence[T]) -> MutableSequence[T]:
     """A pure Python implementation of the insertion sort algorithm
 
     :param collection: some mutable ordered collection with heterogeneous
diff --git a/sorts/intro_sort.py b/sorts/intro_sort.py
index f0e3645adbb7..1184b381b05d 100644
--- a/sorts/intro_sort.py
+++ b/sorts/intro_sort.py
@@ -1,17 +1,29 @@
 """
-Introspective Sort is hybrid sort (Quick Sort + Heap Sort + Insertion Sort)
+Introspective Sort is a hybrid sort (Quick Sort + Heap Sort + Insertion Sort)
 if the size of the list is under 16, use insertion sort
 https://en.wikipedia.org/wiki/Introsort
 """
+
 import math
 
 
 def insertion_sort(array: list, start: int = 0, end: int = 0) -> list:
     """
     >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
-
     >>> insertion_sort(array, 0, len(array))
     [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79]
+    >>> array = [21, 15, 11, 45, -2, -11, 46]
+    >>> insertion_sort(array, 0, len(array))
+    [-11, -2, 11, 15, 21, 45, 46]
+    >>> array = [-2, 0, 89, 11, 48, 79, 12]
+    >>> insertion_sort(array, 0, len(array))
+    [-2, 0, 11, 12, 48, 79, 89]
+    >>> array = ['a', 'z', 'd', 'p', 'v', 'l', 'o', 'o']
+    >>> insertion_sort(array, 0, len(array))
+    ['a', 'd', 'l', 'o', 'o', 'p', 'v', 'z']
+    >>> array = [73.568, 73.56, -45.03, 1.7, 0, 89.45]
+    >>> insertion_sort(array, 0, len(array))
+    [-45.03, 0, 1.7, 73.56, 73.568, 89.45]
     """
     end = end or len(array)
     for i in range(start, end):
@@ -27,8 +39,7 @@ def insertion_sort(array: list, start: int = 0, end: int = 0) -> list:
 def heapify(array: list, index: int, heap_size: int) -> None:  # Max Heap
     """
     >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
-
-    >>> heapify(array, len(array) // 2 ,len(array))
+    >>> heapify(array, len(array) // 2, len(array))
     """
     largest = index
     left_index = 2 * index + 1  # Left Node
@@ -47,10 +58,14 @@ def heapify(array: list, index: int, heap_size: int) -> None:  # Max Heap
 
 def heap_sort(array: list) -> list:
     """
-    >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
-
-    >>> heap_sort(array)
+    >>> heap_sort([4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12])
     [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79]
+    >>> heap_sort([-2, -11, 0, 0, 0, 87, 45, -69, 78, 12, 10, 103, 89, 52])
+    [-69, -11, -2, 0, 0, 0, 10, 12, 45, 52, 78, 87, 89, 103]
+    >>> heap_sort(['b', 'd', 'e', 'f', 'g', 'p', 'x', 'z', 'b', 's', 'e', 'u', 'v'])
+    ['b', 'b', 'd', 'e', 'e', 'f', 'g', 'p', 's', 'u', 'v', 'x', 'z']
+    >>> heap_sort([6.2, -45.54, 8465.20, 758.56, -457.0, 0, 1, 2.879, 1.7, 11.7])
+    [-457.0, -45.54, 0, 1, 1.7, 2.879, 6.2, 11.7, 758.56, 8465.2]
     """
     n = len(array)
 
@@ -69,9 +84,14 @@ def median_of_3(
 ) -> int:
     """
     >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
-
-    >>> median_of_3(array, 0, 0 + ((len(array) - 0) // 2) + 1, len(array) - 1)
+    >>> median_of_3(array, 0, ((len(array) - 0) // 2) + 1, len(array) - 1)
     12
+    >>> array = [13, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
+    >>> median_of_3(array, 0, ((len(array) - 0) // 2) + 1, len(array) - 1)
+    13
+    >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 15, 14, 27, 79, 23, 45, 14, 16]
+    >>> median_of_3(array, 0, ((len(array) - 0) // 2) + 1, len(array) - 1)
+    14
     """
     if (array[first_index] > array[middle_index]) != (
         array[first_index] > array[last_index]
@@ -88,9 +108,17 @@ def median_of_3(
 def partition(array: list, low: int, high: int, pivot: int) -> int:
     """
     >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
-
     >>> partition(array, 0, len(array), 12)
     8
+    >>> array = [21, 15, 11, 45, -2, -11, 46]
+    >>> partition(array, 0, len(array), 15)
+    3
+    >>> array = ['a', 'z', 'd', 'p', 'v', 'l', 'o', 'o']
+    >>> partition(array, 0, len(array), 'p')
+    5
+    >>> array = [6.2, -45.54, 8465.20, 758.56, -457.0, 0, 1, 2.879, 1.7, 11.7]
+    >>> partition(array, 0, len(array), 2.879)
+    6
     """
     i = low
     j = high
@@ -115,22 +143,16 @@ def sort(array: list) -> list:
     Examples:
     >>> sort([4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12])
     [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79]
-
     >>> sort([-1, -5, -3, -13, -44])
     [-44, -13, -5, -3, -1]
-
     >>> sort([])
     []
-
     >>> sort([5])
     [5]
-
     >>> sort([-3, 0, -7, 6, 23, -34])
     [-34, -7, -3, 0, 6, 23]
-
     >>> sort([1.7, 1.0, 3.3, 2.1, 0.3 ])
     [0.3, 1.0, 1.7, 2.1, 3.3]
-
     >>> sort(['d', 'a', 'b', 'e', 'c'])
     ['a', 'b', 'c', 'd', 'e']
     """
@@ -146,9 +168,7 @@ def intro_sort(
 ) -> list:
     """
     >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]
-
     >>> max_depth = 2 * math.ceil(math.log2(len(array)))
-
     >>> intro_sort(array, 0, len(array), 16, max_depth)
     [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79]
     """
@@ -167,7 +187,6 @@ def intro_sort(
     import doctest
 
     doctest.testmod()
-
     user_input = input("Enter numbers separated by a comma : ").strip()
     unsorted = [float(item) for item in user_input.split(",")]
-    print(sort(unsorted))
+    print(f"{sort(unsorted) = }")
diff --git a/sorts/merge_sort.py b/sorts/merge_sort.py
index e80b1cb226ec..0628b848b794 100644
--- a/sorts/merge_sort.py
+++ b/sorts/merge_sort.py
@@ -12,9 +12,13 @@
 
 def merge_sort(collection: list) -> list:
     """
-    :param collection: some mutable ordered collection with heterogeneous
-    comparable items inside
-    :return: the same collection ordered by ascending
+    Sorts a list using the merge sort algorithm.
+
+    :param collection: A mutable ordered collection with comparable items.
+    :return: The same collection ordered in ascending order.
+
+    Time Complexity: O(n log n)
+
     Examples:
     >>> merge_sort([0, 5, 3, 2, 2])
     [0, 2, 2, 3, 5]
@@ -26,31 +30,34 @@ def merge_sort(collection: list) -> list:
 
     def merge(left: list, right: list) -> list:
         """
-        Merge left and right.
+        Merge two sorted lists into a single sorted list.
 
-        :param left: left collection
-        :param right: right collection
-        :return: merge result
+        :param left: Left collection
+        :param right: Right collection
+        :return: Merged result
         """
-
-        def _merge():
-            while left and right:
-                yield (left if left[0] <= right[0] else right).pop(0)
-            yield from left
-            yield from right
-
-        return list(_merge())
+        result = []
+        while left and right:
+            result.append(left.pop(0) if left[0] <= right[0] else right.pop(0))
+        result.extend(left)
+        result.extend(right)
+        return result
 
     if len(collection) <= 1:
         return collection
-    mid = len(collection) // 2
-    return merge(merge_sort(collection[:mid]), merge_sort(collection[mid:]))
+    mid_index = len(collection) // 2
+    return merge(merge_sort(collection[:mid_index]), merge_sort(collection[mid_index:]))
 
 
 if __name__ == "__main__":
     import doctest
 
     doctest.testmod()
-    user_input = input("Enter numbers separated by a comma:\n").strip()
-    unsorted = [int(item) for item in user_input.split(",")]
-    print(*merge_sort(unsorted), sep=",")
+
+    try:
+        user_input = input("Enter numbers separated by a comma:\n").strip()
+        unsorted = [int(item) for item in user_input.split(",")]
+        sorted_list = merge_sort(unsorted)
+        print(*sorted_list, sep=",")
+    except ValueError:
+        print("Invalid input. Please enter valid integers separated by commas.")
diff --git a/sorts/msd_radix_sort.py b/sorts/msd_radix_sort.py
index 03f84c75b9d8..6aba4263663a 100644
--- a/sorts/msd_radix_sort.py
+++ b/sorts/msd_radix_sort.py
@@ -4,6 +4,7 @@
 them.
 https://en.wikipedia.org/wiki/Radix_sort
 """
+
 from __future__ import annotations
 
 
diff --git a/sorts/odd_even_transposition_parallel.py b/sorts/odd_even_transposition_parallel.py
index 9e0d228bdc5b..5d4e09b211c0 100644
--- a/sorts/odd_even_transposition_parallel.py
+++ b/sorts/odd_even_transposition_parallel.py
@@ -10,10 +10,12 @@
 They are synchronized with locks and message passing but other forms of
 synchronization could be used.
 """
-from multiprocessing import Lock, Pipe, Process
+
+import multiprocessing as mp
 
 # lock used to ensure that two processes do not access a pipe at the same time
-process_lock = Lock()
+# NOTE This breaks testing on build runner. May work better locally
+# process_lock = mp.Lock()
 
 """
 The function run by the processes that sorts the list
@@ -27,8 +29,17 @@
 """
 
 
-def oe_process(position, value, l_send, r_send, lr_cv, rr_cv, result_pipe):
-    global process_lock
+def oe_process(
+    position,
+    value,
+    l_send,
+    r_send,
+    lr_cv,
+    rr_cv,
+    result_pipe,
+    multiprocessing_context,
+):
+    process_lock = multiprocessing_context.Lock()
 
     # we perform n swaps since after n swaps we know we are sorted
     # we *could* stop early if we are sorted already, but it takes as long to
@@ -36,27 +47,23 @@ def oe_process(position, value, l_send, r_send, lr_cv, rr_cv, result_pipe):
     for i in range(10):
         if (i + position) % 2 == 0 and r_send is not None:
             # send your value to your right neighbor
-            process_lock.acquire()
-            r_send[1].send(value)
-            process_lock.release()
+            with process_lock:
+                r_send[1].send(value)
 
             # receive your right neighbor's value
-            process_lock.acquire()
-            temp = rr_cv[0].recv()
-            process_lock.release()
+            with process_lock:
+                temp = rr_cv[0].recv()
 
             # take the lower value since you are on the left
             value = min(value, temp)
         elif (i + position) % 2 != 0 and l_send is not None:
             # send your value to your left neighbor
-            process_lock.acquire()
-            l_send[1].send(value)
-            process_lock.release()
+            with process_lock:
+                l_send[1].send(value)
 
             # receive your left neighbor's value
-            process_lock.acquire()
-            temp = lr_cv[0].recv()
-            process_lock.release()
+            with process_lock:
+                temp = lr_cv[0].recv()
 
             # take the higher value since you are on the right
             value = max(value, temp)
@@ -72,39 +79,80 @@ def oe_process(position, value, l_send, r_send, lr_cv, rr_cv, result_pipe):
 
 
 def odd_even_transposition(arr):
+    """
+    >>> odd_even_transposition(list(range(10)[::-1])) == sorted(list(range(10)[::-1]))
+    True
+    >>> odd_even_transposition(["a", "x", "c"]) == sorted(["x", "a", "c"])
+    True
+    >>> odd_even_transposition([1.9, 42.0, 2.8]) == sorted([1.9, 42.0, 2.8])
+    True
+    >>> odd_even_transposition([False, True, False]) == sorted([False, False, True])
+    True
+    >>> odd_even_transposition([1, 32.0, 9]) == sorted([False, False, True])
+    False
+    >>> odd_even_transposition([1, 32.0, 9]) == sorted([1.0, 32, 9.0])
+    True
+    >>> unsorted_list = [-442, -98, -554, 266, -491, 985, -53, -529, 82, -429]
+    >>> odd_even_transposition(unsorted_list) == sorted(unsorted_list)
+    True
+    >>> unsorted_list = [-442, -98, -554, 266, -491, 985, -53, -529, 82, -429]
+    >>> odd_even_transposition(unsorted_list) == sorted(unsorted_list + [1])
+    False
+    """
+    # spawn method is considered safer than fork
+    multiprocessing_context = mp.get_context("spawn")
+
     process_array_ = []
     result_pipe = []
     # initialize the list of pipes where the values will be retrieved
     for _ in arr:
-        result_pipe.append(Pipe())
+        result_pipe.append(multiprocessing_context.Pipe())
     # creates the processes
     # the first and last process only have one neighbor so they are made outside
     # of the loop
-    temp_rs = Pipe()
-    temp_rr = Pipe()
+    temp_rs = multiprocessing_context.Pipe()
+    temp_rr = multiprocessing_context.Pipe()
     process_array_.append(
-        Process(
+        multiprocessing_context.Process(
             target=oe_process,
-            args=(0, arr[0], None, temp_rs, None, temp_rr, result_pipe[0]),
+            args=(
+                0,
+                arr[0],
+                None,
+                temp_rs,
+                None,
+                temp_rr,
+                result_pipe[0],
+                multiprocessing_context,
+            ),
         )
     )
     temp_lr = temp_rs
     temp_ls = temp_rr
 
     for i in range(1, len(arr) - 1):
-        temp_rs = Pipe()
-        temp_rr = Pipe()
+        temp_rs = multiprocessing_context.Pipe()
+        temp_rr = multiprocessing_context.Pipe()
         process_array_.append(
-            Process(
+            multiprocessing_context.Process(
                 target=oe_process,
-                args=(i, arr[i], temp_ls, temp_rs, temp_lr, temp_rr, result_pipe[i]),
+                args=(
+                    i,
+                    arr[i],
+                    temp_ls,
+                    temp_rs,
+                    temp_lr,
+                    temp_rr,
+                    result_pipe[i],
+                    multiprocessing_context,
+                ),
             )
         )
         temp_lr = temp_rs
         temp_ls = temp_rr
 
     process_array_.append(
-        Process(
+        multiprocessing_context.Process(
             target=oe_process,
             args=(
                 len(arr) - 1,
@@ -114,6 +162,7 @@ def odd_even_transposition(arr):
                 temp_lr,
                 None,
                 result_pipe[len(arr) - 1],
+                multiprocessing_context,
             ),
         )
     )
diff --git a/sorts/pigeon_sort.py b/sorts/pigeon_sort.py
index 3e6d4c09c46f..fdfa692f4680 100644
--- a/sorts/pigeon_sort.py
+++ b/sorts/pigeon_sort.py
@@ -1,14 +1,15 @@
 """
-    This is an implementation of Pigeon Hole Sort.
-    For doctests run following command:
+This is an implementation of Pigeon Hole Sort.
+For doctests run following command:
 
-    python3 -m doctest -v pigeon_sort.py
-    or
-    python -m doctest -v pigeon_sort.py
+python3 -m doctest -v pigeon_sort.py
+or
+python -m doctest -v pigeon_sort.py
 
-    For manual testing run:
-    python pigeon_sort.py
+For manual testing run:
+python pigeon_sort.py
 """
+
 from __future__ import annotations
 
 
diff --git a/sorts/quick_sort.py b/sorts/quick_sort.py
index b79d3eac3e48..374d52e75c81 100644
--- a/sorts/quick_sort.py
+++ b/sorts/quick_sort.py
@@ -7,16 +7,17 @@
 For manual testing run:
 python3 quick_sort.py
 """
+
 from __future__ import annotations
 
 from random import randrange
 
 
 def quick_sort(collection: list) -> list:
-    """A pure Python implementation of quick sort algorithm
+    """A pure Python implementation of quicksort algorithm.
 
     :param collection: a mutable collection of comparable items
-    :return: the same collection ordered by ascending
+    :return: the same collection ordered in ascending order
 
     Examples:
     >>> quick_sort([0, 5, 3, 2, 2])
@@ -26,23 +27,26 @@ def quick_sort(collection: list) -> list:
     >>> quick_sort([-2, 5, 0, -45])
     [-45, -2, 0, 5]
     """
+    # Base case: if the collection has 0 or 1 elements, it is already sorted
     if len(collection) < 2:
         return collection
-    pivot_index = randrange(len(collection))  # Use random element as pivot
-    pivot = collection[pivot_index]
-    greater: list[int] = []  # All elements greater than pivot
-    lesser: list[int] = []  # All elements less than or equal to pivot
 
-    for element in collection[:pivot_index]:
-        (greater if element > pivot else lesser).append(element)
+    # Randomly select a pivot index and remove the pivot element from the collection
+    pivot_index = randrange(len(collection))
+    pivot = collection.pop(pivot_index)
 
-    for element in collection[pivot_index + 1 :]:
-        (greater if element > pivot else lesser).append(element)
+    # Partition the remaining elements into two groups: lesser or equal, and greater
+    lesser = [item for item in collection if item <= pivot]
+    greater = [item for item in collection if item > pivot]
 
+    # Recursively sort the lesser and greater groups, and combine with the pivot
     return [*quick_sort(lesser), pivot, *quick_sort(greater)]
 
 
 if __name__ == "__main__":
+    # Get user input and convert it into a list of integers
     user_input = input("Enter numbers separated by a comma:\n").strip()
     unsorted = [int(item) for item in user_input.split(",")]
+
+    # Print the result of sorting the user-provided list
     print(quick_sort(unsorted))
diff --git a/sorts/quick_sort_3_partition.py b/sorts/quick_sort_3_partition.py
index 1a6db6a364f0..279b9a68f5a6 100644
--- a/sorts/quick_sort_3_partition.py
+++ b/sorts/quick_sort_3_partition.py
@@ -1,4 +1,27 @@
 def quick_sort_3partition(sorting: list, left: int, right: int) -> None:
+    """ "
+    Python implementation of quick sort algorithm with 3-way partition.
+    The idea of 3-way quick sort is based on "Dutch National Flag algorithm".
+
+    :param sorting: sort list
+    :param left: left endpoint of sorting
+    :param right: right endpoint of sorting
+    :return: None
+
+    Examples:
+    >>> array1 = [5, -1, -1, 5, 5, 24, 0]
+    >>> quick_sort_3partition(array1, 0, 6)
+    >>> array1
+    [-1, -1, 0, 5, 5, 5, 24]
+    >>> array2 = [9, 0, 2, 6]
+    >>> quick_sort_3partition(array2, 0, 3)
+    >>> array2
+    [0, 2, 6, 9]
+    >>> array3 = []
+    >>> quick_sort_3partition(array3, 0, 0)
+    >>> array3
+    []
+    """
     if right <= left:
         return
     a = i = left
diff --git a/sorts/radix_sort.py b/sorts/radix_sort.py
index 832b6162f349..1dbf5fbd1365 100644
--- a/sorts/radix_sort.py
+++ b/sorts/radix_sort.py
@@ -3,6 +3,7 @@
 
 Source: https://en.wikipedia.org/wiki/Radix_sort
 """
+
 from __future__ import annotations
 
 RADIX = 10
diff --git a/sorts/recursive_insertion_sort.py b/sorts/recursive_insertion_sort.py
index 297dbe9457e6..93465350bee2 100644
--- a/sorts/recursive_insertion_sort.py
+++ b/sorts/recursive_insertion_sort.py
@@ -1,6 +1,7 @@
 """
 A recursive implementation of the insertion sort algorithm
 """
+
 from __future__ import annotations
 
 
diff --git a/sorts/selection_sort.py b/sorts/selection_sort.py
index 28971a5e1aad..506836b53e44 100644
--- a/sorts/selection_sort.py
+++ b/sorts/selection_sort.py
@@ -1,22 +1,9 @@
-"""
-This is a pure Python implementation of the selection sort algorithm
-
-For doctests run following command:
-python -m doctest -v selection_sort.py
-or
-python3 -m doctest -v selection_sort.py
-
-For manual testing run:
-python selection_sort.py
-"""
-
-
 def selection_sort(collection: list[int]) -> list[int]:
-    """Pure implementation of the selection sort algorithm in Python
-    :param collection: some mutable ordered collection with heterogeneous
-    comparable items inside
-    :return: the same collection ordered by ascending
+    """
+    Sorts a list in ascending order using the selection sort algorithm.
 
+    :param collection: A list of integers to be sorted.
+    :return: The sorted list.
 
     Examples:
     >>> selection_sort([0, 5, 3, 2, 2])
@@ -31,16 +18,17 @@ def selection_sort(collection: list[int]) -> list[int]:
 
     length = len(collection)
     for i in range(length - 1):
-        least = i
+        min_index = i
         for k in range(i + 1, length):
-            if collection[k] < collection[least]:
-                least = k
-        if least != i:
-            collection[least], collection[i] = (collection[i], collection[least])
+            if collection[k] < collection[min_index]:
+                min_index = k
+        if min_index != i:
+            collection[i], collection[min_index] = collection[min_index], collection[i]
     return collection
 
 
 if __name__ == "__main__":
     user_input = input("Enter numbers separated by a comma:\n").strip()
     unsorted = [int(item) for item in user_input.split(",")]
-    print(selection_sort(unsorted))
+    sorted_list = selection_sort(unsorted)
+    print("Sorted List:", sorted_list)
diff --git a/sorts/slowsort.py b/sorts/slowsort.py
index a5f4e873ebb2..394e6eed50b1 100644
--- a/sorts/slowsort.py
+++ b/sorts/slowsort.py
@@ -8,6 +8,7 @@
 
 Source: https://en.wikipedia.org/wiki/Slowsort
 """
+
 from __future__ import annotations
 
 
diff --git a/sorts/tim_sort.py b/sorts/tim_sort.py
index 138f11c71bcc..41ab4a10a87b 100644
--- a/sorts/tim_sort.py
+++ b/sorts/tim_sort.py
@@ -19,7 +19,7 @@ def insertion_sort(lst):
     for index in range(1, length):
         value = lst[index]
         pos = binary_search(lst, value, 0, index - 1)
-        lst = lst[:pos] + [value] + lst[pos:index] + lst[index + 1 :]
+        lst = [*lst[:pos], value, *lst[pos:index], *lst[index + 1 :]]
 
     return lst
 
diff --git a/sorts/tree_sort.py b/sorts/tree_sort.py
index dc95856f44c8..056864957d4d 100644
--- a/sorts/tree_sort.py
+++ b/sorts/tree_sort.py
@@ -3,6 +3,7 @@
 
 Build a Binary Search Tree and then iterate thru it to get a sorted list.
 """
+
 from __future__ import annotations
 
 from collections.abc import Iterator
diff --git a/strings/bitap_string_match.py b/strings/bitap_string_match.py
new file mode 100644
index 000000000000..bd8a0f0d73ec
--- /dev/null
+++ b/strings/bitap_string_match.py
@@ -0,0 +1,79 @@
+"""
+Bitap exact string matching
+https://en.wikipedia.org/wiki/Bitap_algorithm
+
+Searches for a pattern inside text, and returns the index of the first occurrence
+of the pattern. Both text and pattern consist of lowercase alphabetical characters only.
+
+Complexity: O(m*n)
+    n = length of text
+    m = length of pattern
+
+Python doctests can be run using this command:
+python3 -m doctest -v bitap_string_match.py
+"""
+
+
+def bitap_string_match(text: str, pattern: str) -> int:
+    """
+    Retrieves the index of the first occurrence of pattern in text.
+
+    Args:
+        text: A string consisting only of lowercase alphabetical characters.
+        pattern: A string consisting only of lowercase alphabetical characters.
+
+    Returns:
+        int: The index where pattern first occurs. Return -1  if not found.
+
+    >>> bitap_string_match('abdabababc', 'ababc')
+    5
+    >>> bitap_string_match('aaaaaaaaaaaaaaaaaa', 'a')
+    0
+    >>> bitap_string_match('zxywsijdfosdfnso', 'zxywsijdfosdfnso')
+    0
+    >>> bitap_string_match('abdabababc', '')
+    0
+    >>> bitap_string_match('abdabababc', 'c')
+    9
+    >>> bitap_string_match('abdabababc', 'fofosdfo')
+    -1
+    >>> bitap_string_match('abdab', 'fofosdfo')
+    -1
+    """
+    if not pattern:
+        return 0
+    m = len(pattern)
+    if m > len(text):
+        return -1
+
+    # Initial state of bit string 1110
+    state = ~1
+    # Bit = 0 if character appears at index, and 1 otherwise
+    pattern_mask: list[int] = [~0] * 27  # 1111
+
+    for i, char in enumerate(pattern):
+        # For the pattern mask for this character, set the bit to 0 for each i
+        # the character appears.
+        pattern_index: int = ord(char) - ord("a")
+        pattern_mask[pattern_index] &= ~(1 << i)
+
+    for i, char in enumerate(text):
+        text_index = ord(char) - ord("a")
+        # If this character does not appear in pattern, it's pattern mask is 1111.
+        # Performing a bitwise OR between state and 1111 will reset the state to 1111
+        # and start searching the start of pattern again.
+        state |= pattern_mask[text_index]
+        state <<= 1
+
+        # If the mth bit (counting right to left) of the state is 0, then we have
+        # found pattern in text
+        if (state & (1 << m)) == 0:
+            return i - m + 1
+
+    return -1
+
+
+if __name__ == "__main__":
+    import doctest
+
+    doctest.testmod()
diff --git a/strings/boyer_moore_search.py b/strings/boyer_moore_search.py
index 117305d32fd3..ad14a504f792 100644
--- a/strings/boyer_moore_search.py
+++ b/strings/boyer_moore_search.py
@@ -11,22 +11,31 @@
 a shift is proposed that moves the entirety of Pattern past
 the point of mismatch in the text.
 
-If there no mismatch then the pattern matches with text block.
+If there is no mismatch then the pattern matches with text block.
 
 Time Complexity : O(n/m)
     n=length of main string
     m=length of pattern string
 """
-from __future__ import annotations
 
 
 class BoyerMooreSearch:
+    """
+    Example usage:
+
+        bms = BoyerMooreSearch(text="ABAABA", pattern="AB")
+        positions = bms.bad_character_heuristic()
+
+    where 'positions' contain the locations where the pattern was matched.
+    """
+
     def __init__(self, text: str, pattern: str):
         self.text, self.pattern = text, pattern
         self.textLen, self.patLen = len(text), len(pattern)
 
     def match_in_pattern(self, char: str) -> int:
-        """finds the index of char in pattern in reverse order
+        """
+        Finds the index of char in pattern in reverse order.
 
         Parameters :
             char (chr): character to be searched
@@ -34,6 +43,10 @@ def match_in_pattern(self, char: str) -> int:
         Returns :
             i (int): index of char from last in pattern
             -1 (int): if char is not found in pattern
+
+        >>> bms = BoyerMooreSearch(text="ABAABA", pattern="AB")
+        >>> bms.match_in_pattern("B")
+        1
         """
 
         for i in range(self.patLen - 1, -1, -1):
@@ -43,8 +56,8 @@ def match_in_pattern(self, char: str) -> int:
 
     def mismatch_in_text(self, current_pos: int) -> int:
         """
-        find the index of mis-matched character in text when compared with pattern
-        from last
+        Find the index of mis-matched character in text when compared with pattern
+        from last.
 
         Parameters :
             current_pos (int): current index position of text
@@ -52,6 +65,10 @@ def mismatch_in_text(self, current_pos: int) -> int:
         Returns :
             i (int): index of mismatched char from last in text
             -1 (int): if there is no mismatch between pattern and text block
+
+        >>> bms = BoyerMooreSearch(text="ABAABA", pattern="AB")
+        >>> bms.mismatch_in_text(2)
+        3
         """
 
         for i in range(self.patLen - 1, -1, -1):
@@ -60,7 +77,14 @@ def mismatch_in_text(self, current_pos: int) -> int:
         return -1
 
     def bad_character_heuristic(self) -> list[int]:
-        # searches pattern in text and returns index positions
+        """
+        Finds the positions of the pattern location.
+
+        >>> bms = BoyerMooreSearch(text="ABAABA", pattern="AB")
+        >>> bms.bad_character_heuristic()
+        [0, 3]
+        """
+
         positions = []
         for i in range(self.textLen - self.patLen + 1):
             mismatch_index = self.mismatch_in_text(i)
@@ -74,13 +98,7 @@ def bad_character_heuristic(self) -> list[int]:
         return positions
 
 
-text = "ABAABA"
-pattern = "AB"
-bms = BoyerMooreSearch(text, pattern)
-positions = bms.bad_character_heuristic()
+if __name__ == "__main__":
+    import doctest
 
-if len(positions) == 0:
-    print("No match found")
-else:
-    print("Pattern found in following positions: ")
-    print(positions)
+    doctest.testmod()
diff --git a/strings/can_string_be_rearranged_as_palindrome.py b/strings/can_string_be_rearranged_as_palindrome.py
index 21d653db1405..95cda8b72180 100644
--- a/strings/can_string_be_rearranged_as_palindrome.py
+++ b/strings/can_string_be_rearranged_as_palindrome.py
@@ -72,9 +72,7 @@ def can_string_be_rearranged_as_palindrome(input_str: str = "") -> bool:
     for character_count in character_freq_dict.values():
         if character_count % 2:
             odd_char += 1
-    if odd_char > 1:
-        return False
-    return True
+    return not odd_char > 1
 
 
 def benchmark(input_str: str = "") -> None:
diff --git a/strings/capitalize.py b/strings/capitalize.py
index e7e97c2beb53..c0b45e0d9614 100644
--- a/strings/capitalize.py
+++ b/strings/capitalize.py
@@ -3,7 +3,8 @@
 
 def capitalize(sentence: str) -> str:
     """
-    This function will capitalize the first letter of a sentence or a word
+    Capitalizes the first letter of a sentence or word.
+
     >>> capitalize("hello world")
     'Hello world'
     >>> capitalize("123 hello world")
@@ -17,6 +18,10 @@ def capitalize(sentence: str) -> str:
     """
     if not sentence:
         return ""
+
+    # Create a dictionary that maps lowercase letters to uppercase letters
+    # Capitalize the first character if it's a lowercase letter
+    # Concatenate the capitalized character with the rest of the string
     lower_to_upper = dict(zip(ascii_lowercase, ascii_uppercase))
     return lower_to_upper.get(sentence[0], sentence[0]) + sentence[1:]
 
diff --git a/strings/check_anagrams.py b/strings/check_anagrams.py
index 9dcdffcfb921..d747368b2373 100644
--- a/strings/check_anagrams.py
+++ b/strings/check_anagrams.py
@@ -1,6 +1,7 @@
 """
 wiki: https://en.wikipedia.org/wiki/Anagram
 """
+
 from collections import defaultdict
 
 
diff --git a/strings/count_vowels.py b/strings/count_vowels.py
new file mode 100644
index 000000000000..8a52b331c81b
--- /dev/null
+++ b/strings/count_vowels.py
@@ -0,0 +1,34 @@
+def count_vowels(s: str) -> int:
+    """
+    Count the number of vowels in a given string.
+
+    :param s: Input string to count vowels in.
+    :return: Number of vowels in the input string.
+
+    Examples:
+    >>> count_vowels("hello world")
+    3
+    >>> count_vowels("HELLO WORLD")
+    3
+    >>> count_vowels("123 hello world")
+    3
+    >>> count_vowels("")
+    0
+    >>> count_vowels("a quick brown fox")
+    5
+    >>> count_vowels("the quick BROWN fox")
+    5
+    >>> count_vowels("PYTHON")
+    1
+    """
+    if not isinstance(s, str):
+        raise ValueError("Input must be a string")
+
+    vowels = "aeiouAEIOU"
+    return sum(1 for char in s if char in vowels)
+
+
+if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
diff --git a/strings/credit_card_validator.py b/strings/credit_card_validator.py
index 78bf45740a63..b8da1c745124 100644
--- a/strings/credit_card_validator.py
+++ b/strings/credit_card_validator.py
@@ -36,7 +36,7 @@ def luhn_validation(credit_card_number: str) -> bool:
         digit = int(cc_number[i])
         digit *= 2
         # If doubling of a number results in a two digit number
-        # i.e greater than 9(e.g., 6 × 2 = 12),
+        # i.e greater than 9(e.g., 6 x 2 = 12),
         # then add the digits of the product (e.g., 12: 1 + 2 = 3, 15: 1 + 5 = 6),
         # to get a single digit number.
         if digit > 9:
diff --git a/strings/detecting_english_programmatically.py b/strings/detecting_english_programmatically.py
index b9000101beb4..e30e2ea8dd8b 100644
--- a/strings/detecting_english_programmatically.py
+++ b/strings/detecting_english_programmatically.py
@@ -25,6 +25,18 @@ def get_english_count(message: str) -> float:
 
 
 def remove_non_letters(message: str) -> str:
+    """
+    >>> remove_non_letters("Hi! how are you?")
+    'Hi how are you'
+    >>> remove_non_letters("P^y%t)h@o*n")
+    'Python'
+    >>> remove_non_letters("1+1=2")
+    ''
+    >>> remove_non_letters("www.google.com/")
+    'wwwgooglecom'
+    >>> remove_non_letters("")
+    ''
+    """
     return "".join(symbol for symbol in message if symbol in LETTERS_AND_SPACE)
 
 
diff --git a/strings/frequency_finder.py b/strings/frequency_finder.py
index 19f97afbbe37..98720dc36d6e 100644
--- a/strings/frequency_finder.py
+++ b/strings/frequency_finder.py
@@ -36,7 +36,7 @@
 
 
 def get_letter_count(message: str) -> dict[str, int]:
-    letter_count = {letter: 0 for letter in string.ascii_uppercase}
+    letter_count = dict.fromkeys(string.ascii_uppercase, 0)
     for letter in message.upper():
         if letter in LETTERS:
             letter_count[letter] += 1
@@ -49,6 +49,15 @@ def get_item_at_index_zero(x: tuple) -> str:
 
 
 def get_frequency_order(message: str) -> str:
+    """
+    Get the frequency order of the letters in the given string
+    >>> get_frequency_order('Hello World')
+    'LOWDRHEZQXJKVBPYGFMUCSNIAT'
+    >>> get_frequency_order('Hello@')
+    'LHOEZQXJKVBPYGFWMUCDRSNIAT'
+    >>> get_frequency_order('h')
+    'HZQXJKVBPYGFWMUCLDRSNIOATE'
+    """
     letter_to_freq = get_letter_count(message)
     freq_to_letter: dict[int, list[str]] = {
         freq: [] for letter, freq in letter_to_freq.items()
@@ -58,7 +67,7 @@ def get_frequency_order(message: str) -> str:
 
     freq_to_letter_str: dict[int, str] = {}
 
-    for freq in freq_to_letter:
+    for freq in freq_to_letter:  # noqa: PLC0206
         freq_to_letter[freq].sort(key=ETAOIN.find, reverse=True)
         freq_to_letter_str[freq] = "".join(freq_to_letter[freq])
 
diff --git a/strings/is_valid_email_address.py b/strings/is_valid_email_address.py
index 205394f81297..c3bf7df7349d 100644
--- a/strings/is_valid_email_address.py
+++ b/strings/is_valid_email_address.py
@@ -101,9 +101,7 @@ def is_valid_email_address(email: str) -> bool:
         return False
 
     # (7.) Validate the placement of "." characters
-    if domain.startswith(".") or domain.endswith(".") or ".." in domain:
-        return False
-    return True
+    return not (domain.startswith(".") or domain.endswith(".") or ".." in domain)
 
 
 if __name__ == "__main__":
diff --git a/strings/jaro_winkler.py b/strings/jaro_winkler.py
index f4a8fbad3ac8..0ce5d83b3c41 100644
--- a/strings/jaro_winkler.py
+++ b/strings/jaro_winkler.py
@@ -3,7 +3,7 @@
 
 def jaro_winkler(str1: str, str2: str) -> float:
     """
-    Jaro–Winkler distance is a string metric measuring an edit distance between two
+    Jaro-Winkler distance is a string metric measuring an edit distance between two
     sequences.
     Output value is between 0.0 and 1.0.
 
@@ -28,12 +28,14 @@ def jaro_winkler(str1: str, str2: str) -> float:
     def get_matched_characters(_str1: str, _str2: str) -> str:
         matched = []
         limit = min(len(_str1), len(_str2)) // 2
-        for i, l in enumerate(_str1):
+        for i, char in enumerate(_str1):
             left = int(max(0, i - limit))
             right = int(min(i + limit + 1, len(_str2)))
-            if l in _str2[left:right]:
-                matched.append(l)
-                _str2 = f"{_str2[0:_str2.index(l)]} {_str2[_str2.index(l) + 1:]}"
+            if char in _str2[left:right]:
+                matched.append(char)
+                _str2 = (
+                    f"{_str2[0 : _str2.index(char)]} {_str2[_str2.index(char) + 1 :]}"
+                )
 
         return "".join(matched)
 
diff --git a/strings/join.py b/strings/join.py
index 5c02f65a20ce..cdcc3a1377f4 100644
--- a/strings/join.py
+++ b/strings/join.py
@@ -24,6 +24,8 @@ def join(separator: str, separated: list[str]) -> str:
     'a'
     >>> join(" ", ["You", "are", "amazing!"])
     'You are amazing!'
+    >>> join(",", ["", "", ""])
+    ',,'
 
     This example should raise an
     exception for non-string elements:
@@ -37,15 +39,33 @@ def join(separator: str, separated: list[str]) -> str:
     'apple-banana-cherry'
     """
 
-    joined = ""
+    # Check that all elements are strings
     for word_or_phrase in separated:
+        # If the element is not a string, raise an exception
         if not isinstance(word_or_phrase, str):
             raise Exception("join() accepts only strings")
+
+    joined: str = ""
+    """
+    The last element of the list is not followed by the separator.
+    So, we need to iterate through the list and join each element
+    with the separator except the last element.
+    """
+    last_index: int = len(separated) - 1
+    """
+    Iterate through the list and join each element with the separator.
+    Except the last element, all other elements are followed by the separator.
+    """
+    for word_or_phrase in separated[:last_index]:
+        # join the element with the separator.
         joined += word_or_phrase + separator
 
-    # Remove the trailing separator
-    # by stripping it from the result
-    return joined.strip(separator)
+    # If the list is not empty, join the last element.
+    if separated != []:
+        joined += separated[last_index]
+
+    # Return the joined string.
+    return joined
 
 
 if __name__ == "__main__":
diff --git a/strings/levenshtein_distance.py b/strings/levenshtein_distance.py
index 7be4074dc39b..3af6608723a5 100644
--- a/strings/levenshtein_distance.py
+++ b/strings/levenshtein_distance.py
@@ -1,20 +1,9 @@
-"""
-This is a Python implementation of the levenshtein distance.
-Levenshtein distance is a string metric for measuring the
-difference between two sequences.
-
-For doctests run following command:
-python -m doctest -v levenshtein-distance.py
-or
-python3 -m doctest -v levenshtein-distance.py
-
-For manual testing run:
-python levenshtein-distance.py
-"""
+from collections.abc import Callable
 
 
 def levenshtein_distance(first_word: str, second_word: str) -> int:
-    """Implementation of the levenshtein distance in Python.
+    """
+    Implementation of the Levenshtein distance in Python.
     :param first_word: the first word to measure the difference.
     :param second_word: the second word to measure the difference.
     :return: the levenshtein distance between the two words.
@@ -47,7 +36,7 @@ def levenshtein_distance(first_word: str, second_word: str) -> int:
         current_row = [i + 1]
 
         for j, c2 in enumerate(second_word):
-            # Calculate insertions, deletions and substitutions
+            # Calculate insertions, deletions, and substitutions
             insertions = previous_row[j + 1] + 1
             deletions = current_row[j] + 1
             substitutions = previous_row[j] + (c1 != c2)
@@ -62,9 +51,75 @@ def levenshtein_distance(first_word: str, second_word: str) -> int:
     return previous_row[-1]
 
 
+def levenshtein_distance_optimized(first_word: str, second_word: str) -> int:
+    """
+    Compute the Levenshtein distance between two words (strings).
+    The function is optimized for efficiency by modifying rows in place.
+    :param first_word: the first word to measure the difference.
+    :param second_word: the second word to measure the difference.
+    :return: the Levenshtein distance between the two words.
+    Examples:
+    >>> levenshtein_distance_optimized("planet", "planetary")
+    3
+    >>> levenshtein_distance_optimized("", "test")
+    4
+    >>> levenshtein_distance_optimized("book", "back")
+    2
+    >>> levenshtein_distance_optimized("book", "book")
+    0
+    >>> levenshtein_distance_optimized("test", "")
+    4
+    >>> levenshtein_distance_optimized("", "")
+    0
+    >>> levenshtein_distance_optimized("orchestration", "container")
+    10
+    """
+    if len(first_word) < len(second_word):
+        return levenshtein_distance_optimized(second_word, first_word)
+
+    if len(second_word) == 0:
+        return len(first_word)
+
+    previous_row = list(range(len(second_word) + 1))
+
+    for i, c1 in enumerate(first_word):
+        current_row = [i + 1] + [0] * len(second_word)
+
+        for j, c2 in enumerate(second_word):
+            insertions = previous_row[j + 1] + 1
+            deletions = current_row[j] + 1
+            substitutions = previous_row[j] + (c1 != c2)
+            current_row[j + 1] = min(insertions, deletions, substitutions)
+
+        previous_row = current_row
+
+    return previous_row[-1]
+
+
+def benchmark_levenshtein_distance(func: Callable) -> None:
+    """
+    Benchmark the Levenshtein distance function.
+    :param str: The name of the function being benchmarked.
+    :param func: The function to be benchmarked.
+    """
+    from timeit import timeit
+
+    stmt = f"{func.__name__}('sitting', 'kitten')"
+    setup = f"from __main__ import {func.__name__}"
+    number = 25_000
+    result = timeit(stmt=stmt, setup=setup, number=number)
+    print(f"{func.__name__:<30} finished {number:,} runs in {result:.5f} seconds")
+
+
 if __name__ == "__main__":
-    first_word = input("Enter the first word:\n").strip()
-    second_word = input("Enter the second word:\n").strip()
+    # Get user input for words
+    first_word = input("Enter the first word for Levenshtein distance:\n").strip()
+    second_word = input("Enter the second word for Levenshtein distance:\n").strip()
+
+    # Calculate and print Levenshtein distances
+    print(f"{levenshtein_distance(first_word, second_word) = }")
+    print(f"{levenshtein_distance_optimized(first_word, second_word) = }")
 
-    result = levenshtein_distance(first_word, second_word)
-    print(f"Levenshtein distance between {first_word} and {second_word} is {result}")
+    # Benchmark the Levenshtein distance functions
+    benchmark_levenshtein_distance(levenshtein_distance)
+    benchmark_levenshtein_distance(levenshtein_distance_optimized)
diff --git a/strings/manacher.py b/strings/manacher.py
index c58c7c19ec44..af1b10cf81fb 100644
--- a/strings/manacher.py
+++ b/strings/manacher.py
@@ -5,13 +5,13 @@ def palindromic_string(input_string: str) -> str:
     >>> palindromic_string('ababa')
     'ababa'
 
-    Manacher’s algorithm which finds Longest palindromic Substring in linear time.
+    Manacher's algorithm which finds Longest palindromic Substring in linear time.
 
     1. first this convert input_string("xyx") into new_string("x|y|x") where odd
         positions are actual input characters.
-    2. for each character in new_string it find corresponding length and store the
-        length and l,r to store previously calculated info.(please look the explanation
-        for details)
+    2. for each character in new_string it find corresponding length and
+        store the length and left,right to store previously calculated info.
+        (please look the explanation for details)
 
     3. return corresponding output_string by removing all "|"
     """
@@ -29,7 +29,7 @@ def palindromic_string(input_string: str) -> str:
 
     # we will store the starting and ending of previous furthest ending palindromic
     # substring
-    l, r = 0, 0
+    left, right = 0, 0
 
     # length[i] shows the length of palindromic substring with center i
     length = [1 for i in range(len(new_input_string))]
@@ -37,7 +37,7 @@ def palindromic_string(input_string: str) -> str:
     # for each character in new_string find corresponding palindromic string
     start = 0
     for j in range(len(new_input_string)):
-        k = 1 if j > r else min(length[l + r - j] // 2, r - j + 1)
+        k = 1 if j > right else min(length[left + right - j] // 2, right - j + 1)
         while (
             j - k >= 0
             and j + k < len(new_input_string)
@@ -47,11 +47,11 @@ def palindromic_string(input_string: str) -> str:
 
         length[j] = 2 * k - 1
 
-        # does this string is ending after the previously explored end (that is r) ?
-        # if yes the update the new r to the last index of this
-        if j + k - 1 > r:
-            l = j - k + 1  # noqa: E741
-            r = j + k - 1
+        # does this string is ending after the previously explored end (that is right) ?
+        # if yes the update the new right to the last index of this
+        if j + k - 1 > right:
+            left = j - k + 1
+            right = j + k - 1
 
         # update max_length and start position
         if max_length < length[j]:
@@ -78,8 +78,9 @@ def palindromic_string(input_string: str) -> str:
 consider the string for which we are calculating the longest palindromic substring is
 shown above where ... are some characters in between and right now we are calculating
 the length of palindromic substring with center at a5 with following conditions :
-i) we have stored the length of palindromic substring which has center at a3 (starts at
-    l ends at r) and it is the furthest ending till now, and it has ending after a6
+i) we have stored the length of palindromic substring which has center at a3
+    (starts at left ends at right) and it is the furthest ending till now,
+    and it has ending after a6
 ii) a2 and a4 are equally distant from a3 so char(a2) == char(a4)
 iii) a0 and a6 are equally distant from a3 so char(a0) == char(a6)
 iv) a1 is corresponding equal character of a5 in palindrome with center a3 (remember
@@ -98,11 +99,11 @@ def palindromic_string(input_string: str) -> str:
 a1 but this only holds if a0 and a6 are inside the limits of palindrome centered at a3
 so finally ..
 
-len_of_palindrome__at(a5) = min(len_of_palindrome_at(a1), r-a5)
-where a3 lies from l to r and we have to keep updating that
+len_of_palindrome__at(a5) = min(len_of_palindrome_at(a1), right-a5)
+where a3 lies from left to right and we have to keep updating that
 
-and if the a5 lies outside of l,r boundary we calculate length of palindrome with
-bruteforce and update l,r.
+and if the a5 lies outside of left,right boundary we calculate length of palindrome with
+bruteforce and update left,right.
 
 it gives the linear time complexity just like z-function
 """
diff --git a/strings/min_cost_string_conversion.py b/strings/min_cost_string_conversion.py
index 0fad0b88c370..87eb5189e16a 100644
--- a/strings/min_cost_string_conversion.py
+++ b/strings/min_cost_string_conversion.py
@@ -17,11 +17,27 @@ def compute_transform_tables(
     delete_cost: int,
     insert_cost: int,
 ) -> tuple[list[list[int]], list[list[str]]]:
+    """
+    Finds the most cost efficient sequence
+    for converting one string into another.
+
+    >>> costs, operations = compute_transform_tables("cat", "cut", 1, 2, 3, 3)
+    >>> costs[0][:4]
+    [0, 3, 6, 9]
+    >>> costs[2][:4]
+    [6, 4, 3, 6]
+    >>> operations[0][:4]
+    ['0', 'Ic', 'Iu', 'It']
+    >>> operations[3][:4]
+    ['Dt', 'Dt', 'Rtu', 'Ct']
+
+    >>> compute_transform_tables("", "", 1, 2, 3, 3)
+    ([[0]], [['0']])
+    """
     source_seq = list(source_string)
     destination_seq = list(destination_string)
     len_source_seq = len(source_seq)
     len_destination_seq = len(destination_seq)
-
     costs = [
         [0 for _ in range(len_destination_seq + 1)] for _ in range(len_source_seq + 1)
     ]
@@ -31,48 +47,73 @@ def compute_transform_tables(
 
     for i in range(1, len_source_seq + 1):
         costs[i][0] = i * delete_cost
-        ops[i][0] = f"D{source_seq[i - 1]:c}"
+        ops[i][0] = f"D{source_seq[i - 1]}"
 
     for i in range(1, len_destination_seq + 1):
         costs[0][i] = i * insert_cost
-        ops[0][i] = f"I{destination_seq[i - 1]:c}"
+        ops[0][i] = f"I{destination_seq[i - 1]}"
 
     for i in range(1, len_source_seq + 1):
         for j in range(1, len_destination_seq + 1):
             if source_seq[i - 1] == destination_seq[j - 1]:
                 costs[i][j] = costs[i - 1][j - 1] + copy_cost
-                ops[i][j] = f"C{source_seq[i - 1]:c}"
+                ops[i][j] = f"C{source_seq[i - 1]}"
             else:
                 costs[i][j] = costs[i - 1][j - 1] + replace_cost
-                ops[i][j] = f"R{source_seq[i - 1]:c}" + str(destination_seq[j - 1])
+                ops[i][j] = f"R{source_seq[i - 1]}" + str(destination_seq[j - 1])
 
             if costs[i - 1][j] + delete_cost < costs[i][j]:
                 costs[i][j] = costs[i - 1][j] + delete_cost
-                ops[i][j] = f"D{source_seq[i - 1]:c}"
+                ops[i][j] = f"D{source_seq[i - 1]}"
 
             if costs[i][j - 1] + insert_cost < costs[i][j]:
                 costs[i][j] = costs[i][j - 1] + insert_cost
-                ops[i][j] = f"I{destination_seq[j - 1]:c}"
+                ops[i][j] = f"I{destination_seq[j - 1]}"
 
     return costs, ops
 
 
 def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]:
+    """
+    Assembles the transformations based on the ops table.
+
+    >>> ops = [['0', 'Ic', 'Iu', 'It'],
+    ...        ['Dc', 'Cc', 'Iu', 'It'],
+    ...        ['Da', 'Da', 'Rau', 'Rat'],
+    ...        ['Dt', 'Dt', 'Rtu', 'Ct']]
+    >>> x = len(ops) - 1
+    >>> y = len(ops[0]) - 1
+    >>> assemble_transformation(ops, x, y)
+    ['Cc', 'Rau', 'Ct']
+
+    >>> ops1 = [['0']]
+    >>> x1 = len(ops1) - 1
+    >>> y1 = len(ops1[0]) - 1
+    >>> assemble_transformation(ops1, x1, y1)
+    []
+
+    >>> ops2 = [['0', 'I1', 'I2', 'I3'],
+    ...         ['D1', 'C1', 'I2', 'I3'],
+    ...         ['D2', 'D2', 'R23', 'R23']]
+    >>> x2 = len(ops2) - 1
+    >>> y2 = len(ops2[0]) - 1
+    >>> assemble_transformation(ops2, x2, y2)
+    ['C1', 'I2', 'R23']
+    """
     if i == 0 and j == 0:
         return []
+    elif ops[i][j][0] in {"C", "R"}:
+        seq = assemble_transformation(ops, i - 1, j - 1)
+        seq.append(ops[i][j])
+        return seq
+    elif ops[i][j][0] == "D":
+        seq = assemble_transformation(ops, i - 1, j)
+        seq.append(ops[i][j])
+        return seq
     else:
-        if ops[i][j][0] in {"C", "R"}:
-            seq = assemble_transformation(ops, i - 1, j - 1)
-            seq.append(ops[i][j])
-            return seq
-        elif ops[i][j][0] == "D":
-            seq = assemble_transformation(ops, i - 1, j)
-            seq.append(ops[i][j])
-            return seq
-        else:
-            seq = assemble_transformation(ops, i, j - 1)
-            seq.append(ops[i][j])
-            return seq
+        seq = assemble_transformation(ops, i, j - 1)
+        seq.append(ops[i][j])
+        return seq
 
 
 if __name__ == "__main__":
@@ -91,7 +132,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]:
             print("".join(string))
 
             if op[0] == "C":
-                file.write("%-16s" % "Copy %c" % op[1])
+                file.write("%-16s" % "Copy %c" % op[1])  # noqa: UP031
                 file.write("\t\t\t" + "".join(string))
                 file.write("\r\n")
 
@@ -99,7 +140,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]:
             elif op[0] == "R":
                 string[i] = op[2]
 
-                file.write("%-16s" % ("Replace %c" % op[1] + " with " + str(op[2])))
+                file.write("%-16s" % ("Replace %c" % op[1] + " with " + str(op[2])))  # noqa: UP031
                 file.write("\t\t" + "".join(string))
                 file.write("\r\n")
 
@@ -107,7 +148,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]:
             elif op[0] == "D":
                 string.pop(i)
 
-                file.write("%-16s" % "Delete %c" % op[1])
+                file.write("%-16s" % "Delete %c" % op[1])  # noqa: UP031
                 file.write("\t\t\t" + "".join(string))
                 file.write("\r\n")
 
@@ -115,7 +156,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]:
             else:
                 string.insert(i, op[1])
 
-                file.write("%-16s" % "Insert %c" % op[1])
+                file.write("%-16s" % "Insert %c" % op[1])  # noqa: UP031
                 file.write("\t\t\t" + "".join(string))
                 file.write("\r\n")
 
diff --git a/strings/prefix_function.py b/strings/prefix_function.py
index 65bbe9100735..04987deef469 100644
--- a/strings/prefix_function.py
+++ b/strings/prefix_function.py
@@ -1,7 +1,7 @@
 """
 https://cp-algorithms.com/string/prefix-function.html
 
-Prefix function Knuth–Morris–Pratt algorithm
+Prefix function Knuth-Morris-Pratt algorithm
 
 Different algorithm than Knuth-Morris-Pratt pattern finding
 
diff --git a/strings/split.py b/strings/split.py
index b62b86d2401f..ed194ec69c2f 100644
--- a/strings/split.py
+++ b/strings/split.py
@@ -14,6 +14,9 @@ def split(string: str, separator: str = " ") -> list:
 
     >>> split("12:43:39",separator = ":")
     ['12', '43', '39']
+
+    >>> split(";abbb;;c;", separator=';')
+    ['', 'abbb', '', 'c', '']
     """
 
     split_words = []
@@ -23,7 +26,7 @@ def split(string: str, separator: str = " ") -> list:
         if char == separator:
             split_words.append(string[last_index:index])
             last_index = index + 1
-        elif index + 1 == len(string):
+        if index + 1 == len(string):
             split_words.append(string[last_index : index + 1])
     return split_words
 
diff --git a/strings/string_switch_case.py b/strings/string_switch_case.py
index 9a07472dfd71..c16d9fa552f9 100644
--- a/strings/string_switch_case.py
+++ b/strings/string_switch_case.py
@@ -28,6 +28,12 @@ def to_simple_case(str_: str) -> str:
     """
     >>> to_simple_case("one two 31235three4four")
     'OneTwo31235three4four'
+    >>> to_simple_case("This should be combined")
+    'ThisShouldBeCombined'
+    >>> to_simple_case("The first letters are capitalized, then string is merged")
+    'TheFirstLettersAreCapitalizedThenStringIsMerged'
+    >>> to_simple_case("special characters :, ', %, ^, $, are ignored")
+    'SpecialCharactersAreIgnored'
     """
     string_split = split_input(str_)
     return "".join(
@@ -37,6 +43,14 @@ def to_simple_case(str_: str) -> str:
 
 def to_complex_case(text: str, upper: bool, separator: str) -> str:
     """
+    Returns the string concatenated with the delimiter we provide.
+
+    Parameters:
+    @text: The string on which we want to perform operation
+    @upper: Boolean value to determine whether we want capitalized result or not
+    @separator: The delimiter with which we want to concatenate words
+
+    Examples:
     >>> to_complex_case("one two 31235three4four", True, "_")
     'ONE_TWO_31235THREE4FOUR'
     >>> to_complex_case("one two 31235three4four", False, "-")
diff --git a/strings/text_justification.py b/strings/text_justification.py
index b0ef12231224..e025edcfe13f 100644
--- a/strings/text_justification.py
+++ b/strings/text_justification.py
@@ -67,19 +67,19 @@ def justify(line: list, width: int, max_width: int) -> str:
     answer = []
     line: list[str] = []
     width = 0
-    for word in words:
-        if width + len(word) + len(line) <= max_width:
+    for inner_word in words:
+        if width + len(inner_word) + len(line) <= max_width:
             # keep adding words until we can fill out max_width
             # width = sum of length of all words (without overall_spaces_count)
-            # len(word) = length of current word
+            # len(inner_word) = length of current inner_word
             # len(line) = number of overall_spaces_count to insert between words
-            line.append(word)
-            width += len(word)
+            line.append(inner_word)
+            width += len(inner_word)
         else:
             # justify the line and add it to result
             answer.append(justify(line, width, max_width))
             # reset new line and new width
-            line, width = [word], len(word)
+            line, width = [inner_word], len(inner_word)
     remaining_spaces = max_width - width - len(line)
     answer.append(" ".join(line) + (remaining_spaces + 1) * " ")
     return answer
diff --git a/strings/top_k_frequent_words.py b/strings/top_k_frequent_words.py
index f3d1e0cd5ca7..40fa7fc85cd1 100644
--- a/strings/top_k_frequent_words.py
+++ b/strings/top_k_frequent_words.py
@@ -13,7 +13,6 @@ def top_k_frequent_words(words, k_value):
     return [x[0] for x in Counter(words).most_common(k_value)]
 """
 
-
 from collections import Counter
 from functools import total_ordering
 
diff --git a/strings/wave.py b/strings/wave_string.py
similarity index 100%
rename from strings/wave.py
rename to strings/wave_string.py
diff --git a/strings/word_patterns.py b/strings/word_patterns.py
index d12d267e7b35..ed603e9fefeb 100644
--- a/strings/word_patterns.py
+++ b/strings/word_patterns.py
@@ -1,11 +1,32 @@
 def get_word_pattern(word: str) -> str:
     """
+    Returns numerical pattern of character appearances in given word
+    >>> get_word_pattern("")
+    ''
+    >>> get_word_pattern(" ")
+    '0'
     >>> get_word_pattern("pattern")
     '0.1.2.2.3.4.5'
     >>> get_word_pattern("word pattern")
     '0.1.2.3.4.5.6.7.7.8.2.9'
     >>> get_word_pattern("get word pattern")
     '0.1.2.3.4.5.6.7.3.8.9.2.2.1.6.10'
+    >>> get_word_pattern()
+    Traceback (most recent call last):
+    ...
+    TypeError: get_word_pattern() missing 1 required positional argument: 'word'
+    >>> get_word_pattern(1)
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'int' object has no attribute 'upper'
+    >>> get_word_pattern(1.1)
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'float' object has no attribute 'upper'
+    >>> get_word_pattern([])
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'list' object has no attribute 'upper'
     """
     word = word.upper()
     next_num = 0
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 000000000000..c75a1070745b
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1302 @@
+version = 1
+revision = 2
+requires-python = ">=3.13"
+resolution-markers = [
+    "sys_platform == 'darwin'",
+    "platform_machine == 'aarch64' and sys_platform == 'linux'",
+    "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
+]
+
+[[package]]
+name = "absl-py"
+version = "2.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/f0/e6342091061ed3a46aadc116b13edd7bb5249c3ab1b3ef07f24b0c248fc3/absl_py-2.2.2.tar.gz", hash = "sha256:bf25b2c2eed013ca456918c453d687eab4e8309fba81ee2f4c1a6aa2494175eb", size = 119982, upload-time = "2025-04-03T12:41:04.55Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f6/d4/349f7f4bd5ea92dab34f5bb0fe31775ef6c311427a14d5a5b31ecb442341/absl_py-2.2.2-py3-none-any.whl", hash = "sha256:e5797bc6abe45f64fd95dc06394ca3f2bedf3b5d895e9da691c9ee3397d70092", size = 135565, upload-time = "2025-04-03T12:41:03.172Z" },
+]
+
+[[package]]
+name = "alabaster"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "idna" },
+    { name = "sniffio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+]
+
+[[package]]
+name = "astroid"
+version = "3.3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/15/58/5260205b9968c20b6457ed82f48f9e3d6edf2f1f95103161798b73aeccf0/astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", size = 275388, upload-time = "2025-05-10T13:33:08.391Z" },
+]
+
+[[package]]
+name = "babel"
+version = "2.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "soupsieve" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.4.26"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+    { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+    { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+    { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+    { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+    { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+    { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+    { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
+    { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
+    { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
+    { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
+    { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
+    { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
+    { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
+    { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
+    { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" },
+    { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" },
+    { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" },
+    { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" },
+    { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" },
+    { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" },
+    { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" },
+    { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" },
+    { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" },
+    { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" },
+    { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" },
+    { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" },
+    { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "docutils"
+version = "0.21.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
+]
+
+[[package]]
+name = "dom-toml"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "domdf-python-tools" },
+    { name = "tomli" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fd/38/5df2387f84efce3c7ef8d211cae6ca2736a730efc13b7d15a9584bb81b30/dom_toml-2.0.1.tar.gz", hash = "sha256:31c5874595c777f41cc191c34c66dbea215c8289a7b148b425fb7a10c3f4fbc8", size = 11180, upload-time = "2025-02-05T11:59:36.552Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/33/c6/57f64b0e93da6f43393dfcc1281ee1ce8eafbeb940d3861056caa28ae818/dom_toml-2.0.1-py3-none-any.whl", hash = "sha256:6681a793c94a5d1fb7b59690009cc3b1dfb1bcaae108fb8054c712a8f128efc7", size = 13459, upload-time = "2025-02-05T11:59:34.706Z" },
+]
+
+[[package]]
+name = "domdf-python-tools"
+version = "3.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "natsort" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/8b/ab2d8a292bba8fe3135cacc8bfd3576710a14b8f2d0a8cde19130d5c9d21/domdf_python_tools-3.10.0.tar.gz", hash = "sha256:2ae308d2f4f1e9145f5f4ba57f840fbfd1c2983ee26e4824347789649d3ae298", size = 100458, upload-time = "2025-02-12T17:34:05.747Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5b/11/208f72084084d3f6a2ed5ebfdfc846692c3f7ad6dce65e400194924f7eed/domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36", size = 126860, upload-time = "2025-02-12T17:34:04.093Z" },
+]
+
+[[package]]
+name = "fake-useragent"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/43/948d10bf42735709edb5ae51e23297d034086f17fc7279fef385a7acb473/fake_useragent-2.2.0.tar.gz", hash = "sha256:4e6ab6571e40cc086d788523cf9e018f618d07f9050f822ff409a4dfe17c16b2", size = 158898, upload-time = "2025-04-14T15:32:19.238Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.58.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/4d037663e2a1fe30fddb655d755d76e18624be44ad467c07412c2319ab97/fonttools-4.58.0.tar.gz", hash = "sha256:27423d0606a2c7b336913254bf0b1193ebd471d5f725d665e875c5e88a011a43", size = 3514522, upload-time = "2025-05-10T17:36:35.886Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0c/d7/d77cae11c445916d767cace93ba8283b3f360197d95d7470b90a9e984e10/fonttools-4.58.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4809790f2371d8a08e59e1ce2b734c954cf09742e75642d7f4c46cfdac488fdd", size = 2728320, upload-time = "2025-05-10T17:35:56.455Z" },
+    { url = "https://files.pythonhosted.org/packages/77/48/7d8b3c519ef4b48081d40310262224a38785e39a8610ccb92a229a6f085d/fonttools-4.58.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b00f240280f204ce4546b05ff3515bf8ff47a9cae914c718490025ea2bb9b324", size = 2302570, upload-time = "2025-05-10T17:35:58.794Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/48/156b83eb8fb7261056e448bfda1b495b90e761b28ec23cee10e3e19f1967/fonttools-4.58.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a62015ad463e1925544e9159dd6eefe33ebfb80938d5ab15d8b1c4b354ff47b", size = 4790066, upload-time = "2025-05-10T17:36:01.174Z" },
+    { url = "https://files.pythonhosted.org/packages/60/49/aaecb1b3cea2b9b9c7cea6240d6bc8090feb5489a6fbf93cb68003be979b/fonttools-4.58.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ceef6f6ab58061a811967e3e32e630747fcb823dcc33a9a2c80e2d0d17cb292", size = 4861076, upload-time = "2025-05-10T17:36:03.663Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/c8/97cbb41bee81ea9daf6109e0f3f70a274a3c69418e5ac6b0193f5dacf506/fonttools-4.58.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7be21ac52370b515cdbdd0f400803fd29432a4fa4ddb4244ac8b322e54f36c0", size = 4858394, upload-time = "2025-05-10T17:36:06.087Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/23/c2c231457361f869a7d7374a557208e303b469d48a4a697c0fb249733ea1/fonttools-4.58.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85836be4c3c4aacf6fcb7a6f263896d0e9ce431da9fa6fe9213d70f221f131c9", size = 5002160, upload-time = "2025-05-10T17:36:08.178Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/e0/c2262f941a43b810c5c192db94b5d1ce8eda91bec2757f7e2416398f4072/fonttools-4.58.0-cp313-cp313-win32.whl", hash = "sha256:2b32b7130277bd742cb8c4379a6a303963597d22adea77a940343f3eadbcaa4c", size = 2171919, upload-time = "2025-05-10T17:36:10.644Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/ee/e4aa7bb4ce510ad57a808d321df1bbed1eeb6e1dfb20aaee1a5d9c076849/fonttools-4.58.0-cp313-cp313-win_amd64.whl", hash = "sha256:75e68ee2ec9aaa173cf5e33f243da1d51d653d5e25090f2722bc644a78db0f1a", size = 2222972, upload-time = "2025-05-10T17:36:12.495Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/1f/4417c26e26a1feab85a27e927f7a73d8aabc84544be8ba108ce4aa90eb1e/fonttools-4.58.0-py3-none-any.whl", hash = "sha256:c96c36880be2268be409df7b08c5b5dacac1827083461a6bc2cb07b8cbcec1d7", size = 1111440, upload-time = "2025-05-10T17:36:33.607Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "h5py"
+version = "3.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/03/2e/a22d6a8bfa6f8be33e7febd985680fba531562795f0a9077ed1eb047bfb0/h5py-3.13.0.tar.gz", hash = "sha256:1870e46518720023da85d0895a1960ff2ce398c5671eac3b1a41ec696b7105c3", size = 414876, upload-time = "2025-02-18T16:04:01.824Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/60/43/f276f27921919a9144074320ce4ca40882fc67b3cfee81c3f5c7df083e97/h5py-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e520ec76de00943dd017c8ea3f354fa1d2f542eac994811943a8faedf2a7d5cb", size = 3358040, upload-time = "2025-02-18T16:03:20.579Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/86/ad4a4cf781b08d4572be8bbdd8f108bb97b266a14835c640dc43dafc0729/h5py-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e79d8368cd9295045956bfb436656bea3f915beaa11d342e9f79f129f5178763", size = 2892766, upload-time = "2025-02-18T16:03:26.831Z" },
+    { url = "https://files.pythonhosted.org/packages/69/84/4c6367d6b58deaf0fa84999ec819e7578eee96cea6cbd613640d0625ed5e/h5py-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56dd172d862e850823c4af02dc4ddbc308f042b85472ffdaca67f1598dff4a57", size = 4664255, upload-time = "2025-02-18T16:03:31.903Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be949b46b7388074c5acae017fbbe3e5ba303fd9daaa52157fdfef30bbdacadd", size = 4927580, upload-time = "2025-02-18T16:03:36.429Z" },
+    { url = "https://files.pythonhosted.org/packages/97/34/165b87ea55184770a0c1fcdb7e017199974ad2e271451fd045cfe35f3add/h5py-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:4f97ecde7ac6513b21cd95efdfc38dc6d19f96f6ca6f2a30550e94e551458e0a", size = 2940890, upload-time = "2025-02-18T16:03:41.037Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "certifi" },
+    { name = "httpcore" },
+    { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "imageio"
+version = "2.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+    { name = "pillow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963, upload-time = "2025-01-20T02:42:37.089Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796, upload-time = "2025-01-20T02:42:34.931Z" },
+]
+
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/08/8bd4a0250247861420a040b33ccf42f43c426ac91d99405374ef117e5872/joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5", size = 330234, upload-time = "2025-05-03T21:09:39.553Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/da/d3/13ee227a148af1c693654932b8b0b02ed64af5e1f7406d56b088b57574cd/joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491", size = 307682, upload-time = "2025-05-03T21:09:37.892Z" },
+]
+
+[[package]]
+name = "keras"
+version = "3.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "absl-py" },
+    { name = "h5py" },
+    { name = "ml-dtypes" },
+    { name = "namex" },
+    { name = "numpy" },
+    { name = "optree" },
+    { name = "packaging" },
+    { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/4e/82a1176f1ea70982ade0f7e360c6f259b68203f64a108cc1967500d4b7ff/keras-3.9.2.tar.gz", hash = "sha256:322aab6418ee3de1e2bd0871b60a07f0e444e744a7e8cba79af8b42408879ecf", size = 1002482, upload-time = "2025-04-02T19:03:08.953Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3f/0a/678ebcf4b6dad6ad63dfc2445d190f79a97fa7bc7150f57a6c505459e2bc/keras-3.9.2-py3-none-any.whl", hash = "sha256:404427856c2dc30e38c9fa6fa6a13ffb1844a8c35af312ca32a8e7dea9840f1e", size = 1341966, upload-time = "2025-04-02T19:03:07.502Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" },
+    { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" },
+    { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" },
+    { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" },
+    { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" },
+    { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" },
+    { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" },
+    { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" },
+    { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" },
+    { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" },
+    { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" },
+]
+
+[[package]]
+name = "lxml"
+version = "5.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
+    { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
+    { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
+    { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
+    { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
+    { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+    { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+    { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+    { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+    { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+    { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "contourpy" },
+    { name = "cycler" },
+    { name = "fonttools" },
+    { name = "kiwisolver" },
+    { name = "numpy" },
+    { name = "packaging" },
+    { name = "pillow" },
+    { name = "pyparsing" },
+    { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" },
+    { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" },
+    { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" },
+]
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markdown-it-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "ml-dtypes"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/49/6e67c334872d2c114df3020e579f3718c333198f8312290e09ec0216703a/ml_dtypes-0.5.1.tar.gz", hash = "sha256:ac5b58559bb84a95848ed6984eb8013249f90b6bab62aa5acbad876e256002c9", size = 698772, upload-time = "2025-01-07T03:34:55.613Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0f/92/bb6a3d18e16fddd18ce6d5f480e1919b33338c70e18cba831c6ae59812ee/ml_dtypes-0.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:12651420130ee7cc13059fc56dac6ad300c3af3848b802d475148c9defd27c23", size = 667696, upload-time = "2025-01-07T03:34:27.526Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/29/cfc89d842767e9a51146043b0fa18332c2b38f8831447e6cb1160e3c6102/ml_dtypes-0.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9945669d3dadf8acb40ec2e57d38c985d8c285ea73af57fc5b09872c516106d", size = 4638365, upload-time = "2025-01-07T03:34:30.43Z" },
+    { url = "https://files.pythonhosted.org/packages/be/26/adc36e3ea09603d9f6d114894e1c1b7b8e8a9ef6d0b031cc270c6624a37c/ml_dtypes-0.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf9975bda82a99dc935f2ae4c83846d86df8fd6ba179614acac8e686910851da", size = 4702722, upload-time = "2025-01-07T03:34:33.813Z" },
+    { url = "https://files.pythonhosted.org/packages/da/8a/a2b9375c94077e5a488a624a195621407846f504068ce22ccf805c674156/ml_dtypes-0.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fd918d4e6a4e0c110e2e05be7a7814d10dc1b95872accbf6512b80a109b71ae1", size = 210850, upload-time = "2025-01-07T03:34:36.897Z" },
+    { url = "https://files.pythonhosted.org/packages/52/38/703169100fdde27957f061d4d0ea3e00525775a09acaccf7e655d9609d55/ml_dtypes-0.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:05f23447a1c20ddf4dc7c2c661aa9ed93fcb2658f1017c204d1e758714dc28a8", size = 693043, upload-time = "2025-01-07T03:34:38.457Z" },
+    { url = "https://files.pythonhosted.org/packages/28/ff/4e234c9c23e0d456f5da5a326c103bf890c746d93351524d987e41f438b3/ml_dtypes-0.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b7fbe5571fdf28fd3aaab3ef4aafc847de9ebf263be959958c1ca58ec8eadf5", size = 4903946, upload-time = "2025-01-07T03:34:40.236Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/45/c1a1ccfdd02bc4173ca0f4a2d327683a27df85797b885eb1da1ca325b85c/ml_dtypes-0.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d13755f8e8445b3870114e5b6240facaa7cb0c3361e54beba3e07fa912a6e12b", size = 5052731, upload-time = "2025-01-07T03:34:45.308Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "myst-parser"
+version = "4.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "docutils" },
+    { name = "jinja2" },
+    { name = "markdown-it-py" },
+    { name = "mdit-py-plugins" },
+    { name = "pyyaml" },
+    { name = "sphinx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" },
+]
+
+[[package]]
+name = "namex"
+version = "0.0.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/03/00afe7f32962d0778d757aed89cd7f970179e5f7fbe42e883fd494ebbbc7/namex-0.0.9.tar.gz", hash = "sha256:8adfea9da5cea5be8f4e632349b4669e30172c7859e1fd97459fdf3b17469253", size = 6569, upload-time = "2025-04-22T18:00:15.808Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/46/16/7f616b55a147d9d4e9eb910a7eacde96cfa7ce3109d4c57ac32b91348ef3/namex-0.0.9-py3-none-any.whl", hash = "sha256:7bd4e4a2cc3876592111609fdf4cbe6ff19883adbe6b3b40d842fd340f77025e", size = 5847, upload-time = "2025-04-22T18:00:14.558Z" },
+]
+
+[[package]]
+name = "natsort"
+version = "8.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/b2/ce4b867d8cd9c0ee84938ae1e6a6f7926ebf928c9090d036fc3c6a04f946/numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", size = 20273920, upload-time = "2025-04-19T23:27:42.561Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e2/a0/0aa7f0f4509a2e07bd7a509042967c2fab635690d4f48c6c7b3afd4f448c/numpy-2.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4", size = 20935102, upload-time = "2025-04-19T22:41:16.234Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/e4/a6a9f4537542912ec513185396fce52cdd45bdcf3e9d921ab02a93ca5aa9/numpy-2.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f", size = 14191709, upload-time = "2025-04-19T22:41:38.472Z" },
+    { url = "https://files.pythonhosted.org/packages/be/65/72f3186b6050bbfe9c43cb81f9df59ae63603491d36179cf7a7c8d216758/numpy-2.2.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9", size = 5149173, upload-time = "2025-04-19T22:41:47.823Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/e9/83e7a9432378dde5802651307ae5e9ea07bb72b416728202218cd4da2801/numpy-2.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191", size = 6684502, upload-time = "2025-04-19T22:41:58.689Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/27/b80da6c762394c8ee516b74c1f686fcd16c8f23b14de57ba0cad7349d1d2/numpy-2.2.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372", size = 14084417, upload-time = "2025-04-19T22:42:19.897Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d", size = 16133807, upload-time = "2025-04-19T22:42:44.433Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/9b/4cc171a0acbe4666f7775cfd21d4eb6bb1d36d3a0431f48a73e9212d2278/numpy-2.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7", size = 15575611, upload-time = "2025-04-19T22:43:09.928Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/45/40f4135341850df48f8edcf949cf47b523c404b712774f8855a64c96ef29/numpy-2.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73", size = 17895747, upload-time = "2025-04-19T22:43:36.983Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/4c/b32a17a46f0ffbde8cc82df6d3daeaf4f552e346df143e1b188a701a8f09/numpy-2.2.5-cp313-cp313-win32.whl", hash = "sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b", size = 6309594, upload-time = "2025-04-19T22:47:10.523Z" },
+    { url = "https://files.pythonhosted.org/packages/13/ae/72e6276feb9ef06787365b05915bfdb057d01fceb4a43cb80978e518d79b/numpy-2.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471", size = 12638356, upload-time = "2025-04-19T22:47:30.253Z" },
+    { url = "https://files.pythonhosted.org/packages/79/56/be8b85a9f2adb688e7ded6324e20149a03541d2b3297c3ffc1a73f46dedb/numpy-2.2.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6", size = 20963778, upload-time = "2025-04-19T22:44:09.251Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/77/19c5e62d55bff507a18c3cdff82e94fe174957bad25860a991cac719d3ab/numpy-2.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba", size = 14207279, upload-time = "2025-04-19T22:44:31.383Z" },
+    { url = "https://files.pythonhosted.org/packages/75/22/aa11f22dc11ff4ffe4e849d9b63bbe8d4ac6d5fae85ddaa67dfe43be3e76/numpy-2.2.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133", size = 5199247, upload-time = "2025-04-19T22:44:40.361Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/6c/12d5e760fc62c08eded0394f62039f5a9857f758312bf01632a81d841459/numpy-2.2.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376", size = 6711087, upload-time = "2025-04-19T22:44:51.188Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/94/ece8280cf4218b2bee5cec9567629e61e51b4be501e5c6840ceb593db945/numpy-2.2.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19", size = 14059964, upload-time = "2025-04-19T22:45:12.451Z" },
+    { url = "https://files.pythonhosted.org/packages/39/41/c5377dac0514aaeec69115830a39d905b1882819c8e65d97fc60e177e19e/numpy-2.2.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0", size = 16121214, upload-time = "2025-04-19T22:45:37.734Z" },
+    { url = "https://files.pythonhosted.org/packages/db/54/3b9f89a943257bc8e187145c6bc0eb8e3d615655f7b14e9b490b053e8149/numpy-2.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a", size = 15575788, upload-time = "2025-04-19T22:46:01.908Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/c4/2e407e85df35b29f79945751b8f8e671057a13a376497d7fb2151ba0d290/numpy-2.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066", size = 17893672, upload-time = "2025-04-19T22:46:28.585Z" },
+    { url = "https://files.pythonhosted.org/packages/29/7e/d0b44e129d038dba453f00d0e29ebd6eaf2f06055d72b95b9947998aca14/numpy-2.2.5-cp313-cp313t-win32.whl", hash = "sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e", size = 6377102, upload-time = "2025-04-19T22:46:39.949Z" },
+    { url = "https://files.pythonhosted.org/packages/63/be/b85e4aa4bf42c6502851b971f1c326d583fcc68227385f92089cf50a7b45/numpy-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8", size = 12750096, upload-time = "2025-04-19T22:47:00.147Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.26.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/55/66/ed9d28946ead0fe1322df2f4fc6ea042340c0fe73b79a1419dc1fdbdd211/nvidia_nccl_cu12-2.26.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adb1bf4adcc5a47f597738a0700da6aef61f8ea4251b375540ae138c7d239588", size = 318058262, upload-time = "2025-05-02T23:32:43.197Z" },
+    { url = "https://files.pythonhosted.org/packages/48/fb/ec4ac065d9b0d56f72eaf1d9b0df601e33da28197b32ca351dc05b342611/nvidia_nccl_cu12-2.26.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea5ed3e053c735f16809bee7111deac62ac35b10128a8c102960a0462ce16cbe", size = 318069637, upload-time = "2025-05-02T23:33:18.306Z" },
+]
+
+[[package]]
+name = "oauthlib"
+version = "3.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352, upload-time = "2022-10-17T20:04:27.471Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" },
+]
+
+[[package]]
+name = "opencv-python"
+version = "4.11.0.86"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" },
+]
+
+[[package]]
+name = "optree"
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4c/10/37411ac8cf8cb07b9db9ddc3e36f84869fd1cabcee3d6af8d347c28744f2/optree-0.15.0.tar.gz", hash = "sha256:d00a45e3b192093ef2cd32bf0d541ecbfc93c1bd73a5f3fe36293499f28a50cf", size = 171403, upload-time = "2025-04-06T16:13:18.435Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/70/e3/99135565340ac34857b6edbb3df6e15eb35aa7160f900f12d83f71d38eeb/optree-0.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4ab606720ae319cb43da47c71d7d5fa7cfbb6a02e6da4857331e6f93800c970e", size = 647666, upload-time = "2025-04-06T16:12:14.072Z" },
+    { url = "https://files.pythonhosted.org/packages/93/fc/c9c04494d2ab54f98f8d8c18cb5095a81ad23fb64105cb05e926bb3d1a0c/optree-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cfc5771115f85b0bfa8f72cce1599186fd6a0ea71c8154d8b2751d9170be428", size = 346205, upload-time = "2025-04-06T16:12:15.91Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/c8/25484ec6784435d63e64e87a7dca32760eed4ca60ddd4be1ca14ed421e08/optree-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f958a20a311854aaab8bdd0f124aab5b9848f07976b54da3e95526a491aa860", size = 372427, upload-time = "2025-04-06T16:12:17.064Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/27/615a2987137fd8add27e62217527e49f7fd2ec391bbec354dbc59a0cd0af/optree-0.15.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47ce7e9d81eaed5a05004df1fa279d2608e063dd5eb236e9c95803b4fa0a286c", size = 421611, upload-time = "2025-04-06T16:12:18.219Z" },
+    { url = "https://files.pythonhosted.org/packages/98/e7/0106ee7ebec2e4cfa09f0a3b857e03c80bc80521227f4d64c289ac9601e8/optree-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6d6ab3717d48e0e747d9e348e23be1fa0f8a812f73632face6303c438d259ba", size = 415804, upload-time = "2025-04-06T16:12:19.611Z" },
+    { url = "https://files.pythonhosted.org/packages/47/0e/a40ccedb0bac4a894b2b0d17d915b7e8cd3fdc44a5104026cec4102a53fb/optree-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c7d101a15be39a9c7c4afae9f0bb85f682eb7d719117e2f9e5fb39c9f6f2c92", size = 386208, upload-time = "2025-04-06T16:12:20.784Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/f6/1d98163b283048d80cb0c8e67d086a1e0ecabe35004d7180d0f28303f611/optree-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aae337ab30b45a096eb5b4ffc3ad8909731617543a7eb288e0b297b9d10a241f", size = 409276, upload-time = "2025-04-06T16:12:22.067Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/65/4275cd763bafb22c41443b7c7bfd5986b9c97f984537669c356e2c7008a1/optree-0.15.0-cp313-cp313-win32.whl", hash = "sha256:eb9c51d728485f5908111191b5403a3f9bc310d121a981f29fad45750b9ff89c", size = 277924, upload-time = "2025-04-06T16:12:23.638Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/fe/3d9f5463609ac6cb5857d0040739ae7709e1fbc134c58f0dd7cb3d2d93d5/optree-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:7f00e6f011f021ae470efe070ec4d2339fb1a8cd0dcdd16fe3dab782a47aba45", size = 309617, upload-time = "2025-04-06T16:12:24.766Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/bb/b7a443be665a91307cd78196018bc45bb0c6ab04fe265f009698311c7bc7/optree-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:17990fbc7f4c461de7ae546fc5661f6a248c3dcee966c89c2e2e5ad7f6228bae", size = 303390, upload-time = "2025-04-06T16:12:25.798Z" },
+    { url = "https://files.pythonhosted.org/packages/30/8d/c6977786cc40626f24068d9bd6acbff377b9bed0cd64b7faff1bc3f2d60c/optree-0.15.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b31c88af70e3f5c14ff2aacd38c4076e6cde98f75169fe0bb59543f01bfb9719", size = 747468, upload-time = "2025-04-06T16:12:27.472Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/3f/e5b50c7af3712a97d0341744cd8ebd8ab19cd38561e26caa7a8a10fdf702/optree-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bc440f81f738d9c822030c3b4f53b6dec9ceb52410f02fd06b9338dc25a8447f", size = 392638, upload-time = "2025-04-06T16:12:28.535Z" },
+    { url = "https://files.pythonhosted.org/packages/54/3b/1aab1a9dba5c8eb85810ba2683b42b057b1ce1de9bb7ca08c5d35448979e/optree-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ffc2dd8c754e95495163dde55b38dc37e6712b6a3bc7f2190b0547a2c403bb", size = 390030, upload-time = "2025-04-06T16:12:29.687Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/95/195be32055ce48cdf8beb31eda74f4643c6a443f1367346c6a8f6606c9eb/optree-0.15.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fa9fb0197cd7b5f2b1fa7e05d30946b3b79bcfc3608fe54dbfc67969895cac9", size = 437350, upload-time = "2025-04-06T16:12:30.84Z" },
+    { url = "https://files.pythonhosted.org/packages/02/01/9eefcdd9112aebc52db668a87f1df2f45c784e34e5a35182d3dad731fc92/optree-0.15.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6828639b01ba1177c04875dd9529d938d7b28122c97e7ae14ec41c68ec22826c", size = 434500, upload-time = "2025-04-06T16:12:33.168Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/b3/0749c3a752b4921d54aa88eb1246315a008b5199c6d0e6127bd672a21c87/optree-0.15.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93c74eed0f52818c30212dba4867f5672e498567bad49dcdffbe8db6703a0d65", size = 404306, upload-time = "2025-04-06T16:12:34.258Z" },
+    { url = "https://files.pythonhosted.org/packages/04/91/1538085ace361f99280a26f9ccdc6023b22e7d0e23fdb849e2ef104fb54f/optree-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12188f6832c29dac37385a2f42fce961e303349909cff6d40e21cb27a8d09023", size = 423468, upload-time = "2025-04-06T16:12:35.325Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/ec/c4d2203a6f1206790ea8b794df943a16918649b23cd03ec6beb7f75a81df/optree-0.15.0-cp313-cp313t-win32.whl", hash = "sha256:d7b8ce7d13580985922dcfbda515da3f004cd7cb1b03320b96ea32d8cfd76392", size = 308837, upload-time = "2025-04-06T16:12:36.889Z" },
+    { url = "https://files.pythonhosted.org/packages/66/db/567ea8d2874eb15e9a8a82598ab84a6b809ca16df083b7650deffb8a6171/optree-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:daccdb583abaab14346f0af316ee570152a5c058e7b9fb09d8f8171fe751f2b3", size = 344774, upload-time = "2025-04-06T16:12:38.396Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/67/26804ab0222febadab6e27af4f4e607fad2d9520bb32422842509b79cf63/optree-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:e0162a36a6cedb0829efe980d0b370d4e5970fdb28a6609daa2c906d547add5f", size = 337502, upload-time = "2025-04-06T16:12:39.711Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.2.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+    { name = "python-dateutil" },
+    { name = "pytz" },
+    { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" },
+    { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" },
+    { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" },
+    { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
+]
+
+[[package]]
+name = "patsy"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/81/74f6a65b848ffd16c18f920620ce999fe45fe27f01ab3911260ce4ed85e4/patsy-1.0.1.tar.gz", hash = "sha256:e786a9391eec818c054e359b737bbce692f051aee4c661f4141cc88fb459c0c4", size = 396010, upload-time = "2024-11-12T14:10:54.642Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/87/2b/b50d3d08ea0fc419c183a84210571eba005328efa62b6b98bc28e9ead32a/patsy-1.0.1-py2.py3-none-any.whl", hash = "sha256:751fb38f9e97e62312e921a1954b81e1bb2bcda4f5eeabaf94db251ee791509c", size = 232923, upload-time = "2024-11-12T14:10:52.85Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" },
+    { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" },
+    { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" },
+    { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" },
+    { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" },
+    { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" },
+    { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" },
+    { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" },
+    { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" },
+    { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" },
+    { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" },
+    { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" },
+    { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "coverage" },
+    { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+    { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+    { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+    { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "charset-normalizer" },
+    { name = "idna" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
+]
+
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "oauthlib" },
+    { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markdown-it-py" },
+    { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
+]
+
+[[package]]
+name = "roman-numerals-py"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "joblib" },
+    { name = "numpy" },
+    { name = "scipy" },
+    { name = "threadpoolctl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001, upload-time = "2025-01-10T08:06:58.613Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360, upload-time = "2025-01-10T08:07:01.556Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004, upload-time = "2025-01-10T08:07:06.931Z" },
+    { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776, upload-time = "2025-01-10T08:07:11.715Z" },
+    { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865, upload-time = "2025-01-10T08:07:16.088Z" },
+    { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804, upload-time = "2025-01-10T08:07:20.385Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530, upload-time = "2025-01-10T08:07:23.675Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852, upload-time = "2025-01-10T08:07:26.817Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256, upload-time = "2025-01-10T08:07:31.084Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.15.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" },
+    { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" },
+    { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" },
+    { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" },
+    { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" },
+    { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" },
+    { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" },
+    { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" },
+    { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "snowballstemmer"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
+]
+
+[[package]]
+name = "sphinx"
+version = "8.2.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "alabaster" },
+    { name = "babel" },
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "docutils" },
+    { name = "imagesize" },
+    { name = "jinja2" },
+    { name = "packaging" },
+    { name = "pygments" },
+    { name = "requests" },
+    { name = "roman-numerals-py" },
+    { name = "snowballstemmer" },
+    { name = "sphinxcontrib-applehelp" },
+    { name = "sphinxcontrib-devhelp" },
+    { name = "sphinxcontrib-htmlhelp" },
+    { name = "sphinxcontrib-jsmath" },
+    { name = "sphinxcontrib-qthelp" },
+    { name = "sphinxcontrib-serializinghtml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
+]
+
+[[package]]
+name = "sphinx-autoapi"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "astroid" },
+    { name = "jinja2" },
+    { name = "pyyaml" },
+    { name = "sphinx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7f/a8/22b379a2a75ccb881217d3d4ae56d7d35f2d1bb4c8c0c51d0253676746a1/sphinx_autoapi-3.6.0.tar.gz", hash = "sha256:c685f274e41d0842ae7e199460c322c4bd7fec816ccc2da8d806094b4f64af06", size = 55417, upload-time = "2025-02-18T01:50:55.241Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/58/17/0eda9dc80fcaf257222b506844207e71b5d59567c41bbdcca2a72da119b9/sphinx_autoapi-3.6.0-py3-none-any.whl", hash = "sha256:f3b66714493cab140b0e896d33ce7137654a16ac1edb6563edcbd47bf975f711", size = 35281, upload-time = "2025-02-18T01:50:52.789Z" },
+]
+
+[[package]]
+name = "sphinx-pyproject"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "dom-toml" },
+    { name = "domdf-python-tools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/97/aa8cec3da3e78f2c396b63332e2fe92fe43f7ff2ad19b3998735f28b0a7f/sphinx_pyproject-0.3.0.tar.gz", hash = "sha256:efc4ee9d96f579c4e4ed1ac273868c64565e88c8e37fe6ec2dc59fbcd57684ab", size = 7695, upload-time = "2023-08-18T21:43:45.473Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/90/d5/89cb47c6399fd57ca451af15361499813c5d53e588cb6e00d89411ce724f/sphinx_pyproject-0.3.0-py3-none-any.whl", hash = "sha256:3aca968919f5ecd390f96874c3f64a43c9c7fcfdc2fd4191a781ad9228501b52", size = 23076, upload-time = "2023-08-18T21:43:43.808Z" },
+]
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
+]
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
+]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
+]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
+]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
+]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
+]
+
+[[package]]
+name = "statsmodels"
+version = "0.14.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+    { name = "packaging" },
+    { name = "pandas" },
+    { name = "patsy" },
+    { name = "scipy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/3b/963a015dd8ea17e10c7b0e2f14d7c4daec903baf60a017e756b57953a4bf/statsmodels-0.14.4.tar.gz", hash = "sha256:5d69e0f39060dc72c067f9bb6e8033b6dccdb0bae101d76a7ef0bcc94e898b67", size = 20354802, upload-time = "2024-10-03T16:15:36.273Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/31/f8/2662e6a101315ad336f75168fa9bac71f913ebcb92a6be84031d84a0f21f/statsmodels-0.14.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5a24f5d2c22852d807d2b42daf3a61740820b28d8381daaf59dcb7055bf1a79", size = 10186886, upload-time = "2024-10-03T17:10:44.074Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/c0/ee6e8ed35fc1ca9c7538c592f4974547bf72274bc98db1ae4a6e87481a83/statsmodels-0.14.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df4f7864606fa843d7e7c0e6af288f034a2160dba14e6ccc09020a3cf67cb092", size = 9880066, upload-time = "2024-10-03T17:10:56.972Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/97/3380ca6d8fd66cfb3d12941e472642f26e781a311c355a4e97aab2ed0216/statsmodels-0.14.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91341cbde9e8bea5fb419a76e09114e221567d03f34ca26e6d67ae2c27d8fe3c", size = 10283521, upload-time = "2024-10-03T17:14:06.216Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/2a/55c5b5c5e5124a202ea3fe0bcdbdeceaf91b4ec6164b8434acb9dd97409c/statsmodels-0.14.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1322286a7bfdde2790bf72d29698a1b76c20b8423a55bdcd0d457969d0041f72", size = 10723228, upload-time = "2024-10-03T17:14:19.587Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/76/67747e49dc758daae06f33aad8247b718cd7d224f091d2cd552681215bb2/statsmodels-0.14.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e31b95ac603415887c9f0d344cb523889cf779bc52d68e27e2d23c358958fec7", size = 10859503, upload-time = "2024-10-03T17:14:32.798Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/eb/cb8b01f5edf8f135eb3d0553d159db113a35b2948d0e51eeb735e7ae09ea/statsmodels-0.14.4-cp313-cp313-win_amd64.whl", hash = "sha256:81030108d27aecc7995cac05aa280cf8c6025f6a6119894eef648997936c2dd0", size = 9817574, upload-time = "2024-10-03T16:14:37.461Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "thealgorithms-python"
+version = "0.0.1"
+source = { virtual = "." }
+dependencies = [
+    { name = "beautifulsoup4" },
+    { name = "fake-useragent" },
+    { name = "httpx" },
+    { name = "imageio" },
+    { name = "keras" },
+    { name = "lxml" },
+    { name = "matplotlib" },
+    { name = "numpy" },
+    { name = "opencv-python" },
+    { name = "pandas" },
+    { name = "pillow" },
+    { name = "rich" },
+    { name = "scikit-learn" },
+    { name = "sphinx-pyproject" },
+    { name = "statsmodels" },
+    { name = "sympy" },
+    { name = "tweepy" },
+    { name = "typing-extensions" },
+    { name = "xgboost" },
+]
+
+[package.dev-dependencies]
+docs = [
+    { name = "myst-parser" },
+    { name = "sphinx-autoapi" },
+    { name = "sphinx-pyproject" },
+]
+euler-validate = [
+    { name = "httpx" },
+    { name = "numpy" },
+]
+test = [
+    { name = "pytest" },
+    { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "beautifulsoup4", specifier = ">=4.12.3" },
+    { name = "fake-useragent", specifier = ">=1.5.1" },
+    { name = "httpx", specifier = ">=0.28.1" },
+    { name = "imageio", specifier = ">=2.36.1" },
+    { name = "keras", specifier = ">=3.7" },
+    { name = "lxml", specifier = ">=5.3" },
+    { name = "matplotlib", specifier = ">=3.9.3" },
+    { name = "numpy", specifier = ">=2.1.3" },
+    { name = "opencv-python", specifier = ">=4.10.0.84" },
+    { name = "pandas", specifier = ">=2.2.3" },
+    { name = "pillow", specifier = ">=11" },
+    { name = "rich", specifier = ">=13.9.4" },
+    { name = "scikit-learn", specifier = ">=1.5.2" },
+    { name = "sphinx-pyproject", specifier = ">=0.3" },
+    { name = "statsmodels", specifier = ">=0.14.4" },
+    { name = "sympy", specifier = ">=1.13.3" },
+    { name = "tweepy", specifier = ">=4.14" },
+    { name = "typing-extensions", specifier = ">=4.12.2" },
+    { name = "xgboost", specifier = ">=2.1.3" },
+]
+
+[package.metadata.requires-dev]
+docs = [
+    { name = "myst-parser", specifier = ">=4" },
+    { name = "sphinx-autoapi", specifier = ">=3.4" },
+    { name = "sphinx-pyproject", specifier = ">=0.3" },
+]
+euler-validate = [
+    { name = "httpx", specifier = ">=0.28.1" },
+    { name = "numpy", specifier = ">=2.1.3" },
+]
+test = [
+    { name = "pytest", specifier = ">=8.3.4" },
+    { name = "pytest-cov", specifier = ">=6" },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
+    { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
+    { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
+    { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
+    { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "tweepy"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "oauthlib" },
+    { name = "requests" },
+    { name = "requests-oauthlib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/99/05/7c2c01bd62900eff24534779f1e1531491dfd872edb6a9d432ae91e18b4b/tweepy-4.15.0.tar.gz", hash = "sha256:1345cbcdf0a75e2d89f424c559fd49fda4d8cd7be25cd5131e3b57bad8a21d76", size = 100268, upload-time = "2025-01-15T21:25:05.307Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/81/53/ca632ec02085b5c432e98ae1f872a21f2b6bb6c3d022dcf586809cc65cd0/tweepy-4.15.0-py3-none-any.whl", hash = "sha256:64adcea317158937059e4e2897b3ceb750b0c2dd5df58938c2da8f7eb3b88e6a", size = 99379, upload-time = "2025-01-15T21:25:02.856Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
+]
+
+[[package]]
+name = "xgboost"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+    { name = "nvidia-nccl-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+    { name = "scipy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/18/f58f9dcbcca811b30be945fd7f890f87a0ee822f7316e2ddd7a3e3056f08/xgboost-3.0.0.tar.gz", hash = "sha256:45e95416df6f6f01d9a62e60cf09fc57e5ee34697f3858337c796fac9ce3b9ed", size = 1156620, upload-time = "2025-03-15T13:45:01.277Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/95/6c/d2a1636591e204667f320481b036acc9c526608bcc2319be71cce148102d/xgboost-3.0.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:ed8cffd7998bd9431c3b0287a70bec8e45c09b43c9474d9dfd261627713bd890", size = 2245638, upload-time = "2025-03-15T13:46:14.653Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/0b/f9f815f240a9610d42367172b9f7ef7e8c9113a09b1bb35d4d85f96b910a/xgboost-3.0.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:314104bd3a1426a40f0c9662eef40e9ab22eb7a8068a42a8d198ce40412db75c", size = 2025491, upload-time = "2025-03-15T13:46:49.556Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/aa/f4597dc989317d2431cd71998cac1f9205c773658e0cf6680bb9011b26eb/xgboost-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72c3405e8dfc37048f9fe339a058fa12b9f0f03bc31d3e56f0887eed2ed2baa1", size = 4841002, upload-time = "2025-03-15T13:47:19.693Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/ba/b89d99b89946afc842feb8f47302e8022f1405f01212d3e186b6d674b2ba/xgboost-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:72d39e74649e9b628c4221111aa6a8caa860f2e853b25480424403ee61085126", size = 4903442, upload-time = "2025-03-15T13:47:44.917Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/68/a7a274bedf43bbf9de120104b438b45e232395f8b9861519040b7b725535/xgboost-3.0.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7bdee5787f86b83bebd75e2c96caf854760788e5f4203d063da50db5bf0efc5f", size = 4602694, upload-time = "2025-03-15T13:48:16.498Z" },
+    { url = "https://files.pythonhosted.org/packages/63/f1/653afe1a1b7e1d03f26fd4bd30f3eebcfac2d8982e1a85b6be3355dcae25/xgboost-3.0.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:61c7e391e373b8a5312503525c0689f83ef1912a1236377022865ab340f465a4", size = 253877333, upload-time = "2025-03-15T13:51:01.139Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/03/15cd49e855c62226ecf1831bbe4c8e73a4324856077a23c495538a36e557/xgboost-3.0.0-py3-none-win_amd64.whl", hash = "sha256:0ea74e97f95b1eddfd27a46b7f22f72ec5a5322e1dc7cb41c9c23fb580763df9", size = 149971978, upload-time = "2025-03-15T13:53:26.596Z" },
+]
diff --git a/web_programming/co2_emission.py b/web_programming/co2_emission.py
index 97927e7ef541..4f62df8447b0 100644
--- a/web_programming/co2_emission.py
+++ b/web_programming/co2_emission.py
@@ -1,22 +1,30 @@
 """
 Get CO2 emission data from the UK CarbonIntensity API
 """
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 from datetime import date
 
-import requests
+import httpx
 
 BASE_URL = "https://api.carbonintensity.org.uk/intensity"
 
 
 # Emission in the last half hour
 def fetch_last_half_hour() -> str:
-    last_half_hour = requests.get(BASE_URL).json()["data"][0]
+    last_half_hour = httpx.get(BASE_URL, timeout=10).json()["data"][0]
     return last_half_hour["intensity"]["actual"]
 
 
 # Emissions in a specific date range
 def fetch_from_to(start, end) -> list:
-    return requests.get(f"{BASE_URL}/{start}/{end}").json()["data"]
+    return httpx.get(f"{BASE_URL}/{start}/{end}", timeout=10).json()["data"]
 
 
 if __name__ == "__main__":
diff --git a/web_programming/covid_stats_via_xpath.py b/web_programming/covid_stats_via_xpath.py
index a95130badad9..f7db51b63169 100644
--- a/web_programming/covid_stats_via_xpath.py
+++ b/web_programming/covid_stats_via_xpath.py
@@ -4,10 +4,18 @@
 more convenient to use in Python web projects (e.g. Django or Flask-based)
 """
 
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+#     "lxml",
+# ]
+# ///
+
 from typing import NamedTuple
 
-import requests
-from lxml import html  # type: ignore
+import httpx
+from lxml import html
 
 
 class CovidData(NamedTuple):
@@ -18,7 +26,9 @@ class CovidData(NamedTuple):
 
 def covid_stats(url: str = "https://www.worldometers.info/coronavirus/") -> CovidData:
     xpath_str = '//div[@class = "maincounter-number"]/span/text()'
-    return CovidData(*html.fromstring(requests.get(url).content).xpath(xpath_str))
+    return CovidData(
+        *html.fromstring(httpx.get(url, timeout=10).content).xpath(xpath_str)
+    )
 
 
 fmt = """Total COVID-19 cases in the world: {}
diff --git a/web_programming/crawl_google_results.py b/web_programming/crawl_google_results.py
index 1f5e6d31992b..0ae13792e048 100644
--- a/web_programming/crawl_google_results.py
+++ b/web_programming/crawl_google_results.py
@@ -1,14 +1,28 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "fake-useragent",
+#     "httpx",
+# ]
+# ///
+
 import sys
 import webbrowser
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 from fake_useragent import UserAgent
 
 if __name__ == "__main__":
     print("Googling.....")
     url = "https://www.google.com/search?q=" + " ".join(sys.argv[1:])
-    res = requests.get(url, headers={"UserAgent": UserAgent().random})
+    res = httpx.get(
+        url,
+        headers={"UserAgent": UserAgent().random},
+        timeout=10,
+        follow_redirects=True,
+    )
     # res.raise_for_status()
     with open("project1a.html", "wb") as out_file:  # only for knowing the class
         for data in res.iter_content(10000):
diff --git a/web_programming/crawl_google_scholar_citation.py b/web_programming/crawl_google_scholar_citation.py
index f92a3d139520..77d7be00c7b6 100644
--- a/web_programming/crawl_google_scholar_citation.py
+++ b/web_programming/crawl_google_scholar_citation.py
@@ -3,7 +3,15 @@
 using title and year of publication, and volume and pages of journal.
 """
 
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
+import httpx
 from bs4 import BeautifulSoup
 
 
@@ -11,7 +19,9 @@ def get_citation(base_url: str, params: dict) -> str:
     """
     Return the citation number.
     """
-    soup = BeautifulSoup(requests.get(base_url, params=params).content, "html.parser")
+    soup = BeautifulSoup(
+        httpx.get(base_url, params=params, timeout=10).content, "html.parser"
+    )
     div = soup.find("div", attrs={"class": "gs_ri"})
     anchors = div.find("div", attrs={"class": "gs_fl"}).find_all("a")
     return anchors[2].get_text()
diff --git a/web_programming/currency_converter.py b/web_programming/currency_converter.py
index 3bbcafa8f89b..a007c9c35a65 100644
--- a/web_programming/currency_converter.py
+++ b/web_programming/currency_converter.py
@@ -3,9 +3,16 @@
 https://www.amdoren.com
 """
 
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 import os
 
-import requests
+import httpx
 
 URL_BASE = "https://www.amdoren.com/api/currency.php"
 
@@ -176,7 +183,7 @@ def convert_currency(
     params = locals()
     # from is a reserved keyword
     params["from"] = params.pop("from_")
-    res = requests.get(URL_BASE, params=params).json()
+    res = httpx.get(URL_BASE, params=params, timeout=10).json()
     return str(res["amount"]) if res["error"] == 0 else res["error_message"]
 
 
diff --git a/web_programming/current_stock_price.py b/web_programming/current_stock_price.py
index 0c06354d8998..531da949ea50 100644
--- a/web_programming/current_stock_price.py
+++ b/web_programming/current_stock_price.py
@@ -1,20 +1,48 @@
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
+import httpx
 from bs4 import BeautifulSoup
 
+"""
+Get the HTML code of finance yahoo and select the current qsp-price
+Current AAPL stock price is   228.43
+Current AMZN stock price is   201.85
+Current IBM  stock price is   210.30
+Current GOOG stock price is   177.86
+Current MSFT stock price is   414.82
+Current ORCL stock price is   188.87
+"""
+
 
 def stock_price(symbol: str = "AAPL") -> str:
+    """
+    >>> stock_price("EEEE")
+    'No <fin-streamer> tag with the specified data-testid attribute found.'
+    >>> isinstance(float(stock_price("GOOG")),float)
+    True
+    """
     url = f"https://finance.yahoo.com/quote/{symbol}?p={symbol}"
-    yahoo_finance_source = requests.get(url, headers={"USER-AGENT": "Mozilla/5.0"}).text
+    yahoo_finance_source = httpx.get(
+        url, headers={"USER-AGENT": "Mozilla/5.0"}, timeout=10, follow_redirects=True
+    ).text
     soup = BeautifulSoup(yahoo_finance_source, "html.parser")
-    specific_fin_streamer_tag = soup.find("fin-streamer", {"data-test": "qsp-price"})
 
-    if specific_fin_streamer_tag:
-        text = specific_fin_streamer_tag.get_text()
-        return text
-    return "No <fin-streamer> tag with the specified data-test attribute found."
+    if specific_fin_streamer_tag := soup.find("span", {"data-testid": "qsp-price"}):
+        return specific_fin_streamer_tag.get_text()
+    return "No <fin-streamer> tag with the specified data-testid attribute found."
 
 
 # Search for the symbol at https://finance.yahoo.com/lookup
 if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
+
     for symbol in "AAPL AMZN IBM GOOG MSFT ORCL".split():
         print(f"Current {symbol:<4} stock price is {stock_price(symbol):>8}")
diff --git a/web_programming/current_weather.py b/web_programming/current_weather.py
index 3ed4c8a95a0c..001eaf9020f4 100644
--- a/web_programming/current_weather.py
+++ b/web_programming/current_weather.py
@@ -1,30 +1,57 @@
-import requests
-
-APPID = ""  # <-- Put your OpenWeatherMap appid here!
-URL_BASE = "https://api.openweathermap.org/data/2.5/"
-
-
-def current_weather(q: str = "Chicago", appid: str = APPID) -> dict:
-    """https://openweathermap.org/api"""
-    return requests.get(URL_BASE + "weather", params=locals()).json()
-
-
-def weather_forecast(q: str = "Kolkata, India", appid: str = APPID) -> dict:
-    """https://openweathermap.org/forecast5"""
-    return requests.get(URL_BASE + "forecast", params=locals()).json()
-
-
-def weather_onecall(lat: float = 55.68, lon: float = 12.57, appid: str = APPID) -> dict:
-    """https://openweathermap.org/api/one-call-api"""
-    return requests.get(URL_BASE + "onecall", params=locals()).json()
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+import httpx
+
+# Put your API key(s) here
+OPENWEATHERMAP_API_KEY = ""
+WEATHERSTACK_API_KEY = ""
+
+# Define the URL for the APIs with placeholders
+OPENWEATHERMAP_URL_BASE = "https://api.openweathermap.org/data/2.5/weather"
+WEATHERSTACK_URL_BASE = "http://api.weatherstack.com/current"
+
+
+def current_weather(location: str) -> list[dict]:
+    """
+    >>> current_weather("location")
+    Traceback (most recent call last):
+        ...
+    ValueError: No API keys provided or no valid data returned.
+    """
+    weather_data = []
+    if OPENWEATHERMAP_API_KEY:
+        params_openweathermap = {"q": location, "appid": OPENWEATHERMAP_API_KEY}
+        response_openweathermap = httpx.get(
+            OPENWEATHERMAP_URL_BASE, params=params_openweathermap, timeout=10
+        )
+        weather_data.append({"OpenWeatherMap": response_openweathermap.json()})
+    if WEATHERSTACK_API_KEY:
+        params_weatherstack = {"query": location, "access_key": WEATHERSTACK_API_KEY}
+        response_weatherstack = httpx.get(
+            WEATHERSTACK_URL_BASE, params=params_weatherstack, timeout=10
+        )
+        weather_data.append({"Weatherstack": response_weatherstack.json()})
+    if not weather_data:
+        raise ValueError("No API keys provided or no valid data returned.")
+    return weather_data
 
 
 if __name__ == "__main__":
     from pprint import pprint
 
-    while True:
-        location = input("Enter a location:").strip()
+    location = "to be determined..."
+    while location:
+        location = input("Enter a location (city name or latitude,longitude): ").strip()
         if location:
-            pprint(current_weather(location))
-        else:
-            break
+            try:
+                weather_data = current_weather(location)
+                for forecast in weather_data:
+                    pprint(forecast)
+            except ValueError as e:
+                print(repr(e))
+                location = ""
diff --git a/web_programming/daily_horoscope.py b/web_programming/daily_horoscope.py
index b0dd1cd65924..b84ea13238cc 100644
--- a/web_programming/daily_horoscope.py
+++ b/web_programming/daily_horoscope.py
@@ -1,4 +1,12 @@
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
+import httpx
 from bs4 import BeautifulSoup
 
 
@@ -7,7 +15,7 @@ def horoscope(zodiac_sign: int, day: str) -> str:
         "https://www.horoscope.com/us/horoscopes/general/"
         f"horoscope-general-daily-{day}.aspx?sign={zodiac_sign}"
     )
-    soup = BeautifulSoup(requests.get(url).content, "html.parser")
+    soup = BeautifulSoup(httpx.get(url, timeout=10).content, "html.parser")
     return soup.find("div", class_="main-horoscope").p.text
 
 
diff --git a/web_programming/download_images_from_google_query.py b/web_programming/download_images_from_google_query.py
index 441347459f8e..659cf6a398a3 100644
--- a/web_programming/download_images_from_google_query.py
+++ b/web_programming/download_images_from_google_query.py
@@ -1,10 +1,18 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
 import json
 import os
 import re
 import sys
 import urllib.request
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 
 headers = {
@@ -39,7 +47,9 @@ def download_images_from_google_query(query: str = "dhaka", max_images: int = 5)
         "ijn": "0",
     }
 
-    html = requests.get("https://www.google.com/search", params=params, headers=headers)
+    html = httpx.get(
+        "https://www.google.com/search", params=params, headers=headers, timeout=10
+    )
     soup = BeautifulSoup(html.text, "html.parser")
     matched_images_data = "".join(
         re.findall(r"AF_initDataCallback\(([^<]+)\);", str(soup.select("script")))
diff --git a/web_programming/emails_from_url.py b/web_programming/emails_from_url.py
index 074ef878c0d7..3fa24085df95 100644
--- a/web_programming/emails_from_url.py
+++ b/web_programming/emails_from_url.py
@@ -1,4 +1,12 @@
 """Get the site emails from URL."""
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
 __author__ = "Muhammad Umer Farooq"
@@ -12,7 +20,7 @@
 from html.parser import HTMLParser
 from urllib import parse
 
-import requests
+import httpx
 
 
 class Parser(HTMLParser):
@@ -29,12 +37,10 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None
         if tag == "a":
             # Check the list of defined attributes.
             for name, value in attrs:
-                # If href is defined, and not empty nor # print it.
-                if name == "href" and value != "#" and value != "":
-                    # If not already in urls.
-                    if value not in self.urls:
-                        url = parse.urljoin(self.domain, value)
-                        self.urls.append(url)
+                # If href is defined, not empty nor # print it and not already in urls.
+                if name == "href" and value not in (*self.urls, "", "#"):
+                    url = parse.urljoin(self.domain, value)
+                    self.urls.append(url)
 
 
 # Get main domain name (example.com)
@@ -73,7 +79,7 @@ def emails_from_url(url: str = "https://github.com") -> list[str]:
 
     try:
         # Open URL
-        r = requests.get(url)
+        r = httpx.get(url, timeout=10, follow_redirects=True)
 
         # pass the raw HTML to the parser to get links
         parser.feed(r.text)
@@ -82,9 +88,15 @@ def emails_from_url(url: str = "https://github.com") -> list[str]:
         valid_emails = set()
         for link in parser.urls:
             # open URL.
-            # read = requests.get(link)
+            # Check if the link is already absolute
+            if not link.startswith("http://") and not link.startswith("https://"):
+                # Prepend protocol only if link starts with domain, normalize otherwise
+                if link.startswith(domain):
+                    link = f"https://{link}"
+                else:
+                    link = parse.urljoin(f"https://{domain}", link)
             try:
-                read = requests.get(link)
+                read = httpx.get(link, timeout=10, follow_redirects=True)
                 # Get the valid email.
                 emails = re.findall("[a-zA-Z0-9]+@" + domain, read.text)
                 # If not in list then append it.
diff --git a/web_programming/fetch_anime_and_play.py b/web_programming/fetch_anime_and_play.py
index 366807785e85..616116595697 100644
--- a/web_programming/fetch_anime_and_play.py
+++ b/web_programming/fetch_anime_and_play.py
@@ -1,8 +1,17 @@
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "fake-useragent",
+#     "httpx",
+# ]
+# ///
+
+import httpx
 from bs4 import BeautifulSoup, NavigableString, Tag
 from fake_useragent import UserAgent
 
-BASE_URL = "https://ww1.gogoanime2.org"
+BASE_URL = "https://ww7.gogoanime2.org"
 
 
 def search_scraper(anime_name: str) -> list:
@@ -25,10 +34,10 @@ def search_scraper(anime_name: str) -> list:
     """
 
     # concat the name to form the search url.
-    search_url = f"{BASE_URL}/search/{anime_name}"
+    search_url = f"{BASE_URL}/search?keyword={anime_name}"
 
-    response = requests.get(
-        search_url, headers={"UserAgent": UserAgent().chrome}
+    response = httpx.get(
+        search_url, headers={"UserAgent": UserAgent().chrome}, timeout=10
     )  # request the url.
 
     # Is the response ok?
@@ -82,7 +91,9 @@ def search_anime_episode_list(episode_endpoint: str) -> list:
 
     request_url = f"{BASE_URL}{episode_endpoint}"
 
-    response = requests.get(url=request_url, headers={"UserAgent": UserAgent().chrome})
+    response = httpx.get(
+        url=request_url, headers={"UserAgent": UserAgent().chrome}, timeout=10
+    )
     response.raise_for_status()
 
     soup = BeautifulSoup(response.text, "html.parser")
@@ -131,8 +142,8 @@ def get_anime_episode(episode_endpoint: str) -> list:
 
     episode_page_url = f"{BASE_URL}{episode_endpoint}"
 
-    response = requests.get(
-        url=episode_page_url, headers={"User-Agent": UserAgent().chrome}
+    response = httpx.get(
+        url=episode_page_url, headers={"User-Agent": UserAgent().chrome}, timeout=10
     )
     response.raise_for_status()
 
@@ -163,7 +174,7 @@ def get_anime_episode(episode_endpoint: str) -> list:
         print(f"Found {len(anime_list)} results: ")
         for i, anime in enumerate(anime_list):
             anime_title = anime["title"]
-            print(f"{i+1}. {anime_title}")
+            print(f"{i + 1}. {anime_title}")
 
         anime_choice = int(input("\nPlease choose from the following list: ").strip())
         chosen_anime = anime_list[anime_choice - 1]
@@ -175,7 +186,7 @@ def get_anime_episode(episode_endpoint: str) -> list:
         else:
             print(f"Found {len(episode_list)} results: ")
             for i, episode in enumerate(episode_list):
-                print(f"{i+1}. {episode['title']}")
+                print(f"{i + 1}. {episode['title']}")
 
             episode_choice = int(input("\nChoose an episode by serial no: ").strip())
             chosen_episode = episode_list[episode_choice - 1]
diff --git a/web_programming/fetch_bbc_news.py b/web_programming/fetch_bbc_news.py
index 7f8bc57b69f5..f3121fee7db4 100644
--- a/web_programming/fetch_bbc_news.py
+++ b/web_programming/fetch_bbc_news.py
@@ -1,13 +1,20 @@
 # Created by sarathkaul on 12/11/19
 
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+import httpx
 
 _NEWS_API = "https://newsapi.org/v1/articles?source=bbc-news&sortBy=top&apiKey="
 
 
 def fetch_bbc_news(bbc_news_api_key: str) -> None:
     # fetching a list of articles in json format
-    bbc_news_page = requests.get(_NEWS_API + bbc_news_api_key).json()
+    bbc_news_page = httpx.get(_NEWS_API + bbc_news_api_key, timeout=10).json()
     # each article in the list is a dict
     for i, article in enumerate(bbc_news_page["articles"], 1):
         print(f"{i}.) {article['title']}")
diff --git a/web_programming/fetch_github_info.py b/web_programming/fetch_github_info.py
index aa4e1d7b1963..1d3d344e177e 100644
--- a/web_programming/fetch_github_info.py
+++ b/web_programming/fetch_github_info.py
@@ -17,12 +17,20 @@
 #!/usr/bin/env bash
 export USER_TOKEN=""
 """
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
 import os
 from typing import Any
 
-import requests
+import httpx
 
 BASE_URL = "https://api.github.com"
 
@@ -35,13 +43,13 @@
 
 def fetch_github_info(auth_token: str) -> dict[Any, Any]:
     """
-    Fetch GitHub info of a user using the requests module
+    Fetch GitHub info of a user using the httpx module
     """
     headers = {
         "Authorization": f"token {auth_token}",
         "Accept": "application/vnd.github.v3+json",
     }
-    return requests.get(AUTHENTICATED_USER_ENDPOINT, headers=headers).json()
+    return httpx.get(AUTHENTICATED_USER_ENDPOINT, headers=headers, timeout=10).json()
 
 
 if __name__ == "__main__":  # pragma: no cover
diff --git a/web_programming/fetch_jobs.py b/web_programming/fetch_jobs.py
index 5af90a0bb239..7a95f997078d 100644
--- a/web_programming/fetch_jobs.py
+++ b/web_programming/fetch_jobs.py
@@ -1,18 +1,27 @@
 """
 Scraping jobs given job title and location from indeed website
 """
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
 from collections.abc import Generator
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 
 url = "https://www.indeed.co.in/jobs?q=mobile+app+development&l="
 
 
-def fetch_jobs(location: str = "mumbai") -> Generator[tuple[str, str], None, None]:
-    soup = BeautifulSoup(requests.get(url + location).content, "html.parser")
+def fetch_jobs(location: str = "mumbai") -> Generator[tuple[str, str]]:
+    soup = BeautifulSoup(httpx.get(url + location, timeout=10).content, "html.parser")
     # This attribute finds out all the specifics listed in a job
     for job in soup.find_all("div", attrs={"data-tn-component": "organicJob"}):
         job_title = job.find("a", attrs={"data-tn-element": "jobTitle"}).text.strip()
diff --git a/web_programming/fetch_quotes.py b/web_programming/fetch_quotes.py
index d557e2d95e74..38113c888d89 100644
--- a/web_programming/fetch_quotes.py
+++ b/web_programming/fetch_quotes.py
@@ -6,19 +6,26 @@
     https://zenquotes.io/
 """
 
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 import pprint
 
-import requests
+import httpx
 
 API_ENDPOINT_URL = "https://zenquotes.io/api"
 
 
 def quote_of_the_day() -> list:
-    return requests.get(API_ENDPOINT_URL + "/today").json()
+    return httpx.get(API_ENDPOINT_URL + "/today", timeout=10).json()
 
 
 def random_quotes() -> list:
-    return requests.get(API_ENDPOINT_URL + "/random").json()
+    return httpx.get(API_ENDPOINT_URL + "/random", timeout=10).json()
 
 
 if __name__ == "__main__":
diff --git a/web_programming/fetch_well_rx_price.py b/web_programming/fetch_well_rx_price.py
index ee51b9a5051b..e34a89c19cc8 100644
--- a/web_programming/fetch_well_rx_price.py
+++ b/web_programming/fetch_well_rx_price.py
@@ -5,12 +5,10 @@
 
 """
 
-from urllib.error import HTTPError
-
+import httpx
 from bs4 import BeautifulSoup
-from requests import exceptions, get
 
-BASE_URL = "https://www.wellrx.com/prescriptions/{0}/{1}/?freshSearch=true"
+BASE_URL = "https://www.wellrx.com/prescriptions/{}/{}/?freshSearch=true"
 
 
 def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None:
@@ -18,8 +16,8 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None:
 
     This function will take input of drug name and zipcode,
     then request to the BASE_URL site.
-    Get the page data and scrape it to the generate the
-    list of lowest prices for the prescription drug.
+    Get the page data and scrape it to generate the
+    list of the lowest prices for the prescription drug.
 
     Args:
         drug_name (str): [Drug name]
@@ -28,12 +26,12 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None:
     Returns:
         list: [List of pharmacy name and price]
 
-    >>> fetch_pharmacy_and_price_list(None, None)
-
-    >>> fetch_pharmacy_and_price_list(None, 30303)
-
-    >>> fetch_pharmacy_and_price_list("eliquis", None)
-
+    >>> print(fetch_pharmacy_and_price_list(None, None))
+    None
+    >>> print(fetch_pharmacy_and_price_list(None, 30303))
+    None
+    >>> print(fetch_pharmacy_and_price_list("eliquis", None))
+    None
     """
 
     try:
@@ -42,10 +40,7 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None:
             return None
 
         request_url = BASE_URL.format(drug_name, zip_code)
-        response = get(request_url)
-
-        # Is the response ok?
-        response.raise_for_status()
+        response = httpx.get(request_url, timeout=10).raise_for_status()
 
         # Scrape the data using bs4
         soup = BeautifulSoup(response.text, "html.parser")
@@ -53,14 +48,14 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None:
         # This list will store the name and price.
         pharmacy_price_list = []
 
-        # Fetch all the grids that contains the items.
+        # Fetch all the grids that contain the items.
         grid_list = soup.find_all("div", {"class": "grid-x pharmCard"})
         if grid_list and len(grid_list) > 0:
             for grid in grid_list:
                 # Get the pharmacy price.
                 pharmacy_name = grid.find("p", {"class": "list-title"}).text
 
-                # Get price of the drug.
+                # Get the price of the drug.
                 price = grid.find("span", {"p", "price price-large"}).text
 
                 pharmacy_price_list.append(
@@ -72,7 +67,7 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None:
 
         return pharmacy_price_list
 
-    except (HTTPError, exceptions.RequestException, ValueError):
+    except (httpx.HTTPError, ValueError):
         return None
 
 
diff --git a/web_programming/get_amazon_product_data.py b/web_programming/get_amazon_product_data.py
index a16175688667..02db708e02bf 100644
--- a/web_programming/get_amazon_product_data.py
+++ b/web_programming/get_amazon_product_data.py
@@ -4,10 +4,18 @@
 information will include title, URL, price, ratings, and the discount available.
 """
 
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+#     "pandas",
+# ]
+# ///
 
 from itertools import zip_longest
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 from pandas import DataFrame
 
@@ -25,7 +33,9 @@ def get_amazon_product_data(product: str = "laptop") -> DataFrame:
         ),
         "Accept-Language": "en-US, en;q=0.5",
     }
-    soup = BeautifulSoup(requests.get(url, headers=header).text, features="lxml")
+    soup = BeautifulSoup(
+        httpx.get(url, headers=header, timeout=10).text, features="lxml"
+    )
     # Initialize a Pandas dataframe with the column titles
     data_frame = DataFrame(
         columns=[
diff --git a/web_programming/get_imdb_top_250_movies_csv.py b/web_programming/get_imdb_top_250_movies_csv.py
index e54b076ebd94..4cd29f0e1426 100644
--- a/web_programming/get_imdb_top_250_movies_csv.py
+++ b/web_programming/get_imdb_top_250_movies_csv.py
@@ -1,16 +1,24 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
 import csv
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 
 
 def get_imdb_top_250_movies(url: str = "") -> dict[str, float]:
     url = url or "https://www.imdb.com/chart/top/?ref_=nv_mv_250"
-    soup = BeautifulSoup(requests.get(url).text, "html.parser")
-    titles = soup.find_all("td", attrs="titleColumn")
-    ratings = soup.find_all("td", class_="ratingColumn imdbRating")
+    soup = BeautifulSoup(httpx.get(url, timeout=10).text, "html.parser")
+    titles = soup.find_all("h3", class_="ipc-title__text")
+    ratings = soup.find_all("span", class_="ipc-rating-star--rating")
     return {
         title.a.text: float(rating.strong.text)
         for title, rating in zip(titles, ratings)
diff --git a/web_programming/get_imdbtop.py b/web_programming/get_imdbtop.py.DISABLED
similarity index 100%
rename from web_programming/get_imdbtop.py
rename to web_programming/get_imdbtop.py.DISABLED
diff --git a/web_programming/get_ip_geolocation.py b/web_programming/get_ip_geolocation.py
new file mode 100644
index 000000000000..153655257324
--- /dev/null
+++ b/web_programming/get_ip_geolocation.py
@@ -0,0 +1,47 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+import httpx
+
+
+# Function to get geolocation data for an IP address
+def get_ip_geolocation(ip_address: str) -> str:
+    try:
+        # Construct the URL for the IP geolocation API
+        url = f"https://ipinfo.io/{ip_address}/json"
+
+        # Send a GET request to the API
+        response = httpx.get(url, timeout=10)
+
+        # Check if the HTTP request was successful
+        response.raise_for_status()
+
+        # Parse the response as JSON
+        data = response.json()
+
+        # Check if city, region, and country information is available
+        if "city" in data and "region" in data and "country" in data:
+            location = f"Location: {data['city']}, {data['region']}, {data['country']}"
+        else:
+            location = "Location data not found."
+
+        return location
+    except httpx.RequestError as e:
+        # Handle network-related exceptions
+        return f"Request error: {e}"
+    except ValueError as e:
+        # Handle JSON parsing errors
+        return f"JSON parsing error: {e}"
+
+
+if __name__ == "__main__":
+    # Prompt the user to enter an IP address
+    ip_address = input("Enter an IP address: ")
+
+    # Get the geolocation data and print it
+    location = get_ip_geolocation(ip_address)
+    print(location)
diff --git a/web_programming/get_top_billionaires.py b/web_programming/get_top_billionaires.py
index 6f986acb9181..b6f3bf0ca5be 100644
--- a/web_programming/get_top_billionaires.py
+++ b/web_programming/get_top_billionaires.py
@@ -3,16 +3,23 @@
 This works for some of us but fails for others.
 """
 
-from datetime import UTC, datetime, timedelta
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+#     "rich",
+# ]
+# ///
 
-import requests
+from datetime import UTC, date, datetime
+
+import httpx
 from rich import box
 from rich import console as rich_console
 from rich import table as rich_table
 
 LIMIT = 10
-TODAY = datetime.now()
-
+TODAY = datetime.now(tz=UTC)
 API_URL = (
     "https://www.forbes.com/forbesapi/person/rtb/0/position/true.json"
     "?fields=personName,gender,source,countryOfCitizenship,birthDate,finalWorth"
@@ -20,45 +27,45 @@
 )
 
 
-def calculate_age(unix_date: float) -> str:
-    """Calculates age from given unix time format.
+def years_old(birth_timestamp: int, today: date | None = None) -> int:
+    """
+    Calculate the age in years based on the given birth date.  Only the year, month,
+    and day are used in the calculation.  The time of day is ignored.
+
+    Args:
+        birth_timestamp: The date of birth.
+        today: (useful for writing tests) or if None then datetime.date.today().
 
     Returns:
-        Age as string
-
-    >>> from datetime import datetime, UTC
-    >>> years_since_create = datetime.now(tz=UTC).year - 2022
-    >>> int(calculate_age(-657244800000)) - years_since_create
-    73
-    >>> int(calculate_age(46915200000)) - years_since_create
-    51
+        int: The age in years.
+
+    Examples:
+    >>> today = date(2024, 1, 12)
+    >>> years_old(birth_timestamp=datetime(1959, 11, 20).timestamp(), today=today)
+    64
+    >>> years_old(birth_timestamp=datetime(1970, 2, 13).timestamp(), today=today)
+    53
+    >>> all(
+    ...     years_old(datetime(today.year - i, 1, 12).timestamp(), today=today) == i
+    ...     for i in range(1, 111)
+    ... )
+    True
     """
-    # Convert date from milliseconds to seconds
-    unix_date /= 1000
-
-    if unix_date < 0:
-        # Handle timestamp before epoch
-        epoch = datetime.fromtimestamp(0, tz=UTC)
-        seconds_since_epoch = (datetime.now(tz=UTC) - epoch).seconds
-        birthdate = (
-            epoch - timedelta(seconds=abs(unix_date) - seconds_since_epoch)
-        ).date()
-    else:
-        birthdate = datetime.fromtimestamp(unix_date, tz=UTC).date()
-    return str(
-        TODAY.year
-        - birthdate.year
-        - ((TODAY.month, TODAY.day) < (birthdate.month, birthdate.day))
+    today = today or TODAY.date()
+    birth_date = datetime.fromtimestamp(birth_timestamp, tz=UTC).date()
+    return (today.year - birth_date.year) - (
+        (today.month, today.day) < (birth_date.month, birth_date.day)
     )
 
 
-def get_forbes_real_time_billionaires() -> list[dict[str, str]]:
-    """Get top 10 realtime billionaires using forbes API.
+def get_forbes_real_time_billionaires() -> list[dict[str, int | str]]:
+    """
+    Get the top 10 real-time billionaires using Forbes API.
 
     Returns:
         List of top 10 realtime billionaires data.
     """
-    response_json = requests.get(API_URL).json()
+    response_json = httpx.get(API_URL, timeout=10).json()
     return [
         {
             "Name": person["personName"],
@@ -66,21 +73,22 @@ def get_forbes_real_time_billionaires() -> list[dict[str, str]]:
             "Country": person["countryOfCitizenship"],
             "Gender": person["gender"],
             "Worth ($)": f"{person['finalWorth'] / 1000:.1f} Billion",
-            "Age": calculate_age(person["birthDate"]),
+            "Age": str(years_old(person["birthDate"] / 1000)),
         }
         for person in response_json["personList"]["personsLists"]
     ]
 
 
-def display_billionaires(forbes_billionaires: list[dict[str, str]]) -> None:
-    """Display Forbes real time billionaires in a rich table.
+def display_billionaires(forbes_billionaires: list[dict[str, int | str]]) -> None:
+    """
+    Display Forbes real-time billionaires in a rich table.
 
     Args:
-        forbes_billionaires (list): Forbes top 10 real time billionaires
+        forbes_billionaires (list): Forbes top 10 real-time billionaires
     """
 
     table = rich_table.Table(
-        title=f"Forbes Top {LIMIT} Real Time Billionaires at {TODAY:%Y-%m-%d %H:%M}",
+        title=f"Forbes Top {LIMIT} Real-Time Billionaires at {TODAY:%Y-%m-%d %H:%M}",
         style="green",
         highlight=True,
         box=box.SQUARE,
@@ -95,4 +103,7 @@ def display_billionaires(forbes_billionaires: list[dict[str, str]]) -> None:
 
 
 if __name__ == "__main__":
+    from doctest import testmod
+
+    testmod()
     display_billionaires(get_forbes_real_time_billionaires())
diff --git a/web_programming/get_top_hn_posts.py b/web_programming/get_top_hn_posts.py
index fbb7c051a88e..7d6308ae70c1 100644
--- a/web_programming/get_top_hn_posts.py
+++ b/web_programming/get_top_hn_posts.py
@@ -1,11 +1,18 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
-import requests
+import httpx
 
 
 def get_hackernews_story(story_id: str) -> dict:
     url = f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json?print=pretty"
-    return requests.get(url).json()
+    return httpx.get(url, timeout=10).json()
 
 
 def hackernews_top_stories(max_stories: int = 10) -> list[dict]:
@@ -13,7 +20,7 @@ def hackernews_top_stories(max_stories: int = 10) -> list[dict]:
     Get the top max_stories posts from HackerNews - https://news.ycombinator.com/
     """
     url = "https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty"
-    story_ids = requests.get(url).json()[:max_stories]
+    story_ids = httpx.get(url, timeout=10).json()[:max_stories]
     return [get_hackernews_story(story_id) for story_id in story_ids]
 
 
diff --git a/web_programming/get_user_tweets.py b/web_programming/get_user_tweets.py.DISABLED
similarity index 100%
rename from web_programming/get_user_tweets.py
rename to web_programming/get_user_tweets.py.DISABLED
diff --git a/web_programming/giphy.py b/web_programming/giphy.py
index a5c3f8f7493e..90f4e51a7242 100644
--- a/web_programming/giphy.py
+++ b/web_programming/giphy.py
@@ -1,5 +1,13 @@
 #!/usr/bin/env python3
-import requests
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+import httpx
 
 giphy_api_key = "YOUR API KEY"
 # Can be fetched from https://developers.giphy.com/dashboard/
@@ -11,7 +19,7 @@ def get_gifs(query: str, api_key: str = giphy_api_key) -> list:
     """
     formatted_query = "+".join(query.split())
     url = f"https://api.giphy.com/v1/gifs/search?q={formatted_query}&api_key={api_key}"
-    gifs = requests.get(url).json()["data"]
+    gifs = httpx.get(url, timeout=10).json()["data"]
     return [gif["url"] for gif in gifs]
 
 
diff --git a/web_programming/instagram_crawler.py b/web_programming/instagram_crawler.py
index 0816cd181051..68271c1c4643 100644
--- a/web_programming/instagram_crawler.py
+++ b/web_programming/instagram_crawler.py
@@ -1,9 +1,19 @@
 #!/usr/bin/env python3
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "fake-useragent",
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
 import json
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 from fake_useragent import UserAgent
 
@@ -39,7 +49,7 @@ def get_json(self) -> dict:
         """
         Return a dict of user information
         """
-        html = requests.get(self.url, headers=headers).text
+        html = httpx.get(self.url, headers=headers, timeout=10).text
         scripts = BeautifulSoup(html, "html.parser").find_all("script")
         try:
             return extract_user_profile(scripts[4])
diff --git a/web_programming/instagram_pic.py b/web_programming/instagram_pic.py
index 8521da674d7d..b6918f35515b 100644
--- a/web_programming/instagram_pic.py
+++ b/web_programming/instagram_pic.py
@@ -1,16 +1,55 @@
-from datetime import datetime
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
 
-import requests
+from datetime import UTC, datetime
+
+import httpx
 from bs4 import BeautifulSoup
 
+
+def download_image(url: str) -> str:
+    """
+    Download an image from a given URL by scraping the 'og:image' meta tag.
+
+    Parameters:
+        url: The URL to scrape.
+
+    Returns:
+        A message indicating the result of the operation.
+    """
+    try:
+        response = httpx.get(url, timeout=10)
+        response.raise_for_status()
+    except httpx.RequestError as e:
+        return f"An error occurred during the HTTP request to {url}: {e!r}"
+
+    soup = BeautifulSoup(response.text, "html.parser")
+    image_meta_tag = soup.find("meta", {"property": "og:image"})
+    if not image_meta_tag:
+        return "No meta tag with property 'og:image' was found."
+
+    image_url = image_meta_tag.get("content")
+    if not image_url:
+        return f"Image URL not found in meta tag {image_meta_tag}."
+
+    try:
+        image_data = httpx.get(image_url, timeout=10).content
+    except httpx.RequestError as e:
+        return f"An error occurred during the HTTP request to {image_url}: {e!r}"
+    if not image_data:
+        return f"Failed to download the image from {image_url}."
+
+    file_name = f"{datetime.now(tz=UTC).astimezone():%Y-%m-%d_%H-%M-%S}.jpg"
+    with open(file_name, "wb") as out_file:
+        out_file.write(image_data)
+    return f"Image downloaded and saved in the file {file_name}"
+
+
 if __name__ == "__main__":
-    url = input("Enter image url: ").strip()
-    print(f"Downloading image from {url} ...")
-    soup = BeautifulSoup(requests.get(url).content, "html.parser")
-    # The image URL is in the content field of the first meta tag with property og:image
-    image_url = soup.find("meta", {"property": "og:image"})["content"]
-    image_data = requests.get(image_url).content
-    file_name = f"{datetime.now():%Y-%m-%d_%H:%M:%S}.jpg"
-    with open(file_name, "wb") as fp:
-        fp.write(image_data)
-    print(f"Done. Image saved to disk as {file_name}.")
+    url = input("Enter image URL: ").strip() or "https://www.instagram.com"
+    print(f"download_image({url}): {download_image(url)}")
diff --git a/web_programming/instagram_video.py b/web_programming/instagram_video.py
index 243cece1a50e..c6f72aef455a 100644
--- a/web_programming/instagram_video.py
+++ b/web_programming/instagram_video.py
@@ -1,17 +1,24 @@
-from datetime import datetime
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
 
-import requests
+from datetime import UTC, datetime
+
+import httpx
 
 
 def download_video(url: str) -> bytes:
     base_url = "https://downloadgram.net/wp-json/wppress/video-downloader/video?url="
-    video_url = requests.get(base_url + url).json()[0]["urls"][0]["src"]
-    return requests.get(video_url).content
+    video_url = httpx.get(base_url + url, timeout=10)
+    return httpx.get(video_url, timeout=10).content
 
 
 if __name__ == "__main__":
     url = input("Enter Video/IGTV url: ").strip()
-    file_name = f"{datetime.now():%Y-%m-%d_%H:%M:%S}.mp4"
+    file_name = f"{datetime.now(tz=UTC).astimezone():%Y-%m-%d_%H-%M-%S}.mp4"
     with open(file_name, "wb") as fp:
         fp.write(download_video(url))
     print(f"Done. Video saved to disk as {file_name}.")
diff --git a/web_programming/nasa_data.py b/web_programming/nasa_data.py
index c0a2c4fdd1a7..1025d2b4ab5f 100644
--- a/web_programming/nasa_data.py
+++ b/web_programming/nasa_data.py
@@ -1,25 +1,30 @@
-import shutil
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
 
-import requests
+import httpx
 
 
-def get_apod_data(api_key: str, download: bool = False, path: str = ".") -> dict:
+def get_apod_data(api_key: str) -> dict:
     """
     Get the APOD(Astronomical Picture of the day) data
     Get your API Key from: https://api.nasa.gov/
     """
     url = "https://api.nasa.gov/planetary/apod"
-    return requests.get(url, params={"api_key": api_key}).json()
+    return httpx.get(url, params={"api_key": api_key}, timeout=10).json()
 
 
 def save_apod(api_key: str, path: str = ".") -> dict:
     apod_data = get_apod_data(api_key)
     img_url = apod_data["url"]
     img_name = img_url.split("/")[-1]
-    response = requests.get(img_url, stream=True)
+    response = httpx.get(img_url, timeout=10)
 
     with open(f"{path}/{img_name}", "wb+") as img_file:
-        shutil.copyfileobj(response.raw, img_file)
+        img_file.write(response.content)
     del response
     return apod_data
 
@@ -29,7 +34,7 @@ def get_archive_data(query: str) -> dict:
     Get the data of a particular query from NASA archives
     """
     url = "https://images-api.nasa.gov/search"
-    return requests.get(url, params={"q": query}).json()
+    return httpx.get(url, params={"q": query}, timeout=10).json()
 
 
 if __name__ == "__main__":
diff --git a/web_programming/open_google_results.py b/web_programming/open_google_results.py
index f61e3666dd7e..f245e4b6d818 100644
--- a/web_programming/open_google_results.py
+++ b/web_programming/open_google_results.py
@@ -1,8 +1,17 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "fake-useragent",
+#     "httpx",
+# ]
+# ///
+
 import webbrowser
 from sys import argv
 from urllib.parse import parse_qs, quote
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 from fake_useragent import UserAgent
 
@@ -13,25 +22,18 @@
 
     url = f"https://www.google.com/search?q={query}&num=100"
 
-    res = requests.get(
+    res = httpx.get(
         url,
         headers={"User-Agent": str(UserAgent().random)},
+        timeout=10,
     )
 
     try:
-        link = (
-            BeautifulSoup(res.text, "html.parser")
-            .find("div", attrs={"class": "yuRUbf"})
-            .find("a")
-            .get("href")
-        )
+        link = BeautifulSoup(res.text, "html.parser").find("div").find("a").get("href")
 
     except AttributeError:
         link = parse_qs(
-            BeautifulSoup(res.text, "html.parser")
-            .find("div", attrs={"class": "kCrYT"})
-            .find("a")
-            .get("href")
+            BeautifulSoup(res.text, "html.parser").find("div").find("a").get("href")
         )["url"][0]
 
     webbrowser.open(link)
diff --git a/web_programming/random_anime_character.py b/web_programming/random_anime_character.py
index f15a9c05d9e5..b94894e7ffe1 100644
--- a/web_programming/random_anime_character.py
+++ b/web_programming/random_anime_character.py
@@ -1,6 +1,15 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "fake-useragent",
+#     "httpx",
+# ]
+# ///
+
 import os
 
-import requests
+import httpx
 from bs4 import BeautifulSoup
 from fake_useragent import UserAgent
 
@@ -12,7 +21,7 @@ def save_image(image_url: str, image_title: str) -> None:
     """
     Saves the image of anime character
     """
-    image = requests.get(image_url, headers=headers)
+    image = httpx.get(image_url, headers=headers, timeout=10)
     with open(image_title, "wb") as file:
         file.write(image.content)
 
@@ -21,7 +30,9 @@ def random_anime_character() -> tuple[str, str, str]:
     """
     Returns the Title, Description, and Image Title of a random anime character .
     """
-    soup = BeautifulSoup(requests.get(URL, headers=headers).text, "html.parser")
+    soup = BeautifulSoup(
+        httpx.get(URL, headers=headers, timeout=10).text, "html.parser"
+    )
     title = soup.find("meta", attrs={"property": "og:title"}).attrs["content"]
     image_url = soup.find("meta", attrs={"property": "og:image"}).attrs["content"]
     description = soup.find("p", id="description").get_text()
diff --git a/web_programming/recaptcha_verification.py b/web_programming/recaptcha_verification.py
index 47c6c42f2ad0..c3a53da0b3da 100644
--- a/web_programming/recaptcha_verification.py
+++ b/web_programming/recaptcha_verification.py
@@ -31,7 +31,15 @@
 Below a Django function for the views.py file contains a login form for demonstrating
 recaptcha verification.
 """
-import requests
+
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+import httpx
 
 try:
     from django.contrib.auth import authenticate, login
@@ -42,7 +50,7 @@
 
 def login_using_recaptcha(request):
     # Enter your recaptcha secret key here
-    secret_key = "secretKey"
+    secret_key = "secretKey"  # noqa: S105
     url = "https://www.google.com/recaptcha/api/siteverify"
 
     # when method is not POST, direct user to login page
@@ -55,7 +63,9 @@ def login_using_recaptcha(request):
     client_key = request.POST.get("g-recaptcha-response")
 
     # post recaptcha response to Google's recaptcha api
-    response = requests.post(url, data={"secret": secret_key, "response": client_key})
+    response = httpx.post(
+        url, data={"secret": secret_key, "response": client_key}, timeout=10
+    )
     # if the recaptcha api verified our keys
     if response.json().get("success", False):
         # authenticate the user
diff --git a/web_programming/reddit.py b/web_programming/reddit.py
index 1c165ecc49ec..863b44f60b16 100644
--- a/web_programming/reddit.py
+++ b/web_programming/reddit.py
@@ -1,6 +1,13 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
 from __future__ import annotations
 
-import requests
+import httpx
 
 valid_terms = set(
     """approved_at_utc approved_by author_flair_background_color
@@ -28,12 +35,14 @@ def get_subreddit_data(
     if invalid_search_terms := ", ".join(sorted(set(wanted_data) - valid_terms)):
         msg = f"Invalid search term: {invalid_search_terms}"
         raise ValueError(msg)
-    response = requests.get(
-        f"https://reddit.com/r/{subreddit}/{age}.json?limit={limit}",
+    response = httpx.get(
+        f"https://www.reddit.com/r/{subreddit}/{age}.json?limit={limit}",
         headers={"User-agent": "A random string"},
+        timeout=10,
     )
+    response.raise_for_status()
     if response.status_code == 429:
-        raise requests.HTTPError(response=response)
+        raise httpx.HTTPError(response=response)
 
     data = response.json()
     if not wanted_data:
diff --git a/web_programming/search_books_by_isbn.py b/web_programming/search_books_by_isbn.py
index d5d4cfe92f20..02d096181fff 100644
--- a/web_programming/search_books_by_isbn.py
+++ b/web_programming/search_books_by_isbn.py
@@ -3,9 +3,17 @@
 
 ISBN: https://en.wikipedia.org/wiki/International_Standard_Book_Number
 """
-from json import JSONDecodeError  # Workaround for requests.exceptions.JSONDecodeError
 
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+from json import JSONDecodeError
+
+import httpx
 
 
 def get_openlibrary_data(olid: str = "isbn/0140328726") -> dict:
@@ -24,7 +32,9 @@ def get_openlibrary_data(olid: str = "isbn/0140328726") -> dict:
     if new_olid.count("/") != 1:
         msg = f"{olid} is not a valid Open Library olid"
         raise ValueError(msg)
-    return requests.get(f"https://openlibrary.org/{new_olid}.json").json()
+    return httpx.get(
+        f"https://openlibrary.org/{new_olid}.json", timeout=10, follow_redirects=True
+    ).json()
 
 
 def summarize_book(ol_book_data: dict) -> dict:
@@ -35,8 +45,7 @@ def summarize_book(ol_book_data: dict) -> dict:
         "title": "Title",
         "publish_date": "Publish date",
         "authors": "Authors",
-        "number_of_pages": "Number of pages:",
-        "first_sentence": "First sentence",
+        "number_of_pages": "Number of pages",
         "isbn_10": "ISBN (10)",
         "isbn_13": "ISBN (13)",
     }
@@ -44,7 +53,6 @@ def summarize_book(ol_book_data: dict) -> dict:
     data["Authors"] = [
         get_openlibrary_data(author["key"])["name"] for author in data["Authors"]
     ]
-    data["First sentence"] = data["First sentence"]["value"]
     for key, value in data.items():
         if isinstance(value, list):
             data[key] = ", ".join(value)
@@ -70,5 +78,5 @@ def summarize_book(ol_book_data: dict) -> dict:
         try:
             book_summary = summarize_book(get_openlibrary_data(f"isbn/{isbn}"))
             print("\n".join(f"{key}: {value}" for key, value in book_summary.items()))
-        except JSONDecodeError:  # Workaround for requests.exceptions.RequestException:
+        except JSONDecodeError:
             print(f"Sorry, there are no results for ISBN: {isbn}.")
diff --git a/web_programming/slack_message.py b/web_programming/slack_message.py
index 5e97d6b64c75..b00dac16688f 100644
--- a/web_programming/slack_message.py
+++ b/web_programming/slack_message.py
@@ -1,11 +1,20 @@
 # Created by sarathkaul on 12/11/19
 
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "httpx",
+# ]
+# ///
+
+import httpx
 
 
 def send_slack_message(message_body: str, slack_url: str) -> None:
     headers = {"Content-Type": "application/json"}
-    response = requests.post(slack_url, json={"text": message_body}, headers=headers)
+    response = httpx.post(
+        slack_url, json={"text": message_body}, headers=headers, timeout=10
+    )
     if response.status_code != 200:
         msg = (
             "Request to slack returned an error "
diff --git a/web_programming/test_fetch_github_info.py b/web_programming/test_fetch_github_info.py
index 2da97c782df7..2c433952fe10 100644
--- a/web_programming/test_fetch_github_info.py
+++ b/web_programming/test_fetch_github_info.py
@@ -1,6 +1,6 @@
 import json
 
-import requests
+import httpx
 
 from .fetch_github_info import AUTHENTICATED_USER_ENDPOINT, fetch_github_info
 
@@ -21,7 +21,7 @@ def mock_response(*args, **kwargs):
         assert "Accept" in kwargs["headers"]
         return FakeResponse(b'{"login":"test","id":1}')
 
-    monkeypatch.setattr(requests, "get", mock_response)
+    monkeypatch.setattr(httpx, "get", mock_response)
     result = fetch_github_info("token")
     assert result["login"] == "test"
     assert result["id"] == 1
diff --git a/web_programming/world_covid19_stats.py b/web_programming/world_covid19_stats.py
index ca81abdc4ce9..99383577d627 100644
--- a/web_programming/world_covid19_stats.py
+++ b/web_programming/world_covid19_stats.py
@@ -5,19 +5,31 @@
 This data is being scrapped from 'https://www.worldometers.info/coronavirus/'.
 """
 
-import requests
+# /// script
+# requires-python = ">=3.13"
+# dependencies = [
+#     "beautifulsoup4",
+#     "httpx",
+# ]
+# ///
+
+import httpx
 from bs4 import BeautifulSoup
 
 
-def world_covid19_stats(url: str = "https://www.worldometers.info/coronavirus") -> dict:
+def world_covid19_stats(
+    url: str = "https://www.worldometers.info/coronavirus/",
+) -> dict:
     """
     Return a dict of current worldwide COVID-19 statistics
     """
-    soup = BeautifulSoup(requests.get(url).text, "html.parser")
-    keys = soup.findAll("h1")
-    values = soup.findAll("div", {"class": "maincounter-number"})
-    keys += soup.findAll("span", {"class": "panel-title"})
-    values += soup.findAll("div", {"class": "number-table-main"})
+    soup = BeautifulSoup(
+        httpx.get(url, timeout=10, follow_redirects=True).text, "html.parser"
+    )
+    keys = soup.find_all("h1")
+    values = soup.find_all("div", {"class": "maincounter-number"})
+    keys += soup.find_all("span", {"class": "panel-title"})
+    values += soup.find_all("div", {"class": "number-table-main"})
     return {key.text.strip(): value.text.strip() for key, value in zip(keys, values)}
 
 
