diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 463120797c74b06f78aa3bf8f05bc365fd218822..8db408b99d0d6f156f5131c85335e813db3bebdb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -33,13 +33,20 @@ repos:
   - id: requirements-txt-fixer
   - id: trailing-whitespace
 
-# Runs Black, pyupgrade, isort, autoflake, blacken-docs
-- repo: https://github.com/Zac-HD/shed
-  rev: 2023.3.1
+# Black, the code formatter, natively supports pre-commit
+- repo: https://github.com/psf/black
+  rev: 23.3.0
   hooks:
-  - id: shed
+  - id: black
     exclude: ^(docs)
 
+# Check linting and style issues
+- repo: https://github.com/charliermarsh/ruff-pre-commit
+  rev: "v0.0.263"
+  hooks:
+    - id: ruff
+      args: ["--fix", "--show-fixes"]
+
 # Changes tabs to spaces
 - repo: https://github.com/Lucas-C/pre-commit-hooks
   rev: v1.5.1
diff --git a/docs/conf.py b/docs/conf.py
index 160bbefb4eddb8e393ba3d1772b89b9db68f5b52..65ccb1fcb90bb08bde663ecf307d863ac324f010 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,9 +10,7 @@
 #
 # All configuration values have a default; values that are commented out
 # serve to show the default.
-
-import os
-import sys
+from __future__ import annotations
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
diff --git a/noxfile.py b/noxfile.py
index 0466a51e1564162c3c87f9bf2205b723502978a7..050d60afd54755b2627ed27c2ddeacc7a7eed368 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import nox
 
 nox.options.sessions = ["lint", "tests"]
diff --git a/pyproject.toml b/pyproject.toml
index 731b5cd19045e48d34bbada9be395deace8e03a9..fc2ef76a34dcbf263dac84d268b4977f41abb888 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,3 +44,46 @@ test-command = "pytest {project}/tests"
 test-extras = ["test"]
 test-skip = ["*universal2:arm64"]
 build-verbosity = 1
+
+
+[tool.ruff]
+select = [
+  "E", "F", "W", # flake8
+  "B",           # flake8-bugbear
+  "I",           # isort
+  "ARG",         # flake8-unused-arguments
+  "C4",          # flake8-comprehensions
+  "EM",          # flake8-errmsg
+  "ICN",         # flake8-import-conventions
+  "ISC",         # flake8-implicit-str-concat
+  "G",           # flake8-logging-format
+  "PGH",         # pygrep-hooks
+  "PIE",         # flake8-pie
+  "PL",          # pylint
+  "PT",          # flake8-pytest-style
+  "PTH",         # flake8-use-pathlib
+  "RET",         # flake8-return
+  "RUF",         # Ruff-specific
+  "SIM",         # flake8-simplify
+  "T20",         # flake8-print
+  "UP",          # pyupgrade
+  "YTT",         # flake8-2020
+  "EXE",         # flake8-executable
+  "NPY",         # NumPy specific rules
+  "PD",          # pandas-vet
+]
+extend-ignore = [
+  "PLR",    # Design related pylint codes
+  "E501",   # Line too long
+]
+target-version = "py37"
+src = ["src"]
+unfixable = [
+  "T20",  # Removes print statements
+  "F841", # Removes unused variables
+]
+flake8-unused-arguments.ignore-variadic-names = true
+isort.required-imports = ["from __future__ import annotations"]
+
+[tool.ruff.per-file-ignores]
+"tests/**" = ["T20"]
diff --git a/src/scikit_build_example/__init__.py b/src/scikit_build_example/__init__.py
index c9fb95873ebe8824a0569201adcba878a7bb8b2f..15188e5cc37117762d47f9458249a18be1ff2344 100644
--- a/src/scikit_build_example/__init__.py
+++ b/src/scikit_build_example/__init__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from ._core import __doc__, __version__, add, subtract
 
 __all__ = ["__doc__", "__version__", "add", "subtract"]
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 080f6272c6bb5ed7f25a08877b16fdf7d28711e7..93da71b5b9ac6fefd5b498397763f2f23525589b 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import scikit_build_example as m