diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 9f69aa5..0000000
--- a/.flake8
+++ /dev/null
@@ -1,11 +0,0 @@
-[flake8]
-ignore =
-    # E203: whitespace before ':' - doesn't work well with black
-    # E402: module level import not at top of file
-    # E501: line too long - let black worry about that
-    # E731: do not assign a lambda expression, use a def
-    # W503: line break before binary operator
-    E203,E402,E501,E731,W503
-exclude=
-    .eggs
-    docs
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 53bbac9..3221625 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,18 +9,6 @@ repos:
       - id: trailing-whitespace
       - id: end-of-file-fixer
       - id: check-docstring-first
-      - id: check-yaml
-      - id: check-toml
-  - repo: https://github.com/MarcoGorelli/absolufy-imports
-    rev: v0.3.1
-    hooks:
-      - id: absolufy-imports
-        name: absolufy-imports
-        files: ^ceos_alos2/
-  - repo: https://github.com/pycqa/isort
-    rev: 5.13.2
-    hooks:
-      - id: isort
   - repo: https://github.com/psf/black
     rev: 24.10.0
     hooks:
@@ -31,16 +19,23 @@ repos:
       - id: blackdoc
         additional_dependencies: ["black==24.10.0"]
       - id: blackdoc-autoupdate-black
-  - repo: https://github.com/pycqa/flake8
-    rev: 7.1.1
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.7.1
     hooks:
-      - id: flake8
+      - id: ruff
+        args: [--fix]
   - repo: https://github.com/kynan/nbstripout
     rev: 0.7.1
     hooks:
       - id: nbstripout
         args: [--extra-keys=metadata.kernelspec metadata.language_info.version]
-  - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: v4.0.0-alpha.8
+  - repo: https://github.com/rbubley/mirrors-prettier
+    rev: v3.3.3
     hooks:
       - id: prettier
+  - repo: https://github.com/ComPWA/taplo-pre-commit
+    rev: v0.9.3
+    hooks:
+      - id: taplo-format
+      - id: taplo-lint
+        args: [--no-schema]
diff --git a/ceos_alos2/datatypes.py b/ceos_alos2/datatypes.py
index 2ff449a..aa63909 100644
--- a/ceos_alos2/datatypes.py
+++ b/ceos_alos2/datatypes.py
@@ -1,8 +1,7 @@
 import datetime
 
-from construct import Adapter
+from construct import Adapter, Struct
 from construct import PaddedString as PaddedString_
-from construct import Struct
 
 
 class AsciiInteger(Adapter):
diff --git a/ceos_alos2/summary.py b/ceos_alos2/summary.py
index 4ed3443..affd106 100644
--- a/ceos_alos2/summary.py
+++ b/ceos_alos2/summary.py
@@ -1,9 +1,8 @@
 import re
 
 from tlz.dicttoolz import dissoc, keyfilter, keymap, merge, valmap
-from tlz.functoolz import compose_left, curry
+from tlz.functoolz import compose_left, curry, pipe
 from tlz.functoolz import identity as passthrough
-from tlz.functoolz import pipe
 from tlz.itertoolz import first, get, groupby, second
 
 from ceos_alos2 import decoders
diff --git a/ceos_alos2/tests/test_caching.py b/ceos_alos2/tests/test_caching.py
index 6c18d60..b983f52 100644
--- a/ceos_alos2/tests/test_caching.py
+++ b/ceos_alos2/tests/test_caching.py
@@ -910,7 +910,7 @@ def fake_read_text(self):
             if self.name == "image2.index":
                 return data
             else:
-                raise IOError
+                raise OSError
 
         monkeypatch.setattr(Path, "is_file", fake_is_file)
         monkeypatch.setattr(Path, "read_text", fake_read_text)
diff --git a/ceos_alos2/utils.py b/ceos_alos2/utils.py
index e5daa6e..a8f5396 100644
--- a/ceos_alos2/utils.py
+++ b/ceos_alos2/utils.py
@@ -112,12 +112,12 @@ def parse_bytes(s: float | str) -> int:
     try:
         n = float(prefix)
     except ValueError as e:
-        raise ValueError("Could not interpret '%s' as a number" % prefix) from e
+        raise ValueError(f"Could not interpret '{prefix}' as a number") from e
 
     try:
         multiplier = byte_sizes[suffix.lower()]
     except KeyError as e:
-        raise ValueError("Could not interpret '%s' as a byte unit" % suffix) from e
+        raise ValueError(f"Could not interpret '{suffix}' as a byte unit") from e
 
     result = n * multiplier
     return int(result)
diff --git a/pyproject.toml b/pyproject.toml
index 227d6cf..254d630 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
 [project]
 name = "xarray-ceos-alos2"
 requires-python = ">= 3.10"
-license = {text = "MIT"}
+license = { text = "MIT" }
 description = "xarray reader for advanced land observing satellite 2 (ALOS2) CEOS files"
 readme = "README.md"
 dependencies = [
@@ -58,16 +58,44 @@ packages = ["ceos_alos2"]
 [tool.setuptools_scm]
 fallback_version = "999"
 
-[tool.isort]
-profile = "black"
-skip_gitignore = true
-float_to_top = true
-default_section = "THIRDPARTY"
-known_first_party = "ceos_alos2"
-
 [tool.black]
 line-length = 100
 
+[tool.ruff]
+target-version = "py310"
+builtins = ["ellipsis"]
+exclude = [".git", ".eggs", "build", "dist", "__pycache__"]
+line-length = 100
+
+[tool.ruff.lint]
+ignore = [
+  "E402",  # module level import not at top of file
+  "E501",  # line too long - let black worry about that
+  "E731",  # do not assign a lambda expression, use a def
+  "UP038", # type union instead of tuple for isinstance etc
+]
+select = [
+  "F",   # Pyflakes
+  "E",   # Pycodestyle
+  "I",   # isort
+  "UP",  # Pyupgrade
+  "TID", # flake8-tidy-imports
+  "W",
+]
+extend-safe-fixes = [
+  "TID252", # absolute imports
+  "UP031",  # percent string interpolation
+]
+fixable = ["I", "TID252", "UP"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["ceos_alos2"]
+known-third-party = ["xarray", "toolz", "construct"]
+
+[tool.ruff.lint.flake8-tidy-imports]
+# Disallow all relative imports.
+ban-relative-imports = "all"
+
 [tool.coverage.run]
 source = ["ceos_alos2"]
 branch = true