Run pyupgrade on tools/ (#29301)

Command used, with pyupgrade 2.31.1 installed:

> find tools -name '*.py' | grep -v third_party | xargs pyupgrade --keep-percent-format --py36-plus

On top of that, a few .encode("utf-8") were put back and flake8 errors fixed.
diff --git a/tools/ci/jobs.py b/tools/ci/jobs.py
index fb1a1b9..a4ffe57 100644
--- a/tools/ci/jobs.py
+++ b/tools/ci/jobs.py
@@ -53,7 +53,7 @@
     return path
 
 
-class Ruleset(object):
+class Ruleset:
     def __init__(self, rules):
         self.include = []
         self.exclude = []
diff --git a/tools/ci/manifest_build.py b/tools/ci/manifest_build.py
index 72e73f9..85fb74b 100644
--- a/tools/ci/manifest_build.py
+++ b/tools/ci/manifest_build.py
@@ -19,7 +19,7 @@
 logger = logging.getLogger(__name__)
 
 
-class Status(object):
+class Status:
     SUCCESS = 0
     FAIL = 1
 
diff --git a/tools/ci/regen_certs.py b/tools/ci/regen_certs.py
index a449bd8..f4973ca 100644
--- a/tools/ci/regen_certs.py
+++ b/tools/ci/regen_certs.py
@@ -76,7 +76,7 @@
     """Calculate the SPKI fingerprint for a given x509 certificate."""
     # We use shell=True as we control the input |cert_path|, and piping
     # correctly across processes is non-trivial in Python.
-    cmd = ("openssl x509 -noout -pubkey -in {cert_path} | ".format(cert_path=cert_path) +
+    cmd = (f"openssl x509 -noout -pubkey -in {cert_path} | " +
            "openssl pkey -pubin -outform der | " +
            "openssl dgst -sha256 -binary")
     dgst_output = subprocess.check_output(cmd, shell=True)
diff --git a/tools/ci/tc/decision.py b/tools/ci/tc/decision.py
index f5c1844..4cbff88 100644
--- a/tools/ci/tc/decision.py
+++ b/tools/ci/tc/decision.py
@@ -323,7 +323,7 @@
     if sink_task:
         logger.info("Scheduling sink-task")
         depends_on_ids = [x[0] for x in task_id_map.values()]
-        sink_task["command"] += " {0}".format(" ".join(depends_on_ids))
+        sink_task["command"] += " {}".format(" ".join(depends_on_ids))
         task_id_map["sink-task"] = create_tc_task(
             event, sink_task, taskgroup_id, depends_on_ids)
     else:
@@ -342,7 +342,7 @@
         try:
             with open(event_path) as f:
                 event_str = f.read()
-        except IOError:
+        except OSError:
             logger.error("Missing event file at path %s" % event_path)
             raise
     elif "TASK_EVENT" in os.environ:
diff --git a/tools/ci/tc/github_checks_output.py b/tools/ci/tc/github_checks_output.py
index 0df01ea..e982ca3 100644
--- a/tools/ci/tc/github_checks_output.py
+++ b/tools/ci/tc/github_checks_output.py
@@ -4,7 +4,7 @@
     from typing import Optional, Text
 
 
-class GitHubChecksOutputter(object):
+class GitHubChecksOutputter:
     """Provides a method to output data to be shown in the GitHub Checks UI.
 
     This can be useful to provide a summary of a given check (e.g. the lint)
diff --git a/tools/ci/tc/sink_task.py b/tools/ci/tc/sink_task.py
index 5e5875d..f89e79e 100644
--- a/tools/ci/tc/sink_task.py
+++ b/tools/ci/tc/sink_task.py
@@ -22,10 +22,10 @@
         status = queue.status(task)
         state = status['status']['state']
         if state == 'failed' or state == 'exception':
-            logger.error('Task {0} failed with state "{1}"'.format(task, state))
+            logger.error(f'Task {task} failed with state "{state}"')
             failed_tasks.append(status)
         elif state != 'completed':
-            logger.error('Task {0} had unexpected state "{1}"'.format(task, state))
+            logger.error(f'Task {task} had unexpected state "{state}"')
             failed_tasks.append(status)
 
     if failed_tasks and github_checks_outputter:
diff --git a/tools/ci/tc/taskgraph.py b/tools/ci/tc/taskgraph.py
index d5c889d..8510ab3 100644
--- a/tools/ci/tc/taskgraph.py
+++ b/tools/ci/tc/taskgraph.py
@@ -115,7 +115,7 @@
         return [task]
 
     map_data = task["$map"]
-    if set(map_data.keys()) != set(["for", "do"]):
+    if set(map_data.keys()) != {"for", "do"}:
         raise ValueError("$map objects must have exactly two properties named 'for' "
                          "and 'do' (got %s)" % ("no properties" if not map_data.keys()
                                                 else ", ". join(map_data.keys())))
diff --git a/tools/gitignore/gitignore.py b/tools/gitignore/gitignore.py
index e9f3e53..2e41a9f 100644
--- a/tools/gitignore/gitignore.py
+++ b/tools/gitignore/gitignore.py
@@ -138,7 +138,7 @@
     return invert, dir_only, literal, pattern
 
 
-class PathFilter(object):
+class PathFilter:
     def __init__(self, root, extras=None, cache=None):
         # type: (bytes, Optional[List[bytes]], Optional[MutableMapping[bytes, bool]]) -> None
         if root:
diff --git a/tools/lint/lint.py b/tools/lint/lint.py
index 5474cac..8b6acc3 100644
--- a/tools/lint/lint.py
+++ b/tools/lint/lint.py
@@ -45,7 +45,7 @@
     # file patterns (e.g. 'foo/*') to a set of specific line numbers for the
     # exception. The line numbers are optional; if missing the entire file
     # ignores the error.
-    Ignorelist = Dict[Text, Dict[Text, Set[Optional[int]]]]
+    Ignorelist = Dict[str, Dict[str, Set[Optional[int]]]]
 
     # Define an arbitrary typevar
     T = TypeVar("T")
@@ -74,7 +74,7 @@
     if prefix:
         format = logging.BASIC_FORMAT
     else:
-        format = str("%(message)s")
+        format = "%(message)s"
     formatter = logging.Formatter(format)
     for handler in logger.handlers:
         handler.setFormatter(formatter)
@@ -277,23 +277,23 @@
 
     for path in paths:
         if os.name == "nt":
-            path = path.replace(u"\\", u"/")
+            path = path.replace("\\", "/")
 
-        if not path.startswith(u"css/"):
+        if not path.startswith("css/"):
             continue
 
-        source_file = SourceFile(repo_root, path, u"/")
+        source_file = SourceFile(repo_root, path, "/")
         if source_file.name_is_non_test:
             # If we're name_is_non_test for a reason apart from support, ignore it.
             # We care about support because of the requirement all support files in css/ to be in
             # a support directory; see the start of check_parsed.
-            offset = path.find(u"/support/")
+            offset = path.find("/support/")
             if offset == -1:
                 continue
 
             parts = source_file.dir_path.split(os.path.sep)
             if (parts[0] in source_file.root_dir_non_test or
-                any(item in source_file.dir_non_test - {u"support"} for item in parts) or
+                any(item in source_file.dir_non_test - {"support"} for item in parts) or
                 any(parts[:len(non_test_path)] == list(non_test_path) for non_test_path in source_file.dir_path_non_test)):
                 continue
 
@@ -303,7 +303,7 @@
             ref_files[source_file.name].add(path)
         else:
             test_name = source_file.name  # type: Text
-            test_name = test_name.replace(u'-manual', u'')
+            test_name = test_name.replace('-manual', '')
             test_files[test_name].add(path)
 
     errors = []
@@ -314,7 +314,7 @@
                 # Only compute by_spec if there are prima-facie collisions because of cost
                 by_spec = defaultdict(set)  # type: Dict[Text, Set[Text]]
                 for path in colliding:
-                    source_file = SourceFile(repo_root, path, u"/")
+                    source_file = SourceFile(repo_root, path, "/")
                     for link in source_file.spec_links:
                         for r in (drafts_csswg_re, w3c_tr_re, w3c_dev_re):
                             m = r.match(link)
@@ -1013,7 +1013,7 @@
     if jobs == 0:
         jobs = multiprocessing.cpu_count()
 
-    with open(os.path.join(repo_root, "lint.ignore"), "r") as f:
+    with open(os.path.join(repo_root, "lint.ignore")) as f:
         ignorelist, skipped_files = parse_ignorelist(f)
 
     if ignore_glob:
diff --git a/tools/lint/rules.py b/tools/lint/rules.py
index d2aa20f..7b9a54b 100644
--- a/tools/lint/rules.py
+++ b/tools/lint/rules.py
@@ -7,7 +7,7 @@
 if MYPY:
     # MYPY is set to True when run under Mypy.
     from typing import Any, List, Match, Optional, Pattern, Text, Tuple, cast
-    Error = Tuple[Text, Text, Text, Optional[int]]
+    Error = Tuple[str, str, str, Optional[int]]
 
 
 def collapse(text):
@@ -32,8 +32,8 @@
     def error(cls, path, context=(), line_no=None):
         # type: (Text, Tuple[Any, ...], Optional[int]) -> Error
         if MYPY:
-            name = cast(Text, cls.name)
-            description = cast(Text, cls.description)
+            name = cast(str, cls.name)
+            description = cast(str, cls.description)
         else:
             name = cls.name
             description = cls.description
diff --git a/tools/lint/tests/test_file_lints.py b/tools/lint/tests/test_file_lints.py
index 355635b..8adc31a 100644
--- a/tools/lint/tests/test_file_lints.py
+++ b/tools/lint/tests/test_file_lints.py
@@ -540,16 +540,16 @@
         check_errors(errors)
 
         if kind == "python":
-            expected = set([("PARSE-FAILED", "Unable to parse file", filename, 1)])
+            expected = {("PARSE-FAILED", "Unable to parse file", filename, 1)}
         elif kind in ["web-lax", "web-strict"]:
-            expected = set([
+            expected = {
                 ("MISSING-TESTDRIVER-VENDOR", "Missing `<script src='/resources/testdriver-vendor.js'>`", filename, None),
                 ("TESTDRIVER-VENDOR-PATH", "testdriver-vendor.js script seen with incorrect path", filename, None),
                 ("TESTDRIVER-VENDOR-PATH", "testdriver-vendor.js script seen with incorrect path", filename, None),
                 ("TESTDRIVER-VENDOR-PATH", "testdriver-vendor.js script seen with incorrect path", filename, None),
                 ("TESTDRIVER-VENDOR-PATH", "testdriver-vendor.js script seen with incorrect path", filename, None),
                 ("TESTDRIVER-VENDOR-PATH", "testdriver-vendor.js script seen with incorrect path", filename, None)
-            ])
+            }
         else:
             expected = set()
 
diff --git a/tools/manifest/XMLParser.py b/tools/manifest/XMLParser.py
index d1c0289..6895334 100644
--- a/tools/manifest/XMLParser.py
+++ b/tools/manifest/XMLParser.py
@@ -39,7 +39,7 @@
 _undefined_entity_code = expat.errors.codes[expat.errors.XML_ERROR_UNDEFINED_ENTITY]  # type: int
 
 
-class XMLParser(object):
+class XMLParser:
     """
     An XML parser with support for XHTML DTDs and all Python-supported encodings
 
diff --git a/tools/manifest/download.py b/tools/manifest/download.py
index c45cc78..4a8b6fc 100644
--- a/tools/manifest/download.py
+++ b/tools/manifest/download.py
@@ -143,13 +143,13 @@
             try:
                 dctx = zstandard.ZstdDecompressor()
                 decompressed = dctx.decompress(resp.read())
-            except IOError:
+            except OSError:
                 logger.warning("Failed to decompress downloaded file")
                 continue
         elif url.endswith(".bz2"):
             try:
                 decompressed = bz2.decompress(resp.read())
-            except IOError:
+            except OSError:
                 logger.warning("Failed to decompress downloaded file")
                 continue
         elif url.endswith(".gz"):
@@ -158,7 +158,7 @@
                 with gzip.GzipFile(fileobj=fileobj) as gzf:
                     data = gzf.read()
                     decompressed = data
-            except IOError:
+            except OSError:
                 logger.warning("Failed to decompress downloaded file")
                 continue
         else:
diff --git a/tools/manifest/item.py b/tools/manifest/item.py
index 58547e6..7c3491a 100644
--- a/tools/manifest/item.py
+++ b/tools/manifest/item.py
@@ -10,8 +10,8 @@
     # MYPY is set to True when run under Mypy.
     from typing import Any, Dict, Hashable, List, Optional, Sequence, Text, Tuple, Type, Union, cast
     from .manifest import Manifest
-    Fuzzy = Dict[Optional[Tuple[Text, Text, Text]], List[int]]
-    PageRanges = Dict[Text, List[int]]
+    Fuzzy = Dict[Optional[Tuple[str, str, str]], List[int]]
+    PageRanges = Dict[str, List[int]]
 
 item_types = {}  # type: Dict[str, Type[ManifestItem]]
 
@@ -23,7 +23,7 @@
 
     def __new__(cls, name, bases, attrs):
         # type: (Type[ManifestItemMeta], str, Tuple[type], Dict[str, Any]) -> ManifestItemMeta
-        inst = super(ManifestItemMeta, cls).__new__(cls, name, bases, attrs)
+        inst = super().__new__(cls, name, bases, attrs)
         if isabstract(inst):
             return inst
 
@@ -113,7 +113,7 @@
                  **extras  # type: Any
                  ):
         # type: (...) -> None
-        super(URLManifestItem, self).__init__(tests_root, path)
+        super().__init__(tests_root, path)
         assert url_base[0] == "/"
         self.url_base = url_base
         assert url is None or url[0] != "/"
@@ -131,7 +131,7 @@
     @property
     def url(self):
         # type: () -> Text
-        rel_url = self._url or self.path.replace(os.path.sep, u"/")
+        rel_url = self._url or self.path.replace(os.path.sep, "/")
         # we can outperform urljoin, because we know we just have path relative URLs
         if self.url_base == "/":
             return "/" + rel_url
@@ -156,7 +156,7 @@
 
     def to_json(self):
         # type: () -> Tuple[Optional[Text], Dict[Any, Any]]
-        rel_url = None if self._url == self.path.replace(os.path.sep, u"/") else self._url
+        rel_url = None if self._url == self.path.replace(os.path.sep, "/") else self._url
         rv = (rel_url, {})  # type: Tuple[Optional[Text], Dict[Any, Any]]
         return rv
 
@@ -205,7 +205,7 @@
 
     def to_json(self):
         # type: () -> Tuple[Optional[Text], Dict[Text, Any]]
-        rv = super(TestharnessTest, self).to_json()
+        rv = super().to_json()
         if self.timeout is not None:
             rv[-1]["timeout"] = self.timeout
         if self.testdriver:
@@ -230,7 +230,7 @@
                  references=None,  # type: Optional[List[Tuple[Text, Text]]]
                  **extras  # type: Any
                  ):
-        super(RefTest, self).__init__(tests_root, path, url_base, url, **extras)
+        super().__init__(tests_root, path, url_base, url, **extras)
         if references is None:
             self.references = []  # type: List[Tuple[Text, Text]]
         else:
@@ -314,7 +314,7 @@
         return self._extras.get("page_ranges", {})
 
     def to_json(self):  # type: ignore
-        rv = super(PrintRefTest, self).to_json()
+        rv = super().to_json()
         if self.page_ranges:
             rv[-1]["page_ranges"] = self.page_ranges
         return rv
@@ -361,7 +361,7 @@
 
     def to_json(self):
         # type: () -> Tuple[Optional[Text], Dict[Text, Any]]
-        rv = super(WebDriverSpecTest, self).to_json()
+        rv = super().to_json()
         if self.timeout is not None:
             rv[-1]["timeout"] = self.timeout
         return rv
diff --git a/tools/manifest/manifest.py b/tools/manifest/manifest.py
index 4936b9c..4b7792e 100644
--- a/tools/manifest/manifest.py
+++ b/tools/manifest/manifest.py
@@ -53,15 +53,15 @@
     pass
 
 
-item_classes = {u"testharness": TestharnessTest,
-                u"reftest": RefTest,
-                u"print-reftest": PrintRefTest,
-                u"crashtest": CrashTest,
-                u"manual": ManualTest,
-                u"wdspec": WebDriverSpecTest,
-                u"conformancechecker": ConformanceCheckerTest,
-                u"visual": VisualTest,
-                u"support": SupportFile}  # type: Dict[Text, Type[ManifestItem]]
+item_classes = {"testharness": TestharnessTest,
+                "reftest": RefTest,
+                "print-reftest": PrintRefTest,
+                "crashtest": CrashTest,
+                "manual": ManualTest,
+                "wdspec": WebDriverSpecTest,
+                "conformancechecker": ConformanceCheckerTest,
+                "visual": VisualTest,
+                "support": SupportFile}  # type: Dict[Text, Type[ManifestItem]]
 
 
 def compute_manifest_items(source_file):
@@ -114,7 +114,7 @@
         return rv
 
 
-class Manifest(object):
+class Manifest:
     def __init__(self, tests_root, url_base="/"):
         # type: (Text, Text) -> None
         assert url_base is not None
@@ -141,8 +141,7 @@
         for type_tests in self._data.values():
             i = type_tests.get(tpath, set())
             assert i is not None
-            for test in i:
-                yield test
+            yield from i
 
     def iterdir(self, dir_name):
         # type: (Text) -> Iterable[ManifestItem]
@@ -152,8 +151,7 @@
         for type_tests in self._data.values():
             for path, tests in type_tests.items():
                 if path[:tpath_len] == tpath:
-                    for test in tests:
-                        yield test
+                    yield from tests
 
     def update(self, tree, parallel=True):
         # type: (Iterable[Tuple[Text, Optional[Text], bool]], bool) -> bool
@@ -358,12 +356,12 @@
         else:
             logger.debug("Creating new manifest at %s" % manifest)
         try:
-            with open(manifest, "r", encoding="utf-8") as f:
+            with open(manifest, encoding="utf-8") as f:
                 rv = Manifest.from_json(tests_root,
                                         jsonlib.load(f),
                                         types=types,
                                         callee_owns_obj=True)
-        except IOError:
+        except OSError:
             return None
         except ValueError:
             logger.warning("%r may be corrupted", manifest)
diff --git a/tools/manifest/sourcefile.py b/tools/manifest/sourcefile.py
index d5f97cb..bf8e9ad 100644
--- a/tools/manifest/sourcefile.py
+++ b/tools/manifest/sourcefile.py
@@ -50,7 +50,7 @@
 
 reference_file_re = re.compile(r'(^|[\-_])(not)?ref[0-9]*([\-_]|$)')
 
-space_chars = u"".join(html5lib.constants.spaceCharacters)  # type: Text
+space_chars = "".join(html5lib.constants.spaceCharacters)  # type: Text
 
 
 def replace_end(s, old, new):
@@ -189,20 +189,20 @@
         return ElementTree.parse(f, XMLParser.XMLParser()).getroot()  # type: ignore
 
 
-class SourceFile(object):
-    parsers = {u"html":_parse_html,
-               u"xhtml":_parse_xml,
-               u"svg":_parse_xml}  # type: Dict[Text, Callable[[BinaryIO], ElementTree.Element]]
+class SourceFile:
+    parsers = {"html":_parse_html,
+               "xhtml":_parse_xml,
+               "svg":_parse_xml}  # type: Dict[Text, Callable[[BinaryIO], ElementTree.Element]]
 
-    root_dir_non_test = {u"common"}
+    root_dir_non_test = {"common"}
 
-    dir_non_test = {u"resources",
-                    u"support",
-                    u"tools"}
+    dir_non_test = {"resources",
+                    "support",
+                    "tools"}
 
-    dir_path_non_test = {(u"css21", u"archive"),
-                         (u"css", u"CSS2", u"archive"),
-                         (u"css", u"common")}  # type: Set[Tuple[Text, ...]]
+    dir_path_non_test = {("css21", "archive"),
+                         ("css", "CSS2", "archive"),
+                         ("css", "common")}  # type: Set[Tuple[Text, ...]]
 
     def __init__(self, tests_root, rel_path, url_base, hash=None, contents=None):
         # type: (Text, Text, Text, Optional[Text], Optional[bytes]) -> None
@@ -217,7 +217,7 @@
         assert not os.path.isabs(rel_path), rel_path
         if os.name == "nt":
             # do slash normalization on Windows
-            rel_path = rel_path.replace(u"/", u"\\")
+            rel_path = rel_path.replace("/", "\\")
 
         dir_path, filename = os.path.split(rel_path)
         name, ext = os.path.splitext(filename)
@@ -336,11 +336,11 @@
         """Check if the file name matches the conditions for the file to
         be a non-test file"""
         return (self.is_dir() or
-                self.name_prefix(u"MANIFEST") or
-                self.filename == u"META.yml" or
-                self.filename.startswith(u".") or
-                self.filename.endswith(u".headers") or
-                self.filename.endswith(u".ini") or
+                self.name_prefix("MANIFEST") or
+                self.filename == "META.yml" or
+                self.filename.startswith(".") or
+                self.filename.endswith(".headers") or
+                self.filename.endswith(".ini") or
                 self.in_non_test_dir())
 
     @property
@@ -440,14 +440,14 @@
 
         if not ext:
             return None
-        if ext[0] == u".":
+        if ext[0] == ".":
             ext = ext[1:]
-        if ext in [u"html", u"htm"]:
-            return u"html"
-        if ext in [u"xhtml", u"xht", u"xml"]:
-            return u"xhtml"
-        if ext == u"svg":
-            return u"svg"
+        if ext in ["html", "htm"]:
+            return "html"
+        if ext in ["xhtml", "xht", "xml"]:
+            return "xhtml"
+        if ext == "svg":
+            return "svg"
         return None
 
     @cached_property
@@ -550,9 +550,9 @@
 
     def parse_ref_keyed_meta(self, node):
         # type: (ElementTree.Element) -> Tuple[Optional[Tuple[Text, Text, Text]], Text]
-        item = node.attrib.get(u"content", u"")  # type: Text
+        item = node.attrib.get("content", "")  # type: Text
 
-        parts = item.rsplit(u":", 1)
+        parts = item.rsplit(":", 1)
         if len(parts) == 1:
             key = None  # type: Optional[Tuple[Text, Text, Text]]
             value = parts[0]
@@ -563,7 +563,7 @@
                 if ref[0] == key_part:
                     reftype = ref[1]
                     break
-            if reftype not in (u"==", u"!="):
+            if reftype not in ("==", "!="):
                 raise ValueError("Key %s doesn't correspond to a reference" % key_part)
             key = (self.url, key_part, reftype)
             value = parts[1]
@@ -590,26 +590,26 @@
         if not self.fuzzy_nodes:
             return rv
 
-        args = [u"maxDifference", u"totalPixels"]
+        args = ["maxDifference", "totalPixels"]
 
         for node in self.fuzzy_nodes:
             key, value = self.parse_ref_keyed_meta(node)
-            ranges = value.split(u";")
+            ranges = value.split(";")
             if len(ranges) != 2:
                 raise ValueError("Malformed fuzzy value %s" % value)
             arg_values = {}  # type: Dict[Text, List[int]]
             positional_args = deque()  # type: Deque[List[int]]
             for range_str_value in ranges:  # type: Text
                 name = None  # type: Optional[Text]
-                if u"=" in range_str_value:
-                    name, range_str_value = [part.strip()
-                                             for part in range_str_value.split(u"=", 1)]
+                if "=" in range_str_value:
+                    name, range_str_value = (part.strip()
+                                             for part in range_str_value.split("=", 1))
                     if name not in args:
                         raise ValueError("%s is not a valid fuzzy property" % name)
                     if arg_values.get(name):
                         raise ValueError("Got multiple values for argument %s" % name)
-                if u"-" in range_str_value:
-                    range_min, range_max = range_str_value.split(u"-")
+                if "-" in range_str_value:
+                    range_min, range_max = range_str_value.split("-")
                 else:
                     range_min = range_str_value
                     range_max = range_str_value
@@ -992,7 +992,7 @@
                 )]
 
         elif self.name_is_multi_global:
-            globals = u""
+            globals = ""
             script_metadata = self.script_metadata
             assert script_metadata is not None
             for (key, value) in script_metadata:
diff --git a/tools/manifest/tests/test_XMLParser.py b/tools/manifest/tests/test_XMLParser.py
index fc130b7..85c81cf 100644
--- a/tools/manifest/tests/test_XMLParser.py
+++ b/tools/manifest/tests/test_XMLParser.py
@@ -26,15 +26,15 @@
     p = XMLParser()
     p.feed(s)
     d = p.close()
-    assert d.tag == u"foo"
-    assert d.text == u"\u00A0"
+    assert d.tag == "foo"
+    assert d.text == "\u00A0"
 
 
 def test_pi():
     p = XMLParser()
     p.feed('<foo><?foo bar?></foo>')
     d = p.close()
-    assert d.tag == u"foo"
+    assert d.tag == "foo"
     assert len(d) == 0
 
 
@@ -42,13 +42,13 @@
     p = XMLParser()
     p.feed('<foo><!-- data --></foo>')
     d = p.close()
-    assert d.tag == u"foo"
+    assert d.tag == "foo"
     assert len(d) == 0
 
 
 def test_unsupported_encoding():
     p = XMLParser()
-    p.feed(u"<?xml version='1.0' encoding='Shift-JIS'?><foo>\u3044</foo>".encode("shift-jis"))
+    p.feed("<?xml version='1.0' encoding='Shift-JIS'?><foo>\u3044</foo>".encode("shift-jis"))
     d = p.close()
-    assert d.tag == u"foo"
-    assert d.text == u"\u3044"
+    assert d.tag == "foo"
+    assert d.text == "\u3044"
diff --git a/tools/manifest/tests/test_item.py b/tools/manifest/tests/test_item.py
index dd1c4e2..ac5f558 100644
--- a/tools/manifest/tests/test_item.py
+++ b/tools/manifest/tests/test_item.py
@@ -79,8 +79,8 @@
 
 
 @pytest.mark.parametrize("fuzzy", [
-    {('/foo/test.html', u'/foo/ref.html', '=='): [[1, 1], [200, 200]]},
-    {('/foo/test.html', u'/foo/ref.html', '=='): [[0, 1], [100, 200]]},
+    {('/foo/test.html', '/foo/ref.html', '=='): [[1, 1], [200, 200]]},
+    {('/foo/test.html', '/foo/ref.html', '=='): [[0, 1], [100, 200]]},
     {None: [[0, 1], [100, 200]]},
     {None: [[1, 1], [200, 200]]},
 ])
@@ -106,8 +106,8 @@
 
 
 @pytest.mark.parametrize("fuzzy", [
-    {('/foo/test.html', u'/foo/ref-2.html', '=='): [[0, 1], [100, 200]]},
-    {None: [[1, 1], [200, 200]], ('/foo/test.html', u'/foo/ref-2.html', '=='): [[0, 1], [100, 200]]},
+    {('/foo/test.html', '/foo/ref-2.html', '=='): [[0, 1], [100, 200]]},
+    {None: [[1, 1], [200, 200]], ('/foo/test.html', '/foo/ref-2.html', '=='): [[0, 1], [100, 200]]},
 ])
 def test_reftest_fuzzy_multi(fuzzy):
     t = RefTest('/',
diff --git a/tools/manifest/tests/test_manifest.py b/tools/manifest/tests/test_manifest.py
index 6919381..bfe76eb 100644
--- a/tools/manifest/tests/test_manifest.py
+++ b/tools/manifest/tests/test_manifest.py
@@ -59,15 +59,15 @@
                     item.ConformanceCheckerTest, item.SupportFile]
     cls = draw(hs.sampled_from(item_classes))
 
-    path = u"a"
+    path = "a"
     rel_path_parts = tuple(path.split(os.path.sep))
-    hash = draw(hs.text(alphabet=u"0123456789abcdef", min_size=40, max_size=40))
+    hash = draw(hs.text(alphabet="0123456789abcdef", min_size=40, max_size=40))
     s = mock.Mock(rel_path=path,
                   rel_path_parts=rel_path_parts,
                   hash=hash)
 
     if cls in (item.RefTest, item.PrintRefTest):
-        ref_path = u"b"
+        ref_path = "b"
         ref_eq = draw(hs.sampled_from(["==", "!="]))
         test = cls("/foobar", path, "/", utils.from_os_path(path), references=[(utils.from_os_path(ref_path), ref_eq)])
     elif cls is item.SupportFile:
@@ -81,7 +81,7 @@
 
 @hs.composite
 def manifest_tree(draw):
-    names = hs.text(alphabet=hs.characters(blacklist_characters=u"\0/\\:*\"?<>|"), min_size=1)
+    names = hs.text(alphabet=hs.characters(blacklist_characters="\0/\\:*\"?<>|"), min_size=1)
     tree = hs.recursive(sourcefile_strategy(),
                         lambda children: hs.dictionaries(names, children, min_size=1),
                         max_leaves=10)
@@ -107,7 +107,7 @@
                         possible_urls = hs.sampled_from(reftest_urls) | names
                     else:
                         possible_urls = names
-                    reference = hs.tuples(hs.sampled_from([u"==", u"!="]),
+                    reference = hs.tuples(hs.sampled_from(["==", "!="]),
                                           possible_urls)
                     references = hs.lists(reference, min_size=1, unique=True)
                     test_item.references = draw(references)
diff --git a/tools/manifest/tests/test_sourcefile.py b/tools/manifest/tests/test_sourcefile.py
index 0ebdf47..203fe97 100644
--- a/tools/manifest/tests/test_sourcefile.py
+++ b/tools/manifest/tests/test_sourcefile.py
@@ -761,7 +761,7 @@
 
 
 def test_no_parse():
-    s = create("foo/bar.xml", u"\uFFFF".encode("utf-8"))
+    s = create("foo/bar.xml", "\uFFFF".encode("utf-8"))
 
     assert not s.name_is_non_test
     assert not s.name_is_manual
@@ -817,14 +817,14 @@
 
     assert item_type == "testharness"
 
-    assert [item.url for item in items] == [u'/_fake_base/html/test.any.html',
-                                            u'/_fake_base/html/test.any.html?wss',
-                                            u'/_fake_base/html/test.any.serviceworker.html',
-                                            u'/_fake_base/html/test.any.serviceworker.html?wss',
-                                            u'/_fake_base/html/test.any.sharedworker.html',
-                                            u'/_fake_base/html/test.any.sharedworker.html?wss',
-                                            u'/_fake_base/html/test.any.worker.html',
-                                            u'/_fake_base/html/test.any.worker.html?wss']
+    assert [item.url for item in items] == ['/_fake_base/html/test.any.html',
+                                            '/_fake_base/html/test.any.html?wss',
+                                            '/_fake_base/html/test.any.serviceworker.html',
+                                            '/_fake_base/html/test.any.serviceworker.html?wss',
+                                            '/_fake_base/html/test.any.sharedworker.html',
+                                            '/_fake_base/html/test.any.sharedworker.html?wss',
+                                            '/_fake_base/html/test.any.worker.html',
+                                            '/_fake_base/html/test.any.worker.html?wss']
 
     assert items[0].url_base == "/_fake_base/"
 
diff --git a/tools/manifest/typedata.py b/tools/manifest/typedata.py
index 6c7080f..75f654a 100644
--- a/tools/manifest/typedata.py
+++ b/tools/manifest/typedata.py
@@ -21,8 +21,8 @@
 
 
 if MYPY:
-    TypeDataType = MutableMapping[Tuple[Text, ...], Set[item.ManifestItem]]
-    PathHashType = MutableMapping[Tuple[Text, ...], Text]
+    TypeDataType = MutableMapping[Tuple[str, ...], Set[item.ManifestItem]]
+    PathHashType = MutableMapping[Tuple[str, ...], str]
 else:
     TypeDataType = MutableMapping
     PathHashType = MutableMapping
diff --git a/tools/manifest/utils.py b/tools/manifest/utils.py
index c8b6e6e..59ddb66 100644
--- a/tools/manifest/utils.py
+++ b/tools/manifest/utils.py
@@ -22,52 +22,52 @@
 def rel_path_to_url(rel_path, url_base="/"):
     # type: (Text, Text) -> Text
     assert not os.path.isabs(rel_path), rel_path
-    if url_base[0] != u"/":
-        url_base = u"/" + url_base
-    if url_base[-1] != u"/":
-        url_base += u"/"
-    return url_base + rel_path.replace(os.sep, u"/")
+    if url_base[0] != "/":
+        url_base = "/" + url_base
+    if url_base[-1] != "/":
+        url_base += "/"
+    return url_base + rel_path.replace(os.sep, "/")
 
 
 def from_os_path(path):
     # type: (Text) -> Text
-    assert os.path.sep == u"/" or sys.platform == "win32"
-    if u"/" == os.path.sep:
+    assert os.path.sep == "/" or sys.platform == "win32"
+    if "/" == os.path.sep:
         rv = path
     else:
-        rv = path.replace(os.path.sep, u"/")
-    if u"\\" in rv:
+        rv = path.replace(os.path.sep, "/")
+    if "\\" in rv:
         raise ValueError("path contains \\ when separator is %s" % os.path.sep)
     return rv
 
 
 def to_os_path(path):
     # type: (Text) -> Text
-    assert os.path.sep == u"/" or sys.platform == "win32"
-    if u"\\" in path:
+    assert os.path.sep == "/" or sys.platform == "win32"
+    if "\\" in path:
         raise ValueError("normalised path contains \\")
-    if u"/" == os.path.sep:
+    if "/" == os.path.sep:
         return path
-    return path.replace(u"/", os.path.sep)
+    return path.replace("/", os.path.sep)
 
 
 def git(path):
     # type: (Text) -> Optional[Callable[..., Text]]
     def gitfunc(cmd, *args):
         # type: (Text, *Text) -> Text
-        full_cmd = [u"git", cmd] + list(args)
+        full_cmd = ["git", cmd] + list(args)
         try:
             return subprocess.check_output(full_cmd, cwd=path, stderr=subprocess.STDOUT).decode('utf8')
         except Exception as e:
             if sys.platform == "win32" and isinstance(e, WindowsError):
-                full_cmd[0] = u"git.bat"
+                full_cmd[0] = "git.bat"
                 return subprocess.check_output(full_cmd, cwd=path, stderr=subprocess.STDOUT).decode('utf8')
             else:
                 raise
 
     try:
         # this needs to be a command that fails if we aren't in a git repo
-        gitfunc(u"rev-parse", u"--show-toplevel")
+        gitfunc("rev-parse", "--show-toplevel")
     except (subprocess.CalledProcessError, OSError):
         return None
     else:
diff --git a/tools/manifest/vcs.py b/tools/manifest/vcs.py
index 88ea5fd..ec59f42 100644
--- a/tools/manifest/vcs.py
+++ b/tools/manifest/vcs.py
@@ -29,11 +29,11 @@
     # type: (Text, Manifest, Optional[Text], Optional[Text], bool, bool) -> FileSystem
     tree = None
     if cache_root is None:
-        cache_root = os.path.join(tests_root, u".wptcache")
+        cache_root = os.path.join(tests_root, ".wptcache")
     if not os.path.exists(cache_root):
         try:
             os.makedirs(cache_root)
-        except IOError:
+        except OSError:
             cache_root = None
 
     if not working_copy:
@@ -48,7 +48,7 @@
     return tree
 
 
-class GitHasher(object):
+class GitHasher:
     def __init__(self, path):
         # type: (Text) -> None
         self.git = git(path)
@@ -85,7 +85,7 @@
 
 
 
-class FileSystem(object):
+class FileSystem:
     def __init__(self, tests_root, url_base, cache_path, manifest_path=None, rebuild=False):
         # type: (Text, Text, Optional[Text], Optional[Text], bool) -> None
         self.tests_root = tests_root
@@ -151,13 +151,13 @@
         data = {}  # type: Dict[Text, Any]
         try:
             if not rebuild:
-                with open(self.path, 'r') as f:
+                with open(self.path) as f:
                     try:
                         data = jsonlib.load(f)
                     except ValueError:
                         pass
                 data = self.check_valid(data)
-        except IOError:
+        except OSError:
             pass
         return data
 
@@ -169,12 +169,12 @@
 
 
 class MtimeCache(CacheFile):
-    file_name = u"mtime.json"
+    file_name = "mtime.json"
 
     def __init__(self, cache_root, tests_root, manifest_path, rebuild=False):
         # type: (Text, Text, Text, bool) -> None
         self.manifest_path = manifest_path
-        super(MtimeCache, self).__init__(cache_root, tests_root, rebuild)
+        super().__init__(cache_root, tests_root, rebuild)
 
     def updated(self, rel_path, stat):
         # type: (Text, stat_result) -> bool
@@ -190,12 +190,12 @@
 
     def check_valid(self, data):
         # type: (Dict[Any, Any]) -> Dict[Any, Any]
-        if data.get(u"/tests_root") != self.tests_root:
+        if data.get("/tests_root") != self.tests_root:
             self.modified = True
         else:
             if self.manifest_path is not None and os.path.exists(self.manifest_path):
                 mtime = os.path.getmtime(self.manifest_path)
-                if data.get(u"/manifest_path") != [self.manifest_path, mtime]:
+                if data.get("/manifest_path") != [self.manifest_path, mtime]:
                     self.modified = True
             else:
                 self.modified = True
@@ -213,7 +213,7 @@
         mtime = os.path.getmtime(self.manifest_path)
         self.data["/manifest_path"] = [self.manifest_path, mtime]
         self.data["/tests_root"] = self.tests_root
-        super(MtimeCache, self).dump()
+        super().dump()
 
 
 class GitIgnoreCache(CacheFile, GitIgnoreCacheType):
@@ -223,10 +223,10 @@
         # type: (Dict[Any, Any]) -> Dict[Any, Any]
         ignore_path = os.path.join(self.tests_root, ".gitignore")
         mtime = os.path.getmtime(ignore_path)
-        if data.get(u"/gitignore_file") != [ignore_path, mtime]:
+        if data.get("/gitignore_file") != [ignore_path, mtime]:
             self.modified = True
             data = {}
-            data[u"/gitignore_file"] = [ignore_path, mtime]
+            data["/gitignore_file"] = [ignore_path, mtime]
         return data
 
     def __contains__(self, key):
diff --git a/tools/runner/report.py b/tools/runner/report.py
index db4c988..524c26c 100644
--- a/tools/runner/report.py
+++ b/tools/runner/report.py
@@ -19,7 +19,7 @@
         return item
 
 
-class Raw(object):
+class Raw:
     """Simple wrapper around a string to stop it being escaped by html_escape"""
     def __init__(self, value):
         self.value = value
@@ -28,7 +28,7 @@
         return unicode(self.value)
 
 
-class Node(object):
+class Node:
     """Node structure used when building HTML"""
     def __init__(self, name, attrs, children):
         #Need list of void elements
@@ -55,7 +55,7 @@
         return unicode(self).encode("utf8")
 
 
-class RootNode(object):
+class RootNode:
     """Special Node representing the document root"""
     def __init__(self, *children):
         self.children = ["<!DOCTYPE html>"] + list(children)
@@ -81,7 +81,7 @@
     return rv
 
 
-class HTML(object):
+class HTML:
     """Simple HTML templating system. An instance of this class can create
     element nodes by calling methods with the same name as the element,
     passing in children as positional arguments or as a list, and attributes
@@ -117,7 +117,7 @@
 h = HTML()
 
 
-class TestResult(object):
+class TestResult:
     """Simple holder for the results of a single test in a single UA"""
     def __init__(self, test):
         self.test = test
diff --git a/tools/serve/serve.py b/tools/serve/serve.py
index e1f5fb1..a5be0f7 100644
--- a/tools/serve/serve.py
+++ b/tools/serve/serve.py
@@ -52,7 +52,7 @@
     return a_parts[slice_index:] != b_parts[slice_index:]
 
 
-class WrapperHandler(object):
+class WrapperHandler:
 
     __meta__ = abc.ABCMeta
 
@@ -120,9 +120,8 @@
         path = self._get_filesystem_path(request)
         try:
             with open(path, "rb") as f:
-                for key, value in read_script_metadata(f, js_meta_re):
-                    yield key, value
-        except IOError:
+                yield from read_script_metadata(f, js_meta_re)
+        except OSError:
             raise HTTPException(404)
 
     def _get_meta(self, request):
@@ -179,7 +178,7 @@
 
     def check_exposure(self, request):
         if self.global_type:
-            globals = u""
+            globals = ""
             for (key, value) in self._get_metadata(request):
                 if key == "global":
                     globals = value
@@ -405,7 +404,7 @@
 rewrites = [("GET", "/resources/WebIDLParser.js", "/resources/webidl2/lib/webidl2.js")]
 
 
-class RoutesBuilder(object):
+class RoutesBuilder:
     def __init__(self):
         self.forbidden_override = [("GET", "/tools/runner/*", handlers.file_handler),
                                    ("POST", "/tools/runner/update_manifest.py",
@@ -489,7 +488,7 @@
     return builder
 
 
-class ServerProc(object):
+class ServerProc:
     def __init__(self, mp_context, scheme=None):
         self.proc = None
         self.daemon = None
@@ -569,7 +568,7 @@
     wrapper.start(start_http_server, host, port, paths, routes,
                   bind_address, config, log_handlers)
 
-    url = "http://{}:{}/".format(host, port)
+    url = f"http://{host}:{port}/"
     connected = False
     for i in range(10):
         try:
@@ -591,7 +590,7 @@
         try:
             urllib.request.urlopen("http://%s:%d/" % (domain, port))
         except Exception:
-            logger.critical("Failed probing domain {}. {}".format(domain, EDIT_HOSTS_HELP))
+            logger.critical(f"Failed probing domain {domain}. {EDIT_HOSTS_HELP}")
             sys.exit(1)
 
     wrapper.stop()
@@ -719,7 +718,7 @@
         startup_failed(logger)
 
 
-class WebSocketDaemon(object):
+class WebSocketDaemon:
     def __init__(self, host, port, doc_root, handlers_root, bind_address, ssl_config):
         logger = logging.getLogger()
         self.host = host
@@ -811,7 +810,7 @@
                                     logger=logger)
     except Exception as error:
         logger.critical(
-            "Failed to start WebTransport over HTTP/3 server: {}".format(error))
+            f"Failed to start WebTransport over HTTP/3 server: {error}")
         sys.exit(0)
 
 
@@ -836,20 +835,20 @@
 
 
 def _make_subdomains_product(s: Set[str], depth: int = 2) -> Set[str]:
-    return {u".".join(x) for x in chain(*(product(s, repeat=i) for i in range(1, depth+1)))}
+    return {".".join(x) for x in chain(*(product(s, repeat=i) for i in range(1, depth+1)))}
 
 
 def _make_origin_policy_subdomains(limit: int) -> Set[str]:
-    return {u"op%d" % x for x in range(1,limit+1)}
+    return {"op%d" % x for x in range(1,limit+1)}
 
 
-_subdomains = {u"www",
-               u"www1",
-               u"www2",
-               u"天気の良い日",
-               u"élève"}
+_subdomains = {"www",
+               "www1",
+               "www2",
+               "天気の良い日",
+               "élève"}
 
-_not_subdomains = {u"nonexistent"}
+_not_subdomains = {"nonexistent"}
 
 _subdomains = _make_subdomains_product(_subdomains)
 
@@ -917,7 +916,7 @@
             kwargs["subdomains"] = _subdomains
         if "not_subdomains" not in kwargs:
             kwargs["not_subdomains"] = _not_subdomains
-        super(ConfigBuilder, self).__init__(
+        super().__init__(
             logger,
             *args,
             **kwargs
@@ -938,7 +937,7 @@
             return os.path.join(data["doc_root"], "websockets", "handlers")
 
     def _get_paths(self, data):
-        rv = super(ConfigBuilder, self)._get_paths(data)
+        rv = super()._get_paths(data)
         rv["ws_doc_root"] = data["ws_doc_root"]
         return rv
 
@@ -1008,7 +1007,7 @@
     return parser
 
 
-class MpContext(object):
+class MpContext:
     def __getattr__(self, name):
         return getattr(multiprocessing, name)
 
@@ -1060,9 +1059,9 @@
         bind_address = config["bind_address"]
 
         if kwargs.get("alias_file"):
-            with open(kwargs["alias_file"], 'r') as alias_file:
+            with open(kwargs["alias_file"]) as alias_file:
                 for line in alias_file:
-                    alias, doc_root = [x.strip() for x in line.split(',')]
+                    alias, doc_root = (x.strip() for x in line.split(','))
                     config["aliases"].append({
                         'url-path': alias,
                         'local-dir': doc_root,
diff --git a/tools/serve/test_functional.py b/tools/serve/test_functional.py
index cdc23f2..d928f00 100644
--- a/tools/serve/test_functional.py
+++ b/tools/serve/test_functional.py
@@ -18,7 +18,7 @@
     instances = None
 
     def start(self, *args, **kwargs):
-        result = super(ServerProcSpy, self).start(*args, **kwargs)
+        result = super().start(*args, **kwargs)
 
         if ServerProcSpy.instances is not None:
             ServerProcSpy.instances.put(self)
diff --git a/tools/wave/configuration_loader.py b/tools/wave/configuration_loader.py
index e8e0d74..c2367e3 100644
--- a/tools/wave/configuration_loader.py
+++ b/tools/wave/configuration_loader.py
@@ -1,6 +1,5 @@
 import json
 import os
-from io import open
 
 from tools.wpt import wpt
 
@@ -90,7 +89,7 @@
         return {}
 
     configuration = None
-    with open(path, "r") as configuration_file:
+    with open(path) as configuration_file:
         configuration_file_content = configuration_file.read()
         configuration = json.loads(configuration_file_content)
     return configuration
diff --git a/tools/wave/data/client.py b/tools/wave/data/client.py
index ab6851a..e495b26 100644
--- a/tools/wave/data/client.py
+++ b/tools/wave/data/client.py
@@ -1,4 +1,4 @@
-class Client(object):
+class Client:
     def __init__(self, session_token):
         self.session_token = session_token
 
diff --git a/tools/wave/data/device.py b/tools/wave/data/device.py
index 3b2ccdf..00fd188 100644
--- a/tools/wave/data/device.py
+++ b/tools/wave/data/device.py
@@ -1,4 +1,4 @@
-class Device(object):
+class Device:
     def __init__(self, token, user_agent, name, last_active):
         self.token = token
         self.user_agent = user_agent
diff --git a/tools/wave/data/event_listener.py b/tools/wave/data/event_listener.py
index 2695df8..6d49e80 100644
--- a/tools/wave/data/event_listener.py
+++ b/tools/wave/data/event_listener.py
@@ -1,6 +1,6 @@
-class EventListener(object):
+class EventListener:
     def __init__(self, dispatcher_token):
-        super(EventListener, self).__init__()
+        super().__init__()
         self.dispatcher_token = dispatcher_token
         self.token = None
 
diff --git a/tools/wave/data/http_polling_client.py b/tools/wave/data/http_polling_client.py
index 740f547..1688e11 100644
--- a/tools/wave/data/http_polling_client.py
+++ b/tools/wave/data/http_polling_client.py
@@ -3,7 +3,7 @@
 
 class HttpPollingClient(Client):
     def __init__(self, session_token, event):
-        super(HttpPollingClient, self).__init__(session_token)
+        super().__init__(session_token)
         self.event = event
 
     def send_message(self, message):
diff --git a/tools/wave/data/http_polling_event_listener.py b/tools/wave/data/http_polling_event_listener.py
index df731d4..3b35e81 100644
--- a/tools/wave/data/http_polling_event_listener.py
+++ b/tools/wave/data/http_polling_event_listener.py
@@ -2,7 +2,7 @@
 
 class HttpPollingEventListener(EventListener):
     def __init__(self, dispatcher_token, event):
-        super(HttpPollingEventListener, self).__init__(dispatcher_token)
+        super().__init__(dispatcher_token)
         self.event = event
         self.message = None
 
diff --git a/tools/wave/data/session.py b/tools/wave/data/session.py
index 8b5c4a6..da8f89a 100644
--- a/tools/wave/data/session.py
+++ b/tools/wave/data/session.py
@@ -8,7 +8,7 @@
 UNKNOWN = "unknown"
 
 
-class Session(object):
+class Session:
     def __init__(
             self,
             token=None,
diff --git a/tools/wave/network/api/api_handler.py b/tools/wave/network/api/api_handler.py
index da21237..425bc8e 100644
--- a/tools/wave/network/api/api_handler.py
+++ b/tools/wave/network/api/api_handler.py
@@ -9,7 +9,7 @@
 logger = logging.getLogger("wave-api-handler")
 
 
-class ApiHandler(object):
+class ApiHandler:
     def __init__(self, web_root):
         self._web_root = web_root
 
@@ -51,7 +51,7 @@
     def handle_exception(self, message):
         info = sys.exc_info()
         traceback.print_tb(info[2])
-        logger.error("{}: {}: {}".format(message, info[0].__name__, info[1].args[0]))
+        logger.error(f"{message}: {info[0].__name__}: {info[1].args[0]}")
 
     def create_hal_list(self, items, uris, index, count, total):
         hal_list = {}
@@ -66,21 +66,21 @@
 
             if "self" in uris:
                 self_uri = uris["self"]
-                self_uri += "?index={}&count={}".format(index, count)
+                self_uri += f"?index={index}&count={count}"
                 links["self"] = {"href": self_uri}
 
                 first_uri = uris["self"]
-                first_uri += "?index={}&count={}".format(0, count)
+                first_uri += f"?index={0}&count={count}"
                 links["first"] = {"href": first_uri}
 
                 last_uri = uris["self"]
-                last_uri += "?index={}&count={}".format(total - (total % count), count)
+                last_uri += f"?index={total - (total % count)}&count={count}"
                 links["last"] = {"href": last_uri}
 
                 if index + count <= total:
                     next_index = index + count
                     next_uri = uris["self"]
-                    next_uri += "?index={}&count={}".format(next_index, count)
+                    next_uri += f"?index={next_index}&count={count}"
                     links["next"] = {"href": next_uri}
 
                 if index != 0:
@@ -88,7 +88,7 @@
                     if previous_index < 0:
                         previous_index = 0
                     previous_uri = uris["self"]
-                    previous_uri += "?index={}&count={}".format(previous_index, count)
+                    previous_uri += f"?index={previous_index}&count={count}"
                     links["previous"] = {"href": previous_uri}
 
         hal_list["_links"] = links
diff --git a/tools/wave/network/api/devices_api_handler.py b/tools/wave/network/api/devices_api_handler.py
index 5e8b02b..c86369d 100644
--- a/tools/wave/network/api/devices_api_handler.py
+++ b/tools/wave/network/api/devices_api_handler.py
@@ -11,7 +11,7 @@
 
 class DevicesApiHandler(ApiHandler):
     def __init__(self, devices_manager, event_dispatcher, web_root):
-        super(DevicesApiHandler, self).__init__(web_root)
+        super().__init__(web_root)
         self._devices_manager = devices_manager
         self._event_dispatcher = event_dispatcher
 
@@ -63,7 +63,7 @@
             token = uri_parts[2]
             query = self.parse_query_parameters(request)
 
-            if u"device_token" in query:
+            if "device_token" in query:
                 self._devices_manager.refresh_device(query["device_token"])
 
             event = threading.Event()
@@ -89,7 +89,7 @@
         try:
             query = self.parse_query_parameters(request)
 
-            if u"device_token" in query:
+            if "device_token" in query:
                 self._devices_manager.refresh_device(query["device_token"])
 
             event = threading.Event()
@@ -108,7 +108,7 @@
                 self.send_json(data=message, response=response)
             self._event_dispatcher.remove_event_listener(event_listener_token)
         except Exception:
-            self.handle_exception(u"Failed to register global event listener")
+            self.handle_exception("Failed to register global event listener")
             response.status = 500
 
     def post_global_event(self, request, response):
@@ -119,7 +119,7 @@
                 event = json.loads(body)
 
             query = self.parse_query_parameters(request)
-            if u"device_token" in query:
+            if "device_token" in query:
                 self._devices_manager.refresh_device(query["device_token"])
 
             event_type = None
@@ -145,7 +145,7 @@
                 event = json.loads(body)
 
             query = self.parse_query_parameters(request)
-            if u"device_token" in query:
+            if "device_token" in query:
                 self._devices_manager.refresh_device(query["device_token"])
 
             event_type = None
@@ -166,18 +166,18 @@
 
         # /api/devices
         if len(uri_parts) == 2:
-            if method == u"POST":
+            if method == "POST":
                 self.create_device(request, response)
                 return
-            if method == u"GET":
+            if method == "GET":
                 self.read_devices(request, response)
                 return
 
         # /api/devices/<function>
         if len(uri_parts) == 3:
             function = uri_parts[2]
-            if method == u"GET":
-                if function == u"events":
+            if method == "GET":
+                if function == "events":
                     self.register_global_event_listener(request, response)
                     return
                 self.read_device(request, response)
@@ -190,8 +190,8 @@
         # /api/devices/<token>/<function>
         if len(uri_parts) == 4:
             function = uri_parts[3]
-            if method == u"GET":
-                if function == u"events":
+            if method == "GET":
+                if function == "events":
                     self.register_event_listener(request, response)
                     return
             if method == "POST":
diff --git a/tools/wave/network/api/general_api_handler.py b/tools/wave/network/api/general_api_handler.py
index bd588d1..c484dba 100644
--- a/tools/wave/network/api/general_api_handler.py
+++ b/tools/wave/network/api/general_api_handler.py
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
 from .api_handler import ApiHandler
 
 TOKEN_LENGTH = 36
@@ -17,7 +14,7 @@
         test_type_selection_enabled,
         test_file_selection_enabled
     ):
-        super(GeneralApiHandler, self).__init__(web_root)
+        super().__init__(web_root)
         self.read_sessions_enabled = read_sessions_enabled
         self.import_results_enabled = import_results_enabled
         self.reports_enabled = reports_enabled
diff --git a/tools/wave/network/api/results_api_handler.py b/tools/wave/network/api/results_api_handler.py
index 1efa988..707c103 100644
--- a/tools/wave/network/api/results_api_handler.py
+++ b/tools/wave/network/api/results_api_handler.py
@@ -7,7 +7,7 @@
 
 class ResultsApiHandler(ApiHandler):
     def __init__(self, results_manager, session_manager, web_root):
-        super(ResultsApiHandler, self).__init__(web_root)
+        super().__init__(web_root)
         self._results_manager = results_manager
         self._sessions_manager = session_manager
 
diff --git a/tools/wave/network/api/sessions_api_handler.py b/tools/wave/network/api/sessions_api_handler.py
index dffca43..35f2067 100644
--- a/tools/wave/network/api/sessions_api_handler.py
+++ b/tools/wave/network/api/sessions_api_handler.py
@@ -20,7 +20,7 @@
         web_root,
         read_sessions_enabled
     ):
-        super(SessionsApiHandler, self).__init__(web_root)
+        super().__init__(web_root)
         self._sessions_manager = sessions_manager
         self._results_manager = results_manager
         self._event_dispatcher = event_dispatcher
@@ -357,8 +357,8 @@
             uri_parts = self.parse_uri(request)
             token = uri_parts[2]
             message = None
-            body = request.body.decode(u"utf-8")
-            if body != u"":
+            body = request.body.decode("utf-8")
+            if body != "":
                 message = json.loads(body)
 
             self._event_dispatcher.dispatch_event(
diff --git a/tools/wave/network/api/tests_api_handler.py b/tools/wave/network/api/tests_api_handler.py
index 5c797d9..a0b0f85 100644
--- a/tools/wave/network/api/tests_api_handler.py
+++ b/tools/wave/network/api/tests_api_handler.py
@@ -25,7 +25,7 @@
         web_root,
         test_loader
     ):
-        super(TestsApiHandler, self).__init__(web_root)
+        super().__init__(web_root)
         self._tests_manager = tests_manager
         self._sessions_manager = sessions_manager
         self._wpt_port = wpt_port
@@ -293,4 +293,4 @@
             query = ""
         if protocol is None:
             protocol = "http"
-        return urlunsplit([protocol, "{}:{}".format(hostname, port), uri, query, ''])
+        return urlunsplit([protocol, f"{hostname}:{port}", uri, query, ''])
diff --git a/tools/wave/network/http_handler.py b/tools/wave/network/http_handler.py
index 5a66383..98f0914 100644
--- a/tools/wave/network/http_handler.py
+++ b/tools/wave/network/http_handler.py
@@ -7,7 +7,7 @@
 global logger
 logger = logging.getLogger("wave-api-handler")
 
-class HttpHandler(object):
+class HttpHandler:
     def __init__(
         self,
         static_handler,
@@ -112,9 +112,9 @@
             response.headers = proxy_response.getheaders()
             response.status = proxy_response.status
 
-        except IOError:
+        except OSError:
             message = "Failed to perform proxy request"
             info = sys.exc_info()
             traceback.print_tb(info[2])
-            logger.error("{}: {}: {}".format(message, info[0].__name__, info[1].args[0]))
+            logger.error(f"{message}: {info[0].__name__}: {info[1].args[0]}")
             response.status = 500
diff --git a/tools/wave/network/static_handler.py b/tools/wave/network/static_handler.py
index fdca361..5891e1a 100644
--- a/tools/wave/network/static_handler.py
+++ b/tools/wave/network/static_handler.py
@@ -1,8 +1,7 @@
 import os
-from io import open
 
 
-class StaticHandler(object):
+class StaticHandler:
     def __init__(self, web_root, http_port, https_port):
         self.static_dir = os.path.join(
             os.getcwd(), "tools/wave/www")
diff --git a/tools/wave/testing/devices_manager.py b/tools/wave/testing/devices_manager.py
index c03572e..9136f27 100644
--- a/tools/wave/testing/devices_manager.py
+++ b/tools/wave/testing/devices_manager.py
@@ -13,7 +13,7 @@
 DEVICE_TIMEOUT = 60000  # 60sec
 RECONNECT_TIME = 5000   # 5sec
 
-class DevicesManager(object):
+class DevicesManager:
     def initialize(self, event_dispatcher):
         self.devices = {}
         self._event_dispatcher = event_dispatcher
@@ -39,7 +39,7 @@
 
     def read_device(self, token):
         if token not in self.devices:
-            raise NotFoundException("Could not find device '{}'".format(token))
+            raise NotFoundException(f"Could not find device '{token}'")
         return self.devices[token]
 
     def read_devices(self):
diff --git a/tools/wave/testing/event_dispatcher.py b/tools/wave/testing/event_dispatcher.py
index 0916539..23cc95f 100644
--- a/tools/wave/testing/event_dispatcher.py
+++ b/tools/wave/testing/event_dispatcher.py
@@ -11,7 +11,7 @@
 DEVICE_ADDED_EVENT = "device_added"
 DEVICE_REMOVED_EVENT = "device_removed"
 
-class EventDispatcher(object):
+class EventDispatcher:
     def __init__(self, event_cache_duration):
         self._listeners = {}
         self._events = {}
diff --git a/tools/wave/testing/results_manager.py b/tools/wave/testing/results_manager.py
index 978c12d..d52348d 100644
--- a/tools/wave/testing/results_manager.py
+++ b/tools/wave/testing/results_manager.py
@@ -23,7 +23,7 @@
 SESSION_RESULTS_TIMEOUT = 60*30  # 30min
 
 
-class ResultsManager(object):
+class ResultsManager:
     def initialize(
         self,
         results_directory_path,
@@ -220,7 +220,7 @@
         api_directory = os.path.join(self._results_directory_path, token, api)
         if not os.path.isdir(api_directory):
             return None
-        return "/results/{}/{}/all.html".format(token, api)
+        return f"/results/{token}/{api}/all.html"
 
     def read_results_wpt_multi_report_uri(self, tokens, api):
         comparison_directory_name = self.get_comparison_identifier(tokens)
@@ -236,7 +236,7 @@
         if not os.path.isdir(api_directory_path):
             self.generate_multi_report(tokens, api)
 
-        return "/results/{}/all.html".format(relative_api_directory_path)
+        return f"/results/{relative_api_directory_path}/all.html"
 
     def delete_results(self, token):
         results_directory = os.path.join(self._results_directory_path, token)
@@ -270,7 +270,7 @@
                     continue
                 file_path = os.path.join(api_directory, file_name)
                 data = None
-                with open(file_path, "r") as file:
+                with open(file_path) as file:
                     data = file.read()
                 result = json.loads(data)
                 results[api] = result["results"]
@@ -486,7 +486,7 @@
         if not os.path.isfile(file_path):
             return None
 
-        with open(file_path, "r") as file:
+        with open(file_path) as file:
             blob = file.read()
             return blob
 
@@ -545,7 +545,7 @@
                 zip.write(file_path, file_name, zipfile.ZIP_DEFLATED)
         zip.close()
 
-        with open(zip_file_name, "r") as file:
+        with open(zip_file_name) as file:
             blob = file.read()
             os.remove(zip_file_name)
 
@@ -554,7 +554,7 @@
     def export_results_overview(self, token):
         session = self._sessions_manager.read_session(token)
         if session is None:
-            raise NotFoundException("Could not find session {}".format(token))
+            raise NotFoundException(f"Could not find session {token}")
 
         tmp_file_name = str(time.time()) + ".zip"
         zip = zipfile.ZipFile(tmp_file_name, "w")
@@ -596,7 +596,7 @@
         if not os.path.isfile(info_file_path):
             return None
 
-        with open(info_file_path, "r") as info_file:
+        with open(info_file_path) as info_file:
             data = info_file.read()
             info_file.close()
             info = json.loads(str(data))
@@ -605,7 +605,7 @@
     def import_results(self, blob):
         if not self.is_import_results_enabled:
             raise PermissionDeniedException()
-        tmp_file_name = "{}.zip".format(str(time.time()))
+        tmp_file_name = f"{str(time.time())}.zip"
 
         with open(tmp_file_name, "w") as file:
             file.write(blob)
diff --git a/tools/wave/testing/sessions_manager.py b/tools/wave/testing/sessions_manager.py
index 900b7c0..bbb0040 100644
--- a/tools/wave/testing/sessions_manager.py
+++ b/tools/wave/testing/sessions_manager.py
@@ -20,7 +20,7 @@
 DEFAULT_TEST_MANUAL_TIMEOUT = 300000
 
 
-class SessionsManager(object):
+class SessionsManager:
     def initialize(self,
                    test_loader,
                    event_dispatcher,
@@ -72,7 +72,7 @@
 
         for test_type in test_types:
             if test_type != "automatic" and test_type != "manual":
-                raise InvalidDataException("Unknown type '{}'".format(test_type))
+                raise InvalidDataException(f"Unknown type '{test_type}'")
 
         token = str(uuid.uuid1())
         pending_tests = self._test_loader.get_tests(
@@ -297,7 +297,7 @@
             return None
 
         info_data = None
-        with open(info_file, "r") as file:
+        with open(info_file) as file:
             info_data = file.read()
         parsed_info_data = json.loads(info_data)
 
@@ -459,7 +459,7 @@
             pattern = re.compile("^" + include_test)
             if pattern.match(test) is not None:
                 query_string += query + "&"
-        return "{}?{}".format(test, query_string)
+        return f"{test}?{query_string}"
 
     def find_token(self, fragment):
         if len(fragment) < 8:
diff --git a/tools/wave/testing/test_loader.py b/tools/wave/testing/test_loader.py
index d9f0691..47fb6da 100644
--- a/tools/wave/testing/test_loader.py
+++ b/tools/wave/testing/test_loader.py
@@ -7,7 +7,7 @@
 TEST_TYPES = [AUTOMATIC, MANUAL]
 
 
-class TestLoader(object):
+class TestLoader:
     def initialize(
         self,
         exclude_list_file_path,
diff --git a/tools/wave/testing/tests_manager.py b/tools/wave/testing/tests_manager.py
index 9935521..5d338fe 100644
--- a/tools/wave/testing/tests_manager.py
+++ b/tools/wave/testing/tests_manager.py
@@ -7,7 +7,7 @@
 from ..data.session import COMPLETED, ABORTED
 
 
-class TestsManager(object):
+class TestsManager:
     def initialize(
         self,
         test_loader,
@@ -120,7 +120,7 @@
             for test in tests[api]:
                 sorted_tests.append(test)
 
-        class compare(object):
+        class compare:
             def __init__(self, tests_manager, test):
                 self.test = test
                 self.tests_manager = tests_manager
diff --git a/tools/wave/tests/test_wave.py b/tools/wave/tests/test_wave.py
index ef3d80d..ae62944 100644
--- a/tools/wave/tests/test_wave.py
+++ b/tools/wave/tests/test_wave.py
@@ -13,7 +13,7 @@
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     try:
         s.bind(("127.0.0.1", 8080))
-    except socket.error as e:
+    except OSError as e:
         if e.errno == errno.EADDRINUSE:
             return True
         else:
diff --git a/tools/wave/utils/deserializer.py b/tools/wave/utils/deserializer.py
index ae794fb..60d1309 100644
--- a/tools/wave/utils/deserializer.py
+++ b/tools/wave/utils/deserializer.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
 from ..data.session import Session, UNKNOWN
 from datetime import datetime
 import dateutil.parser
diff --git a/tools/wave/wave_server.py b/tools/wave/wave_server.py
index 45933db..d1ceeba 100644
--- a/tools/wave/wave_server.py
+++ b/tools/wave/wave_server.py
@@ -21,7 +21,7 @@
 VERSION_STRING = "v3.3.0"
 
 
-class WaveServer(object):
+class WaveServer:
     def initialize(self,
                    tests,
                    configuration_file_path=None,
diff --git a/tools/webdriver/webdriver/bidi/error.py b/tools/webdriver/webdriver/bidi/error.py
index 2d46864..3bcc5a2 100644
--- a/tools/webdriver/webdriver/bidi/error.py
+++ b/tools/webdriver/webdriver/bidi/error.py
@@ -10,7 +10,7 @@
     error_code = None  # type: ClassVar[str]
 
     def __init__(self, error: str, message: str, stacktrace: Optional[str]):
-        super(BidiException, self)
+        super()
 
         self.error = error
         self.message = message
diff --git a/tools/webdriver/webdriver/client.py b/tools/webdriver/webdriver/client.py
index 9a5f26d..33834f2 100644
--- a/tools/webdriver/webdriver/client.py
+++ b/tools/webdriver/webdriver/client.py
@@ -25,7 +25,7 @@
     return inner
 
 
-class Timeouts(object):
+class Timeouts:
 
     def __init__(self, session):
         self.session = session
@@ -71,7 +71,7 @@
             (name, self.script, self.page_load, self.implicit)
 
 
-class ActionSequence(object):
+class ActionSequence:
     """API for creating and performing action sequences.
 
     Each action method adds one or more actions to a queue. When perform()
@@ -265,7 +265,7 @@
         return self
 
 
-class Actions(object):
+class Actions:
     def __init__(self, session):
         self.session = session
 
@@ -293,7 +293,7 @@
         return ActionSequence(self.session, *args, **kwargs)
 
 
-class Window(object):
+class Window:
     identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f"
 
     def __init__(self, session):
@@ -379,7 +379,7 @@
         return cls(uuid, session)
 
 
-class Frame(object):
+class Frame:
     identifier = "frame-075b-4da1-b6ba-e579c2d3230a"
 
     def __init__(self, session):
@@ -391,7 +391,7 @@
         return cls(uuid, session)
 
 
-class ShadowRoot(object):
+class ShadowRoot:
     identifier = "shadow-6066-11e4-a52e-4f735466cecf"
 
     def __init__(self, session, id):
@@ -411,7 +411,7 @@
         return cls(session, uuid)
 
     def send_shadow_command(self, method, uri, body=None):
-        url = "shadow/{}/{}".format(self.id, uri)
+        url = f"shadow/{self.id}/{uri}"
         return self.session.send_session_command(method, url, body)
 
     @command
@@ -427,7 +427,7 @@
         return self.send_shadow_command("POST", "elements", body)
 
 
-class Find(object):
+class Find:
     def __init__(self, session):
         self.session = session
 
@@ -443,7 +443,7 @@
         return self.session.send_session_command("POST", route, body)
 
 
-class Cookies(object):
+class Cookies:
     def __init__(self, session):
         self.session = session
 
@@ -461,7 +461,7 @@
         self.session.send_session_command("POST", "cookie/%s" % name, {})
 
 
-class UserPrompt(object):
+class UserPrompt:
     def __init__(self, session):
         self.session = session
 
@@ -485,7 +485,7 @@
         self.session.send_session_command("POST", "alert/text", body=body)
 
 
-class Session(object):
+class Session:
     def __init__(self,
                  host,
                  port,
@@ -796,7 +796,7 @@
     def screenshot(self):
         return self.send_session_command("GET", "screenshot")
 
-class Element(object):
+class Element:
     """
     Representation of a web element.
 
diff --git a/tools/webdriver/webdriver/error.py b/tools/webdriver/webdriver/error.py
index a413a95..9c80dd9 100644
--- a/tools/webdriver/webdriver/error.py
+++ b/tools/webdriver/webdriver/error.py
@@ -14,7 +14,7 @@
     status_code = None  # type: ClassVar[str]
 
     def __init__(self, http_status=None, status_code=None, message=None, stacktrace=None):
-        super(WebDriverException, self)
+        super()
 
         if http_status is not None:
             self.http_status = http_status
diff --git a/tools/webdriver/webdriver/protocol.py b/tools/webdriver/webdriver/protocol.py
index e71e01d..ff1ad2a 100644
--- a/tools/webdriver/webdriver/protocol.py
+++ b/tools/webdriver/webdriver/protocol.py
@@ -9,7 +9,7 @@
 class Encoder(json.JSONEncoder):
     def __init__(self, *args, **kwargs):
         kwargs.pop("session")
-        super(Encoder, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     def default(self, obj):
         if isinstance(obj, (list, tuple)):
@@ -22,13 +22,13 @@
             return {webdriver.Frame.identifier: obj.id}
         elif isinstance(obj, webdriver.ShadowRoot):
             return {webdriver.ShadowRoot.identifier: obj.id}
-        return super(Encoder, self).default(obj)
+        return super().default(obj)
 
 
 class Decoder(json.JSONDecoder):
     def __init__(self, *args, **kwargs):
         self.session = kwargs.pop("session")
-        super(Decoder, self).__init__(
+        super().__init__(
             object_hook=self.object_hook, *args, **kwargs)
 
     def object_hook(self, payload):
diff --git a/tools/webdriver/webdriver/transport.py b/tools/webdriver/webdriver/transport.py
index 5a1ce28..bed747a 100644
--- a/tools/webdriver/webdriver/transport.py
+++ b/tools/webdriver/webdriver/transport.py
@@ -51,14 +51,13 @@
                 raise
 
     def __iter__(self):
-        for item in self.headers_dict:
-            yield item
+        yield from self.headers_dict
 
     def __len__(self):
         return len(self.headers_dict)
 
 
-class Response(object):
+class Response:
     """
     Describes an HTTP response received from a remote end whose
     body has been read and parsed as appropriate.
@@ -96,7 +95,7 @@
         return cls(http_response.status, body, headers)
 
 
-class HTTPWireProtocol(object):
+class HTTPWireProtocol:
     """
     Transports messages (commands and responses) over the WebDriver
     wire protocol.
diff --git a/tools/webtransport/h3/webtransport_h3_server.py b/tools/webtransport/h3/webtransport_h3_server.py
index 1ff5ca8..cc1e86d 100644
--- a/tools/webtransport/h3/webtransport_h3_server.py
+++ b/tools/webtransport/h3/webtransport_h3_server.py
@@ -148,7 +148,7 @@
                                 CapsuleType.REGISTER_DATAGRAM_CONTEXT,
                                 CapsuleType.CLOSE_DATAGRAM_CONTEXT}:
                 raise ProtocolError(
-                    "Unimplemented capsule type: {}".format(capsule.type))
+                    f"Unimplemented capsule type: {capsule.type}")
             if capsule.type in {CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT,
                                 CapsuleType.CLOSE_WEBTRANSPORT_SESSION}:
                 # We'll handle this case below.
@@ -205,7 +205,7 @@
                 session_id=event.stream_id,
                 path=path,
                 request_headers=event.headers)
-        except IOError:
+        except OSError:
             self._send_error_response(event.stream_id, 404)
             return
 
diff --git a/tools/wpt/browser.py b/tools/wpt/browser.py
index b3c9ce2..889e201 100644
--- a/tools/wpt/browser.py
+++ b/tools/wpt/browser.py
@@ -48,7 +48,7 @@
     return resp
 
 
-class Browser(object):
+class Browser:
     __metaclass__ = ABCMeta
 
     def __init__(self, logger):
@@ -473,7 +473,7 @@
     requirements = "requirements_firefox.txt"
 
     def __init__(self, logger):
-        super(FirefoxAndroid, self).__init__(logger)
+        super().__init__(logger)
         self.apk_path = None
 
     def download(self, dest=None, channel=None, rename=None):
@@ -529,7 +529,7 @@
     }
 
     def __init__(self, logger):
-        super(Chrome, self).__init__(logger)
+        super().__init__(logger)
         self._last_change = None
 
     def download(self, dest=None, channel=None, rename=None):
@@ -571,7 +571,7 @@
         extracted = os.path.join(dest, "mojojs", "gen")
         last_url_file = os.path.join(extracted, "DOWNLOADED_FROM")
         if os.path.exists(last_url_file):
-            with open(last_url_file, "rt") as f:
+            with open(last_url_file) as f:
                 last_url = f.read().strip()
             if last_url == url:
                 self.logger.info("Mojo bindings already up to date")
@@ -819,7 +819,7 @@
     __metaclass__ = ABCMeta  # This is an abstract class.
 
     def __init__(self, logger):
-        super(ChromeAndroidBase, self).__init__(logger)
+        super().__init__(logger)
         self.device_serial = None
 
     def download(self, dest=None, channel=None, rename=None):
@@ -1445,7 +1445,7 @@
 
 
     def _get_osidversion(self):
-        with open('/etc/os-release', 'r') as osrelease_handle:
+        with open('/etc/os-release') as osrelease_handle:
             for line in osrelease_handle.readlines():
                 if line.startswith('ID='):
                     os_id = line.split('=')[1].strip().strip('"')
diff --git a/tools/wpt/markdown.py b/tools/wpt/markdown.py
index 43020cd..1ea9cab 100644
--- a/tools/wpt/markdown.py
+++ b/tools/wpt/markdown.py
@@ -17,11 +17,11 @@
 
 def markdown_adjust(s):
     """Escape problematic markdown sequences."""
-    s = s.replace('\t', u'\\t')
-    s = s.replace('\n', u'\\n')
-    s = s.replace('\r', u'\\r')
-    s = s.replace('`', u'')
-    s = s.replace('|', u'\\|')
+    s = s.replace('\t', '\\t')
+    s = s.replace('\n', '\\n')
+    s = s.replace('\r', '\\r')
+    s = s.replace('`', '')
+    s = s.replace('|', '\\|')
     return s
 
 
diff --git a/tools/wpt/revlist.py b/tools/wpt/revlist.py
index b3f1f34..e9fea30 100644
--- a/tools/wpt/revlist.py
+++ b/tools/wpt/revlist.py
@@ -10,7 +10,7 @@
 
 
 def calculate_cutoff_date(until: int, epoch: int, offset: int) -> int:
-    return ((((until - offset) // epoch)) * epoch) + offset
+    return (((until - offset) // epoch) * epoch) + offset
 
 
 def parse_epoch(string: str) -> int:
@@ -29,9 +29,9 @@
     git = get_git_cmd(wpt_root)
     args = [
         pattern,
-        u'--sort=-committerdate',
-        u'--format=%(refname:lstrip=2) %(objectname) %(committerdate:raw)',
-        u'--count=100000'
+        '--sort=-committerdate',
+        '--format=%(refname:lstrip=2) %(objectname) %(committerdate:raw)',
+        '--count=100000'
     ]
     ref_list = git("for-each-ref", *args)  # type: ignore
     for line in ref_list.splitlines():
diff --git a/tools/wpt/run.py b/tools/wpt/run.py
index f1bd610..cba486d 100644
--- a/tools/wpt/run.py
+++ b/tools/wpt/run.py
@@ -24,7 +24,7 @@
                  dest=argparse.SUPPRESS,
                  default=argparse.SUPPRESS,
                  help=None):
-        super(WptrunnerHelpAction, self).__init__(
+        super().__init__(
             option_strings=option_strings,
             dest=dest,
             default=default,
@@ -131,7 +131,7 @@
         else:
             wpt_path = os.path.join(wpt_root, "wpt")
 
-        with open(hosts_path, "r") as f:
+        with open(hosts_path) as f:
             for line in f:
                 line = line.split("#", 1)[0].strip()
                 parts = line.split()
@@ -153,7 +153,7 @@
                 raise WptrunError(message)
 
 
-class BrowserSetup(object):
+class BrowserSetup:
     name = None  # type: ClassVar[str]
     browser_cls = None  # type: ClassVar[Type[browser.Browser]]
 
@@ -434,7 +434,7 @@
     browser_cls = browser.ChromeAndroid
 
     def setup_kwargs(self, kwargs):
-        super(ChromeAndroid, self).setup_kwargs(kwargs)
+        super().setup_kwargs(kwargs)
         if kwargs["browser_channel"] in self.experimental_channels:
             logger.info("Automatically turning on experimental features for Chrome Dev/Canary")
             kwargs["binary_args"].append("--enable-experimental-web-platform-features")
@@ -456,7 +456,7 @@
     browser_cls = browser.AndroidWeblayer
 
     def setup_kwargs(self, kwargs):
-        super(AndroidWeblayer, self).setup_kwargs(kwargs)
+        super().setup_kwargs(kwargs)
         if kwargs["browser_channel"] in self.experimental_channels:
             logger.info("Automatically turning on experimental features for WebLayer Dev/Canary")
             kwargs["binary_args"].append("--enable-experimental-web-platform-features")
diff --git a/tools/wpt/testfiles.py b/tools/wpt/testfiles.py
index 6a0f579..6a649fb 100644
--- a/tools/wpt/testfiles.py
+++ b/tools/wpt/testfiles.py
@@ -135,17 +135,17 @@
 
 def compile_ignore_rule(rule):
     # type: (Text) -> Pattern[Text]
-    rule = rule.replace(os.path.sep, u"/")
-    parts = rule.split(u"/")
+    rule = rule.replace(os.path.sep, "/")
+    parts = rule.split("/")
     re_parts = []
     for part in parts:
-        if part.endswith(u"**"):
-            re_parts.append(re.escape(part[:-2]) + u".*")
-        elif part.endswith(u"*"):
-            re_parts.append(re.escape(part[:-1]) + u"[^/]*")
+        if part.endswith("**"):
+            re_parts.append(re.escape(part[:-2]) + ".*")
+        elif part.endswith("*"):
+            re_parts.append(re.escape(part[:-1]) + "[^/]*")
         else:
             re_parts.append(re.escape(part))
-    return re.compile(u"^%s$" % u"/".join(re_parts))
+    return re.compile("^%s$" % "/".join(re_parts))
 
 
 def repo_files_changed(revish, include_uncommitted=False, include_new=False):
@@ -163,7 +163,7 @@
         # gives us that (via the merge-base)
         revish = revish.replace("..", "...")
 
-    files_list = git("diff", "--no-renames", "--name-only", "-z", revish).split(u"\0")
+    files_list = git("diff", "--no-renames", "--name-only", "-z", revish).split("\0")
     assert not files_list[-1], f"final item should be empty, got: {files_list[-1]!r}"
     files = set(files_list[:-1])
 
@@ -238,7 +238,7 @@
 def load_manifest(manifest_path=None, manifest_update=True):
     # type: (Optional[Text], bool) -> manifest.Manifest
     if manifest_path is None:
-        manifest_path = os.path.join(wpt_root, u"MANIFEST.json")
+        manifest_path = os.path.join(wpt_root, "MANIFEST.json")
     return manifest.load_and_update(wpt_root, manifest_path, "/",
                                     update=manifest_update)
 
@@ -251,7 +251,7 @@
     # type: (...) -> Tuple[Set[Text], Set[Text]]
     """Determine and return list of test files that reference changed files."""
     if skip_dirs is None:
-        skip_dirs = {u"conformance-checkers", u"docs", u"tools"}
+        skip_dirs = {"conformance-checkers", "docs", "tools"}
     affected_testfiles = set()
     # Exclude files that are in the repo root, because
     # they are not part of any test.
@@ -390,7 +390,7 @@
     # type: (**Any) -> Text
     revish = kwargs.get("revish")
     if revish is None:
-        revish = u"%s..HEAD" % branch_point()
+        revish = "%s..HEAD" % branch_point()
     return revish.strip()
 
 
@@ -402,7 +402,7 @@
                                include_uncommitted=kwargs["modified"],
                                include_new=kwargs["new"])
 
-    separator = u"\0" if kwargs["null"] else u"\n"
+    separator = "\0" if kwargs["null"] else "\n"
 
     for item in sorted(changed):
         line = os.path.relpath(item, wpt_root) + separator
diff --git a/tools/wpt/tests/test_markdown.py b/tools/wpt/tests/test_markdown.py
index dec9ad9..e9410ca 100644
--- a/tools/wpt/tests/test_markdown.py
+++ b/tools/wpt/tests/test_markdown.py
@@ -5,12 +5,12 @@
     assert '# Browser (channel) #' == markdown.format_comment_title("browser:channel")
 
 def test_markdown_adjust():
-    assert u'\\t' == markdown.markdown_adjust('\t')
-    assert u'\\r' == markdown.markdown_adjust('\r')
-    assert u'\\n' == markdown.markdown_adjust('\n')
-    assert u'' == markdown.markdown_adjust('`')
-    assert u'\\|' == markdown.markdown_adjust('|')
-    assert u'\\t\\r\\n\\|' == markdown.markdown_adjust('\t\r\n`|')
+    assert '\\t' == markdown.markdown_adjust('\t')
+    assert '\\r' == markdown.markdown_adjust('\r')
+    assert '\\n' == markdown.markdown_adjust('\n')
+    assert '' == markdown.markdown_adjust('`')
+    assert '\\|' == markdown.markdown_adjust('|')
+    assert '\\t\\r\\n\\|' == markdown.markdown_adjust('\t\r\n`|')
 
 result = ''
 def log(text):
diff --git a/tools/wpt/tests/test_testfiles.py b/tools/wpt/tests/test_testfiles.py
index c9806c5..485a693 100644
--- a/tools/wpt/tests/test_testfiles.py
+++ b/tools/wpt/tests/test_testfiles.py
@@ -6,13 +6,13 @@
 
 
 def test_getrevish_kwarg():
-    assert testfiles.get_revish(revish=u"abcdef") == u"abcdef"
-    assert testfiles.get_revish(revish=u"123456\n") == u"123456"
+    assert testfiles.get_revish(revish="abcdef") == "abcdef"
+    assert testfiles.get_revish(revish="123456\n") == "123456"
 
 
 def test_getrevish_implicit():
-    with patch("tools.wpt.testfiles.branch_point", return_value=u"base"):
-        assert testfiles.get_revish() == u"base..HEAD"
+    with patch("tools.wpt.testfiles.branch_point", return_value="base"):
+        assert testfiles.get_revish() == "base..HEAD"
 
 
 def test_affected_testfiles():
@@ -43,7 +43,7 @@
         full_test_path = os.path.join(
             testfiles.wpt_root, "a", "b", "c", "foo-crash.html")
         tests_changed, _ = testfiles.affected_testfiles([full_test_path])
-        assert tests_changed == set([full_test_path])
+        assert tests_changed == {full_test_path}
 
 
 def test_exclude_ignored():
diff --git a/tools/wpt/virtualenv.py b/tools/wpt/virtualenv.py
index b20a928..149e346 100644
--- a/tools/wpt/virtualenv.py
+++ b/tools/wpt/virtualenv.py
@@ -20,7 +20,7 @@
 
 logger = logging.getLogger(__name__)
 
-class Virtualenv(object):
+class Virtualenv:
     def __init__(self, path, skip_virtualenv_setup):
         self.path = path
         self.skip_virtualenv_setup = skip_virtualenv_setup
@@ -76,7 +76,7 @@
             if IS_WIN:
                 site_packages = os.path.join(base, "Lib", "site-packages")
             else:
-                site_packages = os.path.join(base, "lib", "python{}".format(sys.version[:3]), "site-packages")
+                site_packages = os.path.join(base, "lib", f"python{sys.version[:3]}", "site-packages")
 
         return site_packages
 
diff --git a/tools/wpt/wpt.py b/tools/wpt/wpt.py
index bcab012..87c123e 100644
--- a/tools/wpt/wpt.py
+++ b/tools/wpt/wpt.py
@@ -29,7 +29,7 @@
                     os.path.join(base_dir, path) for path in requirements_paths]
         else:
             raise KeyError(
-                'Unsupported conditional requirement key: {}'.format(key))
+                f'Unsupported conditional requirement key: {key}')
 
     return {
         "commandline_flag": commandline_flag_requirements,
@@ -38,12 +38,12 @@
 
 def load_commands():
     rv = {}
-    with open(os.path.join(here, "paths"), "r") as f:
+    with open(os.path.join(here, "paths")) as f:
         paths = [item.strip().replace("/", os.path.sep) for item in f if item.strip()]
     for path in paths:
         abs_path = os.path.join(wpt_root, path, "commands.json")
         base_dir = os.path.dirname(abs_path)
-        with open(abs_path, "r") as f:
+        with open(abs_path) as f:
             data = json.load(f)
             for command, props in data.items():
                 assert "path" in props
diff --git a/tools/wptrunner/wptrunner/browsers/base.py b/tools/wptrunner/wptrunner/browsers/base.py
index 1ed4e2b..c76b370 100644
--- a/tools/wptrunner/wptrunner/browsers/base.py
+++ b/tools/wptrunner/wptrunner/browsers/base.py
@@ -81,7 +81,7 @@
     pass
 
 
-class Browser(object):
+class Browser:
     """Abstract class serving as the basis for Browser implementations.
 
     The Browser is used in the TestRunnerManager to start and stop the browser
@@ -160,7 +160,7 @@
 
 class NullBrowser(Browser):
     def __init__(self, logger, **kwargs):
-        super(NullBrowser, self).__init__(logger)
+        super().__init__(logger)
 
     def start(self, **kwargs):
         """No-op browser to use in scenarios where the TestRunnerManager shouldn't
@@ -343,7 +343,7 @@
             self._proc.run()
         except OSError as e:
             if e.errno == errno.ENOENT:
-                raise IOError(
+                raise OSError(
                     "WebDriver executable not found: %s" % self.webdriver_binary)
             raise
         self._output_handler.after_process_start(self._proc.pid)
diff --git a/tools/wptrunner/wptrunner/browsers/chrome.py b/tools/wptrunner/wptrunner/browsers/chrome.py
index 8eab89b..0d50d40 100644
--- a/tools/wptrunner/wptrunner/browsers/chrome.py
+++ b/tools/wptrunner/wptrunner/browsers/chrome.py
@@ -95,7 +95,7 @@
         ("https-public", "public"),
     ]
     address_space_overrides_arg = ",".join(
-        "127.0.0.1:{}={}".format(port_number, address_space)
+        f"127.0.0.1:{port_number}={address_space}"
         for port_name, address_space in address_space_overrides_ports
         for port_number in test_environment.config.ports.get(port_name, [])
     )
@@ -124,7 +124,7 @@
     webtranport_h3_port = test_environment.config.ports.get('webtransport-h3')
     if webtranport_h3_port is not None:
         chrome_options["args"].append(
-            "--origin-to-force-quic-on=web-platform.test:{}".format(webtranport_h3_port[0]))
+            f"--origin-to-force-quic-on=web-platform.test:{webtranport_h3_port[0]}")
 
     executor_kwargs["capabilities"] = capabilities
 
diff --git a/tools/wptrunner/wptrunner/browsers/chrome_android.py b/tools/wptrunner/wptrunner/browsers/chrome_android.py
index 957b4c4..c52a17f 100644
--- a/tools/wptrunner/wptrunner/browsers/chrome_android.py
+++ b/tools/wptrunner/wptrunner/browsers/chrome_android.py
@@ -75,7 +75,7 @@
     return {"server_host": "127.0.0.1"}
 
 
-class LogcatRunner(object):
+class LogcatRunner:
     def __init__(self, logger, browser, remote_queue):
         self.logger = logger
         self.browser = browser
diff --git a/tools/wptrunner/wptrunner/browsers/firefox.py b/tools/wptrunner/wptrunner/browsers/firefox.py
index afc7b58..73157c5 100644
--- a/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -360,7 +360,7 @@
     def __init__(self, *args, **kwargs):
         """FirefoxInstanceManager that keeps once Firefox instance preloaded
         to allow rapid resumption after an instance shuts down."""
-        super(PreloadInstanceManager, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.pending = None
 
     def get(self):
@@ -593,7 +593,7 @@
 
         profiles = os.path.join(self.prefs_root, 'profiles.json')
         if os.path.isfile(profiles):
-            with open(profiles, 'r') as fh:
+            with open(profiles) as fh:
                 for name in json.load(fh)['web-platform-tests']:
                     if self.browser_channel in (None, 'nightly'):
                         pref_paths.append(os.path.join(self.prefs_root, name, 'user.js'))
@@ -812,7 +812,7 @@
                                              stackwalk_binary=self.stackwalk_binary,
                                              process=process,
                                              test=test))
-        except IOError:
+        except OSError:
             self.logger.warning("Looking for crash dump files failed")
             return False
 
diff --git a/tools/wptrunner/wptrunner/browsers/firefox_android.py b/tools/wptrunner/wptrunner/browsers/firefox_android.py
index ebee299..8e604a5 100644
--- a/tools/wptrunner/wptrunner/browsers/firefox_android.py
+++ b/tools/wptrunner/wptrunner/browsers/firefox_android.py
@@ -116,9 +116,9 @@
 class ProfileCreator(FirefoxProfileCreator):
     def __init__(self, logger, prefs_root, config, test_type, extra_prefs,
                  enable_fission, debug_test, browser_channel, certutil_binary, ca_certificate_path):
-        super(ProfileCreator, self).__init__(logger, prefs_root, config, test_type, extra_prefs,
-                                             True, enable_fission, debug_test, browser_channel, None,
-                                             certutil_binary, ca_certificate_path)
+        super().__init__(logger, prefs_root, config, test_type, extra_prefs,
+                         True, enable_fission, debug_test, browser_channel, None,
+                         certutil_binary, ca_certificate_path)
 
     def _set_required_prefs(self, profile):
         profile.set_preferences({
@@ -158,7 +158,7 @@
                  install_fonts=False, tests_root=None, specialpowers_path=None, adb_binary=None,
                  debug_test=False, **kwargs):
 
-        super(FirefoxAndroidBrowser, self).__init__(logger)
+        super().__init__(logger)
         self.prefs_root = prefs_root
         self.test_type = test_type
         self.package_name = package_name
@@ -255,14 +255,14 @@
                           interactive=self.debug_info and self.debug_info.interactive)
 
         self.runner.device.device.forward(
-            local="tcp:{}".format(self.marionette_port),
-            remote="tcp:{}".format(self.marionette_port))
+            local=f"tcp:{self.marionette_port}",
+            remote=f"tcp:{self.marionette_port}")
 
         for ports in self.config.ports.values():
             for port in ports:
                 self.runner.device.device.reverse(
-                    local="tcp:{}".format(port),
-                    remote="tcp:{}".format(port))
+                    local=f"tcp:{port}",
+                    remote=f"tcp:{port}")
 
         self.logger.debug("%s Started" % self.package_name)
 
@@ -338,8 +338,8 @@
         for ports in self.config.ports.values():
             for port in ports:
                 self.device.reverse(
-                    local="tcp:{}".format(port),
-                    remote="tcp:{}".format(port))
+                    local=f"tcp:{port}",
+                    remote=f"tcp:{port}")
         super().start(group_metadata, **kwargs)
 
     def stop(self, force=False):
diff --git a/tools/wptrunner/wptrunner/environment.py b/tools/wptrunner/wptrunner/environment.py
index 53dff14..86a9600 100644
--- a/tools/wptrunner/wptrunner/environment.py
+++ b/tools/wptrunner/wptrunner/environment.py
@@ -86,7 +86,7 @@
         self.logging_thread.join(1)
 
 
-class TestEnvironment(object):
+class TestEnvironment:
     """Context manager that owns the test environment i.e. the http and
     websockets servers"""
     def __init__(self, test_paths, testharness_timeout_multipler,
@@ -265,8 +265,8 @@
             if not pending:
                 return
             time.sleep(each_sleep_secs)
-        raise EnvironmentError("Servers failed to start: %s" %
-                               ", ".join("%s:%s" % item for item in failed))
+        raise OSError("Servers failed to start: %s" %
+                      ", ".join("%s:%s" % item for item in failed))
 
     def test_servers(self):
         failed = []
@@ -310,7 +310,7 @@
             so.connect(addr)
         except socket.timeout:
             pass
-        except socket.error as e:
+        except OSError as e:
             if e.errno != errno.ECONNREFUSED:
                 raise
         else:
@@ -319,4 +319,4 @@
         finally:
             so.close()
         time.sleep(0.5)
-    raise socket.error("Service is unavailable: %s:%i" % addr)
+    raise OSError("Service is unavailable: %s:%i" % addr)
diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py
index d4ba89b..817b13b 100644
--- a/tools/wptrunner/wptrunner/executors/actions.py
+++ b/tools/wptrunner/wptrunner/executors/actions.py
@@ -1,5 +1,4 @@
-
-class ClickAction(object):
+class ClickAction:
     name = "click"
 
     def __init__(self, logger, protocol):
@@ -13,7 +12,7 @@
         self.protocol.click.element(element)
 
 
-class DeleteAllCookiesAction(object):
+class DeleteAllCookiesAction:
     name = "delete_all_cookies"
 
     def __init__(self, logger, protocol):
@@ -25,7 +24,7 @@
         self.protocol.cookies.delete_all_cookies()
 
 
-class SendKeysAction(object):
+class SendKeysAction:
     name = "send_keys"
 
     def __init__(self, logger, protocol):
@@ -40,7 +39,7 @@
         self.protocol.send_keys.send_keys(element, keys)
 
 
-class MinimizeWindowAction(object):
+class MinimizeWindowAction:
     name = "minimize_window"
 
     def __init__(self, logger, protocol):
@@ -51,7 +50,7 @@
         return self.protocol.window.minimize()
 
 
-class SetWindowRectAction(object):
+class SetWindowRectAction:
     name = "set_window_rect"
 
     def __init__(self, logger, protocol):
@@ -63,7 +62,7 @@
         self.protocol.window.set_rect(rect)
 
 
-class ActionSequenceAction(object):
+class ActionSequenceAction:
     name = "action_sequence"
 
     def __init__(self, logger, protocol):
@@ -85,7 +84,7 @@
         return self.protocol.select.element_by_selector(element_selector)
 
 
-class GenerateTestReportAction(object):
+class GenerateTestReportAction:
     name = "generate_test_report"
 
     def __init__(self, logger, protocol):
@@ -97,7 +96,7 @@
         self.logger.debug("Generating test report: %s" % message)
         self.protocol.generate_test_report.generate_test_report(message)
 
-class SetPermissionAction(object):
+class SetPermissionAction:
     name = "set_permission"
 
     def __init__(self, logger, protocol):
@@ -113,7 +112,7 @@
         self.logger.debug("Setting permission %s to %s, oneRealm=%s" % (name, state, one_realm))
         self.protocol.set_permission.set_permission(descriptor, state, one_realm)
 
-class AddVirtualAuthenticatorAction(object):
+class AddVirtualAuthenticatorAction:
     name = "add_virtual_authenticator"
 
     def __init__(self, logger, protocol):
@@ -127,7 +126,7 @@
         self.logger.debug("Authenticator created with ID %s" % authenticator_id)
         return authenticator_id
 
-class RemoveVirtualAuthenticatorAction(object):
+class RemoveVirtualAuthenticatorAction:
     name = "remove_virtual_authenticator"
 
     def __init__(self, logger, protocol):
@@ -140,7 +139,7 @@
         return self.protocol.virtual_authenticator.remove_virtual_authenticator(authenticator_id)
 
 
-class AddCredentialAction(object):
+class AddCredentialAction:
     name = "add_credential"
 
     def __init__(self, logger, protocol):
@@ -153,7 +152,7 @@
         self.logger.debug("Adding credential to virtual authenticator %s " % authenticator_id)
         return self.protocol.virtual_authenticator.add_credential(authenticator_id, credential)
 
-class GetCredentialsAction(object):
+class GetCredentialsAction:
     name = "get_credentials"
 
     def __init__(self, logger, protocol):
@@ -165,7 +164,7 @@
         self.logger.debug("Getting credentials from virtual authenticator %s " % authenticator_id)
         return self.protocol.virtual_authenticator.get_credentials(authenticator_id)
 
-class RemoveCredentialAction(object):
+class RemoveCredentialAction:
     name = "remove_credential"
 
     def __init__(self, logger, protocol):
@@ -178,7 +177,7 @@
         self.logger.debug("Removing credential %s from authenticator %s" % (credential_id, authenticator_id))
         return self.protocol.virtual_authenticator.remove_credential(authenticator_id, credential_id)
 
-class RemoveAllCredentialsAction(object):
+class RemoveAllCredentialsAction:
     name = "remove_all_credentials"
 
     def __init__(self, logger, protocol):
@@ -190,7 +189,7 @@
         self.logger.debug("Removing all credentials from authenticator %s" % authenticator_id)
         return self.protocol.virtual_authenticator.remove_all_credentials(authenticator_id)
 
-class SetUserVerifiedAction(object):
+class SetUserVerifiedAction:
     name = "set_user_verified"
 
     def __init__(self, logger, protocol):
@@ -204,7 +203,7 @@
             "Setting user verified flag on authenticator %s to %s" % (authenticator_id, uv["isUserVerified"]))
         return self.protocol.virtual_authenticator.set_user_verified(authenticator_id, uv)
 
-class SetSPCTransactionModeAction(object):
+class SetSPCTransactionModeAction:
     name = "set_spc_transaction_mode"
 
     def __init__(self, logger, protocol):
diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py
index 02760b1..0437987 100644
--- a/tools/wptrunner/wptrunner/executors/base.py
+++ b/tools/wptrunner/wptrunner/executors/base.py
@@ -59,7 +59,7 @@
     return urlunsplit(url_parts)
 
 
-class TestharnessResultConverter(object):
+class TestharnessResultConverter:
     harness_codes = {0: "OK",
                      1: "ERROR",
                      2: "TIMEOUT",
@@ -167,7 +167,7 @@
         self.message = message
 
 
-class TimedRunner(object):
+class TimedRunner:
     def __init__(self, logger, func, protocol, url, timeout, extra_timeout):
         self.func = func
         self.logger = logger
@@ -228,7 +228,7 @@
         raise NotImplementedError
 
 
-class TestExecutor(object):
+class TestExecutor:
     """Abstract Base class for object that actually executes the tests in a
     specific browser. Typically there will be a different TestExecutor
     subclass for each test type and method of executing tests.
@@ -374,7 +374,7 @@
     is_print = True
 
 
-class RefTestImplementation(object):
+class RefTestImplementation:
     def __init__(self, executor):
         self.timeout_multiplier = executor.timeout_multiplier
         self.executor = executor
@@ -646,7 +646,7 @@
                                 timeout=timeout)
 
 
-class WdspecRun(object):
+class WdspecRun:
     def __init__(self, func, path, timeout):
         self.func = func
         self.result = (None, None)
@@ -673,7 +673,7 @@
     def _run(self):
         try:
             self.result = True, self.func(self.path, self.timeout)
-        except (socket.timeout, IOError):
+        except (socket.timeout, OSError):
             self.result = False, ("CRASH", None)
         except Exception as e:
             message = getattr(e, "message")
@@ -685,7 +685,7 @@
             self.result_flag.set()
 
 
-class CallbackHandler(object):
+class CallbackHandler:
     """Handle callbacks from testdriver-using tests.
 
     The default implementation here makes sense for things that are roughly like
@@ -748,7 +748,7 @@
         self.protocol.testdriver.send_message(cmd_id, message_type, status, message=message)
 
 
-class ActionContext(object):
+class ActionContext:
     def __init__(self, logger, protocol, context):
         self.logger = logger
         self.protocol = protocol
diff --git a/tools/wptrunner/wptrunner/executors/executorchrome.py b/tools/wptrunner/wptrunner/executors/executorchrome.py
index 524c86e..e1724ac 100644
--- a/tools/wptrunner/wptrunner/executors/executorchrome.py
+++ b/tools/wptrunner/wptrunner/executors/executorchrome.py
@@ -69,7 +69,7 @@
     protocol_cls = ChromeDriverProtocol
 
     def setup(self, runner):
-        super(ChromeDriverPrintRefTestExecutor, self).setup(runner)
+        super().setup(runner)
         self.protocol.pdf_print.load_runner()
         self.has_window = False
         with open(os.path.join(here, "reftest.js")) as f:
diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py
index cc81888..c5f2b13 100644
--- a/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -71,7 +71,7 @@
 
 class MarionetteBaseProtocolPart(BaseProtocolPart):
     def __init__(self, parent):
-        super(MarionetteBaseProtocolPart, self).__init__(parent)
+        super().__init__(parent)
         self.timeout = None
 
     def setup(self):
@@ -113,7 +113,7 @@
         if socket_timeout:
             try:
                 self.marionette.timeout.script = socket_timeout / 2
-            except IOError:
+            except OSError:
                 self.logger.debug("Socket closed")
                 return
 
@@ -131,7 +131,7 @@
                 # This can happen if we navigate, but just keep going
                 self.logger.debug(e)
                 pass
-            except IOError:
+            except OSError:
                 self.logger.debug("Socket closed")
                 break
             except Exception:
@@ -142,7 +142,7 @@
 
 class MarionetteTestharnessProtocolPart(TestharnessProtocolPart):
     def __init__(self, parent):
-        super(MarionetteTestharnessProtocolPart, self).__init__(parent)
+        super().__init__(parent)
         self.runner_handle = None
         with open(os.path.join(here, "runner.js")) as f:
             self.runner_script = f.read()
@@ -409,7 +409,7 @@
             except errors.NoSuchWindowException:
                 # If the window was already closed
                 self.parent.logger.warning("Failed to get assertion count; window was closed")
-            except (errors.MarionetteException, IOError):
+            except (errors.MarionetteException, OSError):
                 # This usually happens if the process crashed
                 pass
 
@@ -532,7 +532,7 @@
                 error = self.marionette.execute_async_script(script)
                 if error is not None:
                     raise Exception('Failure while resetting counters: %s' % json.dumps(error))
-            except (errors.MarionetteException, IOError):
+            except (errors.MarionetteException, OSError):
                 # This usually happens if the process crashed
                 pass
 
@@ -552,7 +552,7 @@
                 error = self.marionette.execute_async_script(script)
                 if error is not None:
                     raise Exception('Failure while dumping counters: %s' % json.dumps(error))
-            except (errors.MarionetteException, IOError):
+            except (errors.MarionetteException, OSError):
                 # This usually happens if the process crashed
                 pass
 
@@ -703,7 +703,7 @@
     def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False):
         do_delayed_imports()
 
-        super(MarionetteProtocol, self).__init__(executor, browser)
+        super().__init__(executor, browser)
         self.marionette = None
         self.marionette_port = browser.marionette_port
         self.capabilities = capabilities
@@ -730,7 +730,7 @@
             try:
                 self.marionette.raise_for_port()
                 break
-            except IOError:
+            except OSError:
                 # When running in a debugger wait indefinitely for Firefox to start
                 if self.executor.debug_info is None:
                     raise
@@ -753,7 +753,7 @@
                 pass
         if self.marionette is not None:
             self.marionette = None
-        super(MarionetteProtocol, self).teardown()
+        super().teardown()
 
     def is_alive(self):
         try:
@@ -788,7 +788,7 @@
                 # make that possible. It also seems to time out immediately if the
                 # timeout is set too high. This works at least.
                 self.protocol.base.set_timeout(2**28 - 1)
-        except IOError:
+        except OSError:
             msg = "Lost marionette connection before starting test"
             self.logger.error(msg)
             return ("INTERNAL-ERROR", msg)
@@ -805,7 +805,7 @@
         except errors.ScriptTimeoutException:
             self.logger.debug("Got a marionette timeout")
             self.result = False, ("EXTERNAL-TIMEOUT", None)
-        except IOError:
+        except OSError:
             # This can happen on a crash
             # Also, should check after the test if the firefox process is still running
             # and otherwise ignore any other result and set it to crash
@@ -859,7 +859,7 @@
             do_delayed_imports()
 
     def setup(self, runner):
-        super(MarionetteTestharnessExecutor, self).setup(runner)
+        super().setup(runner)
         for extension_path in self.install_extensions:
             self.logger.info("Installing extension from %s" % extension_path)
             addons = Addons(self.protocol.marionette)
@@ -984,7 +984,7 @@
                 else RefTestImplementation)(self)
 
     def setup(self, runner):
-        super(MarionetteRefTestExecutor, self).setup(runner)
+        super().setup(runner)
         for extension_path in self.install_extensions:
             self.logger.info("Installing extension from %s" % extension_path)
             addons = Addons(self.protocol.marionette)
@@ -999,7 +999,7 @@
                 handles = self.protocol.marionette.window_handles
                 if handles:
                     _switch_to_window(self.protocol.marionette, handles[0])
-            super(MarionetteRefTestExecutor, self).teardown()
+            super().teardown()
         except Exception:
             # Ignore errors during teardown
             self.logger.warning("Exception during reftest teardown:\n%s" %
@@ -1238,7 +1238,7 @@
                                            **kwargs)
 
     def setup(self, runner):
-        super(MarionettePrintRefTestExecutor, self).setup(runner)
+        super().setup(runner)
         if not isinstance(self.implementation, InternalRefTestImplementation):
             self.protocol.pdf_print.load_runner()
 
diff --git a/tools/wptrunner/wptrunner/executors/executorselenium.py b/tools/wptrunner/wptrunner/executors/executorselenium.py
index 8ddfcdc..6a47f17 100644
--- a/tools/wptrunner/wptrunner/executors/executorselenium.py
+++ b/tools/wptrunner/wptrunner/executors/executorselenium.py
@@ -74,8 +74,7 @@
 addEventListener("__test_restart", e => {e.preventDefault(); callback(true)})""")
             except exceptions.TimeoutException:
                 pass
-            except (socket.timeout, exceptions.NoSuchWindowException,
-                    exceptions.ErrorInResponseException, IOError):
+            except (socket.timeout, exceptions.NoSuchWindowException, exceptions.ErrorInResponseException, OSError):
                 break
             except Exception:
                 self.logger.error(traceback.format_exc())
@@ -251,7 +250,7 @@
     def __init__(self, executor, browser, capabilities, **kwargs):
         do_delayed_imports()
 
-        super(SeleniumProtocol, self).__init__(executor, browser)
+        super().__init__(executor, browser)
         self.capabilities = capabilities
         self.url = browser.webdriver_url
         self.webdriver = None
diff --git a/tools/wptrunner/wptrunner/executors/executorservo.py b/tools/wptrunner/wptrunner/executors/executorservo.py
index d345cf7..995c75b 100644
--- a/tools/wptrunner/wptrunner/executors/executorservo.py
+++ b/tools/wptrunner/wptrunner/executors/executorservo.py
@@ -157,7 +157,7 @@
         self.result_flag.set()
 
 
-class TempFilename(object):
+class TempFilename:
     def __init__(self, directory):
         self.directory = directory
         self.path = None
diff --git a/tools/wptrunner/wptrunner/executors/executorservodriver.py b/tools/wptrunner/wptrunner/executors/executorservodriver.py
index 89428b5..ebf5bc6 100644
--- a/tools/wptrunner/wptrunner/executors/executorservodriver.py
+++ b/tools/wptrunner/wptrunner/executors/executorservodriver.py
@@ -24,7 +24,7 @@
 
     global ServoCommandExtensions
 
-    class ServoCommandExtensions(object):
+    class ServoCommandExtensions:
         def __init__(self, session):
             self.session = session
 
@@ -127,7 +127,7 @@
 addEventListener("__test_restart", e => {e.preventDefault(); callback(true)})""")
             except webdriver.TimeoutException:
                 pass
-            except (socket.timeout, IOError):
+            except (socket.timeout, OSError):
                 break
             except Exception:
                 self.logger.error(traceback.format_exc())
@@ -144,7 +144,7 @@
             self.result = True, self.func(self.protocol.session, self.url, self.timeout)
         except webdriver.TimeoutException:
             self.result = False, ("EXTERNAL-TIMEOUT", None)
-        except (socket.timeout, IOError):
+        except (socket.timeout, OSError):
             self.result = False, ("CRASH", None)
         except Exception as e:
             message = getattr(e, "message", "")
@@ -184,7 +184,7 @@
             try:
                 self.protocol.session.timeouts.script = timeout
                 self.timeout = timeout
-            except IOError:
+            except OSError:
                 msg = "Lost WebDriver connection"
                 self.logger.error(msg)
                 return ("INTERNAL-ERROR", msg)
@@ -254,7 +254,7 @@
         try:
             result = self.implementation.run_test(test)
             return self.convert_result(test, result)
-        except IOError:
+        except OSError:
             return test.result_cls("CRASH", None), []
         except TimeoutError:
             return test.result_cls("TIMEOUT", None), []
@@ -277,7 +277,7 @@
             try:
                 self.protocol.session.timeouts.script = timeout
                 self.timeout = timeout
-            except IOError:
+            except OSError:
                 msg = "Lost webdriver connection"
                 self.logger.error(msg)
                 return ("INTERNAL-ERROR", msg)
diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py
index 7e95198..1dcd2b8 100644
--- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py
+++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py
@@ -83,10 +83,7 @@
                 # by ignoring it it's possible to reload the test whilst the
                 # harness remains paused
                 pass
-            except (socket.timeout,
-                    error.NoSuchWindowException,
-                    error.UnknownErrorException,
-                    IOError):
+            except (socket.timeout, error.NoSuchWindowException, error.UnknownErrorException, OSError):
                 break
             except Exception:
                 self.logger.error(traceback.format_exc())
@@ -344,7 +341,7 @@
                   WebDriverDebugProtocolPart]
 
     def __init__(self, executor, browser, capabilities, **kwargs):
-        super(WebDriverProtocol, self).__init__(executor, browser)
+        super().__init__(executor, browser)
         self.capabilities = capabilities
         if hasattr(browser, "capabilities"):
             if self.capabilities is None:
diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py
index 0eeebfc..0c01e8e 100644
--- a/tools/wptrunner/wptrunner/executors/protocol.py
+++ b/tools/wptrunner/wptrunner/executors/protocol.py
@@ -17,7 +17,7 @@
             else:
                 target[key] = source_value
 
-class Protocol(object):
+class Protocol:
     """Backend for a specific browser-control protocol.
 
     Each Protocol is composed of a set of ProtocolParts that implement
@@ -90,7 +90,7 @@
             getattr(self, cls.name).teardown()
 
 
-class ProtocolPart(object):
+class ProtocolPart:
     """Base class  for all ProtocolParts.
 
     :param Protocol parent: The parent protocol"""
diff --git a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
index de257b7..d14ffbe 100644
--- a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
+++ b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
@@ -80,7 +80,7 @@
     return (harness.outcome, subtests.results)
 
 
-class HarnessResultRecorder(object):
+class HarnessResultRecorder:
     outcomes = {
         "failed": "ERROR",
         "passed": "OK",
@@ -96,7 +96,7 @@
         self.outcome = (harness_result, None)
 
 
-class SubtestResultRecorder(object):
+class SubtestResultRecorder:
     def __init__(self):
         self.results = []
 
@@ -131,7 +131,7 @@
 
     def record_error(self, report, message):
         # error in setup/teardown
-        message = "{} error: {}".format(report.when, message)
+        message = f"{report.when} error: {message}"
         self.record(report.nodeid, "ERROR", message, report.longrepr)
 
     def record_skip(self, report):
@@ -146,7 +146,7 @@
         self.results.append(new_result)
 
 
-class TemporaryDirectory(object):
+class TemporaryDirectory:
     def __enter__(self):
         self.path = tempfile.mkdtemp(prefix="wdspec-")
         return self.path
diff --git a/tools/wptrunner/wptrunner/expectedtree.py b/tools/wptrunner/wptrunner/expectedtree.py
index 76ade95..0cc442d 100644
--- a/tools/wptrunner/wptrunner/expectedtree.py
+++ b/tools/wptrunner/wptrunner/expectedtree.py
@@ -1,7 +1,7 @@
 from math import log
 from collections import defaultdict
 
-class Node(object):
+class Node:
     def __init__(self, prop, value):
         self.prop = prop
         self.value = value
@@ -20,8 +20,7 @@
     def __iter__(self):
         yield self
         for node in self.children:
-            for item in node:
-                yield item
+            yield from node
 
     def __len__(self):
         return 1 + sum(len(item) for item in self.children)
diff --git a/tools/wptrunner/wptrunner/font.py b/tools/wptrunner/wptrunner/font.py
index 39e248b..c77dbfa 100644
--- a/tools/wptrunner/wptrunner/font.py
+++ b/tools/wptrunner/wptrunner/font.py
@@ -10,7 +10,7 @@
 SYSTEM = platform.system().lower()
 
 
-class FontInstaller(object):
+class FontInstaller:
     def __init__(self, logger, font_dir=None, **fonts):
         self.logger = logger
         self.font_dir = font_dir
diff --git a/tools/wptrunner/wptrunner/formatters/wptreport.py b/tools/wptrunner/wptrunner/formatters/wptreport.py
index 86d4c42..b39150e 100644
--- a/tools/wptrunner/wptrunner/formatters/wptreport.py
+++ b/tools/wptrunner/wptrunner/formatters/wptreport.py
@@ -5,7 +5,7 @@
 from ..executors.base import strip_server
 
 
-LONE_SURROGATE_RE = re.compile(u"[\uD800-\uDFFF]")
+LONE_SURROGATE_RE = re.compile("[\uD800-\uDFFF]")
 
 
 def surrogate_replacement(match):
diff --git a/tools/wptrunner/wptrunner/instruments.py b/tools/wptrunner/wptrunner/instruments.py
index 3f9ffdc..3af0906 100644
--- a/tools/wptrunner/wptrunner/instruments.py
+++ b/tools/wptrunner/wptrunner/instruments.py
@@ -24,7 +24,7 @@
     do_teardown()
 """
 
-class NullInstrument(object):
+class NullInstrument:
     def set(self, stack):
         """Set the current task to stack
 
@@ -46,7 +46,7 @@
         return
 
 
-class InstrumentWriter(object):
+class InstrumentWriter:
     def __init__(self, queue):
         self.queue = queue
 
@@ -63,7 +63,7 @@
         return [item.replace(" ", "_") for item in stack]
 
 
-class Instrument(object):
+class Instrument:
     def __init__(self, file_path):
         """Instrument that collects data from multiple threads and sums the time in each
         thread. The output is in the format required by flamegraph.pl to enable visualisation
diff --git a/tools/wptrunner/wptrunner/manifestexpected.py b/tools/wptrunner/wptrunner/manifestexpected.py
index 1cd035a..3ee00e5 100644
--- a/tools/wptrunner/wptrunner/manifestexpected.py
+++ b/tools/wptrunner/wptrunner/manifestexpected.py
@@ -187,8 +187,8 @@
         arg_values = {None: deque()}
         for range_str_value in ranges:
             if "=" in range_str_value:
-                name, range_str_value = [part.strip()
-                                         for part in range_str_value.split("=", 1)]
+                name, range_str_value = (part.strip()
+                                         for part in range_str_value.split("=", 1))
                 if name not in args:
                     raise ValueError("%s is not a valid fuzzy property" % name)
                 if arg_values.get(name):
@@ -519,7 +519,7 @@
                                   data_cls_getter=data_cls_getter,
                                   test_path=test_path,
                                   url_base=url_base)
-    except IOError:
+    except OSError:
         return None
 
 
@@ -536,5 +536,5 @@
             return static.compile(f,
                                   run_info,
                                   data_cls_getter=lambda x,y: DirectoryManifest)
-    except IOError:
+    except OSError:
         return None
diff --git a/tools/wptrunner/wptrunner/manifestupdate.py b/tools/wptrunner/wptrunner/manifestupdate.py
index b25d49b..6aea391 100644
--- a/tools/wptrunner/wptrunner/manifestupdate.py
+++ b/tools/wptrunner/wptrunner/manifestupdate.py
@@ -59,7 +59,7 @@
         raise ValueError
 
 
-class UpdateProperties(object):
+class UpdateProperties:
     def __init__(self, manifest, **kwargs):
         self._manifest = manifest
         self._classes = kwargs
@@ -319,7 +319,7 @@
     return root
 
 
-class PropertyUpdate(object):
+class PropertyUpdate:
     property_name = None  # type: ClassVar[str]
     cls_default_value = None  # type: ClassVar[Any]
     value_type = None  # type: ClassVar[type]
@@ -517,8 +517,8 @@
             for item in dependent_props.values():
                 update_properties |= set(item)
             for condition in current_conditions:
-                if ((not condition.variables.issubset(update_properties) and
-                     not run_info_by_condition[condition])):
+                if (not condition.variables.issubset(update_properties) and
+                    not run_info_by_condition[condition]):
                     conditions.append((condition.condition_node,
                                        self.from_ini_value(condition.value)))
 
@@ -940,7 +940,7 @@
         with open(manifest_path, "rb") as f:
             rv = compile(f, test_path, url_base,
                          run_info_properties, update_intermittent, remove_intermittent)
-    except IOError:
+    except OSError:
         return None
     return rv
 
diff --git a/tools/wptrunner/wptrunner/metadata.py b/tools/wptrunner/wptrunner/metadata.py
index 9c54e57..bf78d6b 100644
--- a/tools/wptrunner/wptrunner/metadata.py
+++ b/tools/wptrunner/wptrunner/metadata.py
@@ -24,7 +24,7 @@
     import json  # type: ignore
 
 
-class RunInfo(object):
+class RunInfo:
     """A wrapper around RunInfo dicts so that they can be hashed by identity"""
 
     def __init__(self, dict_value):
@@ -44,8 +44,7 @@
         return self.canonical_repr == other.canonical_repr
 
     def iteritems(self):
-        for key, value in self.data.items():
-            yield key, value
+        yield from self.data.items()
 
     def items(self):
         return list(self.items())
@@ -153,7 +152,7 @@
 #   Check if all the RHS values are the same; if so collapse the conditionals
 
 
-class InternedData(object):
+class InternedData:
     """Class for interning data of any (hashable) type.
 
     This class is intended for building a mapping of int <=> value, such
@@ -275,10 +274,9 @@
         with open(log_filename) as f:
             updater.update_from_log(f)
 
-    for item in update_results(id_test_map, update_properties, full_update,
-                               disable_intermittent, update_intermittent=update_intermittent,
-                               remove_intermittent=remove_intermittent):
-        yield item
+    yield from update_results(id_test_map, update_properties, full_update,
+                              disable_intermittent, update_intermittent=update_intermittent,
+                              remove_intermittent=remove_intermittent)
 
 
 def update_results(id_test_map,
@@ -340,7 +338,7 @@
             pass
 
 
-class ExpectedUpdater(object):
+class ExpectedUpdater:
     def __init__(self, id_test_map):
         self.id_test_map = id_test_map
         self.run_info = None
@@ -568,7 +566,7 @@
     return id_test_map
 
 
-class PackedResultList(object):
+class PackedResultList:
     """Class for storing test results.
 
     Results are stored as an array of 2-byte integers for compactness.
@@ -615,7 +613,7 @@
             yield self.unpack(i, item)
 
 
-class TestFileData(object):
+class TestFileData:
     __slots__ = ("url_base", "item_type", "test_path", "metadata_path", "tests",
                  "_requires_update", "data")
 
@@ -669,7 +667,7 @@
             test = expected.get_test(ensure_text(test_id))
             if not test:
                 continue
-            seen_subtests = set(ensure_text(item) for item in subtests.keys() if item is not None)
+            seen_subtests = {ensure_text(item) for item in subtests.keys() if item is not None}
             missing_subtests = set(test.subtests.keys()) - seen_subtests
             for item in missing_subtests:
                 expected_subtest = test.get_subtest(item)
diff --git a/tools/wptrunner/wptrunner/products.py b/tools/wptrunner/wptrunner/products.py
index cf09d90..e20cb2e 100644
--- a/tools/wptrunner/wptrunner/products.py
+++ b/tools/wptrunner/wptrunner/products.py
@@ -20,7 +20,7 @@
     return module
 
 
-class Product(object):
+class Product:
     def __init__(self, config, product):
         module = product_module(config, product)
         data = module.__wptrunner__
diff --git a/tools/wptrunner/wptrunner/testloader.py b/tools/wptrunner/wptrunner/testloader.py
index 2611da2..220c336 100644
--- a/tools/wptrunner/wptrunner/testloader.py
+++ b/tools/wptrunner/wptrunner/testloader.py
@@ -25,7 +25,7 @@
     from manifest.download import download_from_github  # type: ignore
 
 
-class TestGroupsFile(object):
+class TestGroupsFile:
     """
     Mapping object representing {group name: [test ids]}
     """
@@ -74,7 +74,7 @@
     return new_include
 
 
-class TestChunker(object):
+class TestChunker:
     def __init__(self, total_chunks, chunk_number, **kwargs):
         self.total_chunks = total_chunks
         self.chunk_number = chunk_number
@@ -93,8 +93,7 @@
         assert self.total_chunks == 1
 
     def __call__(self, manifest, **kwargs):
-        for item in manifest:
-            yield item
+        yield from manifest
 
 
 class HashChunker(TestChunker):
@@ -125,7 +124,7 @@
                 yield test_type, test_path, tests
 
 
-class TestFilter(object):
+class TestFilter:
     """Callable that restricts the set of tests in a given manifest according
     to initial criteria"""
     def __init__(self, test_manifests, include=None, exclude=None, manifest_path=None, explicit=False):
@@ -157,7 +156,7 @@
                 yield test_type, test_path, include_tests
 
 
-class TagFilter(object):
+class TagFilter:
     def __init__(self, tags):
         self.tags = set(tags)
 
@@ -167,7 +166,7 @@
                 yield test
 
 
-class ManifestLoader(object):
+class ManifestLoader:
     def __init__(self, test_paths, force_manifest_update=False, manifest_download=False,
                  types=None):
         do_delayed_imports()
@@ -201,11 +200,10 @@
 def iterfilter(filters, iter):
     for f in filters:
         iter = f(iter)
-    for item in iter:
-        yield item
+    yield from iter
 
 
-class TestLoader(object):
+class TestLoader:
     """Loads tests according to a WPT manifest and any associated expectation files"""
     def __init__(self,
                  test_manifests,
@@ -359,7 +357,7 @@
     return test_source_cls, test_source_kwargs, chunker_kwargs
 
 
-class TestSource(object):
+class TestSource:
     __metaclass__ = ABCMeta
 
     def __init__(self, test_queue):
diff --git a/tools/wptrunner/wptrunner/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py
index 0c75683..6a5f4f2 100644
--- a/tools/wptrunner/wptrunner/testrunner.py
+++ b/tools/wptrunner/wptrunner/testrunner.py
@@ -30,7 +30,7 @@
         self.send_message("log", data)
 
 
-class TestRunner(object):
+class TestRunner:
     """Class implementing the main loop for running tests.
 
     This class delegates the job of actually running a test to the executor
@@ -158,7 +158,7 @@
             handle_error(e)
 
 
-class BrowserManager(object):
+class BrowserManager:
     def __init__(self, logger, browser, command_queue, no_timeout=False):
         self.logger = logger
         self.browser = browser
@@ -242,7 +242,7 @@
         return self.browser.is_alive()
 
 
-class _RunnerManagerState(object):
+class _RunnerManagerState:
     before_init = namedtuple("before_init", [])
     initializing = namedtuple("initializing",
                               ["test", "test_group", "group_metadata", "failure_count"])
@@ -416,7 +416,7 @@
         try:
             command, data = self.command_queue.get(True, 1)
             self.logger.debug("Got command: %r" % command)
-        except IOError:
+        except OSError:
             self.logger.error("Got IOError from poll")
             return RunnerManagerState.restarting(self.state.test,
                                                  self.state.test_group,
@@ -850,7 +850,7 @@
     return queue
 
 
-class ManagerGroup(object):
+class ManagerGroup:
     """Main thread object that owns all the TestRunnerManager threads."""
     def __init__(self, suite_name, size, test_source_cls, test_source_kwargs,
                  browser_cls, browser_kwargs,
diff --git a/tools/wptrunner/wptrunner/tests/base.py b/tools/wptrunner/wptrunner/tests/base.py
index 35c4582..c8f3122 100644
--- a/tools/wptrunner/wptrunner/tests/base.py
+++ b/tools/wptrunner/wptrunner/tests/base.py
@@ -29,7 +29,7 @@
     _active_products = set(_products)
 
 
-class all_products(object):
+class all_products:
     def __init__(self, arg, marks={}):
         self.arg = arg
         self.marks = marks
@@ -44,7 +44,7 @@
         return pytest.mark.parametrize(self.arg, params)(f)
 
 
-class active_products(object):
+class active_products:
     def __init__(self, arg, marks={}):
         self.arg = arg
         self.marks = marks
diff --git a/tools/wptrunner/wptrunner/tests/test_formatters.py b/tools/wptrunner/wptrunner/tests/test_formatters.py
index 04526da..25d0f6d 100644
--- a/tools/wptrunner/wptrunner/tests/test_formatters.py
+++ b/tools/wptrunner/wptrunner/tests/test_formatters.py
@@ -70,12 +70,12 @@
     logger.suite_start(["test-id-1"])  # no run_info arg!
     logger.test_start("test-id-1")
     logger.test_status("test-id-1",
-                       subtest=u"Name with surrogate\uD800",
+                       subtest="Name with surrogate\uD800",
                        status="FAIL",
-                       message=u"\U0001F601 \uDE0A\uD83D")
+                       message="\U0001F601 \uDE0A\uD83D")
     logger.test_end("test-id-1",
                     status="PASS",
-                    message=u"\uDE0A\uD83D \U0001F601")
+                    message="\uDE0A\uD83D \U0001F601")
     logger.suite_end()
 
     # check nothing got output to stdout/stderr
@@ -88,10 +88,10 @@
     output.seek(0)
     output_obj = json.load(output)
     test = output_obj["results"][0]
-    assert test["message"] == u"U+de0aU+d83d \U0001F601"
+    assert test["message"] == "U+de0aU+d83d \U0001F601"
     subtest = test["subtests"][0]
-    assert subtest["name"] == u"Name with surrogateU+d800"
-    assert subtest["message"] == u"\U0001F601 U+de0aU+d83d"
+    assert subtest["name"] == "Name with surrogateU+d800"
+    assert subtest["message"] == "\U0001F601 U+de0aU+d83d"
 
 
 def test_wptreport_known_intermittent(capfd):
@@ -121,10 +121,10 @@
     output.seek(0)
     output_obj = json.load(output)
     test = output_obj["results"][0]
-    assert test["status"] == u"OK"
+    assert test["status"] == "OK"
     subtest = test["subtests"][0]
-    assert subtest["expected"] == u"PASS"
-    assert subtest["known_intermittent"] == [u'FAIL']
+    assert subtest["expected"] == "PASS"
+    assert subtest["known_intermittent"] == ['FAIL']
 
 
 def test_wptscreenshot_test_end(capfd):
diff --git a/tools/wptrunner/wptrunner/tests/test_manifestexpected.py b/tools/wptrunner/wptrunner/tests/test_manifestexpected.py
index f3e4ce7..1f96df0 100644
--- a/tools/wptrunner/wptrunner/tests/test_manifestexpected.py
+++ b/tools/wptrunner/wptrunner/tests/test_manifestexpected.py
@@ -14,12 +14,12 @@
     (b"totalPixels=200;1", [(None, ((1, 1), (200, 200)))]),
     (b"maxDifference=1;200", [(None, ((1, 1), (200, 200)))]),
     (b"test.html==ref.html:maxDifference=1;totalPixels=200",
-     [((u"test.html", u"ref.html", "=="), ((1, 1), (200, 200)))]),
+     [(("test.html", "ref.html", "=="), ((1, 1), (200, 200)))]),
     (b"test.html!=ref.html:maxDifference=1;totalPixels=200",
-     [((u"test.html", u"ref.html", "!="), ((1, 1), (200, 200)))]),
+     [(("test.html", "ref.html", "!="), ((1, 1), (200, 200)))]),
     (b"[test.html!=ref.html:maxDifference=1;totalPixels=200, test.html==ref1.html:maxDifference=5-10;100]",
-     [((u"test.html", u"ref.html", "!="), ((1, 1), (200, 200))),
-      ((u"test.html", u"ref1.html", "=="), ((5,10), (100, 100)))]),
+     [(("test.html", "ref.html", "!="), ((1, 1), (200, 200))),
+      (("test.html", "ref1.html", "=="), ((5,10), (100, 100)))]),
 ])
 def test_fuzzy(fuzzy, expected):
     data = b"""
diff --git a/tools/wptrunner/wptrunner/tests/test_stability.py b/tools/wptrunner/wptrunner/tests/test_stability.py
index 9a8fccf..cdcf4de 100644
--- a/tools/wptrunner/wptrunner/tests/test_stability.py
+++ b/tools/wptrunner/wptrunner/tests/test_stability.py
@@ -121,22 +121,22 @@
 
 def test_err_string():
     assert stability.err_string(
-        {u'OK': 1, u'FAIL': 1}, 1) == u"**Duplicate subtest name**"
+        {'OK': 1, 'FAIL': 1}, 1) == "**Duplicate subtest name**"
     assert stability.err_string(
-        {u'OK': 2, u'FAIL': 1}, 2) == u"**Duplicate subtest name**"
-    assert stability.err_string({u'SKIP': 1}, 0) == u"Duplicate subtest name"
+        {'OK': 2, 'FAIL': 1}, 2) == "**Duplicate subtest name**"
+    assert stability.err_string({'SKIP': 1}, 0) == "Duplicate subtest name"
     assert stability.err_string(
-        {u'SKIP': 1, u'OK': 1}, 1) == u"Duplicate subtest name"
+        {'SKIP': 1, 'OK': 1}, 1) == "Duplicate subtest name"
 
     assert stability.err_string(
-        {u'FAIL': 1}, 2) == u"**FAIL: 1/2, MISSING: 1/2**"
+        {'FAIL': 1}, 2) == "**FAIL: 1/2, MISSING: 1/2**"
     assert stability.err_string(
-        {u'FAIL': 1, u'OK': 1}, 3) == u"**FAIL: 1/3, OK: 1/3, MISSING: 1/3**"
+        {'FAIL': 1, 'OK': 1}, 3) == "**FAIL: 1/3, OK: 1/3, MISSING: 1/3**"
 
     assert stability.err_string(
-        {u'OK': 1, u'FAIL': 1}, 2) == u"**FAIL: 1/2, OK: 1/2**"
+        {'OK': 1, 'FAIL': 1}, 2) == "**FAIL: 1/2, OK: 1/2**"
 
     assert stability.err_string(
-        {u'OK': 2, u'FAIL': 1, u'SKIP': 1}, 4) == u"FAIL: 1/4, OK: 2/4, SKIP: 1/4"
+        {'OK': 2, 'FAIL': 1, 'SKIP': 1}, 4) == "FAIL: 1/4, OK: 2/4, SKIP: 1/4"
     assert stability.err_string(
-        {u'FAIL': 1, u'SKIP': 1, u'OK': 2}, 4) == u"FAIL: 1/4, OK: 2/4, SKIP: 1/4"
+        {'FAIL': 1, 'SKIP': 1, 'OK': 2}, 4) == "FAIL: 1/4, OK: 2/4, SKIP: 1/4"
diff --git a/tools/wptrunner/wptrunner/update/base.py b/tools/wptrunner/wptrunner/update/base.py
index 47ea7ff..41c90eb 100644
--- a/tools/wptrunner/wptrunner/update/base.py
+++ b/tools/wptrunner/wptrunner/update/base.py
@@ -4,7 +4,7 @@
 exit_clean = object()
 
 
-class Step(object):
+class Step:
     provides = []  # type: ClassVar[List[str]]
 
     def __init__(self, logger):
@@ -46,7 +46,7 @@
             assert key in state
 
 
-class StepRunner(object):
+class StepRunner:
     steps = []  # type: ClassVar[List[Type[Step]]]
 
     def __init__(self, logger, state):
diff --git a/tools/wptrunner/wptrunner/update/state.py b/tools/wptrunner/wptrunner/update/state.py
index a526c24..8bc549c 100644
--- a/tools/wptrunner/wptrunner/update/state.py
+++ b/tools/wptrunner/wptrunner/update/state.py
@@ -3,7 +3,7 @@
 
 here = os.path.abspath(os.path.dirname(__file__))
 
-class BaseState(object):
+class BaseState:
     def __new__(cls, logger):
         rv = cls.load(logger)
         if rv is not None:
@@ -11,7 +11,7 @@
             return rv
 
         logger.debug("No existing state found")
-        return super(BaseState, cls).__new__(cls)
+        return super().__new__(cls)
 
     def __init__(self, logger):
         """Object containing state variables created when running Steps.
@@ -106,7 +106,7 @@
                     return rv
                 except EOFError:
                     logger.warning("Found empty state file")
-        except IOError:
+        except OSError:
             logger.debug("IOError loading stored state")
 
     def save(self):
@@ -115,7 +115,7 @@
             pickle.dump(self, f)
 
     def clear(self):
-        super(SavedState, self).clear()
+        super().clear()
         try:
             os.unlink(self.filename)
         except OSError:
@@ -131,7 +131,7 @@
         return
 
 
-class StateContext(object):
+class StateContext:
     def __init__(self, state, init_values):
         self.state = state
         self.init_values = init_values
diff --git a/tools/wptrunner/wptrunner/update/tree.py b/tools/wptrunner/wptrunner/update/tree.py
index 2f43bac..ddbf524 100644
--- a/tools/wptrunner/wptrunner/update/tree.py
+++ b/tools/wptrunner/wptrunner/update/tree.py
@@ -22,7 +22,7 @@
             return test
     assert False
 
-class NoVCSTree(object):
+class NoVCSTree:
     name = "non-vcs"
 
     def __init__(self, root=None):
@@ -54,7 +54,7 @@
         pass
 
 
-class HgTree(object):
+class HgTree:
     name = "mercurial"
 
     def __init__(self, root=None):
@@ -135,7 +135,7 @@
             return False
 
 
-class GitTree(object):
+class GitTree:
     name = "git"
 
     def __init__(self, root=None, log_error=True):
@@ -359,7 +359,7 @@
             return False
 
 
-class CommitMessage(object):
+class CommitMessage:
     def __init__(self, text):
         self.text = text
         self._parse_message()
@@ -373,7 +373,7 @@
         self.body = "\n".join(lines[1:])
 
 
-class Commit(object):
+class Commit:
     msg_cls = CommitMessage
 
     _sha1_re = re.compile("^[0-9a-f]{40}$")
diff --git a/tools/wptrunner/wptrunner/update/update.py b/tools/wptrunner/wptrunner/update/update.py
index 265a331..388d64b 100644
--- a/tools/wptrunner/wptrunner/update/update.py
+++ b/tools/wptrunner/wptrunner/update/update.py
@@ -133,7 +133,7 @@
              UpdateMetadata]
 
 
-class WPTUpdate(object):
+class WPTUpdate:
     def __init__(self, logger, runner_cls=UpdateRunner, **kwargs):
         """Object that controls the running of a whole wptupdate.
 
diff --git a/tools/wptrunner/wptrunner/wptlogging.py b/tools/wptrunner/wptrunner/wptlogging.py
index b5cb588..4e87092 100644
--- a/tools/wptrunner/wptrunner/wptlogging.py
+++ b/tools/wptrunner/wptrunner/wptlogging.py
@@ -27,7 +27,7 @@
     logging.root = stdadapter.std_logging_adapter(logging.root)
 
 
-class LogLevelRewriter(object):
+class LogLevelRewriter:
     """Filter that replaces log messages at specified levels with messages
     at a different level.
 
@@ -50,7 +50,7 @@
         return self.inner(data)
 
 
-class LoggedAboveLevelHandler(object):
+class LoggedAboveLevelHandler:
     """Filter that records whether any log message above a certain level has been
     seen.
 
@@ -99,7 +99,7 @@
         while True:
             try:
                 data = self.queue.get()
-            except (EOFError, IOError):
+            except (EOFError, OSError):
                 break
             if data is None:
                 # A None message is used to shut down the logging thread
diff --git a/tools/wptrunner/wptrunner/wptmanifest/backends/base.py b/tools/wptrunner/wptrunner/wptmanifest/backends/base.py
index 3069e4c..fc221ec 100644
--- a/tools/wptrunner/wptrunner/wptmanifest/backends/base.py
+++ b/tools/wptrunner/wptrunner/wptmanifest/backends/base.py
@@ -122,7 +122,7 @@
         pass
 
 
-class ManifestItem(object):
+class ManifestItem:
     def __init__(self, node, **kwargs):
         self.parent = None
         self.node = node
@@ -192,16 +192,13 @@
         return rv
 
     def iteritems(self):
-        for item in self._flatten().items():
-            yield item
+        yield from self._flatten().items()
 
     def iterkeys(self):
-        for item in self._flatten().keys():
-            yield item
+        yield from self._flatten().keys()
 
     def itervalues(self):
-        for item in self._flatten().values():
-            yield item
+        yield from self._flatten().values()
 
     def append(self, child):
         child.parent = self
diff --git a/tools/wptrunner/wptrunner/wptmanifest/backends/conditional.py b/tools/wptrunner/wptrunner/wptmanifest/backends/conditional.py
index 30dd144..fa1cc85 100644
--- a/tools/wptrunner/wptrunner/wptmanifest/backends/conditional.py
+++ b/tools/wptrunner/wptrunner/wptmanifest/backends/conditional.py
@@ -5,7 +5,7 @@
 from ..parser import parse
 
 
-class ConditionalValue(object):
+class ConditionalValue:
     def __init__(self, node, condition_func):
         self.node = node
         assert callable(condition_func)
@@ -213,7 +213,7 @@
                 "!=": operator.ne}[node.data]
 
 
-class ManifestItem(object):
+class ManifestItem:
     def __init__(self, node=None, **kwargs):
         self.node = node
         self.parent = None
@@ -235,8 +235,7 @@
     def __iter__(self):
         yield self
         for child in self.children:
-            for node in child:
-                yield node
+            yield from child
 
     @property
     def is_empty(self):
@@ -374,12 +373,10 @@
         return rv
 
     def iteritems(self):
-        for item in self._flatten().items():
-            yield item
+        yield from self._flatten().items()
 
     def iterkeys(self):
-        for item in self._flatten().keys():
-            yield item
+        yield from self._flatten().keys()
 
     def iter_properties(self):
         for item in self._data:
diff --git a/tools/wptrunner/wptrunner/wptmanifest/node.py b/tools/wptrunner/wptrunner/wptmanifest/node.py
index 5e9d2b6..6a58227 100644
--- a/tools/wptrunner/wptrunner/wptmanifest/node.py
+++ b/tools/wptrunner/wptrunner/wptmanifest/node.py
@@ -1,4 +1,4 @@
-class NodeVisitor(object):
+class NodeVisitor:
     def visit(self, node):
         # This is ugly as hell, but we don't have multimethods and
         # they aren't trivial to fake without access to the class
@@ -7,7 +7,7 @@
         return func(node)
 
 
-class Node(object):
+class Node:
     def __init__(self, data=None):
         self.data = data
         self.parent = None
diff --git a/tools/wptrunner/wptrunner/wptmanifest/parser.py b/tools/wptrunner/wptrunner/wptmanifest/parser.py
index 4c97c69..2475a2a 100644
--- a/tools/wptrunner/wptrunner/wptmanifest/parser.py
+++ b/tools/wptrunner/wptrunner/wptmanifest/parser.py
@@ -55,7 +55,7 @@
     return len(operators) - operators.index(operator_node.data)
 
 
-class TokenTypes(object):
+class TokenTypes:
     def __init__(self) -> None:
         for type in ["group_start", "group_end", "paren", "list_start", "list_end", "separator", "ident", "string", "number", "atom", "eof"]:
             setattr(self, type, type)
@@ -63,7 +63,7 @@
 token_types = TokenTypes()
 
 
-class Tokenizer(object):
+class Tokenizer:
     def __init__(self):
         self.reset()
 
@@ -99,8 +99,7 @@
                 states.append(self.state)
                 tokens = self.state()
                 if tokens:
-                    for token in tokens:
-                        yield token
+                    yield from tokens
             self.state()
         while True:
             yield (token_types.eof, None)
@@ -515,7 +514,7 @@
             raise ParseError(self.filename, self.line_number, "Invalid character escape")
 
 
-class Parser(object):
+class Parser:
     def __init__(self):
         self.reset()
 
@@ -549,11 +548,11 @@
     def expect(self, type, value=None):
         if self.token[0] != type:
             raise ParseError(self.tokenizer.filename, self.tokenizer.line_number,
-                             "Token '{}' doesn't equal expected type '{}'".format(self.token[0], type))
+                             f"Token '{self.token[0]}' doesn't equal expected type '{type}'")
         if value is not None:
             if self.token[1] != value:
                 raise ParseError(self.tokenizer.filename, self.tokenizer.line_number,
-                                 "Token '{}' doesn't equal expected value '{}'".format(self.token[1], value))
+                                 f"Token '{self.token[1]}' doesn't equal expected value '{value}'")
 
         self.consume()
 
@@ -573,7 +572,7 @@
             self.consume()
             if self.token[0] != token_types.string:
                 raise ParseError(self.tokenizer.filename, self.tokenizer.line_number,
-                                 "Token '{}' is not a string".format(self.token[0]))
+                                 f"Token '{self.token[0]}' is not a string")
             self.tree.append(DataNode(self.token[1]))
             self.consume()
             self.expect(token_types.paren, "]")
@@ -606,7 +605,7 @@
             self.atom()
         else:
             raise ParseError(self.tokenizer.filename, self.tokenizer.line_number,
-                             "Token '{}' is not a known type".format(self.token[0]))
+                             f"Token '{self.token[0]}' is not a known type")
 
     def list_value(self):
         self.tree.append(ListNode())
@@ -705,7 +704,7 @@
         self.consume()
 
 
-class Treebuilder(object):
+class Treebuilder:
     def __init__(self, root):
         self.root = root
         self.node = root
@@ -724,7 +723,7 @@
         return node
 
 
-class ExpressionBuilder(object):
+class ExpressionBuilder:
     def __init__(self, tokenizer):
         self.operands = []
         self.operators = [None]
diff --git a/tools/wptrunner/wptrunner/wptmanifest/tests/test_serializer.py b/tools/wptrunner/wptrunner/wptmanifest/tests/test_serializer.py
index 02e5281..fdb4509 100644
--- a/tools/wptrunner/wptrunner/wptmanifest/tests/test_serializer.py
+++ b/tools/wptrunner/wptrunner/wptmanifest/tests/test_serializer.py
@@ -156,27 +156,27 @@
 
     def test_escape_2(self):
         self.compare(br"""k\u0045y: \u1234A\uABc6""",
-                     u"""kEy: \u1234A\uabc6
+                     """kEy: \u1234A\uabc6
 """)
 
     def test_escape_3(self):
         self.compare(br"""k\u0045y: \u1234A\uABc6""",
-                     u"""kEy: \u1234A\uabc6
+                     """kEy: \u1234A\uabc6
 """)
 
     def test_escape_4(self):
         self.compare(br"""key: '\u1234A\uABc6'""",
-                     u"""key: \u1234A\uabc6
+                     """key: \u1234A\uabc6
 """)
 
     def test_escape_5(self):
         self.compare(br"""key: [\u1234A\uABc6]""",
-                     u"""key: [\u1234A\uabc6]
+                     """key: [\u1234A\uabc6]
 """)
 
     def test_escape_6(self):
         self.compare(br"""key: [\u1234A\uABc6\,]""",
-                     u"""key: ["\u1234A\uabc6,"]
+                     """key: ["\u1234A\uabc6,"]
 """)
 
     def test_escape_7(self):
@@ -191,12 +191,12 @@
 
     def test_escape_9(self):
         self.compare(br"""key: \U10FFFFabc""",
-                     u"""key: \U0010FFFFabc
+                     """key: \U0010FFFFabc
 """)
 
     def test_escape_10(self):
         self.compare(br"""key: \u10FFab""",
-                     u"""key: \u10FFab
+                     """key: \u10FFab
 """)
 
     def test_escape_11(self):
diff --git a/tools/wptrunner/wptrunner/wpttest.py b/tools/wptrunner/wptrunner/wpttest.py
index 03a89cd..840c7fe 100644
--- a/tools/wptrunner/wptrunner/wpttest.py
+++ b/tools/wptrunner/wptrunner/wpttest.py
@@ -11,7 +11,7 @@
 enabled_tests = {"testharness", "reftest", "wdspec", "crashtest", "print-reftest"}
 
 
-class Result(object):
+class Result:
     def __init__(self,
                  status,
                  message,
@@ -32,7 +32,7 @@
         return "<%s.%s %s>" % (self.__module__, self.__class__.__name__, self.status)
 
 
-class SubtestResult(object):
+class SubtestResult:
     def __init__(self, name, status, message, stack=None, expected=None, known_intermittent=None):
         self.name = name
         if status not in self.statuses:
@@ -204,7 +204,7 @@
     return "http"
 
 
-class Test(object):
+class Test:
 
     result_cls = None  # type: ClassVar[Type[Result]]
     subtest_result_cls = None  # type: ClassVar[Type[SubtestResult]]
@@ -279,8 +279,7 @@
                 if subtest_meta is not None:
                     yield subtest_meta
             yield self._get_metadata()
-        for metadata in reversed(self._inherit_metadata):
-            yield metadata
+        yield from reversed(self._inherit_metadata)
 
     def disabled(self, subtest=None):
         for meta in self.itermeta(subtest):
@@ -674,14 +673,14 @@
     def __init__(self, url_base, tests_root, url, inherit_metadata, test_metadata, references,
                  timeout=None, path=None, viewport_size=None, dpi=None, fuzzy=None,
                  page_ranges=None, protocol="http", subdomain=False):
-        super(PrintReftestTest, self).__init__(url_base, tests_root, url, inherit_metadata, test_metadata,
-                                               references, timeout, path, viewport_size, dpi,
-                                               fuzzy, protocol, subdomain=subdomain)
+        super().__init__(url_base, tests_root, url, inherit_metadata, test_metadata,
+                         references, timeout, path, viewport_size, dpi,
+                         fuzzy, protocol, subdomain=subdomain)
         self._page_ranges = page_ranges
 
     @classmethod
     def cls_kwargs(cls, manifest_test):
-        rv = super(PrintReftestTest, cls).cls_kwargs(manifest_test)
+        rv = super().cls_kwargs(manifest_test)
         rv["page_ranges"] = manifest_test.page_ranges
         return rv
 
diff --git a/tools/wptserve/docs/conf.py b/tools/wptserve/docs/conf.py
index eae1c20..686eb4f 100644
--- a/tools/wptserve/docs/conf.py
+++ b/tools/wptserve/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # wptserve documentation build configuration file, created by
 # sphinx-quickstart on Wed Aug 14 17:23:24 2013.
@@ -41,8 +40,8 @@
 master_doc = 'index'
 
 # General information about the project.
-project = u'wptserve'
-copyright = u'2013, Mozilla Foundation and other wptserve contributers'
+project = 'wptserve'
+copyright = '2013, Mozilla Foundation and other wptserve contributers'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -184,8 +183,8 @@
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'wptserve.tex', u'wptserve Documentation',
-   u'James Graham', 'manual'),
+  ('index', 'wptserve.tex', 'wptserve Documentation',
+   'James Graham', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -214,8 +213,8 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'wptserve', u'wptserve Documentation',
-     [u'James Graham'], 1)
+    ('index', 'wptserve', 'wptserve Documentation',
+     ['James Graham'], 1)
 ]
 
 # If true, show URL addresses after external links.
@@ -228,8 +227,8 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'wptserve', u'wptserve Documentation',
-   u'James Graham', 'wptserve', 'One line description of project.',
+  ('index', 'wptserve', 'wptserve Documentation',
+   'James Graham', 'wptserve', 'One line description of project.',
    'Miscellaneous'),
 ]
 
diff --git a/tools/wptserve/tests/functional/base.py b/tools/wptserve/tests/functional/base.py
index 8aa19e0..be5dc0d 100644
--- a/tools/wptserve/tests/functional/base.py
+++ b/tools/wptserve/tests/functional/base.py
@@ -117,7 +117,7 @@
             os.remove(filename)
 
     def setUp(self):
-        super(TestWrapperHandlerUsingServer, self).setUp()
+        super().setUp()
 
         for filename, content in self.dummy_files.items():
             filepath = os.path.join(doc_root, filename)
@@ -141,7 +141,7 @@
             self.assertEqual(fp.read(), resp.read())
 
     def tearDown(self):
-        super(TestWrapperHandlerUsingServer, self).tearDown()
+        super().tearDown()
 
         for filename, _ in self.dummy_files.items():
             filepath = os.path.join(doc_root, filename)
diff --git a/tools/wptserve/tests/functional/test_request.py b/tools/wptserve/tests/functional/test_request.py
index 9371cd0..aa492f7 100644
--- a/tools/wptserve/tests/functional/test_request.py
+++ b/tools/wptserve/tests/functional/test_request.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 import pytest
 
 from urllib.parse import quote_from_bytes
@@ -125,13 +124,13 @@
         self.server.router.register(*route)
 
         # Try some non-ASCII characters and the server shouldn't crash.
-        encoded_text = u"你好".encode("utf-8")
+        encoded_text = "你好".encode("utf-8")
         resp = self.request(route[1], headers={"foo": encoded_text})
         self.assertEqual(encoded_text, resp.read())
 
         # Try a different encoding from utf-8 to make sure the binary value is
         # returned in verbatim.
-        encoded_text = u"どうも".encode("shift-jis")
+        encoded_text = "どうも".encode("shift-jis")
         resp = self.request(route[1], headers={"foo": encoded_text})
         self.assertEqual(encoded_text, resp.read())
 
@@ -144,7 +143,7 @@
         self.server.router.register(*route)
 
         # We intentionally choose an encoding that's not the default UTF-8.
-        encoded_text = u"どうも".encode("shift-jis")
+        encoded_text = "どうも".encode("shift-jis")
         quoted = quote_from_bytes(encoded_text)
         resp = self.request(route[1], query="foo="+quoted)
         self.assertEqual(encoded_text, resp.read())
@@ -158,7 +157,7 @@
         self.server.router.register(*route)
 
         # We intentionally choose an encoding that's not the default UTF-8.
-        encoded_text = u"どうも".encode("shift-jis")
+        encoded_text = "どうも".encode("shift-jis")
         # After urlencoding, the string should only contain ASCII.
         quoted = quote_from_bytes(encoded_text).encode("ascii")
         resp = self.request(route[1], method="POST", body=b"foo="+quoted)
@@ -178,7 +177,7 @@
         self.assertEqual(200, resp.getcode())
         self.assertEqual([b"test", b"PASS"], resp.read().split(b" "))
 
-        encoded_text = u"どうも".encode("shift-jis")
+        encoded_text = "どうも".encode("shift-jis")
         resp = self.request(route[1], auth=(encoded_text, encoded_text))
         self.assertEqual(200, resp.getcode())
         self.assertEqual([encoded_text, encoded_text], resp.read().split(b" "))
diff --git a/tools/wptserve/tests/functional/test_response.py b/tools/wptserve/tests/functional/test_response.py
index 635e628..4a4611f 100644
--- a/tools/wptserve/tests/functional/test_response.py
+++ b/tools/wptserve/tests/functional/test_response.py
@@ -94,7 +94,7 @@
         resp = self.request(route[1])
         assert resp.getcode() == 200
         assert resp.read() == resp_content
-        assert sorted([x.lower() for x in resp.info().keys()]) == sorted(['test-header', 'date', 'server', 'content-length'])
+        assert sorted(x.lower() for x in resp.info().keys()) == sorted(['test-header', 'date', 'server', 'content-length'])
 
     def test_write_content_no_status_no_required_headers(self):
         resp_content = b"TEST"
diff --git a/tools/wptserve/tests/functional/test_stash.py b/tools/wptserve/tests/functional/test_stash.py
index 1c4c9ad..03561bc 100644
--- a/tools/wptserve/tests/functional/test_stash.py
+++ b/tools/wptserve/tests/functional/test_stash.py
@@ -12,7 +12,7 @@
 class TestResponseSetCookie(TestUsingServer):
     def run(self, result=None):
         with StashServer(None, authkey=str(uuid.uuid4())):
-            super(TestResponseSetCookie, self).run(result)
+            super().run(result)
 
     def test_put_take(self):
         @wptserve.handlers.handler
diff --git a/tools/wptserve/tests/test_request.py b/tools/wptserve/tests/test_request.py
index f793a29..a2161e9 100644
--- a/tools/wptserve/tests/test_request.py
+++ b/tools/wptserve/tests/test_request.py
@@ -28,7 +28,7 @@
 
     def getallmatchingheaders(self, key):
         values = dict.__getitem__(self, key.lower())
-        return ["{}: {}\n".format(key, v) for v in values]
+        return [f"{key}: {v}\n" for v in values]
 
 
 def test_request_headers_get():
diff --git a/tools/wptserve/tests/test_response.py b/tools/wptserve/tests/test_response.py
index 34adc86..d10554b 100644
--- a/tools/wptserve/tests/test_response.py
+++ b/tools/wptserve/tests/test_response.py
@@ -5,7 +5,7 @@
 
 
 def test_response_status():
-    cases = [200, (200, b'OK'), (200, u'OK'), ('200', 'OK')]
+    cases = [200, (200, b'OK'), (200, 'OK'), ('200', 'OK')]
 
     for case in cases:
         handler = mock.Mock()
diff --git a/tools/wptserve/wptserve/config.py b/tools/wptserve/wptserve/config.py
index 82b1b59..493b67a 100644
--- a/tools/wptserve/wptserve/config.py
+++ b/tools/wptserve/wptserve/config.py
@@ -85,7 +85,7 @@
     raise ValueError
 
 
-class ConfigBuilder(object):
+class ConfigBuilder:
     """Builder object for setting the wptserve config.
 
     Configuration can be passed in as a dictionary to the constructor, or
@@ -275,7 +275,7 @@
 
         rv = {}
         for name, host in hosts.items():
-            rv[name] = {subdomain: (subdomain.encode("idna").decode("ascii") + u"." + host)
+            rv[name] = {subdomain: (subdomain.encode("idna").decode("ascii") + "." + host)
                         for subdomain in data["subdomains"]}
             rv[name][""] = host
         return rv
@@ -287,7 +287,7 @@
 
         rv = {}
         for name, host in hosts.items():
-            rv[name] = {subdomain: (subdomain.encode("idna").decode("ascii") + u"." + host)
+            rv[name] = {subdomain: (subdomain.encode("idna").decode("ascii") + "." + host)
                         for subdomain in data["not_subdomains"]}
         return rv
 
diff --git a/tools/wptserve/wptserve/handlers.py b/tools/wptserve/wptserve/handlers.py
index 880086d..80d0ae3 100644
--- a/tools/wptserve/wptserve/handlers.py
+++ b/tools/wptserve/wptserve/handlers.py
@@ -48,7 +48,7 @@
     return new_path
 
 
-class DirectoryHandler(object):
+class DirectoryHandler:
     def __init__(self, base_path=None, url_base="/"):
         self.base_path = base_path
         self.url_base = url_base
@@ -182,7 +182,7 @@
         try:
             with open(headers_path, "rb") as headers_file:
                 data = headers_file.read()
-        except IOError:
+        except OSError:
             return []
         else:
             if use_sub:
@@ -194,7 +194,7 @@
             _load(request, path))
 
 
-class FileHandler(object):
+class FileHandler:
     def __init__(self, base_path=None, url_base="/"):
         self.base_path = base_path
         self.url_base = url_base
@@ -226,7 +226,7 @@
             response = wrap_pipeline(path, request, response)
             return response
 
-        except (OSError, IOError):
+        except OSError:
             raise HTTPException(404)
 
     def get_headers(self, request, path):
@@ -276,7 +276,7 @@
 file_handler = FileHandler()
 
 
-class PythonScriptHandler(object):
+class PythonScriptHandler:
     def __init__(self, base_path=None, url_base="/"):
         self.base_path = base_path
         self.url_base = url_base
@@ -305,7 +305,7 @@
             if func is not None:
                 return func(request, response, environ, path)
 
-        except IOError:
+        except OSError:
             raise HTTPException(404)
 
     def __call__(self, request, response):
@@ -350,7 +350,7 @@
 python_script_handler = PythonScriptHandler()
 
 
-class FunctionHandler(object):
+class FunctionHandler:
     def __init__(self, func):
         self.func = func
 
@@ -383,7 +383,7 @@
     return FunctionHandler(func)
 
 
-class JsonHandler(object):
+class JsonHandler:
     def __init__(self, func):
         self.func = func
 
@@ -409,7 +409,7 @@
     return JsonHandler(func)
 
 
-class AsIsHandler(object):
+class AsIsHandler:
     def __init__(self, base_path=None, url_base="/"):
         self.base_path = base_path
         self.url_base = url_base
@@ -422,14 +422,14 @@
                 response.writer.write_raw_content(f.read())
             wrap_pipeline(path, request, response)
             response.close_connection = True
-        except IOError:
+        except OSError:
             raise HTTPException(404)
 
 
 as_is_handler = AsIsHandler()
 
 
-class BasicAuthHandler(object):
+class BasicAuthHandler:
     def __init__(self, handler, user, password):
         """
          A Basic Auth handler
@@ -459,7 +459,7 @@
 basic_auth_handler = BasicAuthHandler(file_handler, None, None)
 
 
-class ErrorHandler(object):
+class ErrorHandler:
     def __init__(self, status):
         self.status = status
 
@@ -467,7 +467,7 @@
         response.set_error(self.status)
 
 
-class StringHandler(object):
+class StringHandler:
     def __init__(self, data, content_type, **headers):
         """Handler that returns a fixed data string and headers
 
@@ -507,4 +507,4 @@
             if format_args:
                 data = data % format_args
 
-        return super(StaticHandler, self).__init__(data, content_type, **headers)
+        return super().__init__(data, content_type, **headers)
diff --git a/tools/wptserve/wptserve/pipes.py b/tools/wptserve/wptserve/pipes.py
index 740a226..f3b303d 100644
--- a/tools/wptserve/wptserve/pipes.py
+++ b/tools/wptserve/wptserve/pipes.py
@@ -15,7 +15,7 @@
     return b"".join(item for item in response.iter_content(read_file=True))
 
 
-class Pipeline(object):
+class Pipeline:
     pipes = {}
 
     def __init__(self, pipe_string):
@@ -38,7 +38,7 @@
         return response
 
 
-class PipeTokenizer(object):
+class PipeTokenizer:
     def __init__(self):
         #This whole class can likely be replaced by some regexps
         self.state = None
@@ -105,7 +105,7 @@
         return escapes.get(char, char)
 
 
-class pipe(object):
+class pipe:
     def __init__(self, *arg_converters):
         self.arg_converters = arg_converters
         self.max_args = len(self.arg_converters)
@@ -133,7 +133,7 @@
         return f
 
 
-class opt(object):
+class opt:
     def __init__(self, f):
         self.f = f
 
@@ -253,8 +253,7 @@
                 if i != len(delays) - 1:
                     continue
                 while offset[0] < len(content):
-                    for item in add_content(delays[-(value + 1):-1], True):
-                        yield item
+                    yield from add_content(delays[-(value + 1):-1], True)
 
         if not repeat and offset[0] < len(content):
             yield content[offset[0]:]
@@ -280,7 +279,7 @@
     return response
 
 
-class ReplacementTokenizer(object):
+class ReplacementTokenizer:
     def arguments(self, token):
         unwrapped = token[1:-1].decode('utf8')
         return ("arguments", re.split(r",\s*", unwrapped) if unwrapped else [])
@@ -310,7 +309,7 @@
                           (br"\([^)]*\)", arguments)])
 
 
-class FirstWrapper(object):
+class FirstWrapper:
     def __init__(self, params):
         self.params = params
 
@@ -390,7 +389,7 @@
     response.content = new_content
     return response
 
-class SubFunctions(object):
+class SubFunctions:
     @staticmethod
     def uuid(request):
         return str(uuid.uuid4())
@@ -414,7 +413,7 @@
         try:
             with open(absolute_path, "rb") as f:
                 hash_obj.update(f.read())
-        except IOError:
+        except OSError:
             # In this context, an unhandled IOError will be interpreted by the
             # server as an indication that the template file is non-existent.
             # Although the generic "Exception" is less precise, it avoids
diff --git a/tools/wptserve/wptserve/ranges.py b/tools/wptserve/wptserve/ranges.py
index 104dca2..c9b98fa 100644
--- a/tools/wptserve/wptserve/ranges.py
+++ b/tools/wptserve/wptserve/ranges.py
@@ -1,7 +1,7 @@
 from .utils import HTTPException
 
 
-class RangeParser(object):
+class RangeParser:
     def __call__(self, header, file_size):
         try:
             header = header.decode("ascii")
@@ -49,7 +49,7 @@
         return rv[::-1]
 
 
-class Range(object):
+class Range:
     def __init__(self, lower, upper, file_size):
         self.file_size = file_size
         self.lower, self.upper = self._abs(lower, upper)
diff --git a/tools/wptserve/wptserve/request.py b/tools/wptserve/wptserve/request.py
index 86f8c43..7e49b79 100644
--- a/tools/wptserve/wptserve/request.py
+++ b/tools/wptserve/wptserve/request.py
@@ -12,7 +12,7 @@
 missing = object()
 
 
-class Server(object):
+class Server:
     """Data about the server environment
 
     .. attribute:: config
@@ -39,7 +39,7 @@
         return self._stash
 
 
-class InputFile(object):
+class InputFile:
     max_buffer_size = 1024*1024
 
     def __init__(self, rfile, length):
@@ -157,7 +157,7 @@
         return self
 
 
-class Request(object):
+class Request:
     """Object representing a HTTP request.
 
     .. attribute:: doc_root
@@ -365,7 +365,7 @@
     def __init__(self, request_handler):
         self.h2_stream_id = request_handler.h2_stream_id
         self.frames = []
-        super(H2Request, self).__init__(request_handler)
+        super().__init__(request_handler)
 
 
 class RequestHeaders(dict):
@@ -444,7 +444,7 @@
             yield self[item]
 
 
-class CookieValue(object):
+class CookieValue:
     """Representation of cookies.
 
     Note that cookies are considered read-only and the string value
@@ -630,7 +630,7 @@
         """
         assert isinstance(rawdata, bytes)
         # BaseCookie.load expects a native string
-        super(BinaryCookieParser, self).load(isomorphic_decode(rawdata))
+        super().load(isomorphic_decode(rawdata))
 
 
 class Cookies(MultiDict):
@@ -645,7 +645,7 @@
         return self.last(key)
 
 
-class Authentication(object):
+class Authentication:
     """Object for dealing with HTTP Authentication
 
     .. attribute:: username
diff --git a/tools/wptserve/wptserve/response.py b/tools/wptserve/wptserve/response.py
index 98344a5..a47e776 100644
--- a/tools/wptserve/wptserve/response.py
+++ b/tools/wptserve/wptserve/response.py
@@ -15,7 +15,7 @@
 missing = object()
 
 
-class Response(object):
+class Response:
     """Object representing the response to a HTTP request
 
     :param handler: RequestHandler being used for this response
@@ -228,7 +228,7 @@
         self.write_status_headers()
         self.write_content()
 
-    def set_error(self, code, message=u""):
+    def set_error(self, code, message=""):
         """Set the response status headers and return a JSON error object:
 
         {"error": {"code": code, "message": message}}
@@ -251,7 +251,7 @@
             self.logger.info(message)
 
 
-class MultipartContent(object):
+class MultipartContent:
     def __init__(self, boundary=None, default_content_type=None):
         self.items = []
         if boundary is None:
@@ -281,7 +281,7 @@
         yield self
 
 
-class MultipartPart(object):
+class MultipartPart:
     def __init__(self, data, content_type=None, headers=None):
         assert isinstance(data, bytes), data
         self.headers = ResponseHeaders()
@@ -317,7 +317,7 @@
     return isomorphic_encode(s)
 
 
-class ResponseHeaders(object):
+class ResponseHeaders:
     """Dictionary-like object holding the headers for the response"""
     def __init__(self):
         self.data = OrderedDict()
@@ -394,7 +394,7 @@
 class H2Response(Response):
 
     def __init__(self, handler, request):
-        super(H2Response, self).__init__(handler, request, response_writer_cls=H2ResponseWriter)
+        super().__init__(handler, request, response_writer_cls=H2ResponseWriter)
 
     def write_status_headers(self):
         self.writer.write_headers(self.headers, *self.status)
@@ -416,7 +416,7 @@
                     self.writer.write_data(item, last=True)
 
 
-class H2ResponseWriter(object):
+class H2ResponseWriter:
 
     def __init__(self, handler, response):
         self.socket = handler.request
@@ -654,7 +654,7 @@
             raise ValueError
 
 
-class ResponseWriter(object):
+class ResponseWriter:
     """Object providing an API to write out a HTTP response.
 
     :param handler: The RequestHandler being used.
diff --git a/tools/wptserve/wptserve/router.py b/tools/wptserve/wptserve/router.py
index 5a91de3..2f39f0e 100644
--- a/tools/wptserve/wptserve/router.py
+++ b/tools/wptserve/wptserve/router.py
@@ -6,7 +6,7 @@
 
 any_method = object()
 
-class RouteTokenizer(object):
+class RouteTokenizer:
     def literal(self, scanner, token):
         return ("literal", token)
 
@@ -26,7 +26,7 @@
                               (r"(?:\\.|[^{\*/])*", self.literal),])
         return scanner.scan(input_str)
 
-class RouteCompiler(object):
+class RouteCompiler:
     def __init__(self):
         self.reset()
 
@@ -84,7 +84,7 @@
 
     return compiler.compile(tokens)
 
-class Router(object):
+class Router:
     """Object for matching handler functions to requests.
 
     :param doc_root: Absolute path of the filesystem location from
diff --git a/tools/wptserve/wptserve/server.py b/tools/wptserve/wptserve/server.py
index 418674c..9816c8d 100644
--- a/tools/wptserve/wptserve/server.py
+++ b/tools/wptserve/wptserve/server.py
@@ -78,7 +78,7 @@
 """
 
 
-class RequestRewriter(object):
+class RequestRewriter:
     def __init__(self, rules):
         """Object for rewriting the request path.
 
@@ -435,7 +435,7 @@
 
         protocol = ""
         for key, value in frame.headers:
-            if key in (b':protocol', u':protocol'):
+            if key in (b':protocol', ':protocol'):
                 protocol = isomorphic_encode(value)
                 break
         if protocol != b"websocket":
@@ -630,7 +630,7 @@
             response.write()
 
 
-class H2ConnectionGuard(object):
+class H2ConnectionGuard:
     """H2Connection objects are not threadsafe, so this keeps thread safety"""
     lock = threading.Lock()
 
@@ -666,7 +666,7 @@
         return ['dummy function']
 
 
-class H2HandlerCopy(object):
+class H2HandlerCopy:
     def __init__(self, handler, req_frame, rfile):
         self.headers = H2Headers(req_frame.headers)
         self.command = self.headers['method']
@@ -727,7 +727,7 @@
             self.close_connection = True
         return True
 
-class WebTestHttpd(object):
+class WebTestHttpd:
     """
     :param host: Host from which to serve (default: 127.0.0.1)
     :param port: Port from which to serve (default: 8000)
@@ -806,9 +806,9 @@
 
         if use_ssl:
             if not os.path.exists(key_file):
-                raise ValueError("SSL certificate not found: {}".format(key_file))
+                raise ValueError(f"SSL certificate not found: {key_file}")
             if not os.path.exists(certificate):
-                raise ValueError("SSL key not found: {}".format(certificate))
+                raise ValueError(f"SSL key not found: {certificate}")
 
         try:
             self.httpd = server_cls((host, port),
@@ -871,7 +871,7 @@
                            path, query, fragment))
 
 
-class _WebSocketConnection(object):
+class _WebSocketConnection:
     def __init__(self, request_handler, response):
         """Mimic mod_python mp_conn.
 
@@ -892,7 +892,7 @@
         return self._request_handler.rfile.read(length)
 
 
-class _WebSocketRequest(object):
+class _WebSocketRequest:
     def __init__(self, request_handler, response):
         """Mimic mod_python request.
 
diff --git a/tools/wptserve/wptserve/sslutils/base.py b/tools/wptserve/wptserve/sslutils/base.py
index 63ef45c..bea15fd 100644
--- a/tools/wptserve/wptserve/sslutils/base.py
+++ b/tools/wptserve/wptserve/sslutils/base.py
@@ -1,4 +1,4 @@
-class NoSSLEnvironment(object):
+class NoSSLEnvironment:
     ssl_enabled = False
 
     def __init__(self, *args, **kwargs):
diff --git a/tools/wptserve/wptserve/sslutils/openssl.py b/tools/wptserve/wptserve/sslutils/openssl.py
index 365055b..166c79b 100644
--- a/tools/wptserve/wptserve/sslutils/openssl.py
+++ b/tools/wptserve/wptserve/sslutils/openssl.py
@@ -12,7 +12,7 @@
 CERT_EXPIRY_BUFFER = dict(hours=6)
 
 
-class OpenSSL(object):
+class OpenSSL:
     def __init__(self, logger, binary, base_path, conf_path, hosts, duration,
                  base_conf_path=None):
         """Context manager for interacting with OpenSSL.
@@ -214,7 +214,7 @@
 
     return rv
 
-class OpenSSLEnvironment(object):
+class OpenSSLEnvironment:
     ssl_enabled = True
 
     def __init__(self, logger, openssl_binary="openssl", base_path=None,
diff --git a/tools/wptserve/wptserve/sslutils/pregenerated.py b/tools/wptserve/wptserve/sslutils/pregenerated.py
index 672a106..725cb56 100644
--- a/tools/wptserve/wptserve/sslutils/pregenerated.py
+++ b/tools/wptserve/wptserve/sslutils/pregenerated.py
@@ -1,4 +1,4 @@
-class PregeneratedSSLEnvironment(object):
+class PregeneratedSSLEnvironment:
     """SSL environment to use with existing key/certificate files
     e.g. when running on a server with a public domain name
     """
diff --git a/tools/wptserve/wptserve/stash.py b/tools/wptserve/wptserve/stash.py
index bf3070f..2a7aac3 100644
--- a/tools/wptserve/wptserve/stash.py
+++ b/tools/wptserve/wptserve/stash.py
@@ -54,7 +54,7 @@
                       proxytype=QueueProxy)
 
 
-class StashServer(object):
+class StashServer:
     def __init__(self, address=None, authkey=None, mp_context=None):
         self.address = address
         self.authkey = authkey
@@ -102,7 +102,7 @@
     return (manager, address, manager._authkey)
 
 
-class LockWrapper(object):
+class LockWrapper:
     def __init__(self, lock):
         self.lock = lock
 
@@ -122,7 +122,7 @@
 #TODO: Consider expiring values after some fixed time for long-running
 #servers
 
-class Stash(object):
+class Stash:
     """Key-value store for persisting data across HTTP/S and WS/S requests.
 
     This data store is specifically designed for persisting data across server
diff --git a/tools/wptserve/wptserve/ws_h2_handshake.py b/tools/wptserve/wptserve/ws_h2_handshake.py
index 5a9ff58..31c6fde 100644
--- a/tools/wptserve/wptserve/ws_h2_handshake.py
+++ b/tools/wptserve/wptserve/ws_h2_handshake.py
@@ -14,7 +14,7 @@
 
 
 def check_connect_method(request):
-    if request.method != u'CONNECT':
+    if request.method != 'CONNECT':
         raise HandshakeException('Method is not CONNECT: %r' % request.method)
 
 
@@ -29,7 +29,7 @@
         WsH2Handshaker will add attributes such as ws_resource during handshake.
         """
 
-        super(WsH2Handshaker, self).__init__(request, dispatcher)
+        super().__init__(request, dispatcher)
 
     def _transform_header(self, header):
         return header.lower()