diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 28d44d4589b4f9b1a434e06bb5b22f5e2626b32a..c4743389d07ce5a85776e458a76c0c9aa13a5e65 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,6 +23,11 @@ repos:
     - id: mypy
       args: [--no-strict-optional, --ignore-missing-imports]
       exclude: '^.*/data/second_config\.py$'
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v3.1.0
+    hooks:
+    - id: pyupgrade
+      args: [--py38-plus]
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: "v4.3.0"
     hooks:
diff --git a/src/exposed/click.py b/src/exposed/click.py
index a1613dd40b0ab6cbd805b190ac74d48c782fde14..e48375d513bf57db92fee4781893cd139f333e3f 100644
--- a/src/exposed/click.py
+++ b/src/exposed/click.py
@@ -709,11 +709,11 @@ def config_group(
             }
 
             # all modules with configuration resources
-            modules: set[str] = set(
+            modules: set[str] = {
                 # note: k.module does not exist on Python < 3.9
                 k.value.split(":")[0].rsplit(".", 1)[0]
                 for k in entry_points.values()
-            )
+            }
             keep_modules: set[str] = set()
             for k in sorted(modules):
                 if k not in keep_modules and not any(
@@ -744,7 +744,7 @@ def config_group(
                 # 79 - 4 spaces = 75 (see string above)
                 description_leftover = 75 - longest_name_length
 
-                click.echo("module: %s" % (config_type,))
+                click.echo(f"module: {config_type}")
                 for name in sorted(entry_points_by_module[config_type]):
                     ep = entry_points[name]
 
@@ -824,7 +824,7 @@ def config_group(
                     ):
                         fname = inspect.getfile(mod)
                         click.echo("Contents:")
-                        with open(fname, "r") as f:
+                        with open(fname) as f:
                             click.echo(f.read())
                     else:  # only output documentation, if module
                         doc = inspect.getdoc(mod)
@@ -862,7 +862,7 @@ def config_group(
             ep = entry_points[source]
             mod = ep.load()
             src_name = inspect.getfile(mod)
-            logger.info("cp %s -> %s" % (src_name, destination))
+            logger.info(f"cp {src_name} -> {destination}")
             shutil.copyfile(src_name, destination)
 
         return group_wrapper
diff --git a/src/exposed/config.py b/src/exposed/config.py
index 2507e614c93e8955e7b74ecb15ad8386e7b43c90..533827c5659b5eb3720c7ac6a6b7f6f89f9ba1b8 100644
--- a/src/exposed/config.py
+++ b/src/exposed/config.py
@@ -1,5 +1,3 @@
-# vim: set fileencoding=utf-8 :
-
 """Functionality to implement python-based config file parsing and loading."""
 
 from __future__ import annotations
@@ -88,7 +86,7 @@ def _get_module_filename(module_name: str) -> str | None:
 
 
 def _object_name(
-    path: typing.Union[str, pathlib.Path], common_name: str | None
+    path: str | pathlib.Path, common_name: str | None
 ) -> tuple[str, str | None]:
 
     if isinstance(path, pathlib.Path):
@@ -119,7 +117,7 @@ def _retrieve_entry_points(group: str) -> typing.Iterable[EntryPoint]:
 
 
 def _resolve_entry_point_or_modules(
-    paths: list[typing.Union[str, pathlib.Path]],
+    paths: list[str | pathlib.Path],
     entry_point_group: str | None = None,
     common_name: str | None = None,
 ) -> tuple[list[str], list[str], list[str]]:
@@ -217,11 +215,11 @@ def _resolve_entry_point_or_modules(
 
 
 def load(
-    paths: list[typing.Union[str, pathlib.Path]],
+    paths: list[str | pathlib.Path],
     context: dict[str, typing.Any] | None = None,
     entry_point_group: str | None = None,
     attribute_name: str | None = None,
-) -> typing.Union[types.ModuleType, typing.Any]:
+) -> types.ModuleType | typing.Any:
     """Loads a set of configuration files, in sequence.
 
     This method will load one or more configuration files. Every time a
diff --git a/src/exposed/rc.py b/src/exposed/rc.py
index 8fd2e7e785fcf6edd72e2a8e35ba84d7bd2852d2..deab6ec70b0283742f6becd07e906acb694b5ab1 100644
--- a/src/exposed/rc.py
+++ b/src/exposed/rc.py
@@ -1,5 +1,3 @@
-# vim: set fileencoding=utf-8 :
-
 """Implements a global configuration system setup and readout."""
 
 from __future__ import annotations
@@ -53,7 +51,7 @@ class UserDefaults(collections.abc.MutableMapping):
 
     def __init__(
         self,
-        path: typing.Union[str, pathlib.Path],
+        path: str | pathlib.Path,
         envname: str | None = None,
         logger: logging.Logger | None = None,
     ) -> None:
diff --git a/tests/test_click.py b/tests/test_click.py
index 260e7ad48e895c54f81a03807bd7badb60de9f4f..31c1aae190b742e615b35f9a5c16fd2f6ccd5d25 100644
--- a/tests/test_click.py
+++ b/tests/test_click.py
@@ -59,7 +59,7 @@ def test_commands_with_config_2():
     @click.option("-a", type=click.INT, cls=ResourceOption)
     def cli(a, **_):
         assert type(a) == int, (type(a), a)
-        click.echo("{}".format(a))
+        click.echo(f"{a}")
 
     runner = CliRunner()
 
@@ -85,7 +85,7 @@ def test_commands_with_config_3():
     @click.command(cls=ConfigCommand, entry_point_group="exposed.test.config")
     @click.option("-a", cls=ResourceOption, required=True)
     def cli(a, **_):
-        click.echo("{}".format(a))
+        click.echo(f"{a}")
 
     runner = CliRunner()
 
@@ -110,7 +110,7 @@ def test_commands_with_config_3():
 
 
 def _assert_config_dump(output, ref, ref_date):
-    with output.open("rt") as f, open(ref, "rt") as f2:
+    with output.open("rt") as f, open(ref) as f2:
         diff = difflib.ndiff(f.readlines(), f2.readlines())
         important_diffs = [k for k in diff if k.startswith(("+", "-"))]
 
diff --git a/tests/test_rc.py b/tests/test_rc.py
index 00297467f2330f3a683520d66ac727b04632472d..d25ea27cf691d094482afa5a98c173f9fdd3bfb1 100644
--- a/tests/test_rc.py
+++ b/tests/test_rc.py
@@ -175,7 +175,7 @@ def test_rc_str(tmp_path):
     rc["section1.an_int"] = 15
     rc.write()
 
-    assert open(tmp_path / "new-rc", "rt").read() == str(rc)
+    assert open(tmp_path / "new-rc").read() == str(rc)
 
 
 def test_rc_json_legacy(datadir, tmp_path):