diff --git a/newsfragments/4075.minor b/newsfragments/4075.minor
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/allmydata/grid_manager.py b/src/allmydata/grid_manager.py
index f366391fc..662f402d8 100644
--- a/src/allmydata/grid_manager.py
+++ b/src/allmydata/grid_manager.py
@@ -11,6 +11,7 @@ from typing import (
     Optional,
     Union,
     List,
+    IO
 )
 
 from twisted.python.filepath import FilePath
@@ -178,6 +179,7 @@ def load_grid_manager(config_path: Optional[FilePath]):
     :raises: ValueError if the confguration is invalid or IOError if
         expected files can't be opened.
     """
+    config_file: Union[IO[bytes], IO[str]]
     if config_path is None:
         config_file = sys.stdin
     else:
diff --git a/src/allmydata/node.py b/src/allmydata/node.py
index 33e8fd260..fdb89e13f 100644
--- a/src/allmydata/node.py
+++ b/src/allmydata/node.py
@@ -200,14 +200,14 @@ def read_config(basedir, portnumfile, generated_files: Iterable = (), _valid_con
 
     config_path = FilePath(basedir).child("tahoe.cfg")
     try:
-        config_str = config_path.getContent()
+        config_bytes = config_path.getContent()
     except EnvironmentError as e:
         if e.errno != errno.ENOENT:
             raise
         # The file is missing, just create empty ConfigParser.
         config_str = u""
     else:
-        config_str = config_str.decode("utf-8-sig")
+        config_str = config_bytes.decode("utf-8-sig")
 
     return config_from_string(
         basedir,
diff --git a/src/allmydata/util/cputhreadpool.py b/src/allmydata/util/cputhreadpool.py
index 032a3a823..3835701fa 100644
--- a/src/allmydata/util/cputhreadpool.py
+++ b/src/allmydata/util/cputhreadpool.py
@@ -15,7 +15,7 @@ scheduler affinity or cgroups, but that's not the end of the world.
 """
 
 import os
-from typing import TypeVar, Callable
+from typing import TypeVar, Callable, cast
 from functools import partial
 import threading
 from typing_extensions import ParamSpec
@@ -24,8 +24,9 @@ from unittest import TestCase
 from twisted.python.threadpool import ThreadPool
 from twisted.internet.threads import deferToThreadPool
 from twisted.internet import reactor
+from twisted.internet.interfaces import IReactorFromThreads
 
-_CPU_THREAD_POOL = ThreadPool(minthreads=0, maxthreads=os.cpu_count(), name="TahoeCPU")
+_CPU_THREAD_POOL = ThreadPool(minthreads=0, maxthreads=os.cpu_count() or 1, name="TahoeCPU")
 if hasattr(threading, "_register_atexit"):
     # This is a private API present in Python 3.8 or later, specifically
     # designed for thread pool shutdown. Since it's private, it might go away
@@ -64,7 +65,7 @@ async def defer_to_thread(f: Callable[P, R], *args: P.args, **kwargs: P.kwargs)
         return f(*args, **kwargs)
 
     # deferToThreadPool has no type annotations...
-    result = await deferToThreadPool(reactor, _CPU_THREAD_POOL, f, *args, **kwargs)
+    result = await deferToThreadPool(cast(IReactorFromThreads, reactor), _CPU_THREAD_POOL, f, *args, **kwargs)
     return result
 
 
diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py
index 73eaeaef3..cf6eaecff 100644
--- a/src/allmydata/web/common.py
+++ b/src/allmydata/web/common.py
@@ -874,6 +874,6 @@ def add_static_children(root: IResource):
     for child in static_dir.iterdir():
         child_path = child.name.encode("utf-8")
         root.putChild(child_path, static.File(
-            temporary_file_manager.enter_context(as_file(child))
+            str(temporary_file_manager.enter_context(as_file(child)))
         ))
     weakref.finalize(root, temporary_file_manager.close)
diff --git a/tox.ini b/tox.ini
index bf69c097f..913f5523b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -101,11 +101,7 @@ skip_install = true
 deps =
      # Pin a specific version so we get consistent outcomes; update this
      # occasionally:
-     ruff == 0.0.287
-     # towncrier doesn't work with importlib_resources 6.0.0
-     # https://github.com/twisted/towncrier/issues/528
-     # Will be fixed in first version of Towncrier that is larger than 2023.6.
-     importlib_resources < 6.0.0
+     ruff == 0.1.6
      towncrier
 # On macOS, git inside of towncrier needs $HOME.
 passenv = HOME
@@ -137,8 +133,13 @@ deps =
     types-pyOpenSSL
     foolscap
     # Upgrade when new releases come out:
-    Twisted==23.8.0
-commands = mypy src
+    Twisted==23.10.0
+commands =
+    # Different versions of Python have a different standard library, and we
+    # want to be compatible with all the variations. For speed's sake we only do
+    # the earliest and latest versions.
+    mypy --python-version=3.8 src
+    mypy --python-version=3.12 src
 
 
 [testenv:draftnews]
@@ -146,7 +147,7 @@ passenv = TAHOE_LAFS_*,PIP_*,SUBUNITREPORTER_*,USERPROFILE,HOMEDRIVE,HOMEPATH,CO
 deps =
     # see comment in [testenv] about "certifi"
     certifi
-    towncrier==21.3.0
+    towncrier==23.11.0
 commands =
     python -m towncrier --draft --config towncrier.toml
 
@@ -158,7 +159,7 @@ whitelist_externals =
 deps =
     # see comment in [testenv] about "certifi"
     certifi
-    towncrier==21.3.0
+    towncrier==23.11.0
 commands =
     python -m towncrier --yes --config towncrier.toml
     # commit the changes