diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 1cc049a69..000000000
--- a/.flake8
+++ /dev/null
@@ -1,26 +0,0 @@
-[flake8]
-
-show-source = True
-count = True
-statistics = True
-
-# E266 = too many leading '#' for block comment
-# E731 = do not assign a lambda expression, use a def
-# TC002 = move third party import to TYPE_CHECKING
-# TC, TC2 = flake8-type-checking
-
-# select = C,E,F,W ANN, TC, TC2  # to enable code. Disabled if not listed, including builtin codes
-enable-extensions = TC, TC2  # only needed for extensions not enabled by default
-
-ignore = E266, E731
-
-exclude = .tox, .venv, build, dist, doc, git/ext/
-
-rst-roles =  # for flake8-RST-docstrings
-    attr, class, func, meth, mod, obj, ref, term, var  # used by sphinx
-
-min-python-version = 3.7.0
-
-# for `black` compatibility
-max-line-length = 120
-extend-ignore = E203, W503
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1ac5baa00..cd5f58441 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,14 +14,13 @@ repos:
     name: black (format)
     exclude: ^git/ext/
 
-- repo: https://github.com/PyCQA/flake8
-  rev: 6.1.0
+- repo: https://github.com/astral-sh/ruff-pre-commit
+  rev: v0.3.0
   hooks:
-  - id: flake8
-    additional_dependencies:
-    - flake8-bugbear==23.9.16
-    - flake8-comprehensions==3.14.0
-    - flake8-typing-imports==1.14.0
+  #- id: ruff-format  # todo: eventually replace Black with Ruff for consistency
+  #  args: ["--preview"]
+  - id: ruff
+    args: ["--fix"]
     exclude: ^doc|^git/ext/
 
 - repo: https://github.com/shellcheck-py/shellcheck-py
diff --git a/README.md b/README.md
index 7faeae23b..1e4a59d7f 100644
--- a/README.md
+++ b/README.md
@@ -187,7 +187,7 @@ The same linting, and running tests on all the different supported Python versio
 Specific tools:
 
 - Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`.
-- Configuration for `flake8` is in the `./.flake8` file.
+- Configuration for `ruff` is in the `pyproject.toml` file.
 
 Orchestration tools:
 
diff --git a/git/index/base.py b/git/index/base.py
index 249144d54..985b1bccf 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -248,7 +248,7 @@ def write(
         # Make sure we have our entries read before getting a write lock.
         # Otherwise it would be done when streaming.
         # This can happen if one doesn't change the index, but writes it right away.
-        self.entries
+        self.entries  # noqa: B018
         lfd = LockedFD(file_path or self._file_path)
         stream = lfd.open(write=True, stream=True)
 
@@ -397,7 +397,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             with TemporaryFileSwap(join_path_native(repo.git_dir, "index")):
                 repo.git.read_tree(*arg_list, **kwargs)
                 index = cls(repo, tmp_index)
-                index.entries  # Force it to read the file as we will delete the temp-file.
+                index.entries  # noqa: B018 # Force it to read the file as we will delete the temp-file.
                 return index
             # END index merge handling
 
@@ -1339,7 +1339,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
             # Make sure we have our entries loaded before we start checkout_index, which
             # will hold a lock on it. We try to get the lock as well during our entries
             # initialization.
-            self.entries
+            self.entries  # noqa: B018
 
             args.append("--stdin")
             kwargs["as_process"] = True
@@ -1379,7 +1379,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
                 self._flush_stdin_and_wait(proc, ignore_stdout=True)
             except GitCommandError:
                 # Without parsing stdout we don't know what failed.
-                raise CheckoutError(
+                raise CheckoutError(  # noqa: B904
                     "Some files could not be checked out from the index, probably because they didn't exist.",
                     failed_files,
                     [],
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 253ceccb5..4035c3e7c 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -6,7 +6,11 @@
 from mimetypes import guess_type
 from . import base
 
-from git.types import Literal
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 __all__ = ("Blob",)
 
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 06ab0898b..dcb3be695 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -44,7 +44,12 @@
     Dict,
 )
 
-from git.types import PathLike, Literal
+from git.types import PathLike
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index cdd7c8e1b..e5933b116 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -44,7 +44,12 @@
 from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
 from typing import Any, Iterator, Union
 
-from git.types import Commit_ish, Literal, PathLike, TBD
+from git.types import Commit_ish, PathLike, TBD
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.index import IndexFile
@@ -1445,7 +1450,7 @@ def exists(self) -> bool:
 
         try:
             try:
-                self.path
+                self.path  # noqa: B018
                 return True
             except Exception:
                 return False
diff --git a/git/objects/tag.py b/git/objects/tag.py
index f455c55fc..d8815e436 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -16,7 +16,10 @@
 
 from typing import List, TYPE_CHECKING, Union
 
-from git.types import Literal
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/objects/tree.py b/git/objects/tree.py
index a506bba7d..731ab5fa1 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -31,7 +31,12 @@
     TYPE_CHECKING,
 )
 
-from git.types import PathLike, Literal
+from git.types import PathLike
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/objects/util.py b/git/objects/util.py
index 6f4e7d087..71eb9c230 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -439,7 +439,7 @@ def _list_traverse(
 
         if not as_edge:
             out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id)
-            out.extend(self.traverse(as_edge=as_edge, *args, **kwargs))
+            out.extend(self.traverse(as_edge=as_edge, *args, **kwargs))  # noqa: B026
             return out
             # Overloads in subclasses (mypy doesn't allow typing self: subclass).
             # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]]
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 16aada0a7..465acf872 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -496,7 +496,7 @@ def is_valid(self) -> bool:
             valid object or reference.
         """
         try:
-            self.object
+            self.object  # noqa: B018
         except (OSError, ValueError):
             return False
         else:
@@ -510,7 +510,7 @@ def is_detached(self) -> bool:
             instead to another reference.
         """
         try:
-            self.ref
+            self.ref  # noqa: B018
             return False
         except TypeError:
             return True
diff --git a/pyproject.toml b/pyproject.toml
index 7109389d7..af0e52ca4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,6 @@ warn_unreachable = true
 show_error_codes = true
 implicit_reexport = true
 # strict = true
-
 # TODO: Remove when 'gitdb' is fully annotated.
 exclude = ["^git/ext/gitdb"]
 [[tool.mypy.overrides]]
@@ -47,3 +46,38 @@ omit = ["*/git/ext/*"]
 line-length = 120
 target-version = ["py37"]
 extend-exclude = "git/ext/gitdb"
+
+[tool.ruff]
+target-version = "py37"
+line-length = 120
+# Exclude a variety of commonly ignored directories.
+exclude = [
+    "git/ext/",
+    "doc",
+    "build",
+    "dist",
+]
+# Enable Pyflakes `E` and `F` codes by default.
+lint.select = [
+    "E",
+    "W", # see: https://pypi.org/project/pycodestyle
+    "F", # see: https://pypi.org/project/pyflakes
+#    "I", #see: https://pypi.org/project/isort/
+#    "S", # see: https://pypi.org/project/flake8-bandit
+#    "UP", # see: https://docs.astral.sh/ruff/rules/#pyupgrade-up
+]
+lint.extend-select = [
+    #"A",    # see: https://pypi.org/project/flake8-builtins
+    "B",    # see: https://pypi.org/project/flake8-bugbear
+    "C4",   # see: https://pypi.org/project/flake8-comprehensions
+    "TCH004", # see: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
+]
+lint.ignore = [
+    "E203",
+    "E731", # Do not assign a `lambda` expression, use a `def`
+]
+lint.ignore-init-module-imports = true
+lint.unfixable = ["F401"]
+
+[tool.ruff.lint.per-file-ignores]
+"test/**" = ["B018"]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index e3030c597..69a79d13d 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -3,7 +3,5 @@
 
 # libraries for additional local testing/linting - to be added to test-requirements.txt when all pass
 
-flake8-type-checking;python_version>="3.8"      # checks for TYPE_CHECKING only imports
-
 pytest-icdiff
 # pytest-profiling