diff --git a/.github/workflows/codespell-private.yml b/.github/workflows/codespell-private.yml
index 9aee75d231..f58feeae36 100644
--- a/.github/workflows/codespell-private.yml
+++ b/.github/workflows/codespell-private.yml
@@ -63,6 +63,12 @@ jobs:
   flake8-annotation:
     runs-on: ubuntu-latest
     steps:
+      - name: Setup python
+        uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
       - uses: actions/checkout@v3
+      - name: Install codespell dependencies
+        run: pip install -e ".[dev]"
       - name: Flake8 with annotations
         uses: TrueBrain/actions-flake8@v2
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000..570d846a57
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,111 @@
+---
+files: ^(.*\.(py|json|md|sh|yaml|cfg|txt))$
+exclude: ^(\.[^/]*cache/.*)$
+repos:
+  - repo: https://github.com/executablebooks/mdformat
+    # Do this before other tools "fixing" the line endings
+    rev: 0.7.16
+    hooks:
+      - id: mdformat
+        name: Format Markdown
+        entry: mdformat  # Executable to run, with fixed options
+        language: python
+        types: [markdown]
+        args: [--wrap, '75', --number]
+        additional_dependencies:
+          - mdformat-toc
+          - mdformat-beautysh
+          # -mdformat-shfmt
+          # -mdformat-tables
+          - mdformat-config
+          - mdformat-black
+          - mdformat-web
+          - mdformat-gfm
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v3.3.1
+    hooks:
+      - id: pyupgrade
+        args: [--py37-plus]
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+      - id: no-commit-to-branch
+        args: [--branch, main]
+      - id: check-yaml
+        args: [--unsafe]
+      - id: debug-statements
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: check-json
+      - id: mixed-line-ending
+      - id: check-builtin-literals
+      - id: check-ast
+      - id: check-merge-conflict
+      - id: check-executables-have-shebangs
+      - id: check-shebang-scripts-are-executable
+      - id: check-docstring-first
+      - id: fix-byte-order-marker
+      - id: check-case-conflict
+      - id: check-toml
+  - repo: https://github.com/adrienverge/yamllint.git
+    rev: v1.28.0
+    hooks:
+      - id: yamllint
+        args:
+          - --no-warnings
+          - -d
+          - '{extends: relaxed, rules: {line-length: {max: 90}}}'
+  - repo: https://github.com/psf/black
+    rev: 22.10.0
+    hooks:
+      - id: black
+  - repo: https://github.com/Lucas-C/pre-commit-hooks-bandit
+    rev: v1.0.6
+    hooks:
+      - id: python-bandit-vulnerability-check
+  - repo:  https://github.com/PyCQA/autoflake
+    rev: v2.0.0
+    hooks:
+      - id: autoflake
+  - repo: https://github.com/PyCQA/flake8
+    rev: 6.0.0
+    hooks:
+      - id: flake8
+        additional_dependencies:
+          - flake8-pyproject>=1.2.2
+          - flake8-bugbear>=22.7.1
+          - flake8-comprehensions>=3.10.0
+          - flake8-2020>=1.7.0
+          - mccabe>=0.7.0
+          - pycodestyle>=2.9.1
+          - pyflakes>=2.5.0
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.10.1
+    hooks:
+      - id: isort
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.2.2
+    hooks:
+      - id: codespell
+        args: [--toml, pyproject-codespell.precommit-toml]
+        additional_dependencies:
+          - tomli
+  - repo: https://github.com/pre-commit/mirrors-pylint
+    rev: v3.0.0a5
+    hooks:
+      - id: pylint
+        additional_dependencies:
+          - chardet
+          - pytest
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v0.991
+    hooks:
+      - id: mypy
+        args: [--no-warn-unused-ignores, --config-file, pyproject.toml, --disable-error-code,
+          import]
+        additional_dependencies:
+          - chardet
+          - pytest
+          - pytest-cov
+          - pytest-dependency
+          - types-chardet
diff --git a/MANIFEST.in b/MANIFEST.in
index d4cf114e40..f5358f6819 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -9,3 +9,4 @@ exclude .git-blame-ignore-revs
 exclude example example/* snap snap/* tools tools/*
 exclude Makefile
 exclude codespell.1.include
+exclude pyproject-codespell.precommit-toml
\ No newline at end of file
diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py
index 8811a049e6..e1783b5a2a 100644
--- a/codespell_lib/_codespell.py
+++ b/codespell_lib/_codespell.py
@@ -352,7 +352,7 @@ def parse_options(
         "This option can be specified multiple times.",
     )
     builtin_opts = "\n- ".join(
-        [""] + ["%r %s" % (d[0], d[1]) for d in _builtin_dictionaries]
+        [""] + [f"{d[0]!r} {d[1]}" for d in _builtin_dictionaries]
     )
     parser.add_argument(
         "--builtin",
@@ -693,7 +693,7 @@ def ask_for_word_fix(
         r = ""
         fixword = fix_case(wrongword, misspelling.data)
         while not r:
-            print("%s\t%s ==> %s (Y/n) " % (line, wrongword, fixword), end="")
+            print(f"{line}\t{wrongword} ==> {fixword} (Y/n) ", end="")
             r = sys.stdin.readline().strip().upper()
             if not r:
                 r = "Y"
@@ -743,7 +743,7 @@ def print_context(
     # context = (context_before, context_after)
     for i in range(index - context[0], index + context[1] + 1):
         if 0 <= i < len(lines):
-            print("%s %s" % (">" if i == index else ":", lines[i].rstrip()))
+            print("{} {}".format(">" if i == index else ":", lines[i].rstrip()))
 
 
 def extract_words(
@@ -806,14 +806,14 @@ def parse_file(
                 if summary and fix:
                     summary.update(lword)
 
-                cfilename = "%s%s%s" % (colors.FILE, filename, colors.DISABLE)
-                cwrongword = "%s%s%s" % (colors.WWORD, word, colors.DISABLE)
-                crightword = "%s%s%s" % (colors.FWORD, fixword, colors.DISABLE)
+                cfilename = f"{colors.FILE}{filename}{colors.DISABLE}"
+                cwrongword = f"{colors.WWORD}{word}{colors.DISABLE}"
+                crightword = f"{colors.FWORD}{fixword}{colors.DISABLE}"
 
                 if misspellings[lword].reason:
                     if options.quiet_level & QuietLevels.DISABLED_FIXES:
                         continue
-                    creason = "  | %s%s%s" % (
+                    creason = "  | {}{}{}".format(
                         colors.FILE,
                         misspellings[lword].reason,
                         colors.DISABLE,
@@ -843,7 +843,7 @@ def parse_file(
         try:
             text = is_text_file(filename)
         except PermissionError as e:
-            print("WARNING: %s: %s" % (e.strerror, filename), file=sys.stderr)
+            print(f"WARNING: {e.strerror}: {filename}", file=sys.stderr)
             return bad_count
         except OSError:
             return bad_count
@@ -917,16 +917,16 @@ def parse_file(
                 ):
                     continue
 
-                cfilename = "%s%s%s" % (colors.FILE, filename, colors.DISABLE)
+                cfilename = f"{colors.FILE}{filename}{colors.DISABLE}"
                 cline = "%s%d%s" % (colors.FILE, i + 1, colors.DISABLE)
-                cwrongword = "%s%s%s" % (colors.WWORD, word, colors.DISABLE)
-                crightword = "%s%s%s" % (colors.FWORD, fixword, colors.DISABLE)
+                cwrongword = f"{colors.WWORD}{word}{colors.DISABLE}"
+                crightword = f"{colors.FWORD}{fixword}{colors.DISABLE}"
 
                 if misspellings[lword].reason:
                     if options.quiet_level & QuietLevels.DISABLED_FIXES:
                         continue
 
-                    creason = "  | %s%s%s" % (
+                    creason = "  | {}{}{}".format(
                         colors.FILE,
                         misspellings[lword].reason,
                         colors.DISABLE,
@@ -976,7 +976,7 @@ def parse_file(
         else:
             if not options.quiet_level & QuietLevels.FIXES:
                 print(
-                    "%sFIXED:%s %s" % (colors.FWORD, colors.DISABLE, filename),
+                    f"{colors.FWORD}FIXED:{colors.DISABLE} {filename}",
                     file=sys.stderr,
                 )
             with open(filename, "w", encoding=encoding, newline="") as f:
@@ -1010,7 +1010,7 @@ def main(*args: str) -> int:
     try:
         word_regex = re.compile(word_regex)
     except re.error as err:
-        print('ERROR: invalid --regex "%s" (%s)' % (word_regex, err), file=sys.stderr)
+        print(f'ERROR: invalid --regex "{word_regex}" ({err})', file=sys.stderr)
         parser.print_help()
         return EX_USAGE
 
@@ -1019,7 +1019,9 @@ def main(*args: str) -> int:
             ignore_word_regex = re.compile(options.ignore_regex)
         except re.error as err:
             print(
-                'ERROR: invalid --ignore-regex "%s" (%s)' % (options.ignore_regex, err),
+                'ERROR: invalid --ignore-regex "{}" ({})'.format(
+                    options.ignore_regex, err
+                ),
                 file=sys.stderr,
             )
             parser.print_help()
@@ -1044,7 +1046,8 @@ def main(*args: str) -> int:
         uri_regex = re.compile(uri_regex)
     except re.error as err:
         print(
-            'ERROR: invalid --uri-regex "%s" (%s)' % (uri_regex, err), file=sys.stderr
+            f'ERROR: invalid --uri-regex "{uri_regex}" ({err})',
+            file=sys.stderr,
         )
         parser.print_help()
         return EX_USAGE
@@ -1063,12 +1066,13 @@ def main(*args: str) -> int:
                 for builtin in _builtin_dictionaries:
                     if builtin[0] == u:
                         use_dictionaries.append(
-                            os.path.join(_data_root, "dictionary%s.txt" % (builtin[2],))
+                            os.path.join(_data_root, f"dictionary{builtin[2]}.txt")
                         )
                         break
                 else:
                     print(
-                        "ERROR: Unknown builtin dictionary: %s" % (u,), file=sys.stderr
+                        f"ERROR: Unknown builtin dictionary: {u}",
+                        file=sys.stderr,
                     )
                     parser.print_help()
                     return EX_USAGE
diff --git a/codespell_lib/tests/test_dictionary.py b/codespell_lib/tests/test_dictionary.py
index 56c373e311..515c3d2d0a 100644
--- a/codespell_lib/tests/test_dictionary.py
+++ b/codespell_lib/tests/test_dictionary.py
@@ -89,14 +89,14 @@ def _check_aspell(
         spellers[lang].check(phrase.encode(spellers[lang].ConfigKeys()["encoding"][1]))
         for lang in languages
     )
-    end = "be in aspell dictionaries (%s) for dictionary %s" % (
+    end = "be in aspell dictionaries ({}) for dictionary {}".format(
         ", ".join(languages),
         fname,
     )
     if in_aspell:  # should be an error in aspell
-        assert this_in_aspell, "%s should %s" % (msg, end)
+        assert this_in_aspell, f"{msg} should {end}"
     else:  # shouldn't be
-        assert not this_in_aspell, "%s should not %s" % (msg, end)
+        assert not this_in_aspell, f"{msg} should not {end}"
 
 
 whitespace = re.compile(r"\s")
@@ -118,15 +118,18 @@ def _check_err_rep(
 ) -> None:
     assert whitespace.search(err) is None, "error %r has whitespace" % err
     assert "," not in err, "error %r has a comma" % err
-    assert len(rep) > 0, "error %s: correction %r must be non-empty" % (err, rep)
+    assert len(rep) > 0, f"error {err}: correction {rep!r} must be non-empty"
     assert not start_whitespace.match(
         rep
-    ), "error %s: correction %r cannot start with whitespace" % (err, rep)
-    _check_aspell(err, "error %r" % (err,), in_aspell[0], fname, languages[0])
-    prefix = "error %s: correction %r" % (err, rep)
+    ), f"error {err}: correction {rep!r} cannot start with whitespace"
+    _check_aspell(err, f"error {err!r}", in_aspell[0], fname, languages[0])
+    prefix = f"error {err}: correction {rep!r}"
     for (regex, msg) in [
         (start_comma, "%s starts with a comma"),
-        (whitespace_comma, "%s contains a whitespace character followed by a comma"),
+        (
+            whitespace_comma,
+            "%s contains a whitespace character followed by a comma",
+        ),
         (
             comma_whitespaces,
             "%s contains a comma followed by multiple whitespace characters",
@@ -144,9 +147,13 @@ def _check_err_rep(
     reps = [r.strip() for r in rep.split(",")]
     reps = [r for r in reps if len(r)]
     for r in reps:
-        assert err != r.lower(), "error %r corrects to itself amongst others" % (err,)
+        assert err != r.lower(), f"error {err!r} corrects to itself amongst others"
         _check_aspell(
-            r, "error %s: correction %r" % (err, r), in_aspell[1], fname, languages[1]
+            r,
+            f"error {err}: correction {r!r}",
+            in_aspell[1],
+            fname,
+            languages[1],
         )
 
     # aspell dictionary is case sensitive, so pass the original case into there
@@ -180,7 +187,11 @@ def test_error_checking(err: str, rep: str, match: str) -> None:
     """Test that our error checking works."""
     with pytest.raises(AssertionError, match=match):
         _check_err_rep(
-            err, rep, (None, None), "dummy", (supported_languages, supported_languages)
+            err,
+            rep,
+            (None, None),
+            "dummy",
+            (supported_languages, supported_languages),
         )
 
 
@@ -205,7 +216,13 @@ def test_error_checking(err: str, rep: str, match: str) -> None:
         ("a", "bar back", None, False, "should not be in aspell"),
         ("a", "bar back Wednesday", None, False, "should not be in aspell"),
         # Second multi-word, both parts
-        ("a", "bar back, abcdef uvwxyz, bar,", None, True, "should be in aspell"),
+        (
+            "a",
+            "bar back, abcdef uvwxyz, bar,",
+            None,
+            True,
+            "should be in aspell",
+        ),
         (
             "a",
             "abcdef uvwxyz, bar back, ghijkl,",
@@ -263,7 +280,7 @@ def test_dictionary_looping(
         for line in fid:
             err, rep = line.split("->")
             err = err.lower()
-            assert err not in this_err_dict, "error %r already exists in %s" % (
+            assert err not in this_err_dict, "error {!r} already exists in {}".format(
                 err,
                 short_fname,
             )
@@ -286,7 +303,7 @@ def test_dictionary_looping(
         for err in this_err_dict:
             assert (
                 err not in other_err_dict
-            ), "error %r in dictionary %s already exists in dictionary %s" % (
+            ), "error {!r} in dictionary {} already exists in dictionary {}".format(
                 err,
                 short_fname,
                 other_fname,
@@ -297,7 +314,7 @@ def test_dictionary_looping(
             for err in this_err_dict:
                 assert (
                     err not in other_err_dict
-                ), "error %r in dictionary %s already exists in dictionary %s" % (
+                ), "error {!r} in dictionary {} already exists in dictionary {}".format(
                     err,
                     short_fname,
                     other_fname,
diff --git a/pyproject-codespell.precommit-toml b/pyproject-codespell.precommit-toml
new file mode 100644
index 0000000000..41a15c09dc
--- /dev/null
+++ b/pyproject-codespell.precommit-toml
@@ -0,0 +1,7 @@
+[tool.codespell]
+#builtin = ["clear","rare","informal","usage","code","names"]
+builtin = "clear,rare,informal,usage,code,names"
+#ignore-words-list = ["uint"]
+ignore-words-list = "uint"
+#skip=[ "./.*","codespell_lib/data/*","codespell_lib/tests/*"]
+skip="./.*,codespell_lib/data/*,codespell_lib/tests/*"
diff --git a/pyproject.toml b/pyproject.toml
index 32c9e55404..4dc98082c8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,6 +34,7 @@ dynamic = ["version"]
 dev = [
     "check-manifest",
     "flake8",
+    "flake8-pyproject",
     "pytest",
     "pytest-cov",
     "pytest-dependency",
@@ -49,6 +50,8 @@ toml = [
 types = [
     "mypy",
     "pytest",
+    "pytest-cov",
+    "pytest-dependency",
     "types-chardet",
 ]
 
@@ -79,9 +82,28 @@ codespell_lib = [
     "py.typed",
 ]
 
+[tool.autoflake]
+in-place = true
+recursive = true
+expand-star-imports = true
+
+[tool.bandit]
+skip = "B101,B404,B603"
+recursive = true
+
 [tool.check-manifest]
 ignore = ["codespell_lib/_version.py"]
 
+# TODO: reintegrate codespell configuration after updating test cases
+#[tool.codespell]
+#builtin = ["clear","rare","informal","usage","code","names"]
+#ignore-words-list = ["uint"]
+#skip=[ "./.*","codespell_lib/data/*","codespell_lib/tests/*"]
+
+[tool.flake8]
+max-line-length = "88"
+extend-ignore = "E203"
+
 [tool.isort]
 profile = "black"
 
@@ -90,5 +112,45 @@ pretty = true
 show_error_codes = true
 strict = true
 
+[tool.pylint]
+reports=false
+py-version="3.7"
+disable = [
+          "broad-except",
+          "consider-using-f-string",
+          "consider-using-dict-items",
+          "consider-using-with",
+          "fixme",
+          "import-error",
+          "import-outside-toplevel",
+          "invalid-name",
+          "line-too-long",
+          "missing-class-docstring",
+          "missing-module-docstring",
+          "missing-function-docstring",
+          "no-else-raise",
+          "no-else-return",
+          "raise-missing-from",
+          "redefined-outer-name",
+          "subprocess-run-check",
+          "too-many-arguments",
+          "too-many-lines",
+          "too-many-locals",
+          "too-many-branches",
+          "too-many-statements",
+          "too-many-return-statements",
+          "too-few-public-methods",
+          "unneeded-not",
+          "unspecified-encoding",
+          "unused-argument",
+          "unused-variable",
+          "use-maxsplit-arg"
+]
+
+
+[tool.pylint.FORMAT]
+good-names=["F","r","i","n"]
+# include-naming-hint=yes
+
 [tool.pytest.ini_options]
 addopts = "--cov=codespell_lib -rs --cov-report= --tb=short --junit-xml=junit-results.xml"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 8dd399ab55..0000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-[flake8]
-max-line-length = 88
-extend-ignore = E203