diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f10a5bc1..cbd920f6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,8 +1,10 @@
 version: 2
 updates:
-  # Set update schedule for GitHub Actions
   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
-      # Check for updates to GitHub Actions every weekday
+      interval: "weekly"
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
       interval: "weekly"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a0d879b9..8a47563e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -50,12 +50,18 @@ jobs:
           codecov
       - run: hatch version 100.100.100
 
-  pre_commit:
-    runs-on: ubuntu-20.04
+  test_lint:
+    name: Test Lint
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
-      - uses: jupyterlab/maintainer-tools/.github/actions/pre-commit@v1
+      -  name: Run Linters
+         run: |
+          hatch run typing:test
+          hatch run lint:style
+          pipx run 'validate-pyproject[all]' pyproject.toml
+          pipx run doc8 --max-line-length=200
 
   docs:
     runs-on: ubuntu-latest
@@ -142,7 +148,7 @@ jobs:
     if: always()
     needs:
       - tests
-      - pre_commit
+      - test_lint
       - docs
       - test_minimum_versions
       - test_prereleases
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 99467558..ff708b29 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,3 +1,6 @@
+ci:
+  autoupdate_schedule: monthly
+
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.4.0
@@ -15,59 +18,23 @@ repos:
       - id: check-builtin-literals
       - id: trailing-whitespace
 
-  - repo: https://github.com/psf/black
-    rev: 22.10.0
-    hooks:
-      - id: black
-
-  - repo: https://github.com/PyCQA/isort
-    rev: 5.10.1
-    hooks:
-      - id: isort
-        files: \.py$
-
-  - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.991
-    hooks:
-      - id: mypy
-        additional_dependencies: [types-requests, traitlets, jupyter_core]
-        stages: [manual]
-
-  - repo: https://github.com/abravalheri/validate-pyproject
-    rev: v0.10.1
+  - repo: https://github.com/python-jsonschema/check-jsonschema
+    rev: 0.19.2
     hooks:
-      - id: validate-pyproject
-        stages: [manual]
+      - id: check-github-workflows
 
   - repo: https://github.com/executablebooks/mdformat
     rev: 0.7.16
     hooks:
       - id: mdformat
 
-  - repo: https://github.com/asottile/pyupgrade
-    rev: v3.3.0
-    hooks:
-      - id: pyupgrade
-        args: [--py37-plus]
-
-  - repo: https://github.com/PyCQA/doc8
-    rev: v1.0.0
-    hooks:
-      - id: doc8
-        args: [--max-line-length=200]
-        exclude: docs/source/other/full-config.rst
-        stages: [manual]
-
-  - repo: https://github.com/john-hen/Flake8-pyproject
-    rev: 1.2.2
+  - repo: https://github.com/psf/black
+    rev: 22.10.0
     hooks:
-      - id: Flake8-pyproject
-        alias: flake8
-        additional_dependencies:
-          ["flake8-bugbear==22.6.22", "flake8-implicit-str-concat==0.2.0"]
-        stages: [manual]
+      - id: black
 
-  - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.19.2
+  - repo: https://github.com/charliermarsh/ruff-pre-commit
+    rev: v0.0.165
     hooks:
-      - id: check-github-workflows
+      - id: ruff
+        args: ["--fix"]
diff --git a/nbformat/_imports.py b/nbformat/_imports.py
index 1c6c97ac..59fe2f0e 100644
--- a/nbformat/_imports.py
+++ b/nbformat/_imports.py
@@ -33,7 +33,7 @@ def import_item(name):
         try:
             pak = getattr(module, obj)
         except AttributeError:
-            raise ImportError("No module named %s" % obj)
+            raise ImportError("No module named %s" % obj) from None
         return pak
     else:
         # called with un-dotted string
diff --git a/nbformat/_struct.py b/nbformat/_struct.py
index f1ab4e47..cb70211e 100644
--- a/nbformat/_struct.py
+++ b/nbformat/_struct.py
@@ -102,7 +102,7 @@ def __setattr__(self, key, value):
         try:
             self.__setitem__(key, value)
         except KeyError as e:
-            raise AttributeError(e)
+            raise AttributeError(e) from None
 
     def __getattr__(self, key):
         """Get an attr by calling :meth:`dict.__getitem__`.
@@ -127,7 +127,7 @@ def __getattr__(self, key):
         try:
             result = self[key]
         except KeyError:
-            raise AttributeError(key)
+            raise AttributeError(key) from None
         else:
             return result
 
diff --git a/nbformat/converter.py b/nbformat/converter.py
index d49a197e..e6969926 100644
--- a/nbformat/converter.py
+++ b/nbformat/converter.py
@@ -62,7 +62,7 @@ def convert(nb, to_version):
         except AttributeError as e:
             raise ValidationError(
                 f"Notebook could not be converted from version {version} to version {step_version} because it's missing a key: {e}"
-            )
+            ) from None
 
         # Recursively convert until target version is reached.
         return convert(converted, to_version)
diff --git a/nbformat/json_compat.py b/nbformat/json_compat.py
index 737a9111..7cd87a03 100644
--- a/nbformat/json_compat.py
+++ b/nbformat/json_compat.py
@@ -47,7 +47,7 @@ def validate(self, data):
         try:
             self._validator(data)
         except _JsonSchemaException as error:
-            raise ValidationError(str(error), schema_path=error.path)
+            raise ValidationError(str(error), schema_path=error.path) from error
 
     def iter_errors(self, data, schema=None):
         if schema is not None:
diff --git a/nbformat/reader.py b/nbformat/reader.py
index 54d91110..47759b45 100644
--- a/nbformat/reader.py
+++ b/nbformat/reader.py
@@ -74,7 +74,9 @@ def reads(s, **kwargs):
         try:
             return versions[major].to_notebook_json(nb_dict, minor=minor)
         except AttributeError as e:
-            raise ValidationError(f"The notebook is invalid and is missing an expected key: {e}")
+            raise ValidationError(
+                f"The notebook is invalid and is missing an expected key: {e}"
+            ) from None
     else:
         raise NBFormatError("Unsupported nbformat version %s" % major)
 
diff --git a/nbformat/sign.py b/nbformat/sign.py
index 6009e9ba..6011ef37 100644
--- a/nbformat/sign.py
+++ b/nbformat/sign.py
@@ -23,18 +23,7 @@
 from base64 import encodebytes
 
 from jupyter_core.application import JupyterApp, base_flags
-from traitlets import (
-    Any,
-    Bool,
-    Bytes,
-    Callable,
-    Enum,
-    Instance,
-    Integer,
-    Unicode,
-    default,
-    observe,
-)
+from traitlets import Any, Bool, Bytes, Callable, Enum, Instance, Integer, Unicode, default, observe
 from traitlets.config import LoggingConfigurable, MultipleInstanceError
 
 from . import NO_CONVERT, __version__, read, reads
@@ -139,9 +128,9 @@ def close(self):
             self.db.close()
 
     def _connect_db(self, db_file):
-        kwargs: t.Dict[str, t.Any] = dict(
-            detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
-        )
+        kwargs: t.Dict[str, t.Any] = {
+            "detect_types": sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
+        }
         db = None
         try:
             db = sqlite3.connect(db_file, **kwargs)
diff --git a/nbformat/v1/nbbase.py b/nbformat/v1/nbbase.py
index 441f6fa9..5b1c0c41 100644
--- a/nbformat/v1/nbbase.py
+++ b/nbformat/v1/nbbase.py
@@ -18,6 +18,7 @@
 
 from .._struct import Struct
 
+
 # -----------------------------------------------------------------------------
 # Code
 # -----------------------------------------------------------------------------
diff --git a/nbformat/v1/nbjson.py b/nbformat/v1/nbjson.py
index aeb7b349..d319e60d 100644
--- a/nbformat/v1/nbjson.py
+++ b/nbformat/v1/nbjson.py
@@ -21,6 +21,7 @@
 from .nbbase import from_dict
 from .rwbase import NotebookReader, NotebookWriter
 
+
 # -----------------------------------------------------------------------------
 # Code
 # -----------------------------------------------------------------------------
diff --git a/nbformat/v2/convert.py b/nbformat/v2/convert.py
index 501b90f6..dd3ab798 100644
--- a/nbformat/v2/convert.py
+++ b/nbformat/v2/convert.py
@@ -19,6 +19,7 @@
 
 from .nbbase import new_code_cell, new_notebook, new_text_cell, new_worksheet
 
+
 # -----------------------------------------------------------------------------
 # Code
 # -----------------------------------------------------------------------------
diff --git a/nbformat/v2/nbbase.py b/nbformat/v2/nbbase.py
index bc9f3ab4..72cf751f 100644
--- a/nbformat/v2/nbbase.py
+++ b/nbformat/v2/nbbase.py
@@ -23,6 +23,7 @@
 
 from .._struct import Struct
 
+
 # -----------------------------------------------------------------------------
 # Code
 # -----------------------------------------------------------------------------
diff --git a/nbformat/v2/nbjson.py b/nbformat/v2/nbjson.py
index 02eff5ba..59397333 100644
--- a/nbformat/v2/nbjson.py
+++ b/nbformat/v2/nbjson.py
@@ -20,13 +20,8 @@
 import json
 
 from .nbbase import from_dict
-from .rwbase import (
-    NotebookReader,
-    NotebookWriter,
-    rejoin_lines,
-    restore_bytes,
-    split_lines,
-)
+from .rwbase import NotebookReader, NotebookWriter, rejoin_lines, restore_bytes, split_lines
+
 
 # -----------------------------------------------------------------------------
 # Code
diff --git a/nbformat/v2/rwbase.py b/nbformat/v2/rwbase.py
index edfc8deb..af23ade5 100644
--- a/nbformat/v2/rwbase.py
+++ b/nbformat/v2/rwbase.py
@@ -18,6 +18,7 @@
 
 from base64 import decodebytes, encodebytes
 
+
 # -----------------------------------------------------------------------------
 # Code
 # -----------------------------------------------------------------------------
diff --git a/nbformat/v3/nbjson.py b/nbformat/v3/nbjson.py
index 341e3e1c..f6efe8bb 100644
--- a/nbformat/v3/nbjson.py
+++ b/nbformat/v3/nbjson.py
@@ -7,13 +7,7 @@
 import json
 
 from .nbbase import from_dict
-from .rwbase import (
-    NotebookReader,
-    NotebookWriter,
-    rejoin_lines,
-    split_lines,
-    strip_transient,
-)
+from .rwbase import NotebookReader, NotebookWriter, rejoin_lines, split_lines, strip_transient
 
 
 class BytesEncoder(json.JSONEncoder):
diff --git a/nbformat/v4/nbjson.py b/nbformat/v4/nbjson.py
index 4b5c5642..70566749 100644
--- a/nbformat/v4/nbjson.py
+++ b/nbformat/v4/nbjson.py
@@ -7,13 +7,7 @@
 import json
 
 from ..notebooknode import from_dict
-from .rwbase import (
-    NotebookReader,
-    NotebookWriter,
-    rejoin_lines,
-    split_lines,
-    strip_transient,
-)
+from .rwbase import NotebookReader, NotebookWriter, rejoin_lines, split_lines, strip_transient
 
 
 class BytesEncoder(json.JSONEncoder):
diff --git a/nbformat/validator.py b/nbformat/validator.py
index 901d4fd0..ede37484 100644
--- a/nbformat/validator.py
+++ b/nbformat/validator.py
@@ -417,14 +417,14 @@ def validate(
     version : int
     version_minor : int
     relax_add_props : bool
-        Deprecated since 5.5.0 – will be removed in the future.
+        Deprecated since 5.5.0 - will be removed in the future.
         Wether to allow extra property in the Json schema validating the
         notebook.
     nbjson
     repair_duplicate_cell_ids : boolny
-        Deprecated since 5.5.0 – will be removed in the future.
+        Deprecated since 5.5.0 - will be removed in the future.
     strip_invalid_metadata : bool
-        Deprecated since 5.5.0 – will be removed in the future.
+        Deprecated since 5.5.0 - will be removed in the future.
 
     Returns
     -------
diff --git a/pyproject.toml b/pyproject.toml
index 702917cd..fedd5e69 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,12 +49,13 @@ docs = [
 "sphinxcontrib_github_alt"
 ]
 test = [
-    "check-manifest",
     "testpath",
     "pytest",
     "pre-commit",
     "pep440"
 ]
+lint = ["black[jupyter]>=22.6.0", "mdformat>0.7", "ruff>=0.0.156"]
+typing = ["mypy>=0.990"]
 
 [project.scripts]
 jupyter-trust = "nbformat.sign:TrustNotebookApp.launch_instance"
@@ -80,11 +81,24 @@ dependencies = ["coverage", "pytest-cov"]
 test = "python -m pytest -vv --cov nbformat --cov-branch --cov-report term-missing:skip-covered {args}"
 nowarn = "test -W default {args}"
 
-[tool.black]
-line_length = 100
-
-[tool.isort]
-profile = "black"
+[tool.hatch.envs.typing]
+features = ["typing", "test"]
+[tool.hatch.envs.typing.scripts]
+test = "mypy --install-types --non-interactive {args:nbformat tests}"
+
+[tool.hatch.envs.lint]
+features = ["lint"]
+[tool.hatch.envs.lint.scripts]
+style = [
+  "ruff {args:.}",
+  "black --check --diff {args:.}",
+  "mdformat --check {args:*.md}"
+]
+fmt = [
+  "black {args:.}",
+  "ruff --fix {args:.}",
+  "mdformat {args:*.md}"
+]
 
 [tool.pytest.ini_options]
 addopts = "-raXs --durations 10 --color=yes --doctest-modules"
@@ -133,26 +147,65 @@ module = [
 ]
 ignore_missing_imports = true
 
-[tool.flake8]
-ignore = "E501, W503, E402"
-builtins = "c, get_config"
-exclude = [
-    ".cache",
-    ".github",
-    "docs",
-    "setup.py",
+[tool.black]
+line-length = 100
+skip-string-normalization = true
+target-version = ["py38"]
+extend-exclude = "^/tests.*ipynb$"
+
+[tool.ruff]
+target-version = "py38"
+line-length = 100
+select = [
+  "A", "B", "C", "E", "F", "FBT", "I", "N", "Q", "RUF", "S", "T",
+  "UP", "W", "YTT",
 ]
-enable-extensions = "G"
-extend-ignore = [
-    "G001", "G002", "G004", "G200", "G201", "G202",
-    # black adds spaces around ':'
-    "E203",
+ignore = [
+  # Allow non-abstract empty methods in abstract base classes
+  "B027",
+  # Ignore McCabe complexity
+  "C901",
+  # Allow boolean positional values in function calls, like `dict.get(... True)`
+  "FBT003",
+  # Use of `assert` detected
+  "S101",
+  # Line too long
+  "E501",
+  # Relative imports are banned
+  "I252",
+  # Boolean ... in function definition
+  "FBT001", "FBT002",
+  # Module level import not at top of file
+  "E402",
+  # A001/A002/A003 .. is shadowing a python builtin
+  "A001", "A002", "A003",
+  # Possible hardcoded password
+  "S105", "S106",
+  # Q000 Single quotes found but double quotes preferred
+  "Q000",
+  # N806 Variable `B` in function should be lowercase
+  "N806",
+  # T201 `print` found
+  "T201",
+  # N802 Function name `CreateWellKnownSid` should be lowercase
+  "N802", "N803"
 ]
-per-file-ignores = [
-    # B011: Do not call assert False since python -O removes these calls
-    # F841 local variable 'foo' is assigned to but never used
-    # B007 Loop control variable 'foo' not used within the loop body
-    "tests/*: B011", "F841", "B007",
-    # F401 '.foo' imported but unused
-    "nbformat/*/__init__.py: F401",
+unfixable = [
+  # Don't touch print statements
+  "T201",
+  # Don't touch noqa lines
+  "RUF100",
 ]
+
+[tool.ruff.per-file-ignores]
+# B011 Do not call assert False since python -O removes these calls
+# F841 local variable 'foo' is assigned to but never used
+# C408 Unnecessary `dict` call
+# E402 Module level import not at top of file
+# T201 `print` found
+# B007 Loop control variable `i` not used within the loop body.
+# N802 Function name `assertIn` should be lowercase
+# RUF001 contains ambiguous unicode character '–' (did you mean '-'?)
+"tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "RUF001", "RUF002"]
+# F401 `nbxml.to_notebook` imported but unused
+"nbformat/*__init__.py" = ["F401"]
diff --git a/tests/invalid.ipynb b/tests/invalid.ipynb
index e53a2df0..2435c06e 100644
--- a/tests/invalid.ipynb
+++ b/tests/invalid.ipynb
@@ -72,12 +72,15 @@
    ],
    "source": [
     "from IPython.display import HTML\n",
-    "HTML(\"\"\"\n",
+    "\n",
+    "HTML(\n",
+    "    \"\"\"\n",
     "<script>\n",
     "console.log(\"hello\");\n",
     "</script>\n",
     "<b>HTML</b>\n",
-    "\"\"\")"
+    "\"\"\"\n",
+    ")"
    ]
   },
   {
@@ -296,6 +299,7 @@
    ],
    "source": [
     "from IPython.display import Image\n",
+    "\n",
     "Image(\"http://ipython.org/_static/IPy_header.png\")"
    ]
   }
diff --git a/tests/test4.5.ipynb b/tests/test4.5.ipynb
index ba09ecbd..076be786 100644
--- a/tests/test4.5.ipynb
+++ b/tests/test4.5.ipynb
@@ -76,12 +76,15 @@
    ],
    "source": [
     "from IPython.display import HTML\n",
-    "HTML(\"\"\"\n",
+    "\n",
+    "HTML(\n",
+    "    \"\"\"\n",
     "<script>\n",
     "console.log(\"hello\");\n",
     "</script>\n",
     "<b>HTML</b>\n",
-    "\"\"\")"
+    "\"\"\"\n",
+    ")"
    ]
   },
   {
@@ -136,6 +139,7 @@
    ],
    "source": [
     "from IPython.display import Image\n",
+    "\n",
     "Image(\"http://ipython.org/_static/IPy_header.png\")"
    ]
   }
diff --git a/tests/test4.ipynb b/tests/test4.ipynb
index 20afe94d..7231e3a0 100644
--- a/tests/test4.ipynb
+++ b/tests/test4.ipynb
@@ -74,12 +74,15 @@
    ],
    "source": [
     "from IPython.display import HTML\n",
-    "HTML(\"\"\"\n",
+    "\n",
+    "HTML(\n",
+    "    \"\"\"\n",
     "<script>\n",
     "console.log(\"hello\");\n",
     "</script>\n",
     "<b>HTML</b>\n",
-    "\"\"\")"
+    "\"\"\"\n",
+    ")"
    ]
   },
   {
@@ -298,6 +301,7 @@
    ],
    "source": [
     "from IPython.display import Image\n",
+    "\n",
     "Image(\"http://ipython.org/_static/IPy_header.png\")"
    ]
   }
diff --git a/tests/test4custom.ipynb b/tests/test4custom.ipynb
index 31fa9b11..68a0da91 100644
--- a/tests/test4custom.ipynb
+++ b/tests/test4custom.ipynb
@@ -26,11 +26,7 @@
     "import IPython\n",
     "\n",
     "bundle = {}\n",
-    "bundle['application/vnd.raw.v1+json'] = {\n",
-    "    'apples': ['🍎', '🍏'],\n",
-    "    'bananas': 2,\n",
-    "    'oranges': 'apples'\n",
-    "}\n",
+    "bundle['application/vnd.raw.v1+json'] = {'apples': ['🍎', '🍏'], 'bananas': 2, 'oranges': 'apples'}\n",
     "\n",
     "IPython.display.display(bundle, raw=True)"
    ]
@@ -45,8 +41,7 @@
    "source": []
   }
  ],
- "metadata": {
- },
+ "metadata": {},
  "nbformat": 4,
  "nbformat_minor": 2
 }
diff --git a/tests/test4docinfo.ipynb b/tests/test4docinfo.ipynb
index 7c308921..e0bb9f3d 100644
--- a/tests/test4docinfo.ipynb
+++ b/tests/test4docinfo.ipynb
@@ -74,12 +74,15 @@
    ],
    "source": [
     "from IPython.display import HTML\n",
-    "HTML(\"\"\"\n",
+    "\n",
+    "HTML(\n",
+    "    \"\"\"\n",
     "<script>\n",
     "console.log(\"hello\");\n",
     "</script>\n",
     "<b>HTML</b>\n",
-    "\"\"\")"
+    "\"\"\"\n",
+    ")"
    ]
   },
   {
@@ -298,13 +301,18 @@
    ],
    "source": [
     "from IPython.display import Image\n",
+    "\n",
     "Image(\"http://ipython.org/_static/IPy_header.png\")"
    ]
   }
  ],
  "metadata": {
-   "title": "Test Notebook",
-   "authors": [{"name": "Jean Tester"}]
+  "title": "Test Notebook",
+  "authors": [
+   {
+    "name": "Jean Tester"
+   }
+  ]
  },
  "nbformat": 4,
  "nbformat_minor": 0
diff --git a/tests/test4jupyter_metadata_timings.ipynb b/tests/test4jupyter_metadata_timings.ipynb
index 2e724a55..96b01e7b 100644
--- a/tests/test4jupyter_metadata_timings.ipynb
+++ b/tests/test4jupyter_metadata_timings.ipynb
@@ -25,7 +25,7 @@
     }
    ],
    "source": [
-    "1+1"
+    "1 + 1"
    ]
   },
   {
diff --git a/tests/test4plus.ipynb b/tests/test4plus.ipynb
index 522df071..2b61a123 100644
--- a/tests/test4plus.ipynb
+++ b/tests/test4plus.ipynb
@@ -84,12 +84,15 @@
    ],
    "source": [
     "from IPython.display import HTML\n",
-    "HTML(\"\"\"\n",
+    "\n",
+    "HTML(\n",
+    "    \"\"\"\n",
     "<script>\n",
     "console.log(\"hello\");\n",
     "</script>\n",
     "<b>HTML</b>\n",
-    "\"\"\")"
+    "\"\"\"\n",
+    ")"
    ]
   },
   {
@@ -313,6 +316,7 @@
    ],
    "source": [
     "from IPython.display import Image\n",
+    "\n",
     "Image(\"http://ipython.org/_static/IPy_header.png\")"
    ]
   },
diff --git a/tests/test_reader.py b/tests/test_reader.py
index 4f3e2f5f..fbdf99f6 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -17,6 +17,7 @@
 
 from .base import TestsBase
 
+
 # -----------------------------------------------------------------------------
 # Classes and functions
 # -----------------------------------------------------------------------------