From 368df1cf2bb640348227ff65d56b6f17da1830c2 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Wed, 13 Oct 2021 15:30:01 -0700 Subject: [PATCH] Switch entirely off of the implicit-format() message signature, to finally stop the 'panic when an f-string gets some curly braces in it' errors. --- bikeshed/Spec.py | 19 +-- bikeshed/biblio.py | 8 +- bikeshed/boilerplate.py | 26 ++-- bikeshed/caniuse.py | 12 +- bikeshed/config/retrieve.py | 13 +- bikeshed/config/status.py | 22 +-- bikeshed/datablocks.py | 127 ++++------------- bikeshed/dfns/attributeInfo.py | 16 +-- bikeshed/fonts.py | 19 +-- bikeshed/h/dom.py | 12 +- bikeshed/headings.py | 4 +- bikeshed/highlight.py | 31 +--- bikeshed/idl.py | 7 +- bikeshed/includes.py | 34 ++--- bikeshed/inlineTags/__init__.py | 21 +-- bikeshed/issuelist.py | 18 +-- bikeshed/lint/accidental2119.py | 6 +- bikeshed/lint/brokenLinks.py | 12 +- bikeshed/lint/exampleIDs.py | 2 +- bikeshed/lint/missingExposed.py | 6 +- bikeshed/lint/requiredIDs.py | 2 +- bikeshed/lint/unusedInternalDfns.py | 6 +- bikeshed/markdown/markdown.py | 16 +-- bikeshed/messages.py | 57 +++----- bikeshed/metadata.py | 196 ++++++-------------------- bikeshed/railroadparser.py | 71 +++------- bikeshed/refs/RefSource.py | 2 +- bikeshed/refs/ReferenceManager.py | 127 ++++------------- bikeshed/shorthands/element.py | 5 +- bikeshed/shorthands/idl.py | 6 +- bikeshed/shorthands/oldShorthands.py | 33 ++--- bikeshed/shorthands/propdesc.py | 4 +- bikeshed/unsortedJunk.py | 157 +++++++-------------- bikeshed/update/main.py | 8 +- bikeshed/update/manifest.py | 31 ++-- bikeshed/update/updateBiblio.py | 14 +- bikeshed/update/updateBoilerplates.py | 15 +- bikeshed/update/updateCanIUse.py | 15 +- bikeshed/update/updateCrossRefs.py | 14 +- bikeshed/update/updateLanguages.py | 4 +- bikeshed/update/updateLinkDefaults.py | 4 +- bikeshed/update/updateMdn.py | 14 +- bikeshed/update/updateTestSuites.py | 6 +- bikeshed/update/updateWpt.py | 7 +- bikeshed/wpt/wptElement.py | 15 +- 45 files changed, 346 insertions(+), 898 deletions(-) diff --git a/bikeshed/Spec.py b/bikeshed/Spec.py index fd4fa563e2..81beed35d0 100644 --- a/bikeshed/Spec.py +++ b/bikeshed/Spec.py @@ -117,13 +117,10 @@ def initializeState(self): if inputContent.date is not None: self.mdBaseline.addParsedData("Date", inputContent.date) except FileNotFoundError: - die( - "Couldn't find the input file at the specified location '{0}'.", - self.inputSource, - ) + die(f"Couldn't find the input file at the specified location '{self.inputSource}'.") return False except OSError: - die("Couldn't open the input file '{0}'.", self.inputSource) + die(f"Couldn't open the input file '{self.inputSource}'.") return False return True @@ -304,7 +301,7 @@ def serialize(self): try: rendered = h.Serializer(self.md.opaqueElements, self.md.blockElements).serialize(self.document) except Exception as e: - die("{0}", e) + die(str(e)) return rendered = finalHackyCleanup(rendered) return rendered @@ -334,11 +331,7 @@ def finish(self, outputFilename=None, newline=None): with open(outputFilename, "w", encoding="utf-8", newline=newline) as f: f.write(rendered) except Exception as e: - die( - "Something prevented me from saving the output document to {0}:\n{1}", - outputFilename, - e, - ) + die(f"Something prevented me from saving the output document to {outputFilename}:\n{e}") def printResultMessage(self): # If I reach this point, I've succeeded, but maybe with reservations. @@ -351,7 +344,7 @@ def printResultMessage(self): success("Successfully generated, but fatal errors were suppressed") return if links: - success("Successfully generated, with {0} linking errors", links) + success(f"Successfully generated, with {links} linking errors") return if warnings: success("Successfully generated, with warnings") @@ -416,7 +409,7 @@ def log_message(self, format, *args): thread.join() sys.exit(0) except Exception as e: - die("Something went wrong while watching the file:\n{0}", e) + die(f"Something went wrong while watching the file:\n{e}") def fixText(self, text, moreMacros={}): # Do several textual replacements that need to happen *before* the document is parsed as h. diff --git a/bikeshed/biblio.py b/bikeshed/biblio.py index 443c378bc5..ee6adbda9b 100644 --- a/bikeshed/biblio.py +++ b/bikeshed/biblio.py @@ -223,7 +223,7 @@ def processReferBiblioFile(lines, storage, order): if match: letter, value = match.groups() else: - die("Biblio line in unexpected format:\n{0}", line) + die(f"Biblio line in unexpected format:\n{line}") continue if letter in singularReferCodes: @@ -233,7 +233,7 @@ def processReferBiblioFile(lines, storage, order): elif letter in unusedReferCodes: pass else: - die("Unknown line type ") + die(f"Unknown line type {letter}:\n{line}") if biblio is not None: storage[biblio["linkText"].lower()] = biblio return storage @@ -277,7 +277,7 @@ def processSpecrefBiblioFile(text, storage, order): try: datas = json.loads(text) except Exception as e: - die("Couldn't read the local JSON file:\n{0}", str(e)) + die(f"Couldn't read the local JSON file:\n{e}") return storage # JSON field name: BiblioEntry name @@ -372,7 +372,7 @@ def loadBiblioDataFile(lines, storage): } line = next(lines) # Eat the - else: - die("Unknown biblio prefix '{0}' on key '{1}'", prefix, fullKey) + die(f"Unknown biblio prefix '{prefix}' on key '{fullKey}'") continue storage[key].append(b) except StopIteration: diff --git a/bikeshed/boilerplate.py b/bikeshed/boilerplate.py index d3ebb3c8fb..c1a305d0ca 100644 --- a/bikeshed/boilerplate.py +++ b/bikeshed/boilerplate.py @@ -359,9 +359,7 @@ def addExplicitIndexes(doc): status = el.get("status") if status and status not in config.specStatuses: die( - " has unknown value '{0}' for status. Must be {1}.", - status, - config.englishFromList(config.specStatuses), + f" has unknown value '{status}' for status. Must be {config.englishFromList(config.specStatuses)}.", el=el, ) continue @@ -371,7 +369,7 @@ def addExplicitIndexes(doc): for t in types: if t not in config.dfnTypes: die( - "Unknown type value '{}' on {}".format(t, outerHTML(el)), + f"Unknown type value '{t}' on {outerHTML(el)}", el=el, ) types.remove(t) @@ -383,7 +381,7 @@ def addExplicitIndexes(doc): specs = {x.strip() for x in el.get("data-link-spec").split(",")} for s in list(specs): if s not in doc.refs.specs: - die("Unknown spec name '{}' on {}".format(s, outerHTML(el)), el=el) + die(f"Unknown spec name '{s}' on {outerHTML(el)}", el=el) specs.remove(s) else: specs = None @@ -401,7 +399,7 @@ def addExplicitIndexes(doc): export = False else: die( - "Unknown export value '{}' (should be boolish) on {}".format(exportVal, outerHTML(el)), + f"Unknown export value '{exportVal}' (should be boolish) on {outerHTML(el)}", el=el, ) export = None @@ -599,8 +597,7 @@ def extractKeyValFromRow(row, table): result = re.match(r"(.*):", textContent(row[0]).strip()) if result is None: die( - "Propdef row headers must be a word followed by a colon. Got:\n{0}", - textContent(row[0]).strip(), + f"Propdef row headers must be a word followed by a colon. Got:\n{textContent(row[0]).strip()}", el=table, ) return "", "" @@ -802,20 +799,14 @@ def addTOCSection(doc): if isinstance(container, int): # Saw a low-level heading without first seeing a higher heading. die( - "Saw an without seeing an first. Please order your headings properly.\n{2}", - level, - level - 1, - outerHTML(header), + f"Saw an without seeing an first. Please order your headings properly.\n{outerHTML(header)}", el=header, ) return if level > previousLevel + 1: # Jumping two levels is a no-no. die( - "Heading level jumps more than one level, from h{0} to h{1}:\n {2}", - previousLevel, - level, - textContent(header).replace("\n", " "), + f"Heading level jumps more than one level, from h{previousLevel} to h{level}:\n{outerHTML(el)}", el=header, ) return @@ -920,8 +911,7 @@ def printTranslation(tr): missingInfo = True if missingInfo: warn( - "Bikeshed doesn't have all the translation info for '{0}'. Please add to bikeshed/spec-data/readonly/languages.json and submit a PR!", - lang, + f"Bikeshed doesn't have all the translation info for '{lang}'. Please add to bikeshed/spec-data/readonly/languages.json and submit a PR!" ) if nativeName: return E.span( diff --git a/bikeshed/caniuse.py b/bikeshed/caniuse.py index 65ace66d60..2822db64ef 100644 --- a/bikeshed/caniuse.py +++ b/bikeshed/caniuse.py @@ -33,7 +33,7 @@ def addCanIUsePanels(doc): featId = featId.lower() if not doc.canIUse.hasFeature(featId): - die("Unrecognized Can I Use feature ID: {0}", featId, el=dfn) + die(f"Unrecognized Can I Use feature ID: {featId}", el=dfn) feature = doc.canIUse.getFeature(featId) addClass(dfn, "caniuse-paneled") @@ -45,11 +45,7 @@ def addCanIUsePanels(doc): ) dfnId = dfn.get("id") if not dfnId: - die( - "Elements with `caniuse` attribute need to have an ID as well. Got:\n{0}", - serializeTag(dfn), - el=dfn, - ) + die(f"Elements with `caniuse` attribute need to have an ID as well. Got:\n{serializeTag(dfn)}", el=dfn) continue panel.set("data-dfn-id", dfnId) appendChild(doc.body, panel) @@ -207,9 +203,9 @@ def validateCanIUseURLs(doc, elements): unusedFeatures = urlFeatures - docFeatures if unusedFeatures: + featureList = "\n".join(" * {0} - https://caniuse.com/#feat={0}".format(x) for x in sorted(unusedFeatures)) warn( - "The following Can I Use features are associated with your URLs, but don't show up in your spec:\n{0}", - "\n".join(" * {0} - https://caniuse.com/#feat={0}".format(x) for x in sorted(unusedFeatures)), + f"The following Can I Use features are associated with your URLs, but don't show up in your spec:\n{featureList}" ) diff --git a/bikeshed/config/retrieve.py b/bikeshed/config/retrieve.py index a7517644a3..8977b44528 100644 --- a/bikeshed/config/retrieve.py +++ b/bikeshed/config/retrieve.py @@ -107,11 +107,9 @@ def boilerplatePath(*segs): for f in (statusFile, genericFile): if doc.inputSource.cheaplyExists(f): warn( - ( - "Found {0} next to the specification without a matching\n" - + "Local Boilerplate: {1} yes\n" - + "in the metadata. This include won't be found when building via a URL." - ).format(f, name) + f"Found {f} next to the specification without a matching\n" + + f"Local Boilerplate: {name} yes\n" + + "in the metadata. This include won't be found when building via a URL." ) # We should remove this after giving specs time to react to the warning: sources.append(doc.inputSource.relative(f)) @@ -138,9 +136,6 @@ def boilerplatePath(*segs): else: if error: die( - "Couldn't find an appropriate include file for the {0} inclusion, given group='{1}' and status='{2}'.", - name, - group, - status, + f"Couldn't find an appropriate include file for the {name} inclusion, given group='{group}' and status='{status}'." ) return "" diff --git a/bikeshed/config/status.py b/bikeshed/config/status.py index 7ae6c0473d..4f9ae79b16 100644 --- a/bikeshed/config/status.py +++ b/bikeshed/config/status.py @@ -295,7 +295,7 @@ def canonicalizeStatus(rawStatus, group): def validateW3Cstatus(group, status, rawStatus): if status == "DREAM": - warn("You used Status: DREAM for a W3C document." + " Consider UD instead.") + warn("You used Status: DREAM for a W3C document. Consider UD instead.") return if "w3c/" + status in shortToLongStatus: @@ -304,14 +304,9 @@ def validateW3Cstatus(group, status, rawStatus): def formatStatusSet(statuses): return ", ".join(sorted({status.split("/")[-1] for status in statuses})) - msg = "You used Status: {0}, but {1} limited to these statuses: {2}." - if group in w3cIgs and status not in w3cIGStatuses: warn( - msg, - rawStatus, - "W3C Interest Groups are", - formatStatusSet(w3cIGStatuses), + f"You used Status: {rawStatus}, but W3C Interest Groups are limited to these statuses: {formatStatusSet(w3cIGStatuses)}." ) if group == "tag" and status not in w3cTAGStatuses: @@ -319,10 +314,7 @@ def formatStatusSet(statuses): if group in w3cCgs and status not in w3cCommunityStatuses: warn( - msg, - rawStatus, - "W3C Community and Business Groups are", - formatStatusSet(w3cCommunityStatuses), + f"You used Status: {rawStatus}, but W3C Community and Business Groups are limited to these statuses: {formatStatusSet(w3cCommunityStatuses)}." ) def megaGroupsForStatus(status): @@ -380,9 +372,7 @@ def megaGroupsForStatus(status): ) else: if len(possibleMgs) == 1: - msg += " That status can only be used with the org '{0}', like `Status: {0}/{1}`".format( - possibleMgs[0], status - ) + msg += f" That status can only be used with the org '{possibleMgs[0]}', like `Status: {possibleMgs[0]}/{status}`" else: msg += " That status can only be used with the orgs {}.".format( englishFromList(f"'{x}'" for x in possibleMgs) @@ -393,7 +383,7 @@ def megaGroupsForStatus(status): msg = f"Unknown Status metadata '{canonStatus}'. Check the docs for valid Status values." else: msg = f"Status '{status}' can't be used with the org '{megaGroup}'. Check the docs for valid Status values." - die("{0}", msg) + die(msg) return canonStatus # Otherwise, they provided a bare status. @@ -425,7 +415,7 @@ def megaGroupsForStatus(status): msg += ", and you don't have a Group metadata. Please declare your Group, or check the docs for statuses that can be used by anyone." else: msg = f"Unknown Status metadata '{canonStatus}'. Check the docs for valid Status values." - die("{0}", msg) + die(msg) return canonStatus diff --git a/bikeshed/datablocks.py b/bikeshed/datablocks.py index 9232ddcb25..0d83485784 100644 --- a/bikeshed/datablocks.py +++ b/bikeshed/datablocks.py @@ -78,9 +78,9 @@ def transformDataBlocks(doc, lines): blockType = seenClasses[0] else: die( - "Found {0} classes on the <{1}>, so can't tell which to process the block as. Please use only one.", - config.englishFromList((f"'{x}'" for x in seenClasses), "and"), - tagName, + "Found {} classes on the <{}>, so can't tell which to process the block as. Please use only one.".format( + config.englishFromList((f"'{x}'" for x in seenClasses), "and"), tagName + ), lineNum=line.i, ) blockType = "pre" @@ -92,11 +92,7 @@ def transformDataBlocks(doc, lines): # Single-line
.
                 match = re.match(r"(\s*<{0}[^>]*>)(.*)(.*)".format(tagName), line.text, re.I)
                 if not match:
-                    die(
-                        "Can't figure out how to parse this datablock line:\n{0}",
-                        line.text,
-                        lineNum=line.i,
-                    )
+                    die(f"Can't figure out how to parse this datablock line:\n{line.text}", lineNum=line.i)
                     blockLines = []
                     continue
                 repl = blockTypes[blockType](
@@ -280,12 +276,7 @@ def transformPropdef(lines, doc, firstLine, lineNum=None, **kwargs):  # pylint:
             val = parsedAttrs[key]
         elif val is None:
             # Required key, not provided
-            die(
-                "The propdef for '{0}' is missing a '{1}' line.",
-                parsedAttrs.get("Name", "???"),
-                key,
-                lineNum=lineNum,
-            )
+            die(f"The propdef for '{parsedAttrs.get('Name', '???')}' is missing a '{key}' line.", lineNum=lineNum)
             continue
         else:
             # Optional key, just use default
@@ -364,12 +355,7 @@ def transformDescdef(lines, doc, firstLine, lineNum=None, **kwargs):  # pylint:
         elif key in vals:
             ret.append("{}:{}".format(key, vals.get(key, "")))
         else:
-            die(
-                "The descdef for '{0}' is missing a '{1}' line.",
-                vals.get("Name", "???"),
-                key,
-                lineNum=lineNum,
-            )
+            die(f"The descdef for '{vals.get('Name', '???')}' is missing a '{key}' line.", lineNum=lineNum)
             continue
     for key, val in vals.items():
         if key in requiredKeys:
@@ -450,12 +436,7 @@ def transformElementdef(lines, doc, firstLine, lineNum=None, **kwargs):  # pylin
             else:
                 ret.append(f"{key}:{val}")
         else:
-            die(
-                "The elementdef for '{0}' is missing a '{1}' line.",
-                parsedAttrs.get("Name", "???"),
-                key,
-                lineNum=lineNum,
-            )
+            die(f"The elementdef for '{parsedAttrs.get('Name', '???')}' is missing a '{key}' line.", lineNum=lineNum)
             continue
     for key, val in parsedAttrs.items():
         if key in attrs:
@@ -481,18 +462,11 @@ def transformArgumentdef(lines, firstLine, lineNum=None, **kwargs):  # pylint: d
         if "/" in forValue:
             interface, method = forValue.split("/")
         else:
-            die(
-                "Argumentdef for='' values need to specify interface/method(). Got '{0}'.",
-                forValue,
-                lineNum=lineNum,
-            )
+            die(f"Argumentdef for='' values need to specify interface/method(). Got '{forValue}'.", lineNum=lineNum)
             return []
         removeAttr(el, "for")
     else:
-        die(
-            "Argumentdef blocks need a for='' attribute specifying their method.",
-            lineNum=lineNum,
-        )
+        die("Argumentdef blocks need a for='' attribute specifying their method.", lineNum=lineNum)
         return []
     addClass(el, "data")
     rootAttrs = " ".join("{}='{}'".format(k, escapeAttr(v)) for k, v in el.attrib.items())
@@ -544,13 +518,7 @@ def parseDefBlock(lines, type, capitalizeKeys=True, lineNum=None):
                 key = lastKey
                 val = line.strip()
             else:
-                die(
-                    "Incorrectly formatted {2} line for '{0}':\n{1}",
-                    vals.get("Name", "???"),
-                    line,
-                    type,
-                    lineNum=lineNum,
-                )
+                die(f"Incorrectly formatted {type} line for '{vals.get('Name', '???')}':\n{line}", lineNum=lineNum)
                 continue
         else:
             key = match.group(1).strip()
@@ -647,23 +615,14 @@ def transformAnchors(lines, doc, lineNum=None, **kwargs):  # pylint: disable=unu
 def processAnchors(anchors, doc, lineNum=None):
     for anchor in anchors:
         if "type" not in anchor or len(anchor["type"]) != 1:
-            die(
-                "Each anchor needs exactly one type. Got:\n{0}",
-                config.printjson(anchor),
-                lineNum=lineNum,
-            )
+            die(f"Each anchor needs exactly one type. Got:\n{config.printjson(anchor)}", lineNum=lineNum)
             continue
         if "text" not in anchor or len(anchor["text"]) != 1:
-            die(
-                "Each anchor needs exactly one text. Got:\n{0}",
-                config.printjson(anchor),
-                lineNum=lineNum,
-            )
+            die(f"Each anchor needs exactly one text. Got:\n{config.printjson(anchor)}", lineNum=lineNum)
             continue
         if "url" not in anchor and "urlPrefix" not in anchor:
             die(
-                "Each anchor needs a url and/or at least one urlPrefix. Got:\n{0}",
-                config.printjson(anchor),
+                f"Each anchor needs a url and/or at least one urlPrefix. Got:\n{config.printjson(anchor)}",
                 lineNum=lineNum,
             )
             continue
@@ -702,9 +661,7 @@ def processAnchors(anchors, doc, lineNum=None):
                 pass
             else:
                 die(
-                    "Anchor statuses must be {1}. Got '{0}'.",
-                    status,
-                    config.englishFromList(config.linkStatuses),
+                    f"Anchor statuses must be {config.englishFromList(config.linkStatuses)}. Got '{status}'.",
                     lineNum=lineNum,
                 )
                 continue
@@ -739,31 +696,19 @@ def transformLinkDefaults(lines, doc, lineNum=None, **kwargs):  # pylint: disabl
 def processLinkDefaults(lds, doc, lineNum=None):
     for ld in lds:
         if len(ld.get("type", [])) != 1:
-            die(
-                "Every link default needs exactly one type. Got:\n{0}",
-                config.printjson(ld),
-                lineNum=lineNum,
-            )
+            die(f"Every link default needs exactly one type. Got:\n{config.printjson(ld)}", lineNum=lineNum)
             continue
 
         type = ld["type"][0]
 
         if len(ld.get("spec", [])) != 1:
-            die(
-                "Every link default needs exactly one spec. Got:\n{0}",
-                config.printjson(ld),
-                lineNum=lineNum,
-            )
+            die(f"Every link default needs exactly one spec. Got:\n{config.printjson(ld)}", lineNum=lineNum)
             continue
 
         spec = ld["spec"][0]
 
         if len(ld.get("text", [])) != 1:
-            die(
-                "Every link default needs exactly one text. Got:\n{0}",
-                config.printjson(ld),
-                lineNum=lineNum,
-            )
+            die(f"Every link default needs exactly one text. Got:\n{config.printjson(ld)}", lineNum=lineNum)
             continue
 
         text = ld["text"][0]
@@ -785,16 +730,14 @@ def processIgnoredSpecs(specs, doc, lineNum=None):
     for spec in specs:
         if len(spec.get("spec", [])) == 0:
             die(
-                "Every ignored spec line needs at least one 'spec' value. Got:\n{0}",
-                config.printjson(spec),
+                f"Every ignored spec line needs at least one 'spec' value. Got:\n{config.printjson(spec)}",
                 lineNum=lineNum,
             )
             continue
         specNames = spec.get("spec")
         if len(spec.get("replacedBy", [])) > 1:
             die(
-                "Every ignored spec line needs at most one 'replacedBy' value. Got:\n{0}",
-                config.printjson(spec),
+                f"Every ignored spec line needs at most one 'replacedBy' value. Got:\n{config.printjson(spec)}",
                 lineNum=lineNum,
             )
             continue
@@ -824,15 +767,11 @@ def processInfo(infos, doc, lineNum=None):
     infoCollections = defaultdict(list)
     for info in infos:
         if len(info.get("info", [])) != 1:
-            die(
-                "Every info-block line needs exactly one 'info' type. Got:\n{0}",
-                config.printjson(info),
-                lineNum=lineNum,
-            )
+            die(f"Every info-block line needs exactly one 'info' type. Got:\n{config.printjson(info)}", lineNum=lineNum)
             continue
         infoType = info.get("info")[0].lower()
         if infoType not in knownInfoTypes:
-            die("Unknown info-block type '{0}'", infoType, lineNum=lineNum)
+            die(f"Unknown info-block type '{infoType}'", lineNum=lineNum)
             continue
         infoCollections[infoType].append(info)
     for infoType, info in infoCollections.items():
@@ -860,8 +799,7 @@ def transformInclude(lines, doc, firstLine, lineNum=None, **kwargs):  # pylint:
                     macros[k] = v[0]
                 else:
                     die(
-                        "Include block defines the '{0}' local macro more than once.",
-                        k,
+                        f"Include block defines the '{k}' local macro more than once.",
                         lineNum=lineNum,
                     )
     if path:
@@ -1010,18 +948,13 @@ def extendData(datas, infoLevels):
         if wsLen % indent != 0:
             visibleWs = ws.replace("\t", "\\t").replace(" ", "\\s")
             die(
-                "Line has inconsistent indentation; use tabs or {1} spaces:\n{0}",
-                visibleWs + text,
-                indent,
-                lineNum=thisLine,
+                f"Line has inconsistent indentation; use tabs or {indent} spaces:\n{visibleWS + text}", lineNum=thisLine
             )
             return []
         wsLen = wsLen // indent
         if wsLen >= lastIndent + 2:
             die(
-                "Line jumps {1} indent levels:\n{0}",
-                text,
-                wsLen - lastIndent,
+                f"Line jumps {wsLen - lastIndent} indent levels:\n{text}",
                 lineNum=thisLine,
             )
             return []
@@ -1035,11 +968,7 @@ def extendData(datas, infoLevels):
                 continue
             match = re.match(r"([^:]+):\s*(.*)", piece)
             if not match:
-                die(
-                    "Line doesn't match the grammar `k:v; k:v; k:v`:\n{0}",
-                    line,
-                    lineNum=thisLine,
-                )
+                die(f"Line doesn't match the grammar `k:v; k:v; k:v`:\n{line}", lineNum=thisLine)
                 return []
             key = match.group(1).strip()
             val = match.group(2).strip()
@@ -1078,11 +1007,7 @@ def parseTag(text, lineNumber):
 
     def parseerror(index, state):
         die(
-            "Tried to parse a start tag from '{0}', but failed at character {1} '{2}' and parse-state '{3}'.",
-            text,
-            index,
-            text[index],
-            state,
+            f"Tried to parse a start tag from '{text}', but failed at character {index} '{text[index]}' and parse-state '{state}'.",
             lineNum=lineNumber,
         )
 
diff --git a/bikeshed/dfns/attributeInfo.py b/bikeshed/dfns/attributeInfo.py
index 96ec041f6c..375460dbeb 100644
--- a/bikeshed/dfns/attributeInfo.py
+++ b/bikeshed/dfns/attributeInfo.py
@@ -71,22 +71,10 @@ def getTargetInfo(doc, el):
         )
 
     if len(targets) == 0:
-        die(
-            "Couldn't find target {1} '{0}':\n{2}",
-            referencedAttribute,
-            refType,
-            h.outerHTML(el),
-            el=el,
-        )
+        die(f"Couldn't find target {referencedAttribute} '{refType}':\n{h.outerHTML(el)}", el=el)
         return
     elif len(targets) > 1:
-        die(
-            "Multiple potential target {1}s '{0}':\n{2}",
-            referencedAttribute,
-            refType,
-            h.outerHTML(el),
-            el=el,
-        )
+        die(f"Multiple potential target {referencedAttribute}s '{refType}':\n{h.outerHTML(el)}", el=el)
         return
 
     target = targets[0]
diff --git a/bikeshed/fonts.py b/bikeshed/fonts.py
index 0ba8865200..3bfbf09ca7 100755
--- a/bikeshed/fonts.py
+++ b/bikeshed/fonts.py
@@ -66,7 +66,7 @@ def __init__(self, fontfilename=config.scriptPath("bigblocks.bsfont")):
             with open(fontfilename, encoding="utf-8") as fh:
                 lines = fh.readlines()
         except Exception as e:
-            die("Couldn't find font file “{0}”:\n{1}", fontfilename, e)
+            die(f"Couldn't find font file “{fontfilename}”:\n{e}")
         self.metadata, lines = parseMetadata(lines)
         self.characters = parseCharacters(self.metadata, lines)
 
@@ -79,7 +79,7 @@ def write(self, text):
                         output[i] += " "
                     output[i] += line
             else:
-                die("The character “{0}” doesn't appear in the specified font.", letter)
+                die(f"The character “{letter}” doesn't appear in the specified font.")
         output = [line + "\n" for line in output]
         return output
 
@@ -101,7 +101,7 @@ def parseMetadata(lines):
         if key in nameMapping:
             key = nameMapping[key]
         else:
-            die("Unrecognized font metadata “{0}”", key)
+            die(f"Unrecognized font metadata “{key}”")
         if key in valProcessors:
             val = valProcessors[key](val)
         md[key] = val
@@ -176,13 +176,10 @@ def getInputLines(inputFilename):
             with open(inputFilename, encoding="utf-8") as fh:
                 lines = fh.readlines()
     except FileNotFoundError:
-        die(
-            "Couldn't find the input file at the specified location '{0}'.",
-            inputFilename,
-        )
+        die(f"Couldn't find the input file at the specified location '{inputFilename}'.")
         return []
     except OSError:
-        die("Couldn't open the input file '{0}'.", inputFilename)
+        die(f"Couldn't open the input file '{inputFilename}'.")
         return []
     return lines, inputFilename
 
@@ -197,11 +194,7 @@ def writeOutputLines(outputFilename, inputFilename, lines):
             with open(outputFilename, "w", encoding="utf-8") as f:
                 f.write("".join(lines))
     except Exception as e:
-        die(
-            "Something prevented me from saving the output document to {0}:\n{1}",
-            outputFilename,
-            e,
-        )
+        die(f"Something prevented me from saving the output document to {outputFilename}:\n{e}")
 
 
 if __name__ == "__main__":
diff --git a/bikeshed/h/dom.py b/bikeshed/h/dom.py
index f4e7c86823..fe711ba01f 100644
--- a/bikeshed/h/dom.py
+++ b/bikeshed/h/dom.py
@@ -31,7 +31,7 @@ def findAll(sel, context):
     try:
         return CSSSelector(sel, namespaces={"svg": "http://www.w3.org/2000/svg"})(context)
     except Exception as e:
-        die("The selector '{0}' returned an error:\n{1}", sel, e)
+        die(f"The selector '{sel}' returned an error:\n{e}")
         return []
 
 
@@ -51,7 +51,7 @@ def escapeCSSIdent(val):
     firstCode = val[0]
     for i, code in enumerate(ord(x) for x in val):
         if code == 0:
-            die("Invalid character: the string '{0}' somehow has a NUL in it.", val)
+            die(f"Invalid character: the string '{val}' somehow has a NUL in it.")
             return ""
         if (
             0x1 <= code <= 0x1F
@@ -726,10 +726,7 @@ def macroReplacer(match):
         # Nothing has matched, so start failing the macros.
         if optional:
             return ""
-        die(
-            "Found unmatched text macro {0}. Correct the macro, or escape it with a leading backslash.",
-            fullText,
-        )
+        die(f"Found unmatched text macro {fullText}. Correct the macro, or escape it with a leading backslash.")
         return fullText
 
     return re.sub(r"(\\|\[)?\[([A-Z0-9-]+)(\??)\]", macroReplacer, text)
@@ -816,8 +813,7 @@ def dedupIDs(doc):
             # Try to de-dup the id by appending an integer after it.
             if warnAboutDupes:
                 warn(
-                    "Multiple elements have the same ID '{0}'.\nDeduping, but this ID may not be stable across revisions.",
-                    dupeId,
+                    f"Multiple elements have the same ID '{dupeId}'.\nDeduping, but this ID may not be stable across revisions.",
                     el=el,
                 )
             for x in ints:
diff --git a/bikeshed/headings.py b/bikeshed/headings.py
index d3e1c1c6c1..cb10849ebd 100644
--- a/bikeshed/headings.py
+++ b/bikeshed/headings.py
@@ -51,8 +51,8 @@ def addHeadingIds(doc, headings):
     addOldIDs(headings)
     if len(neededIds) > 0:
         warn(
-            "You should manually provide IDs for your headings:\n{0}",
-            "\n".join("  " + outerHTML(el) for el in neededIds),
+            "You should manually provide IDs for your headings:\n"
+            + "\n".join("  " + outerHTML(el) for el in neededIds),
         )
 
 
diff --git a/bikeshed/highlight.py b/bikeshed/highlight.py
index a50955b87c..bdf0809c91 100644
--- a/bikeshed/highlight.py
+++ b/bikeshed/highlight.py
@@ -91,11 +91,7 @@ def determineLineNumbers(doc, el):
         try:
             lineStart = int(lineStart)
         except ValueError:
-            die(
-                "line-start attribute must have an integer value. Got '{0}'.",
-                lineStart,
-                el=el,
-            )
+            die(f"line-start attribute must have an integer value. Got '{lineStart}'.", el=el)
             lineStart = 1
 
     lh = el.get("line-highlight")
@@ -112,29 +108,17 @@ def determineLineNumbers(doc, el):
                     low = int(low)
                     high = int(high)
                 except ValueError:
-                    die(
-                        "Error parsing line-highlight range '{0}' - must be `int-int`.",
-                        item,
-                        el=el,
-                    )
+                    die(f"Error parsing line-highlight range '{item}' - must be `int-int`.", el=el)
                     continue
                 if low >= high:
-                    die(
-                        "line-highlight ranges must be well-formed lo-hi - got '{0}'.",
-                        item,
-                        el=el,
-                    )
+                    die(f"line-highlight ranges must be well-formed lo-hi - got '{item}'.", el=el)
                     continue
                 lineHighlights.update(list(range(low, high + 1)))
             else:
                 try:
                     item = int(item)
                 except ValueError:
-                    die(
-                        "Error parsing line-highlight value '{0}' - must be integers.",
-                        item,
-                        el=el,
-                    )
+                    die(f"Error parsing line-highlight value '{item}' - must be integers.", el=el)
                     continue
                 lineHighlights.add(item)
 
@@ -165,7 +149,7 @@ def highlightWithWebIDL(text, el):
 
     class IDLUI:
         def warn(self, msg):
-            die("{0}", msg.rstrip())
+            die(msg.rstrip())
 
     class HighlightMarker:
         # Just applies highlighting classes to IDL stuff.
@@ -244,9 +228,8 @@ def highlightWithPygments(text, lang, el):
     lexer = lexerFromLang(lang)
     if lexer is None:
         die(
-            "'{0}' isn't a known syntax-highlighting language. See http://pygments.org/docs/lexers/. Seen on:\n{1}",
-            lang,
-            outerHTML(el),
+            f"'{lang}' isn't a known syntax-highlighting language. See http://pygments.org/docs/lexers/. Seen on:\n"
+            + outerHTML(el),
             el=el,
         )
         return
diff --git a/bikeshed/idl.py b/bikeshed/idl.py
index 0ffdead13f..e2f2451ef4 100644
--- a/bikeshed/idl.py
+++ b/bikeshed/idl.py
@@ -10,7 +10,7 @@
 
 class IDLUI:
     def warn(self, msg):
-        die("{0}", msg.rstrip())
+        die(msg.rstrip())
 
 
 class IDLSilent:
@@ -203,10 +203,7 @@ def markup_name(self, text, construct):  # pylint: disable=unused-argument
             elif hasattr(construct.member, "attribute"):
                 rest = construct.member.attribute
             else:
-                die(
-                    "Can't figure out how to construct attribute-info from:\n  {0}",
-                    construct,
-                )
+                die("Can't figure out how to construct attribute-info from:\n  " + construct)
             if rest.readonly is not None:
                 readonly = "data-readonly"
             else:
diff --git a/bikeshed/includes.py b/bikeshed/includes.py
index 79e12f17f5..e37d0e268f 100644
--- a/bikeshed/includes.py
+++ b/bikeshed/includes.py
@@ -41,7 +41,7 @@ def handleBikeshedInclude(el, doc):
         try:
             lines = includedInputSource.read().rawLines
         except Exception as err:
-            die("Couldn't find include file '{0}'. Error was:\n{1}", path, err, el=el)
+            die(f"Couldn't find include file '{path}'. Error was:\n{err}", el=el)
             removeNode(el)
             return
         # hash the content + path together for identity
@@ -53,7 +53,7 @@ def handleBikeshedInclude(el, doc):
             # This came from another included file, check if it's a loop-include
             if hash in el.get("hash"):
                 # WHOOPS
-                die("Include loop detected - “{0}” is included in itself.", path, el=el)
+                die(f"Include loop detected - “{path}” is included in itself.", el=el)
                 removeNode(el)
                 return
             hash += " " + el.get("hash")
@@ -95,7 +95,7 @@ def handleCodeInclude(el, doc):
     try:
         lines = includedInputSource.read().rawLines
     except Exception as err:
-        die("Couldn't find include-code file '{0}'. Error was:\n{1}", path, err, el=el)
+        die(f"Couldn't find include-code file '{path}'. Error was:\n{err}", el=el)
         removeNode(el)
         return
     if el.get("data-code-show"):
@@ -103,11 +103,7 @@ def handleCodeInclude(el, doc):
         if len(showLines) == 0:
             pass
         elif len(showLines) >= 2:
-            die(
-                "Can only have one include-code 'show' segment, got '{0}'.",
-                el.get("data-code-show"),
-                el=el,
-            )
+            die(f"Can only have one include-code 'show' segment, got '{el.get('data-code-show')}'.", el=el)
             return
         else:
             start, end = showLines[0]
@@ -136,7 +132,7 @@ def handleRawInclude(el, doc):
     try:
         content = includedInputSource.read().content
     except Exception as err:
-        die("Couldn't find include-raw file '{0}'. Error was:\n{1}", path, err, el=el)
+        die(f"Couldn't find include-raw file '{path}'. Error was:\n{err}", el=el)
         removeNode(el)
         return
     subtree = parseHTML(content)
@@ -158,10 +154,7 @@ def parseSingleRange(item):
             try:
                 low = int(low)
             except ValueError:
-                die(
-                    "Error parsing include-code 'show' range '{0}' - must be `int-int`.",
-                    item,
-                )
+                die(f"Error parsing include-code 'show' range '{item}' - must be `int-int`.")
                 return
         if high == "*":
             high = None
@@ -169,16 +162,10 @@ def parseSingleRange(item):
             try:
                 high = int(high)
             except ValueError:
-                die(
-                    "Error parsing include-code 'show' range '{0}' - must be `int-int`.",
-                    item,
-                )
+                die(f"Error parsing include-code 'show' range '{item}' - must be `int-int`.")
                 return
         if low >= high:
-            die(
-                "include-code 'show' ranges must be well-formed lo-hi - got '{0}'.",
-                item,
-            )
+            die(f"include-code 'show' ranges must be well-formed lo-hi - got '{item}'.")
             return
         return [low, high]
     if item == "*":
@@ -187,7 +174,4 @@ def parseSingleRange(item):
         val = int(item)
         return [val, val]
     except ValueError:
-        die(
-            "Error parsing include-code 'show' value '{0}' - must be an int or *.",
-            item,
-        )
+        die(f"Error parsing include-code 'show' value '{item}' - must be an int or *.")
diff --git a/bikeshed/inlineTags/__init__.py b/bikeshed/inlineTags/__init__.py
index 999d6d8f23..c6a08b00f6 100644
--- a/bikeshed/inlineTags/__init__.py
+++ b/bikeshed/inlineTags/__init__.py
@@ -12,7 +12,7 @@ def processTags(doc):
             return
         tag = el.get("data-span-tag")
         if tag not in doc.md.inlineTagCommands:
-            die("Unknown inline tag '{0}' found:\n  {1}", tag, outerHTML(el), el=el)
+            die(f"Unknown inline tag '{tag}' found:\n  {outerHTML(el)}", el=el)
             continue
         command = doc.md.inlineTagCommands[tag]
         with Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True) as p:
@@ -20,27 +20,14 @@ def processTags(doc):
             try:
                 out = out.decode("utf-8")
             except UnicodeDecodeError as e:
-                die(
-                    "When trying to process {0}, got invalid unicode in stdout:\n{1}",
-                    outerHTML(el),
-                    e,
-                    el=el,
-                )
+                die(f"When trying to process {outerHTML(el)}, got invalid unicode in stdout:\n{err}", el=el)
             try:
                 err = err.decode("utf-8")
             except UnicodeDecodeError as e:
-                die(
-                    "When trying to process {0}, got invalid unicode in stderr:\n{1}",
-                    outerHTML(el),
-                    e,
-                    el=el,
-                )
+                die(f"When trying to process {outerHTML(el)}, got invalid unicode in stderr:\n{err}", el=el)
             if p.returncode:
                 die(
-                    "When trying to process {0}, got return code {1} and the following stderr:\n{2}",
-                    outerHTML(el),
-                    p.returncode,
-                    err,
+                    f"When trying to process {outerHTML(el)}, got return code {p.returncode} and the following stderr:\n{err}",
                     el=el,
                 )
                 continue
diff --git a/bikeshed/issuelist.py b/bikeshed/issuelist.py
index 64c480437a..db558a4395 100644
--- a/bikeshed/issuelist.py
+++ b/bikeshed/issuelist.py
@@ -53,7 +53,7 @@ def printIssueList(infilename=None, outfilename=None):
         try:
             outfile = open(outfilename, "w", encoding="utf-8")
         except Exception as e:
-            die("Couldn't write to outfile:\n{0}", str(e))
+            die(f"Couldn't write to outfile:\n{e}")
             return
 
     printHeader(outfile, headerInfo)
@@ -109,10 +109,7 @@ def extractHeaderInfo(lines, infilename):
                 date = match.group(2).rstrip()
                 cdate = date
                 if not re.match(r"(\d{4})-(\d\d)-(\d\d)$", date):
-                    die(
-                        "Incorrect Date format. Expected YYYY-MM-DD, but got:\n{0}",
-                        date,
-                    )
+                    die(f"Incorrect Date format. Expected YYYY-MM-DD, but got:\n{date}")
             elif match.group(1) == "ED":
                 ed = match.group(2).rstrip()
     if url is None:
@@ -137,8 +134,7 @@ def extractHeaderInfo(lines, infilename):
             date = "{}-{}-{}".format(*re.match(r"(\d{4})(\d\d)(\d\d)", cdate).groups())
     else:
         warn(
-            "Autodetection of Shortname, Date, and Status failed; draft url does not match the format /status-shortname-date/. Got:\n{0}",
-            url,
+            f"Autodetection of Shortname, Date, and Status failed; draft url does not match the format /status-shortname-date/. Got:\n{url}"
         )
 
     if date is None:
@@ -255,7 +251,7 @@ def printIssues(outfile, lines):
         if match:
             index = match.group(1)
         else:
-            die("Issues must contain a line like 'Issue 1.'. Got:\n{0}", originaltext)
+            die(f"Issues must contain a line like 'Issue 1.'. Got:\n{originalText}")
 
         # Color coding
         if re.search(r"\nVerified:\s*\S+", issue):
@@ -268,11 +264,7 @@ def printIssues(outfile, lines):
             else:
                 code = ""
                 if match.group(1) == "Closed":
-                    warn(
-                        "Unknown status value found for issue #{num}: “{code}”",
-                        code=code,
-                        num=index,
-                    )
+                    warn(f"Unknown status value found for issue #{index}: “{code}”")
         else:
             code = ""
         if re.search(r"\nOpen", issue):
diff --git a/bikeshed/lint/accidental2119.py b/bikeshed/lint/accidental2119.py
index df54be99b7..4b82a3e78c 100644
--- a/bikeshed/lint/accidental2119.py
+++ b/bikeshed/lint/accidental2119.py
@@ -28,8 +28,7 @@ def searchFor2119(el):
                 match = re.search(keywords, el.text)
                 if match:
                     warn(
-                        "RFC2119 keyword in non-normative section (use: might, can, has to, or override with ): {0}",
-                        el.text,
+                        f"RFC2119 keyword in non-normative section (use: might, can, has to, or override with ): {el.text}",
                         el=el,
                     )
             for child in el:
@@ -37,8 +36,7 @@ def searchFor2119(el):
                     match = re.search(keywords, child.tail)
                     if match:
                         warn(
-                            "RFC2119 keyword in non-normative section (use: might, can, has to, or override with ): {0}",
-                            child.tail,
+                            f"RFC2119 keyword in non-normative section (use: might, can, has to, or override with ): {child.tail}",
                             el=el,
                         )
         for child in el:
diff --git a/bikeshed/lint/brokenLinks.py b/bikeshed/lint/brokenLinks.py
index 9980939262..1c91235ecb 100644
--- a/bikeshed/lint/brokenLinks.py
+++ b/bikeshed/lint/brokenLinks.py
@@ -26,16 +26,8 @@ def brokenLinks(doc):
         try:
             res = requests.get(href, verify=False)
         except Exception as e:
-            warn(
-                "The following link caused an error when I tried to request it:\n{0}\n{1}",
-                outerHTML(el),
-                e,
-            )
+            warn(f"The following link caused an error when I tried to request it:\n{outerHTML(el)}\n{e}")
             continue
         if res.status_code >= 400:
-            warn(
-                "Got a {0} status when fetching the link for:\n{1}",
-                res.status_code,
-                outerHTML(el),
-            )
+            warn(f"Got a {res.status_code} status when fetching the link for:\n{outerHTML(el)}")
     say("Done checking links!")
diff --git a/bikeshed/lint/exampleIDs.py b/bikeshed/lint/exampleIDs.py
index 85645a98c6..eea496f7ce 100644
--- a/bikeshed/lint/exampleIDs.py
+++ b/bikeshed/lint/exampleIDs.py
@@ -9,4 +9,4 @@ def exampleIDs(doc):
     if not doc.md.complainAbout["missing-example-ids"]:
         return
     for el in findAll(".example:not([id])", doc):
-        warn("Example needs ID:\n{0}", outerHTML(el)[0:100], el=el)
+        warn(f"Example needs ID:\n{outerHTML(el)[0:100]}", el=el)
diff --git a/bikeshed/lint/missingExposed.py b/bikeshed/lint/missingExposed.py
index c0553a3edc..d34b02bd12 100644
--- a/bikeshed/lint/missingExposed.py
+++ b/bikeshed/lint/missingExposed.py
@@ -28,8 +28,7 @@ def missingExposed(doc):
                     break
             if not good:
                 warn(
-                    "The '{0}' namespace is missing an [Exposed] extended attribute. Does it need [Exposed=Window], or something more?",
-                    construct.name,
+                    f"The '{construct.name}' namespace is missing an [Exposed] extended attribute. Does it need [Exposed=Window], or something more?"
                 )
         elif construct.idl_type == "interface":
             good = False
@@ -42,8 +41,7 @@ def missingExposed(doc):
                     break
             if not good:
                 warn(
-                    "The '{0}' interface is missing an [Exposed] extended attribute. Does it need [Exposed=Window], or something more?",
-                    construct.name,
+                    f"The '{construct.name}' interface is missing an [Exposed] extended attribute. Does it need [Exposed=Window], or something more?"
                 )
         elif construct.idl_type == "callback":
             if not hasattr(construct, "interface"):
diff --git a/bikeshed/lint/requiredIDs.py b/bikeshed/lint/requiredIDs.py
index 4116938b0e..82a1c82b8c 100644
--- a/bikeshed/lint/requiredIDs.py
+++ b/bikeshed/lint/requiredIDs.py
@@ -10,4 +10,4 @@ def requiredIDs(doc):
         if id.startswith("#"):
             id = id[1:]
         if id not in doc_ids:
-            die("Required ID '{0}' was not found in the document.", id)
+            die(f"Required ID '{id}' was not found in the document.")
diff --git a/bikeshed/lint/unusedInternalDfns.py b/bikeshed/lint/unusedInternalDfns.py
index 73c20cdab1..0a9f267565 100644
--- a/bikeshed/lint/unusedInternalDfns.py
+++ b/bikeshed/lint/unusedInternalDfns.py
@@ -23,8 +23,4 @@ def local(el):
 
     for el in noexportDfns:
         if el.get("id") not in localHrefs:
-            warn(
-                "Unexported dfn that's not referenced locally - did you mean to export it?\n{0}",
-                outerHTML(el),
-                el=el,
-            )
+            warn(f"Unexported dfn that's not referenced locally - did you mean to export it?\n{outerHTML(el)}", el=el)
diff --git a/bikeshed/markdown/markdown.py b/bikeshed/markdown/markdown.py
index 71bd514d9c..9940de2d8f 100644
--- a/bikeshed/markdown/markdown.py
+++ b/bikeshed/markdown/markdown.py
@@ -346,11 +346,7 @@ def stripPrefix(token, numSpacesForIndentation, len):
             offset += numSpacesForIndentation
         else:
             die(
-                'Line {i} isn\'t indented enough (needs {0} indent{plural}) to be valid Markdown:\n"{1}"',
-                len,
-                text[:-1],
-                plural="" if len == 1 else "s",
-                i=token["line"].i,
+                f'Line {token["line"].i} isn\'t indented enough (needs {len} indent{"" if len == 1 else "s"}) to be valid Markdown:\n"{text[:-1]}"'
             )
     return text[offset:]
 
@@ -406,7 +402,7 @@ def parseTokens(tokens, numSpacesForIndentation):
             stream.advance()
 
     # for line in lines:
-    #    print "«{0}»".format(line.text.rstrip())
+    #    print f"«{line.text.rstrip()}»"
 
     return lines
 
@@ -447,10 +443,10 @@ def parseMultiLineHeading(stream):
         level = 3
     else:
         die(
-            "Markdown parser error: tried to parse a multiline heading from:\n{0}{1}{2}",
-            stream.prevraw(),
-            stream.currraw(),
-            stream.nextraw(),
+            "Markdown parser error: tried to parse a multiline heading from:\n"
+            + stream.prevraw()
+            + stream.currraw()
+            + stream.nextraw()
         )
     match = re.search(r"(.*?)\s*\{\s*#([^ }]+)\s*\}\s*$", stream.currtext())
     if match:
diff --git a/bikeshed/messages.py b/bikeshed/messages.py
index 9f01ba033f..f7494ed225 100644
--- a/bikeshed/messages.py
+++ b/bikeshed/messages.py
@@ -34,13 +34,10 @@ def p(msg, sep=None, end=None):
             print(msg.encode("ascii", "xmlcharrefreplace"), sep=sep, end=end)
 
 
-def die(msg, *formatArgs, **namedArgs):
-    lineNum = None
-    if "el" in namedArgs and namedArgs["el"].get("line-number"):
-        lineNum = namedArgs["el"].get("line-number")
-    elif namedArgs.get("lineNum", None):
-        lineNum = namedArgs["lineNum"]
-    msg = formatMessage("fatal", msg.format(*formatArgs, **namedArgs), lineNum=lineNum)
+def die(msg, el=None, lineNum=None):
+    if lineNum is None and el is not None and el.get("line-number"):
+        lineNum = el.get("line-number")
+    msg = formatMessage("fatal", msg, lineNum=lineNum)
     if msg not in messages:
         messageCounts["fatal"] += 1
         messages.add(msg)
@@ -50,21 +47,16 @@ def die(msg, *formatArgs, **namedArgs):
         errorAndExit()
 
 
-def linkerror(msg, *formatArgs, **namedArgs):
-    lineNum = None
+def linkerror(msg, el=None, lineNum=None):
+    if lineNum is None and el is not None and el.get("line-number"):
+        lineNum = el.get("line-number")
     suffix = ""
-    if "el" in namedArgs:
-        el = namedArgs["el"]
-        if el.get("line-number"):
-            lineNum = el.get("line-number")
+    if el is not None:
+        if el.get("bs-autolink-syntax"):
+            suffix = "\n" + el.get("bs-autolink-syntax")
         else:
-            if el.get("bs-autolink-syntax"):
-                suffix = "\n{}".format(el.get("bs-autolink-syntax"))
-            else:
-                suffix = "\n{}".format(lxml.html.tostring(namedArgs["el"], with_tail=False, encoding="unicode"))
-    elif namedArgs.get("lineNum", None):
-        lineNum = namedArgs["lineNum"]
-    msg = formatMessage("link", msg.format(*formatArgs, **namedArgs) + suffix, lineNum=lineNum)
+            suffix = "\n" + lxml.html.tostring(el, with_tail=False, encoding="unicode")
+    msg = formatMessage("link", msg + suffix, lineNum=lineNum)
     if msg not in messages:
         messageCounts["linkerror"] += 1
         messages.add(msg)
@@ -74,13 +66,10 @@ def linkerror(msg, *formatArgs, **namedArgs):
         errorAndExit()
 
 
-def warn(msg, *formatArgs, **namedArgs):
-    lineNum = None
-    if "el" in namedArgs and namedArgs["el"].get("line-number"):
-        lineNum = namedArgs["el"].get("line-number")
-    elif namedArgs.get("lineNum", None):
-        lineNum = namedArgs["lineNum"]
-    msg = formatMessage("warning", msg.format(*formatArgs, **namedArgs), lineNum=lineNum)
+def warn(msg, el=None, lineNum=None):
+    if lineNum is None and el is not None and el.get("line-number"):
+        lineNum = el.get("line-number")
+    msg = formatMessage("warning", msg, lineNum=lineNum)
     if msg not in messages:
         messageCounts["warning"] += 1
         messages.add(msg)
@@ -90,21 +79,19 @@ def warn(msg, *formatArgs, **namedArgs):
         errorAndExit()
 
 
-def say(msg, *formatArgs, **namedArgs):
+def say(msg):
     if constants.quiet < 1:
-        p(formatMessage("message", msg.format(*formatArgs, **namedArgs)))
+        p(formatMessage("message", msg))
 
 
-def success(msg, *formatArgs, **namedArgs):
+def success(msg):
     if constants.quiet < 4:
-        msg = formatMessage("success", msg.format(*formatArgs, **namedArgs))
-        p(msg)
+        p(formatMessage("success", msg))
 
 
-def failure(msg, *formatArgs, **namedArgs):
+def failure(msg):
     if constants.quiet < 4:
-        msg = formatMessage("failure", msg.format(*formatArgs, **namedArgs))
-        p(msg)
+        p(formatMessage("failure", msg))
 
 
 def resetSeenMessages():
diff --git a/bikeshed/metadata.py b/bikeshed/metadata.py
index e8e21fedff..d04acf1392 100644
--- a/bikeshed/metadata.py
+++ b/bikeshed/metadata.py
@@ -147,11 +147,7 @@ def addData(self, key, val, lineNum=None):
             key = key.title()
 
         if key not in knownKeys:
-            die(
-                'Unknown metadata key "{0}". Prefix custom keys with "!".',
-                key,
-                lineNum=lineNum,
-            )
+            die(f'Unknown metadata key "{key}". Prefix custom keys with "!".', lineNum=lineNum)
             return
         md = knownKeys[key]
 
@@ -223,7 +219,7 @@ def validate(self):
         if self.status not in config.unlevelledStatuses:
             requiredSingularKeys["level"] = "Level"
         if self.status not in config.shortToLongStatus:
-            die("Unknown Status '{0}' used.", self.status)
+            die(f"Unknown Status '{self.status}' used.")
         if not self.noEditor:
             requiredMultiKeys["editors"] = "Editor"
         if not self.noAbstract:
@@ -245,9 +241,9 @@ def validate(self):
             if len(getattr(self, attrName)) == 0:
                 errors.append(f"    Must provide at least one '{name}' entry.")
         if warnings:
-            warn("Some recommended metadata is missing:\n{0}", "\n".join(warnings))
+            warn("Some recommended metadata is missing:\n" + "\n".join(warnings))
         if errors:
-            die("Not all required metadata was provided:\n{0}", "\n".join(errors))
+            die("Not all required metadata was provided:\n" + "\n".join(errors))
             return
 
     def fillTextMacros(self, macros, doc):
@@ -357,12 +353,7 @@ def parseDate(key, val, lineNum):
     try:
         return datetime.strptime(val, "%Y-%m-%d").date()
     except ValueError:
-        die(
-            'The {0} field must be in the format YYYY-MM-DD - got "{1}" instead.',
-            key,
-            val,
-            lineNum=lineNum,
-        )
+        die(f'The {key} field must be in the format YYYY-MM-DD - got "{val}" instead.', lineNum=lineNum)
         return None
 
 
@@ -377,9 +368,7 @@ def parseDateOrDuration(key, val, lineNum):
         return datetime.strptime(val, "%Y-%m-%d").date()
     except ValueError:
         die(
-            "The {0} field must be an ISO 8601 duration, a date in the format YYYY-MM-DD, now, never, false, no, n, or off. Got '{1}' instead.",
-            key,
-            val,
+            f"The {key} field must be an ISO 8601 duration, a date in the format YYYY-MM-DD, now, never, false, no, n, or off. Got '{val}' instead.",
             lineNum=lineNum,
         )
         return None
@@ -396,7 +385,7 @@ def canonicalizeExpiryDate(base, expires):
         return expires.date()
     if isinstance(expires, date):
         return expires
-    die("Unexpected expiry type: canonicalizeExpiryDate({0}, {1})", base, expires)
+    die(f"Unexpected expiry type: canonicalizeExpiryDate({base}, {expires})", base, expires)
     return None
 
 
@@ -414,12 +403,7 @@ def parseInteger(key, val, lineNum):  # pylint: disable=unused-argument
 def parseBoolean(key, val, lineNum):
     b = boolish(val)
     if b is None:
-        die(
-            "The {0} field must be true/false, yes/no, y/n, or on/off. Got '{1}' instead.",
-            key,
-            val,
-            lineNum=lineNum,
-        )
+        die(f"The {key} field must be true/false, yes/no, y/n, or on/off. Got '{val}' instead.", lineNum=lineNum)
     return b
 
 
@@ -429,10 +413,7 @@ def parseSoftBoolean(key, val, lineNum):
         return b
     if val.lower() in ["maybe", "if possible", "if needed"]:
         return "maybe"
-    die(
-        f"The {key} field must be boolish, or 'maybe'. Got '{val}' instead.",
-        lineNum=lineNum,
-    )
+    die(f"The {key} field must be boolish, or 'maybe'. Got '{val}' instead.", lineNum=lineNum)
 
 
 def boolish(val):
@@ -463,7 +444,7 @@ def parseWarning(key, val, lineNum):
     if match:
         return "warning-new-version", match.group(1)
     die(
-        """Unknown value "{1}" for "{0}" metadata. Expected one of:
+        f"""Unknown value "{val}" for "{key}" metadata. Expected one of:
   obsolete
   not ready
   replaced by [new url]
@@ -471,8 +452,6 @@ def parseWarning(key, val, lineNum):
   commit [snapshot id] [snapshot url] replaced by [master url]
   branch [branch name] [branch url] replaced by [master url]
   custom""",
-        key,
-        val,
         lineNum=lineNum,
     )
     return None
@@ -539,9 +518,7 @@ def looksEmailish(string):
         pass
     else:
         die(
-            "'{0}' format is ', ?, ?'. Got:\n{1}",
-            key,
-            val,
+            f"'{key}' format is ', ?, ?'. Got:\n{val}",
             lineNum=lineNum,
         )
         return []
@@ -587,9 +564,7 @@ def parseLinkDefaults(key, val, lineNum):
                 defaultSpecs[term.strip()].append((spec, type, status, dfnFor))
         else:
             die(
-                "'{0}' is a comma-separated list of ' () '. Got:\n{1}",
-                key,
-                default,
+                f"'{key}' is a comma-separated list of ' () '. Got:\n{default}",
                 lineNum=lineNum,
             )
             continue
@@ -602,8 +577,7 @@ def parseBoilerplate(key, val, lineNum):  # pylint: disable=unused-argument
         pieces = command.lower().strip().split()
         if len(pieces) != 2:
             die(
-                "Boilerplate metadata pieces are a boilerplate label and a boolean. Got:\n{0}",
-                command,
+                f"Boilerplate metadata pieces are a boilerplate label and a boolean. Got:\n{command}",
                 lineNum=lineNum,
             )
             continue
@@ -614,8 +588,7 @@ def parseBoilerplate(key, val, lineNum):  # pylint: disable=unused-argument
             onoff = boolish(pieces[1])
             if onoff is None:
                 die(
-                    "Boilerplate metadata pieces are a boilerplate label and a boolean. Got:\n{0}",
-                    command,
+                    f"Boilerplate metadata pieces are a boilerplate label and a boolean. Got:\n{command}",
                     lineNum=lineNum,
                 )
                 continue
@@ -627,12 +600,7 @@ def parseBiblioDisplay(key, val, lineNum):
     val = val.strip().lower()
     if val in constants.biblioDisplay:
         return val
-    die(
-        "'{0}' must be either 'inline' or 'index'. Got '{1}'",
-        key,
-        val,
-        lineNum=lineNum,
-    )
+    die(f"'{key}' must be either 'inline' or 'index'. Got '{val}'", lineNum=lineNum)
     return constants.biblioDisplay.index
 
 
@@ -643,12 +611,7 @@ def parseRefStatus(key, val, lineNum):
         val = "snapshot"
     if val in constants.refStatus:
         return val
-    die(
-        "'{0}' must be either 'current' or 'snapshot'. Got '{1}'",
-        key,
-        val,
-        lineNum=lineNum,
-    )
+    die(f"'{key}' must be either 'current' or 'snapshot'. Got '{val}'", lineNum=lineNum)
     return constants.refStatus.current
 
 
@@ -677,25 +640,17 @@ def parseBoolishList(key, val, default=None, validLabels=None, extraValues=None,
     elif default in (True, False):
         boolset = config.BoolSet(default=default)
     else:
-        die(
-            "Programming error - parseBoolishList() got a non-bool default value: '{0}'",
-            default,
-        )
+        die(f"Programming error - parseBoolishList() got a non-bool default value: '{default}'")
     if extraValues is None:
         extraValues = {}
     vals = [v.strip() for v in val.split(",")]
     for v in vals:
         name, _, boolstring = v.strip().rpartition(" ")
         if not name or not boolstring:
-            die(
-                "{0} metadata pieces are a label and a boolean. Got:\n{1}",
-                key,
-                v,
-                lineNum=lineNum,
-            )
+            die(f"{key} metadata pieces are a label and a boolean. Got:\n{v}", lineNum=lineNum)
             continue
         if validLabels and name not in validLabels:
-            die("Unknown {0} label '{1}'.", key, name, lineNum=lineNum)
+            die(f"Unknown {key} label '{name}'.", lineNum=lineNum)
             continue
         if boolstring in extraValues:
             boolset[name] = extraValues[boolstring]
@@ -704,12 +659,7 @@ def parseBoolishList(key, val, default=None, validLabels=None, extraValues=None,
             if isinstance(onoff, bool):
                 boolset[name] = onoff
             else:
-                die(
-                    "{0} metadata pieces are a shorthand category and a boolean. Got:\n{1}",
-                    key,
-                    v,
-                    lineNum=lineNum,
-                )
+                die(f"{key} metadata pieces are a shorthand category and a boolean. Got:\n{v}", lineNum=lineNum)
                 continue
     return boolset
 
@@ -732,21 +682,16 @@ def parseMarkupShorthands(key, val, lineNum):  # pylint: disable=unused-argument
     for v in vals:
         pieces = v.split()
         if len(pieces) != 2:
-            die(
-                "Markup Shorthand metadata pieces are a shorthand category and a boolean. Got:\n{0}",
-                v,
-                lineNum=lineNum,
-            )
+            die(f"Markup Shorthand metadata pieces are a shorthand category and a boolean. Got:\n{v}", lineNum=lineNum)
             continue
         name, boolstring = pieces
         if name not in validCategories:
-            die("Unknown Markup Shorthand category '{0}'.", name, lineNum=lineNum)
+            die(f"Unknown Markup Shorthand category '{name}'.", lineNum=lineNum)
             continue
         onoff = boolish(boolstring)
         if onoff is None:
             die(
-                "Markup Shorthand metadata pieces are a shorthand category and a boolean. Got:\n{0}",
-                v,
+                f"Markup Shorthand metadata pieces are a shorthand category and a boolean. Got:\n{v}",
                 lineNum=lineNum,
             )
             continue
@@ -760,11 +705,7 @@ def parseInlineGithubIssues(key, val, lineNum):  # pylint: disable=unused-argume
         return val
     b = boolish(val)
     if b is None:
-        die(
-            "Inline Github Issues must be 'title', 'full' or a boolish value. Got: '{0}'",
-            val,
-            lineNum=lineNum,
-        )
+        die(f"Inline Github Issues must be 'title', 'full' or a boolish value. Got: '{val}'", lineNum=lineNum)
         return False
     if b is True:
         return "full"
@@ -777,18 +718,10 @@ def parseTextMacro(key, val, lineNum):  # pylint: disable=unused-argument
     try:
         name, text = val.lstrip().split(None, 1)
     except ValueError:
-        die(
-            "Text Macro lines must contain a macro name followed by the macro text. Got:\n{0}",
-            val,
-            lineNum=lineNum,
-        )
+        die(f"Text Macro lines must contain a macro name followed by the macro text. Got:\n{val}", lineNum=lineNum)
         return []
     if not re.match(r"[A-Z0-9-]+$", name):
-        die(
-            "Text Macro names must be all-caps and alphanumeric. Got '{0}'",
-            name,
-            lineNum=lineNum,
-        )
+        die(f"Text Macro names must be all-caps and alphanumeric. Got '{name}'", lineNum=lineNum)
         return []
     return [(name, text)]
 
@@ -807,8 +740,7 @@ def parseWorkStatus(key, val, lineNum):  # pylint: disable=unused-argument
         "abandoned",
     ):
         die(
-            "Work Status must be one of (completed, stable, testing, refining, revising, exploring, rewriting, abandoned). Got '{0}'. See http://fantasai.inkedblade.net/weblog/2011/inside-csswg/process for details.",
-            val,
+            f"Work Status must be one of (completed, stable, testing, refining, revising, exploring, rewriting, abandoned). Got '{val}'. See http://fantasai.inkedblade.net/weblog/2011/inside-csswg/process for details.",
             lineNum=lineNum,
         )
         return None
@@ -834,11 +766,7 @@ def parseRepository(key, val, lineNum):  # pylint: disable=unused-argument
             return GithubRepository("com", *match.groups())
         # Otherwise just use the url as the shortname
         return Repository(url=val)
-    die(
-        "Repository must be a url, optionally followed by a shortname. Got '{0}'",
-        val,
-        lineNum=lineNum,
-    )
+    die(f"Repository must be a url, optionally followed by a shortname. Got '{val}'", lineNum=lineNum)
     return config.Nil()
 
 
@@ -847,11 +775,7 @@ def parseTranslateIDs(key, val, lineNum):  # pylint: disable=unused-argument
     for v in val.split(","):
         pieces = v.strip().split()
         if len(pieces) != 2:
-            die(
-                "‘Translate IDs’ values must be an old ID followed by a new ID. Got '{0}'",
-                v,
-                lineNum=lineNum,
-            )
+            die(f"‘Translate IDs’ values must be an old ID followed by a new ID. Got '{v}'", lineNum=lineNum)
             continue
         old, new = pieces
         translations[old] = new
@@ -863,16 +787,14 @@ def parseTranslation(key, val, lineNum):  # pylint: disable=unused-argument
     pieces = val.split(",")
     if not (1 <= len(pieces) <= 3):
         die(
-            "Format of a Translation line is   [ [ , name  ] || [ , native-name  ] ]?. Got:\n{0}",
-            val,
+            f"Format of a Translation line is   [ [ , name  ] || [ , native-name  ] ]?. Got:\n{val}",
             lineNum=lineNum,
         )
         return
     firstParts = pieces[0].split()
     if len(firstParts) != 2:
         die(
-            "First part of a Translation line must be a lang-code followed by a url. Got:\n{0}",
-            pieces[0],
+            f"First part of a Translation line must be a lang-code followed by a url. Got:\n{pieces[0]}",
             lineNum=lineNum,
         )
         return
@@ -887,8 +809,7 @@ def parseTranslation(key, val, lineNum):  # pylint: disable=unused-argument
             nativeName = v
         else:
             die(
-                "Later parts of a Translation line must start with 'name' or 'native-name'. Got:\n{0}",
-                piece,
+                f"Later parts of a Translation line must start with 'name' or 'native-name'. Got:\n{piece}",
                 lineNum=lineNum,
             )
     return [{"lang-code": langCode, "url": url, "name": name, "native-name": nativeName}]
@@ -932,7 +853,7 @@ def parseAudience(key, val, lineNum):  # pylint: disable=unused-argument
             elif re.match(r"WG\d+|SG\d+", v):
                 ret.append(v)
             else:
-                die("Unknown 'Audience' value '{0}'.", v, lineNum=lineNum)
+                die(f"Unknown 'Audience' value '{v}'.", lineNum=lineNum)
                 continue
         return ret
 
@@ -942,8 +863,8 @@ def parseEditorTerm(key, val, lineNum):  # pylint: disable=unused-argument
     if len(values) == 2:
         return {"singular": values[0], "plural": values[1]}
     die(
-        "Editor Term metadata must be two comma-separated terms, giving the singular and plural term for editors. Got '{0}'.",
-        val,
+        f"Editor Term metadata must be two comma-separated terms, giving the singular and plural term for editors. Got '{val}'.",
+        lineNum=lineNum,
     )
     return {"singular": "Editor", "plural": "Editors"}
 
@@ -955,15 +876,13 @@ def parseMaxToCDepth(key, val, lineNum):  # pylint: disable=unused-argument
         v = int(val)
     except ValueError:
         die(
-            "Max ToC Depth metadata must be 'none' or an integer 1-5. Got '{0}'.",
-            val,
+            f"Max ToC Depth metadata must be 'none' or an integer 1-5. Got '{val}'.",
             lineNum=lineNum,
         )
         return float("inf")
     if not (1 <= v <= 5):
         die(
-            "Max ToC Depth metadata must be 'none' or an integer 1-5. Got '{0}'.",
-            val,
+            f"Max ToC Depth metadata must be 'none' or an integer 1-5. Got '{val}'.",
             lineNum=lineNum,
         )
         return float("inf")
@@ -980,8 +899,7 @@ def parseWptDisplay(key, val, lineNum):  # pylint: disable=unused-argument
     if val in ("none", "inline", "open", "closed"):
         return val
     die(
-        "WPT Display metadata only accepts the values 'none', 'closed', 'open', or 'inline'. Got '{0}'.",
-        val,
+        f"WPT Display metadata only accepts the values 'none', 'closed', 'open', or 'inline'. Got '{val}'.",
         lineNum=lineNum,
     )
     return "none"
@@ -1038,8 +956,7 @@ def parse(lines):
                 lastKey = match.group(1)
             else:
                 die(
-                    "Incorrectly formatted metadata line:\n{0}",
-                    line.text,
+                    f"Incorrectly formatted metadata line:\n{line.text}",
                     lineNum=line.i,
                 )
                 continue
@@ -1080,11 +997,10 @@ def fromJson(data, source=""):
         if data != "":
             if source == "computed-metadata":
                 die(
-                    "Error loading computed-metadata JSON.\nCheck if you need to JSON-escape some characters in a text macro?\n{0}",
-                    str(e),
+                    f"Error loading computed-metadata JSON.\nCheck if you need to JSON-escape some characters in a text macro?\n{e}",
                 )
             else:
-                die("Error loading {1} JSON:\n{0}", str(e), source)
+                die(f"Error loading {source} JSON:\n{e}")
         return md
     for key, val in defaults.items():
         if isinstance(val, str):
@@ -1093,10 +1009,7 @@ def fromJson(data, source=""):
             for indivVal in val:
                 md.addData(key, indivVal)
         else:
-            die(
-                "JSON metadata values must be strings or arrays of strings. '{0}' is something else.",
-                key,
-            )
+            die(f"JSON metadata values must be strings or arrays of strings. '{key}' is something else.")
             return md
     return md
 
@@ -1258,10 +1171,7 @@ def parseLiteralList(key, val, lineNum):  # pylint: disable=unused-argument
     "Group": Metadata("Group", "group", joinValue, parseLiteral),
     "H1": Metadata("H1", "h1", joinValue, parseLiteral),
     "Ignore Can I Use Url Failure": Metadata(
-        "Ignore Can I Use Url Failure",
-        "ignoreCanIUseUrlFailure",
-        joinList,
-        parseLiteralList,
+        "Ignore Can I Use Url Failure", "ignoreCanIUseUrlFailure", joinList, parseLiteralList
     ),
     "Ignored Terms": Metadata("Ignored Terms", "ignoredTerms", joinList, parseCommaSeparated),
     "Ignored Vars": Metadata("Ignored Vars", "ignoredVars", joinList, parseCommaSeparated),
@@ -1281,10 +1191,7 @@ def parseLiteralList(key, val, lineNum):  # pylint: disable=unused-argument
     "Line Numbers": Metadata("Line Numbers", "lineNumbers", joinValue, parseBoolean),
     "Link Defaults": Metadata("Link Defaults", "linkDefaults", joinDdList, parseLinkDefaults),
     "Local Boilerplate": Metadata(
-        "Local Boilerplate",
-        "localBoilerplate",
-        joinBoolSet,
-        partial(parseBoolishList, default=False),
+        "Local Boilerplate", "localBoilerplate", joinBoolSet, partial(parseBoolishList, default=False)
     ),
     "Logo": Metadata("Logo", "logo", joinValue, parseLiteral),
     "Mailing List Archives": Metadata("Mailing List Archives", "mailingListArchives", joinValue, parseLiteral),
@@ -1292,10 +1199,7 @@ def parseLiteralList(key, val, lineNum):  # pylint: disable=unused-argument
     "Markup Shorthands": Metadata("Markup Shorthands", "markupShorthands", joinBoolSet, parseMarkupShorthands),
     "Max Toc Depth": Metadata("Max ToC Depth", "maxToCDepth", joinValue, parseMaxToCDepth),
     "Metadata Include": Metadata(
-        "Metadata Include",
-        "metadataInclude",
-        joinBoolSet,
-        partial(parseBoolishList, default=True),
+        "Metadata Include", "metadataInclude", joinBoolSet, partial(parseBoolishList, default=True)
     ),
     "Metadata Order": Metadata("Metadata Order", "metadataOrder", joinValue, parseMetadataOrder),
     "No Abstract": Metadata("No Abstract", "noAbstract", joinValue, parseBoolean),
@@ -1320,16 +1224,10 @@ def parseLiteralList(key, val, lineNum):  # pylint: disable=unused-argument
     "Tracking Vector Class": Metadata("Tracking Vector Class", "trackingVectorClass", joinValue, parseLiteralOrNone),
     "Tracking Vector Image": Metadata("Tracking Vector Image", "trackingVectorImage", joinValue, parseLiteralOrNone),
     "Tracking Vector Image Width": Metadata(
-        "Tracking Vector Image Width",
-        "trackingVectorImageWidth",
-        joinValue,
-        parseLiteral,
+        "Tracking Vector Image Width", "trackingVectorImageWidth", joinValue, parseLiteral
     ),
     "Tracking Vector Image Height": Metadata(
-        "Tracking Vector Image Height",
-        "trackingVectorImageHeight",
-        joinValue,
-        parseLiteral,
+        "Tracking Vector Image Height", "trackingVectorImageHeight", joinValue, parseLiteral
     ),
     "Tracking Vector Alt Text": Metadata("Tracking Vector Alt Text", "trackingVectorAltText", joinValue, parseLiteral),
     "Tracking Vector Title": Metadata("Tracking Vector Title", "trackingVectorTitle", joinValue, parseLiteral),
diff --git a/bikeshed/railroadparser.py b/bikeshed/railroadparser.py
index b0ca97fe1b..a9c0579b65 100644
--- a/bikeshed/railroadparser.py
+++ b/bikeshed/railroadparser.py
@@ -27,10 +27,7 @@ def parse(string):
         if line.startswith(initialIndent):
             lines[i] = line[len(initialIndent) :]
         else:
-            die(
-                "Inconsistent indentation: line {0} is indented less than the first line.",
-                i,
-            )
+            die(f"Inconsistent indentation: line {i} is indented less than the first line.")
             return rr.Diagram()
 
     # Determine subsequent indentation
@@ -54,21 +51,13 @@ def parse(string):
             indent += 1
             line = line[len(indentText) :]
         if indent > lastIndent + 1:
-            die(
-                "Line {0} jumps more than 1 indent level from the previous line:\n{1}",
-                i,
-                line.strip(),
-            )
+            die(f"Line {i} jumps more than 1 indent level from the previous line:\n{line.strip()}")
             return rr.Diagram()
         lastIndent = indent
         if re.match(fr"\s*({blockNames})\W", line):
             match = re.match(r"\s*(\w+)\s*:\s*(.*)", line)
             if not match:
-                die(
-                    "Line {0} doesn't match the grammar 'Command: optional-prelude'. Got:\n{1}",
-                    i,
-                    line.strip(),
-                )
+                die(f"Line {i} doesn't match the grammar 'Command: optional-prelude'. Got:\n{line.strip()}")
                 return rr.Diagram()
             command = match.group(1)
             prelude = match.group(2).strip()
@@ -76,11 +65,7 @@ def parse(string):
         elif re.match(fr"\s*({textNames})\W", line):
             match = re.match(r"\s*(\w+)(\s[\w\s]+)?:\s*(.*)", line)
             if not match:
-                die(
-                    "Line {0} doesn't match the grammar 'Command [optional prelude]: text'. Got:\n{1},",
-                    i,
-                    line.strip(),
-                )
+                die(f"Line {i} doesn't match the grammar 'Command [optional prelude]: text'. Got:\n{line.strip()},")
                 return rr.Diagram()
             command = match.group(1)
             if match.group(2):
@@ -97,9 +82,7 @@ def parse(string):
             }
         else:
             die(
-                "Line {0} doesn't contain a valid railroad-diagram command. Got:\n{1}",
-                i,
-                line.strip(),
+                f"Line {i} doesn't contain a valid railroad-diagram command. Got:\n{line.strip()}",
             )
             return
 
@@ -120,34 +103,34 @@ def _createDiagram(command, prelude, children, text=None, line=-1):
         return rr.Diagram(*children)
     if command in ("T", "Terminal"):
         if children:
-            return die("Line {0} - Terminal commands cannot have children.", line)
+            return die(f"Line {line} - Terminal commands cannot have children.")
         return rr.Terminal(text, prelude)
     if command in ("N", "NonTerminal"):
         if children:
-            return die("Line {0} - NonTerminal commands cannot have children.", line)
+            return die(f"Line {line} - NonTerminal commands cannot have children.")
         return rr.NonTerminal(text, prelude)
     if command in ("C", "Comment"):
         if children:
-            return die("Line {0} - Comment commands cannot have children.", line)
+            return die(f"Line {line} - Comment commands cannot have children.")
         return rr.Comment(text, prelude)
     if command in ("S", "Skip"):
         if children:
-            return die("Line {0} - Skip commands cannot have children.", line)
+            return die(f"Line {line} - Skip commands cannot have children.")
         if text:
-            return die("Line {0} - Skip commands cannot have text.", line)
+            return die(f"Line {line} - Skip commands cannot have text.")
         return rr.Skip()
     if command in ("And", "Seq", "Sequence"):
         if prelude:
-            return die("Line {0} - Sequence commands cannot have preludes.", line)
+            return die(f"Line {line} - Sequence commands cannot have preludes.")
         if not children:
-            return die("Line {0} - Sequence commands need at least one child.", line)
+            return die(f"Line {line} - Sequence commands need at least one child.")
         children = [_f for _f in [_createDiagram(**child) for child in children] if _f]
         return rr.Sequence(*children)
     if command in ("Stack",):
         if prelude:
-            return die("Line {0} - Stack commands cannot have preludes.", line)
+            return die(f"Line {line} - Stack commands cannot have preludes.")
         if not children:
-            return die("Line {0} - Stack commands need at least one child.", line)
+            return die(f"Line {line} - Stack commands need at least one child.")
         children = [_f for _f in [_createDiagram(**child) for child in children] if _f]
         return rr.Stack(*children)
     if command in ("Or", "Choice"):
@@ -157,39 +140,31 @@ def _createDiagram(command, prelude, children, text=None, line=-1):
             try:
                 default = int(prelude)
             except ValueError:
-                die(
-                    "Line {0} - Choice preludes must be an integer. Got:\n{1}",
-                    line,
-                    prelude,
-                )
+                die(f"Line {line} - Choice preludes must be an integer. Got:\n{prelude}")
                 default = 0
         if not children:
-            return die("Line {0} - Choice commands need at least one child.", line)
+            return die(f"Line {line} - Choice commands need at least one child.")
         children = [_f for _f in [_createDiagram(**child) for child in children] if _f]
         return rr.Choice(default, *children)
     if command in ("Opt", "Optional"):
         if prelude not in ("", "skip"):
-            return die(
-                "Line {0} - Optional preludes must be nothing or 'skip'. Got:\n{1}",
-                line,
-                prelude,
-            )
+            return die(f"Line {line} - Optional preludes must be nothing or 'skip'. Got:\n{prelude}")
         if len(children) != 1:
-            return die("Line {0} - Optional commands need exactly one child.", line)
+            return die(f"Line {line} - Optional commands need exactly one child.")
         children = [_f for _f in [_createDiagram(**child) for child in children] if _f]
         return rr.Optional(*children, skip=(prelude == "skip"))
     if command in ("Plus", "OneOrMore"):
         if prelude:
-            return die("Line {0} - OneOrMore commands cannot have preludes.", line)
+            return die(f"Line {line} - OneOrMore commands cannot have preludes.")
         if 0 == len(children) > 2:
-            return die("Line {0} - OneOrMore commands must have one or two children.", line)
+            return die(f"Line {line} - OneOrMore commands must have one or two children.")
         children = [_f for _f in [_createDiagram(**child) for child in children] if _f]
         return rr.OneOrMore(*children)
     if command in ("Star", "ZeroOrMore"):
         if prelude:
-            return die("Line {0} - ZeroOrMore commands cannot have preludes.", line)
+            return die(f"Line {line} - ZeroOrMore commands cannot have preludes.")
         if 0 == len(children) > 2:
-            return die("Line {0} - ZeroOrMore commands must have one or two children.", line)
+            return die(f"Line {line} - ZeroOrMore commands must have one or two children.")
         children = [_f for _f in [_createDiagram(**child) for child in children] if _f]
         return rr.ZeroOrMore(*children)
-    return die("Line {0} - Unknown command '{1}'.", line, command)
+    return die(f"Line {line} - Unknown command '{command}'.")
diff --git a/bikeshed/refs/RefSource.py b/bikeshed/refs/RefSource.py
index f4a8540401..b42dae42b8 100644
--- a/bikeshed/refs/RefSource.py
+++ b/bikeshed/refs/RefSource.py
@@ -160,7 +160,7 @@ def forRefsIterator(targetFors):
                 linkTypes = list(config.linkTypeToDfnType[linkType])
             else:
                 if error:
-                    linkerror("Unknown link type '{0}'.", linkType)
+                    linkerror(f"Unknown link type '{linkType}'.")
                 return [], "type"
             refs = [x for x in refs if x.type in linkTypes]
         if not refs:
diff --git a/bikeshed/refs/ReferenceManager.py b/bikeshed/refs/ReferenceManager.py
index 7f2ee8a1e4..bc4c02ee0b 100644
--- a/bikeshed/refs/ReferenceManager.py
+++ b/bikeshed/refs/ReferenceManager.py
@@ -236,9 +236,7 @@ def addLocalDfns(self, dfns):
                 linkTexts = config.linkTextsFromElement(el)
             except config.DuplicatedLinkText as e:
                 die(
-                    "The term '{0}' is in both lt and local-lt of the element {1}.",
-                    e.offendingText,
-                    outerHTML(e.el),
+                    f"The term '{e.offendingText}' is in both lt and local-lt of the element {outerHTML(e.el)}.",
                     el=e.el,
                 )
                 linkTexts = e.allTexts
@@ -247,12 +245,7 @@ def addLocalDfns(self, dfns):
                 linkText = re.sub(r"\s+", " ", linkText)
                 linkType = treeAttr(el, "data-dfn-type")
                 if linkType not in config.dfnTypes:
-                    die(
-                        "Unknown local dfn type '{0}':\n  {1}",
-                        linkType,
-                        outerHTML(el),
-                        el=el,
-                    )
+                    die(f"Unknown local dfn type '{linkType}':\n  {outerHTML(el)}", el=el)
                     continue
                 if linkType in config.lowercaseTypes:
                     linkText = linkText.lower()
@@ -263,12 +256,7 @@ def addLocalDfns(self, dfns):
                         0
                     ]
                     if existingRefs and existingRefs[0].el is not el:
-                        die(
-                            "Multiple local '{1}' s have the same linking text '{0}'.",
-                            linkText,
-                            linkType,
-                            el=el,
-                        )
+                        die(f"Multiple local '{linkType}' s have the same linking text '{linkText}'.", el=el)
                         continue
                 else:
                     dfnFor = set(config.splitForValues(dfnFor))
@@ -283,10 +271,7 @@ def addLocalDfns(self, dfns):
                         if existingRefs and existingRefs[0].el is not el:
                             encounteredError = True
                             die(
-                                "Multiple local '{1}' s for '{2}' have the same linking text '{0}'.",
-                                linkText,
-                                linkType,
-                                singleFor,
+                                f"Multiple local '{linkType}' s for '{singleFor}' have the same linking text '{linkText}'.",
                                 el=el,
                             )
                             break
@@ -366,9 +351,7 @@ def getRef(
         if status not in config.linkStatuses and status is not None:
             if error:
                 die(
-                    "Unknown spec status '{0}'. Status must be {1}.",
-                    status,
-                    config.englishFromList(config.linkStatuses),
+                    f"Unknown spec status '{status}'. Status must be {config.englishFromList(config.linkStatuses)}.",
                     el=el,
                 )
             return None
@@ -524,9 +507,7 @@ def getRef(
                 if len(possibleMethods) > 1:
                     # Too many to disambiguate.
                     linkerror(
-                        "The argument autolink '{0}' for '{1}' has too many possible overloads to disambiguate. Please specify the full method signature this argument is for.",
-                        text,
-                        linkFor,
+                        f"The argument autolink '{text}' for '{linkFor}' has too many possible overloads to disambiguate. Please specify the full method signature this argument is for.",
                         el=el,
                     )
                 # Try out all the combinations of interface/status/signature
@@ -581,9 +562,7 @@ def getRef(
             if zeroRefsError and len(methodRefs) > 1:
                 # More than one possible foo() overload, can't tell which to link to
                 linkerror(
-                    "Too many possible method targets to disambiguate '{0}/{1}'. Please specify the names of the required args, like 'foo(bar, baz)', in the 'for' attribute.",
-                    linkFor,
-                    text,
+                    f"Too many possible method targets to disambiguate '{linkFor}/{text}'. Please specify the names of the required args, like 'foo(bar, baz)', in the 'for' attribute.",
                     el=el,
                 )
                 return
@@ -594,83 +573,39 @@ def getRef(
                 # Custom properties/descriptors aren't ever defined anywhere
                 return None
             if zeroRefsError:
-                linkerror("No '{0}' refs found for '{1}'.", linkType, text, el=el)
+                linkerror(f"No '{linkType}' refs found for '{text}'.", el=el)
             return None
         elif failure == "export":
             if zeroRefsError:
-                linkerror(
-                    "No '{0}' refs found for '{1}' that are marked for export.",
-                    linkType,
-                    text,
-                    el=el,
-                )
+                linkerror(f"No '{linkType}' refs found for '{text}' that are marked for export.", el=el)
             return None
         elif failure == "spec":
             if zeroRefsError:
-                linkerror(
-                    "No '{0}' refs found for '{1}' with spec '{2}'.",
-                    linkType,
-                    text,
-                    spec,
-                    el=el,
-                )
+                linkerror(f"No '{linkType}' refs found for '{text}' with spec '{spec}'.", el=el)
             return None
         elif failure == "for":
             if zeroRefsError:
                 if spec is None:
-                    linkerror(
-                        "No '{0}' refs found for '{1}' with for='{2}'.",
-                        linkType,
-                        text,
-                        linkFor,
-                        el=el,
-                    )
+                    linkerror(f"No '{linkType}' refs found for '{text}' with for='{linkFor}'.", el=el)
                 else:
-                    linkerror(
-                        "No '{0}' refs found for '{1}' with for='{2}' in spec '{3}'.",
-                        linkType,
-                        text,
-                        linkFor,
-                        spec,
-                        el=el,
-                    )
+                    linkerror(f"No '{linkType}' refs found for '{text}' with for='{linkFor}' in spec '{spec}'.", el=el)
             return None
         elif failure == "status":
             if zeroRefsError:
                 if spec is None:
-                    linkerror(
-                        "No '{0}' refs found for '{1}' compatible with status '{2}'.",
-                        linkType,
-                        text,
-                        status,
-                        el=el,
-                    )
+                    linkerror(f"No '{linkType}' refs found for '{text}' compatible with status '{status}'.", el=el)
                 else:
                     linkerror(
-                        "No '{0}' refs found for '{1}' compatible with status '{2}' in spec '{3}'.",
-                        linkType,
-                        text,
-                        status,
-                        spec,
+                        f"No '{linkType}' refs found for '{text}' compatible with status '{status}' in spec '{spec}'.",
                         el=el,
                     )
             return None
         elif failure == "ignored-specs":
             if zeroRefsError:
-                linkerror(
-                    "The only '{0}' refs for '{1}' were in ignored specs:\n{2}",
-                    linkType,
-                    text,
-                    outerHTML(el),
-                    el=el,
-                )
+                linkerror(f"The only '{linkType}' refs for '{text}' were in ignored specs:\n{outerHTML(el)}", el=el)
             return None
         elif failure:
-            die(
-                "Programming error - I'm not catching '{0}'-type link failures. Please report!",
-                failure,
-                el=el,
-            )
+            die(f"Programming error - I'm not catching '{failure}'-type link failures. Please report!", el=el)
             return None
 
         if len(refs) == 1:
@@ -708,10 +643,7 @@ def getBiblioRef(
         depth=0,
     ):
         if depth > 100:
-            die(
-                "Data error in biblio files; infinitely recursing trying to find [{0}].",
-                text,
-            )
+            die(f"Data error in biblio files; infinitely recursing trying to find [{text}].")
             return
         key = text.lower()
         while True:
@@ -746,27 +678,21 @@ def getBiblioRef(
             if failFromWrongSuffix and not quiet:
                 numericSuffixes = self.biblioNumericSuffixes[unversionedKey]
                 die(
-                    "A biblio link references {0}, but only {1} exists in SpecRef.",
-                    text,
-                    config.englishFromList(numericSuffixes),
+                    f"A biblio link references {text}, but only {config.englishFromList(numericSuffixes)} exists in SpecRef."
                 )
             return None
 
         candidate = self._bestCandidateBiblio(candidates)
         # TODO: When SpecRef definitely has all the CSS specs, turn on this code.
         # if candidates[0]['order'] > 3: # 3 is SpecRef level
-        #    warn("Bibliography term '{0}' wasn't found in SpecRef.\n         Please find the equivalent key in SpecRef, or submit a PR to SpecRef.", text)
+        #    warn(f"Bibliography term '{text}' wasn't found in SpecRef.\n         Please find the equivalent key in SpecRef, or submit a PR to SpecRef.")
         if candidate["biblioFormat"] == "string":
             bib = biblio.StringBiblioEntry(**candidate)
         elif candidate["biblioFormat"] == "alias":
             # Follow the chain to the real candidate
             bib = self.getBiblioRef(candidate["aliasOf"], status=status, el=el, quiet=True, depth=depth + 1)
             if bib is None:
-                die(
-                    "Biblio ref [{0}] claims to be an alias of [{1}], which doesn't exist.",
-                    text,
-                    candidate["aliasOf"],
-                )
+                die(f"Biblio ref [{text}] claims to be an alias of [{candidate['aliasOf']}], which doesn't exist.")
                 return None
         elif candidate.get("obsoletedBy", "").strip():
             # Obsoleted by something. Unless otherwise indicated, follow the chain.
@@ -782,9 +708,7 @@ def getBiblioRef(
                 )
                 if not quiet:
                     die(
-                        "Obsolete biblio ref: [{0}] is replaced by [{1}]. Either update the reference, or use [{0} obsolete] if this is an intentionally-obsolete reference.",
-                        candidate["linkText"],
-                        bib.linkText,
+                        f"Obsolete biblio ref: [{candidate['linkText']}] is replaced by [{bib.linkText}]. Either update the reference, or use [{candidate['linkText']} obsolete] if this is an intentionally-obsolete reference."
                     )
         else:
             bib = biblio.BiblioEntry(preferredURL=status, **candidate)
@@ -905,7 +829,7 @@ def reportMultiplePossibleRefs(possibleRefs, linkText, linkType, linkFor, defaul
             mergedRefs.append(refs)
 
     if linkFor:
-        error = "Multiple possible '{}' {} refs for '{}'.".format(linkText, linkType, linkFor)
+        error = f"Multiple possible '{linkText}' {linkType} refs for '{linkFor}'."
     else:
         error = f"Multiple possible '{linkText}' {linkType} refs."
     error += f"\nArbitrarily chose {defaultRef.url}"
@@ -922,10 +846,9 @@ def reportMultiplePossibleRefs(possibleRefs, linkText, linkType, linkFor, defaul
 
 
 def reportAmbiguousForlessLink(el, text, forlessRefs, localRefs):
+    localRefText = "\n".join([refToText(ref) for ref in simplifyPossibleRefs(localRefs, alwaysShowFor=True)])
+    forlessRefText = "\n".join([refToText(ref) for ref in simplifyPossibleRefs(forlessRefs, alwaysShowFor=True)])
     linkerror(
-        "Ambiguous for-less link for '{0}', please see  for instructions:\nLocal references:\n{1}\nfor-less references:\n{2}",
-        text,
-        "\n".join([refToText(ref) for ref in simplifyPossibleRefs(localRefs, alwaysShowFor=True)]),
-        "\n".join([refToText(ref) for ref in simplifyPossibleRefs(forlessRefs, alwaysShowFor=True)]),
+        f"Ambiguous for-less link for '{text}', please see  for instructions:\nLocal references:\n{localRefText}\nfor-less references:\n{forlessRefText}",
         el=el,
     )
diff --git a/bikeshed/shorthands/element.py b/bikeshed/shorthands/element.py
index 19875509dc..339f0d6f3d 100644
--- a/bikeshed/shorthands/element.py
+++ b/bikeshed/shorthands/element.py
@@ -64,10 +64,7 @@ def respondEnd(self):
 
         if self.linkType not in config.markupTypes and self.linkType != "element-sub":
             die(
-                "Shorthand {0} gives type as '{1}', but only markup types ({2}) are allowed.",
-                self.bsAutolink,
-                self.linkType,
-                config.englishFromList(config.idlTypes),
+                f"Shorthand {self.bsAutolink} gives type as '{self.linkType}', but only markup types ({config.englishFromList(config.markupTypes)}) are allowed."
             )
             return steps.Success(E.span({}, self.bsAutolink))
 
diff --git a/bikeshed/shorthands/idl.py b/bikeshed/shorthands/idl.py
index 160f1b5573..e544a1da1c 100644
--- a/bikeshed/shorthands/idl.py
+++ b/bikeshed/shorthands/idl.py
@@ -55,11 +55,7 @@ def respondEnd(self):
         self.bsAutolink += "}}"
 
         if self.linkType not in config.idlTypes:
-            die(
-                "Shorthand {0} gives type as '{1}', but only IDL types are allowed.",
-                self.bsAutolink,
-                self.linkType,
-            )
+            die(f"Shorthand {self.bsAutolink} gives type as '{self.linkType}', but only IDL types are allowed.")
             return steps.Success(E.span({}, self.bsAutolink))
 
         if not self.linkText:
diff --git a/bikeshed/shorthands/oldShorthands.py b/bikeshed/shorthands/oldShorthands.py
index 9b75312221..17a6a08d61 100644
--- a/bikeshed/shorthands/oldShorthands.py
+++ b/bikeshed/shorthands/oldShorthands.py
@@ -35,9 +35,7 @@ def transformProductionPlaceholders(doc):
                 linkType = match.group(2)
             else:
                 die(
-                    "Shorthand <<{0}>> gives type as '{1}', but only 'property' and 'descriptor' are allowed.",
-                    match.group(0),
-                    match.group(3),
+                    f"Shorthand <<{match.group(0)}>> gives type as '{match.group(3)}', but only 'property' and 'descriptor' are allowed.",
                     el=el,
                 )
                 el.tag = "span"
@@ -80,12 +78,11 @@ def transformProductionPlaceholders(doc):
                 rangeStart = formatValue(rangeStart)
                 rangeEnd = formatValue(rangeEnd)
                 if rangeStart is None or rangeEnd is None:
-                    die("Shorthand <<{0}>> has an invalid range.", text, el=el)
+                    die(f"Shorthand <<{text}>> has an invalid range.", el=el)
                 try:
                     if not correctlyOrderedRange(rangeStart, rangeEnd):
                         die(
-                            "Shorthand <<{0}>> has a range whose start is not less than its end.",
-                            text,
+                            f"Shorthand <<{text}>> has a range whose start is not less than its end.",
                             el=el,
                         )
                 except:
@@ -95,11 +92,7 @@ def transformProductionPlaceholders(doc):
                 el.set("data-lt", f"<{term}>")
             el.text = f"<{interior}>"
             continue
-        die(
-            "Shorthand <<{0}>> does not match any recognized shorthand grammar.",
-            text,
-            el=el,
-        )
+        die(f"Shorthand <<{text}>> does not match any recognized shorthand grammar.", el=el)
         el.tag = "span"
         el.text = el.get("bs-autolink-syntax")
         continue
@@ -165,9 +158,7 @@ def transformMaybePlaceholders(doc):
                 linkType = match.group(3)
             else:
                 die(
-                    "Shorthand ''{0}'' gives type as '{1}', but only “maybe” types are allowed.",
-                    match.group(0),
-                    match.group(3),
+                    f"Shorthand ''{match.group(0)}'' gives type as '{match.group(3)}', but only “maybe” types are allowed.",
                     el=el,
                 )
                 el.tag = "css"
@@ -281,11 +272,7 @@ def replacer(reg, rep, el, text):
             continue
         if replacer(varRe, varReplacer, el, text):
             continue
-        die(
-            " element doesn't contain a recognized autolinking syntax:\n{0}",
-            outerHTML(el),
-            el=el,
-        )
+        die(f" element doesn't contain a recognized autolinking syntax:\n{outerHTML(el)}", el=el)
         el.tag = "span"
 
 
@@ -482,9 +469,7 @@ def propdescReplacer(match):
         pass
     else:
         die(
-            "Shorthand {0} gives type as '{1}', but only 'property' and 'descriptor' are allowed.",
-            match.group(0),
-            linkType,
+            f"Shorthand {match.group(0)} gives type as '{linkType}', but only 'property' and 'descriptor' are allowed.",
         )
         return E.span(match.group(0))
     if linkText is None:
@@ -526,9 +511,7 @@ def idlReplacer(match):
         pass
     else:
         die(
-            "Shorthand {0} gives type as '{1}', but only IDL types are allowed.",
-            match.group(0),
-            linkType,
+            f"Shorthand {match.group(0)} gives type as '{linkType}', but only IDL types are allowed.",
         )
         return E.span(match.group(0))
     if linkText is None:
diff --git a/bikeshed/shorthands/propdesc.py b/bikeshed/shorthands/propdesc.py
index 4e7ca47ec8..332dbb20ac 100644
--- a/bikeshed/shorthands/propdesc.py
+++ b/bikeshed/shorthands/propdesc.py
@@ -59,9 +59,7 @@ def respondEnd(self):
 
         if self.linkType not in ["property", "descriptor", "propdesc"]:
             die(
-                "Shorthand {0} gives type as '{1}', but only 'property' and 'descriptor' are allowed.",
-                self.bsAutolink,
-                self.linkType,
+                f"Shorthand {self.bsAutolink} gives type as '{self.linkType}', but only 'property' and 'descriptor' are allowed.",
             )
             return steps.Success(E.span(self.bsAutolink))
 
diff --git a/bikeshed/unsortedJunk.py b/bikeshed/unsortedJunk.py
index 4af566483c..0a4454a0a5 100644
--- a/bikeshed/unsortedJunk.py
+++ b/bikeshed/unsortedJunk.py
@@ -285,8 +285,9 @@ def algoName(el):
                 varLines.append(f"  '{var}'")
     if varLines:
         warn(
-            "The following s were only used once in the document:\n{0}\nIf these are not typos, please add an ignore='' attribute to the .",
-            "\n".join(varLines),
+            f"The following s were only used once in the document:\n"
+            + "\n".join(varLines)
+            + "\nIf these are not typos, please add an ignore='' attribute to the ."
         )
 
     if atLeastOneAlgo:
@@ -422,15 +423,10 @@ def fixIntraDocumentReferences(doc):
     for el in findAll("a[href^='#']:not([href='#']):not(.self-link):not([data-link-type])", doc):
         targetID = parse.unquote(el.get("href")[1:])
         if el.get("data-section") is not None and targetID not in headingIDs:
-            die(
-                "Couldn't find target document section {0}:\n{1}",
-                targetID,
-                outerHTML(el),
-                el=el,
-            )
+            die(f"Couldn't find target document section {targetID}:\n{outerHTML(el)}", el=el)
             continue
         if targetID not in ids:
-            die("Couldn't find target anchor {0}:\n{1}", targetID, outerHTML(el), el=el)
+            die(f"Couldn't find target anchor {targetID}:\n{outerHTML(el)}", el=el)
             continue
         if isEmpty(el):
             # TODO Allow this to respect "safe" markup (, etc) in the title
@@ -438,14 +434,14 @@ def fixIntraDocumentReferences(doc):
             content = find(".content", target)
             if content is None:
                 die(
-                    "Tried to generate text for a section link, but the target isn't a heading:\n{0}",
+                    f"Tried to generate text for a section link, but the target isn't a heading:\n{outerHTML(el)}",
                     outerHTML(el),
                     el=el,
                 )
                 continue
             text = textContent(content).strip()
             if target.get("data-level") is not None:
-                text = "§\u202f{1} {0}".format(text, target.get("data-level"))
+                text = f"§\u202f{target.get('data-level')} {text}"
             appendChild(el, text)
 
 
@@ -455,15 +451,13 @@ def fixInterDocumentReferences(doc):
         section = el.get("spec-section", "")
         if spec is None:
             die(
-                "Spec-section autolink doesn't have a 'spec' attribute:\n{0}",
-                outerHTML(el),
+                f"Spec-section autolink doesn't have a 'spec' attribute:\n{outerHTML(el)}",
                 el=el,
             )
             continue
         if section is None:
             die(
-                "Spec-section autolink doesn't have a 'spec-section' attribute:\n{0}",
-                outerHTML(el),
+                f"Spec-section autolink doesn't have a 'spec-section' attribute:\n{outerHTML(el)}",
                 el=el,
             )
             continue
@@ -478,12 +472,9 @@ def fixInterDocumentReferences(doc):
                 fillInterDocumentReferenceFromShepherd(doc, el, vNames[0], section)
                 if len(vNames) > 1:
                     die(
-                        "Section autolink {2} attempts to link to unversioned spec name '{0}', "
-                        + "but that spec is versioned as {1}. "
+                        f"Section autolink {outerHTML(el)} attempts to link to unversioned spec name '{spec}', "
+                        + "but that spec is versioned as {}. ".format(config.englishFromList(f"'{x}'" for x in vNames))
                         + "Please choose a versioned spec name.",
-                        spec,
-                        config.englishFromList(f"'{x}'" for x in vNames),
-                        outerHTML(el),
                         el=el,
                     )
                 continue
@@ -493,9 +484,7 @@ def fixInterDocumentReferences(doc):
             continue
         # Unknown spec
         die(
-            "Spec-section autolink tried to link to non-existent '{0}' spec:\n{1}",
-            spec,
-            outerHTML(el),
+            f"Spec-section autolink tried to link to non-existent '{spec}' spec:\n{outerHTML(el)}",
             el=el,
         )
 
@@ -506,10 +495,7 @@ def fillInterDocumentReferenceFromShepherd(doc, el, spec, section):
         heading = specData[section]
     else:
         die(
-            "Couldn't find section '{0}' in spec '{1}':\n{2}",
-            section,
-            spec,
-            outerHTML(el),
+            f"Couldn't find section '{section}' in spec '{spec}':\n{outerHTML(el)}",
             el=el,
         )
         return
@@ -521,10 +507,8 @@ def fillInterDocumentReferenceFromShepherd(doc, el, spec, section):
         else:
             # multiple headings of this id, user needs to disambiguate
             die(
-                "Multiple headings with id '{0}' for spec '{1}'. Please specify:\n{2}",
-                section,
-                spec,
-                "\n".join("  [[{}]]".format(spec + x) for x in heading),
+                f"Multiple headings with id '{section}' for spec '{spec}'. Please specify:\n"
+                + "\n".join("  [[{}]]".format(spec + x) for x in heading),
                 el=el,
             )
             return
@@ -549,11 +533,7 @@ def fillInterDocumentReferenceFromShepherd(doc, el, spec, section):
 def fillInterDocumentReferenceFromSpecref(doc, el, spec, section):
     bib = doc.refs.getBiblioRef(spec)
     if isinstance(bib, biblio.StringBiblioEntry):
-        die(
-            "Can't generate a cross-spec section ref for '{0}', because the biblio entry has no url.",
-            spec,
-            el=el,
-        )
+        die(f"Can't generate a cross-spec section ref for '{spec}', because the biblio entry has no url.", el=el)
         return
     el.tag = "a"
     el.set("href", bib.url + section)
@@ -610,27 +590,23 @@ def classifyDfns(doc, dfns):
     for el in dfns:
         dfnType = determineDfnType(el, inferCSS=doc.md.inferCSSDfns)
         if dfnType not in config.dfnTypes:
-            die("Unknown dfn type '{0}' on:\n{1}", dfnType, outerHTML(el), el=el)
+            die(f"Unknown dfn type '{dfnType}':\n{outerHTML(el)}", el=el)
             continue
         dfnFor = treeAttr(el, "data-dfn-for")
         primaryDfnText = config.firstLinkTextFromElement(el)
         if primaryDfnText is None:
-            die("Dfn has no linking text:\n{0}", outerHTML(el), el=el)
+            die(f"Dfn has no linking text:\n{outerHTML(el)}", el=el)
             continue
         if len(primaryDfnText) > 300:
             # Almost certainly accidentally missed the end tag
             warn(
-                "Dfn has extremely long text - did you forget the  tag?\n{0}",
-                outerHTML(el),
+                f"Dfn has extremely long text - did you forget the  tag?\n{outerHTML(el)}",
                 el=el,
             )
         # Check for invalid fors, as it's usually some misnesting.
         if dfnFor and dfnType in config.typesNotUsingFor:
             die(
-                "'{0}' definitions don't use a 'for' attribute, but this one claims it's for '{1}' (perhaps inherited from an ancestor). This is probably a markup error.\n{2}",
-                dfnType,
-                dfnFor,
-                outerHTML(el),
+                f"'{dfnType}' definitions don't use a 'for' attribute, but this one claims it's for '{dfnFor}' (perhaps inherited from an ancestor). This is probably a markup error.\n{outerHTML(el)}",
                 el=el,
             )
         # Push the dfn type down to the  itself.
@@ -641,9 +617,7 @@ def classifyDfns(doc, dfns):
             el.set("data-dfn-for", dfnFor)
         elif dfnType in config.typesUsingFor:
             die(
-                "'{0}' definitions need to specify what they're for.\nAdd a 'for' attribute to {1}, or add 'dfn-for' to an ancestor.",
-                dfnType,
-                outerHTML(el),
+                f"'{dfnType}' definitions need to specify what they're for.\nAdd a 'for' attribute to {outerHTML(el)}, or add 'dfn-for' to an ancestor.",
                 el=el,
             )
             continue
@@ -651,15 +625,13 @@ def classifyDfns(doc, dfns):
         if dfnType in config.functionishTypes:
             if not re.search(r"\(.*\)$", primaryDfnText):
                 die(
-                    "Function/methods must end with a () arglist in their linking text. Got '{0}'.",
-                    primaryDfnText,
+                    f"Function/methods must end with a () arglist in their linking text. Got '{primaryDfnText}'.\n{outerHTML(el)}",
                     el=el,
                 )
                 continue
             if not re.match(r"^[\w\[\]-]+\s*\(", primaryDfnText):
                 die(
-                    "Function/method names can only contain alphanums, underscores, dashes, or []. Got '{0}'.",
-                    primaryDfnText,
+                    f"Function/method names can only contain alphanums, underscores, dashes, or []. Got '{primaryDfnText}'.\n{outerHTML(el)}",
                     el=el,
                 )
                 continue
@@ -676,8 +648,7 @@ def classifyDfns(doc, dfns):
                     el.set("data-lt", "|".join(names))
                 else:
                     die(
-                        "BIKESHED ERROR: Unhandled functionish type '{0}' in classifyDfns. Please report this to Bikeshed's maintainer.",
-                        dfnType,
+                        f"BIKESHED ERROR: Unhandled functionish type '{dfnType}' in classifyDfns. Please report this to Bikeshed's maintainer.",
                         el=el,
                     )
         # If type=argument, try to infer what it's for.
@@ -690,8 +661,7 @@ def classifyDfns(doc, dfns):
                 )
             elif treeAttr(el, "data-dfn-for") is None:
                 die(
-                    "'argument' dfns need to specify what they're for, or have it be inferrable from their parent. Got:\n{0}",
-                    outerHTML(el),
+                    f"'argument' dfns need to specify what they're for, or have it be inferrable from their parent. Got:\n{outerHTML(el)}",
                     el=el,
                 )
                 continue
@@ -771,7 +741,7 @@ def determineLinkType(el):
     if linkType:
         if linkType in config.linkTypes:
             return linkType
-        die("Unknown link type '{0}' on:\n{1}", linkType, outerHTML(el), el=el)
+        die(f"Unknown link type '{linkType}':\n{outerHTML(el)}", el=el)
         return "unknown-type"
     # 2. Introspect on the text
     text = textContent(el)
@@ -800,7 +770,7 @@ def determineLinkText(el):
         linkText = contents
     linkText = foldWhitespace(linkText)
     if len(linkText) == 0:
-        die("Autolink {0} has no linktext.", outerHTML(el), el=el)
+        die(f"Autolink {outerHTML(el)} has no linktext.", el=el)
     return linkText
 
 
@@ -836,9 +806,7 @@ def processBiblioLinks(doc):
             storage = doc.informativeRefs
         else:
             die(
-                "Unknown data-biblio-type value '{0}' on {1}. Only 'normative' and 'informative' allowed.",
-                biblioType,
-                outerHTML(el),
+                f"Unknown data-biblio-type value '{biblioType}' on {outerHTML(el)}. Only 'normative' and 'informative' allowed.",
                 el=el,
             )
             continue
@@ -865,9 +833,8 @@ def processBiblioLinks(doc):
             if not okayToFail:
                 closeBiblios = biblio.findCloseBiblios(doc.refs.biblioKeys, linkText)
                 die(
-                    "Couldn't find '{0}' in bibliography data. Did you mean:\n{1}",
-                    linkText,
-                    "\n".join("  " + b for b in closeBiblios),
+                    f"Couldn't find '{linkText}' in bibliography data. Did you mean:\n"
+                    + "\n".join("  " + b for b in closeBiblios),
                     el=el,
                 )
             el.tag = "span"
@@ -883,10 +850,7 @@ def processBiblioLinks(doc):
             else:
                 # Oh no! I'm using two different names to refer to the same biblio!
                 die(
-                    "The biblio refs [[{0}]] and [[{1}]] are both aliases of the same base reference [[{2}]]. Please choose one name and use it consistently.",
-                    linkText,
-                    ref.linkText,
-                    ref.originalLinkText,
+                    f"The biblio refs [[{linkText}]] and [[{ref.linkText}]] are both aliases of the same base reference [[{ref.originalLinkText}]]. Please choose one name and use it consistently.",
                     el=el,
                 )
                 # I can keep going, tho - no need to skip this ref
@@ -928,8 +892,8 @@ def verifyUsageOfAllLocalBiblios(doc):
             unusedBiblioKeys.append(b)
     if unusedBiblioKeys:
         warn(
-            "The following locally-defined biblio entries are unused and can be removed:\n{0}",
-            "\n".join(f"  * {b}" for b in unusedBiblioKeys),
+            f"The following locally-defined biblio entries are unused and can be removed:\n"
+            + "\n".join(f"  * {b}" for b in unusedBiblioKeys),
         )
 
 
@@ -971,7 +935,7 @@ def processAutolinks(doc):
         elif status in config.linkStatuses or status is None:
             pass
         else:
-            die("Unknown link status '{0}' on {1}", status, outerHTML(el))
+            die(f"Unknown link status '{status}' on {outerHTML(el)}")
             continue
 
         ref = doc.refs.getRef(
@@ -1140,8 +1104,7 @@ def makeSelfLink(el):
         for el in dfnElements:
             if list(el.iterancestors("a")):
                 warn(
-                    "Found  ancestor, skipping self-link. Swap / order?\n  {0}",
-                    outerHTML(el),
+                    f"Found  ancestor, skipping self-link. Swap / order?\n  {outerHTML(el)}",
                     el=el,
                 )
                 continue
@@ -1256,8 +1219,7 @@ def cleanupHTML(doc):
         # Look for nested  elements, and warn about them.
         if el.tag == "a" and hasAncestor(el, lambda x: x.tag == "a"):
             warn(
-                "The following (probably auto-generated) link is illegally nested in another link:\n{0}",
-                outerHTML(el),
+                f"The following (probably auto-generated) link is illegally nested in another link:\n{outerHTML(el)}",
                 el=el,
             )
 
@@ -1386,8 +1348,7 @@ def formatElementdefTables(doc):
             )
             if len(groupAttrs) == 0:
                 die(
-                    "The element-attr group '{0}' doesn't have any attributes defined for it.",
-                    groupName,
+                    f"The element-attr group '{groupName}' doesn't have any attributes defined for it.",
                     el=el,
                 )
                 continue
@@ -1420,7 +1381,7 @@ def formatArgumentdefTables(doc):
         forMethod = doc.widl.normalized_method_names(table.get("data-dfn-for"))
         method = doc.widl.find(table.get("data-dfn-for"))
         if not method:
-            die("Can't find method '{0}'.", forMethod, el=table)
+            die(f"Can't find method '{forMethod}'.", el=table)
             continue
         for tr in findAll("tbody > tr", table):
             tds = findAll("td", tr)
@@ -1488,7 +1449,7 @@ def inlineRemoteIssues(doc):
             if key in responses:
                 data = responses[key]
             else:
-                warn("Connection error fetching issue #{0}", issue.num)
+                warn(f"Connection error fetching issue #{issue.num}")
                 continue
         if res is None:
             # Already handled in the except block
@@ -1503,14 +1464,10 @@ def inlineRemoteIssues(doc):
         elif res.status_code == 401:
             error = res.json()
             if error["message"] == "Bad credentials":
-                die(
-                    "'{0}' is not a valid GitHub OAuth token. See https://github.com/settings/tokens",
-                    doc.token,
-                )
+                die(f"'{doc.token}' is not a valid GitHub OAuth token. See https://github.com/settings/tokens")
             else:
                 die(
-                    "401 error when fetching GitHub Issues:\n{0}",
-                    config.printjson(error),
+                    "401 error when fetching GitHub Issues:\n" + config.printjson(error),
                 )
             continue
         elif res.status_code == 403:
@@ -1521,8 +1478,7 @@ def inlineRemoteIssues(doc):
                 )
             else:
                 die(
-                    "403 error when fetching GitHub Issues:\n{0}",
-                    config.printjson(error),
+                    "403 error when fetching GitHub Issues:\n" + config.printjson(error),
                 )
             continue
         elif res.status_code >= 400:
@@ -1530,7 +1486,7 @@ def inlineRemoteIssues(doc):
                 error = config.printjson(res.json())
             except ValueError:
                 error = "First 100 characters of error:\n" + res.text[0:100]
-            die("{0} error when fetching GitHub Issues:\n{1}", res.status_code, error)
+            die(f"{res.status_code} error when fetching GitHub Issues:\n" + error)
             continue
         responses[key] = data
         # Put the issue data into the DOM
@@ -1552,7 +1508,7 @@ def inlineRemoteIssues(doc):
                 el,
                 E.a(
                     {"href": href, "class": "marker"},
-                    "Issue #{} on GitHub: “{}”".format(data["number"], data["title"]),
+                    f"Issue #{data['number']} on GitHub: “{data['title']}”",
                 ),
                 *parseHTML(data["body_html"]),
             )
@@ -1564,7 +1520,7 @@ def inlineRemoteIssues(doc):
         with open(config.scriptPath("spec-data", "github-issues.json"), "w", encoding="utf-8") as f:
             f.write(json.dumps(responses, ensure_ascii=False, indent=2, sort_keys=True))
     except Exception as e:
-        warn("Couldn't save GitHub Issues cache to disk.\n{0}", e)
+        warn(f"Couldn't save GitHub Issues cache to disk.\n{e}")
     return
 
 
@@ -1625,9 +1581,8 @@ def addImageSize(doc):
             m = re.match(r"^[ \t\n]*([^ \t\n]+)[ \t\n]+(\d+)x[ \t\n]*$", srcset)
             if m is None:
                 die(
-                    "Couldn't parse 'srcset' attribute: \"{0}\"\n"
+                    f"Couldn't parse 'srcset' attribute: \"{srcset}\"\n"
                     + "Bikeshed only supports a single image followed by an integer resolution. If not targeting Bikeshed specifically, HTML requires a 'src' attribute (and probably a 'width' and 'height' attribute too). This warning can also be suppressed by adding a 'no-autosize' attribute.",
-                    srcset,
                     el=el,
                 )
                 continue
@@ -1639,15 +1594,13 @@ def addImageSize(doc):
             # If the input source can't tell whether a file cheaply exists,
             # PIL very likely can't use it either.
             warn(
-                "At least one  doesn't have its size set ({0}), but given the type of input document, Bikeshed can't figure out what the size should be.\nEither set 'width'/'height' manually, or opt out of auto-detection by setting the 'no-autosize' attribute.",
-                outerHTML(el),
+                f"At least one  doesn't have its size set ({outerHTML(el)}), but given the type of input document, Bikeshed can't figure out what the size should be.\nEither set 'width'/'height' manually, or opt out of auto-detection by setting the 'no-autosize' attribute.",
                 el=el,
             )
             return
         if re.match(r"^(https?:/)?/", src):
             warn(
-                "Autodetection of image dimensions is only supported for local files, skipping this image: {0}\nConsider setting 'width' and 'height' manually or opting out of autodetection by setting the 'no-autosize' attribute.",
-                outerHTML(el),
+                f"Autodetection of image dimensions is only supported for local files, skipping this image: {outerHTML(el)}\nConsider setting 'width' and 'height' manually or opting out of autodetection by setting the 'no-autosize' attribute.",
                 el=el,
             )
             continue
@@ -1657,9 +1610,7 @@ def addImageSize(doc):
             w, h = im.size
         except Exception as e:
             warn(
-                "Couldn't determine width and height of this image: {0}\n{1}",
-                src,
-                e,
+                f"Couldn't determine width and height of this image: {src}\n{e}",
                 el=el,
             )
             continue
@@ -1667,19 +1618,13 @@ def addImageSize(doc):
             el.set("width", str(int(w / res)))
         else:
             warn(
-                "The width ({0}px) of this image is not a multiple of the declared resolution ({1}): {2}\nConsider fixing the image so its width is a multiple of the resolution, or setting its 'width' and 'height' attribute manually.",
-                w,
-                res,
-                src,
+                f"The width ({w}px) of this image is not a multiple of the declared resolution ({res}): {src}\nConsider fixing the image so its width is a multiple of the resolution, or setting its 'width' and 'height' attribute manually.",
                 el=el,
             )
         if h % res == 0:
             el.set("height", str(int(h / res)))
         else:
             warn(
-                "The height ({0}px) of this image is not a multiple of the declared resolution ({1}): {2}\nConsider fixing the image so its height is a multiple of the resolution, or setting its 'width' and 'height' attribute manually.",
-                h,
-                res,
-                src,
+                f"The height ({h}px) of this image is not a multiple of the declared resolution ({res}): {src}\nConsider fixing the image so its height is a multiple of the resolution, or setting its 'width' and 'height' attribute manually.",
                 el=el,
             )
diff --git a/bikeshed/update/main.py b/bikeshed/update/main.py
index c7a01ce9f5..dcbaf1306f 100644
--- a/bikeshed/update/main.py
+++ b/bikeshed/update/main.py
@@ -80,7 +80,7 @@ def fixupDataFiles():
         with open(remotePath("version.txt")) as fh:
             remoteVersion = int(fh.read())
     except OSError as err:
-        warn("Couldn't check the datafile version. Bikeshed may be unstable.\n{0}", err)
+        warn(f"Couldn't check the datafile version. Bikeshed may be unstable.\n{err}")
         return
 
     if localVersion == remoteVersion:
@@ -94,7 +94,7 @@ def fixupDataFiles():
         for filename in os.listdir(remotePath()):
             copyanything(remotePath(filename), localPath(filename))
     except Exception as err:
-        warn("Couldn't update datafiles from cache. Bikeshed may be unstable.\n{0}", err)
+        warn(f"Couldn't update datafiles from cache. Bikeshed may be unstable.\n{err}")
         return
 
 
@@ -111,7 +111,7 @@ def updateReadonlyDataFiles():
                 continue
             copyanything(localPath(filename), remotePath(filename))
     except Exception as err:
-        warn("Error copying over the datafiles:\n{0}", err)
+        warn(f"Error copying over the datafiles:\n{err}")
         return
 
 
@@ -148,7 +148,7 @@ def cleanupFiles(root, touchedPaths, dryRun=False):
             os.remove(absPath)
             oldPaths.append(relPath)
     if oldPaths:
-        say("Success! Deleted {} old files.".format(len(oldPaths)))
+        say(f"Success! Deleted {len(oldPaths)} old files.")
     else:
         say("Success! Nothing to delete.")
 
diff --git a/bikeshed/update/manifest.py b/bikeshed/update/manifest.py
index 5aac005d83..904ce6e2df 100644
--- a/bikeshed/update/manifest.py
+++ b/bikeshed/update/manifest.py
@@ -113,7 +113,7 @@ def updateByManifest(path, dryRun=False):
             localDt = dtFromManifest(fh.readlines())
     except Exception as e:
         localDt = "error"
-        warn("Couldn't find local manifest file.\n{0}", e)
+        warn(f"Couldn't find local manifest file.\n{e}")
 
     # Get the actual file data by regenerating the local manifest,
     # to guard against mistakes or shenanigans
@@ -127,8 +127,7 @@ def updateByManifest(path, dryRun=False):
         remoteFiles = dictFromManifest(remoteManifest)
     except Exception as e:
         warn(
-            "Couldn't download remote manifest file, so can't update. Please report this!\n{0}",
-            e,
+            f"Couldn't download remote manifest file, so can't update. Please report this!\n{e}",
         )
         warn("Update manually with `bikeshed update --skip-manifest`.")
         return False
@@ -147,17 +146,12 @@ def updateByManifest(path, dryRun=False):
                 "Remote data is more than two days old; the update process has probably fallen over. Please report this!"
             )
         if localDt == remoteDt and localDt != 0:
-            say(
-                "Local data is already up-to-date with remote ({0})",
-                localDt.strftime("%Y-%m-%d %H:%M:%S"),
-            )
+            say(f"Local data is already up-to-date with remote ({localDt.strftime('%Y-%m-%d %H:%M:%S')})")
             return True
         elif localDt > remoteDt:
             # No need to update, local data is more recent.
             say(
-                "Local data is fresher ({0}) than remote ({1}), so nothing to update.",
-                localDt.strftime("%Y-%m-%d %H:%M:%S"),
-                remoteDt.strftime("%Y-%m-%d %H:%M:%S"),
+                f"Local data is fresher ({localDt.strftime('%Y-%m-%d %H:%M:%S')}) than remote ({remoteDt.strftime('%Y-%m-%d %H:%M:%S')}), so nothing to update.",
             )
             return True
 
@@ -182,17 +176,13 @@ def updateByManifest(path, dryRun=False):
 
     if not dryRun:
         if newPaths:
-            say(
-                "Updating {0} file{1}...",
-                len(newPaths),
-                "s" if len(newPaths) > 1 else "",
-            )
+            say(f"Updating {len(newPaths)} file{'s' if len(newPaths) > 1 else ''}...")
         goodPaths, badPaths = asyncio.run(updateFiles(path, newPaths))
         try:
             with open(os.path.join(path, "manifest.txt"), "w", encoding="utf-8") as fh:
                 fh.write(createFinishedManifest(remoteManifest, goodPaths, badPaths))
         except Exception as e:
-            warn("Couldn't save new manifest file.\n{0}", e)
+            warn(f"Couldn't save new manifest file.\n{e}")
             return False
     if not badPaths:
         say("Done!")
@@ -225,14 +215,9 @@ async def updateFiles(localPrefix, newPaths):
             currFileTime = time.time()
             if (currFileTime - lastMsgTime) >= messageDelta:
                 if not badPaths:
-                    say("Updated {0}/{1}...", len(goodPaths), len(newPaths))
+                    say(f"Updated {len(goodPaths)}/{len(newPaths)}...")
                 else:
-                    say(
-                        "Updated {0}/{1}, {2} errors...",
-                        len(goodPaths),
-                        len(newPaths),
-                        len(badPaths),
-                    )
+                    say(f"Updated {len(goodPaths)}/{len(newPaths)}, {len(badPaths)} errors...")
                 lastMsgTime = currFileTime
     return goodPaths, badPaths
 
diff --git a/bikeshed/update/updateBiblio.py b/bikeshed/update/updateBiblio.py
index 8f7a9f37fa..c047652e4c 100644
--- a/bikeshed/update/updateBiblio.py
+++ b/bikeshed/update/updateBiblio.py
@@ -27,7 +27,7 @@ def update(path, dryRun=False):
                 with open(p, "w", encoding="utf-8") as fh:
                     writeBiblioFile(fh, biblios)
             except Exception as e:
-                die("Couldn't save biblio database to disk.\n{0}", e)
+                die(f"Couldn't save biblio database to disk.\n{e}")
                 return
 
         # biblio-keys is used to help correct typos,
@@ -74,7 +74,7 @@ def update(path, dryRun=False):
             with open(p, "w", encoding="utf-8") as fh:
                 fh.write(str(json.dumps(reducedNames, indent=0, ensure_ascii=False, sort_keys=True)))
         except Exception as e:
-            die("Couldn't save biblio database to disk.\n{0}", e)
+            die(f"Couldn't save biblio database to disk.\n{e}")
             return
 
         # Collect all the number-suffix names which also exist un-numbered
@@ -85,7 +85,7 @@ def update(path, dryRun=False):
             with open(p, "w", encoding="utf-8") as fh:
                 fh.write(str(json.dumps(numberedNames, indent=0, ensure_ascii=False, sort_keys=True)))
         except Exception as e:
-            die("Couldn't save biblio numeric-suffix information to disk.\n{0}", e)
+            die(f"Couldn't save biblio numeric-suffix information to disk.\n{e}")
     say("Success!")
     return writtenPaths
 
@@ -94,7 +94,7 @@ def getSpecrefData():
     try:
         return requests.get("https://api.specref.org/bibrefs").text
     except Exception as e:
-        die("Couldn't download the SpecRef biblio data.\n{0}", e)
+        die(f"Couldn't download the SpecRef biblio data.\n{e}")
         return "{}"
 
 
@@ -102,7 +102,7 @@ def getWG21Data():
     try:
         return requests.get("https://wg21.link/specref.json").text
     except Exception as e:
-        die("Couldn't download the WG21 biblio data.\n{0}", e)
+        die(f"Couldn't download the WG21 biblio data.\n{e}")
         return "{}"
 
 
@@ -110,7 +110,7 @@ def getCSSWGData():
     try:
         return requests.get("https://raw.githubusercontent.com/w3c/csswg-drafts/master/biblio.ref").text.splitlines()
     except Exception as e:
-        die("Couldn't download the CSSWG biblio data.\n{0}", e)
+        die(f"Couldn't download the CSSWG biblio data.\n{e}")
         return []
 
 
@@ -173,7 +173,7 @@ def writeBiblioFile(fh, biblios):
             fh.write(b["linkText"] + "\n")
             fh.write(b["aliasOf"] + "\n")
         else:
-            die("The biblio key '{0}' has an unknown biblio type '{1}'.", key, format)
+            die(f"The biblio key '{key}' has an unknown biblio type '{format}'.")
             continue
         fh.write("-" + "\n")
 
diff --git a/bikeshed/update/updateBoilerplates.py b/bikeshed/update/updateBoilerplates.py
index 33fb226c3f..2c59022f41 100644
--- a/bikeshed/update/updateBoilerplates.py
+++ b/bikeshed/update/updateBoilerplates.py
@@ -18,16 +18,14 @@ def update(path, dryRun=False):
         say("Downloading boilerplates...")
         data = requests.get(ghPrefix + "manifest.txt").text
     except Exception as e:
-        die("Couldn't download boilerplates manifest.\n{0}", e)
+        die(f"Couldn't download boilerplates manifest.\n{e}")
         return
 
     newPaths = pathsFromManifest(data)
 
     if not dryRun:
         say(
-            "Updating {0} file{1}...",
-            len(newPaths),
-            "s" if len(newPaths) > 1 else "",
+            f"Updating {len(newPaths)} file{'s' if len(newPaths) > 1 else ''}...",
         )
         goodPaths, badPaths = asyncio.run(updateFiles(path, newPaths))
     if not badPaths:
@@ -66,14 +64,9 @@ async def updateFiles(localPrefix, newPaths):
             currFileTime = time.time()
             if (currFileTime - lastMsgTime) >= messageDelta:
                 if not badPaths:
-                    say("Updated {0}/{1}...", len(goodPaths), len(newPaths))
+                    say(f"Updated {len(goodPaths)}/{len(newPaths)}...")
                 else:
-                    say(
-                        "Updated {0}/{1}, {2} errors...",
-                        len(goodPaths),
-                        len(newPaths),
-                        len(badPaths),
-                    )
+                    say(f"Updated {len(goodPaths)}/{len(newPaths)}, {len(badPaths)} errors...")
                 lastMsgTime = currFileTime
     return goodPaths, badPaths
 
diff --git a/bikeshed/update/updateCanIUse.py b/bikeshed/update/updateCanIUse.py
index 2ab15a18d4..5d2b9d7e42 100644
--- a/bikeshed/update/updateCanIUse.py
+++ b/bikeshed/update/updateCanIUse.py
@@ -12,16 +12,13 @@ def update(path, dryRun=False):
     try:
         response = requests.get("https://raw.githubusercontent.com/Fyrd/caniuse/master/fulldata-json/data-2.0.json")
     except Exception as e:
-        die("Couldn't download the Can I Use data.\n{0}", e)
+        die(f"Couldn't download the Can I Use data.\n{e}")
         return
 
     try:
         data = response.json(encoding="utf-8", object_pairs_hook=OrderedDict)
     except Exception as e:
-        die(
-            "The Can I Use data wasn't valid JSON for some reason. Try downloading again?\n{0}",
-            e,
-        )
+        die(f"The Can I Use data wasn't valid JSON for some reason. Try downloading again?\n{e}")
         return
 
     basicData = {"agents": [], "features": {}, "updated": data["updated"]}
@@ -45,11 +42,7 @@ def simplifyStatus(s, *rest):
         elif "u" in s:
             return "u"
         else:
-            die(
-                "Unknown CanIUse Status '{0}' for {1}/{2}/{3}. Please report this as a Bikeshed issue.",
-                s,
-                *rest,
-            )
+            die(f"Unknown CanIUse Status '{s}' for {'/'.join(rest)}. Please report this as a Bikeshed issue.")
             return None
 
     def simplifyVersion(v):
@@ -104,7 +97,7 @@ def simplifyVersion(v):
                 with open(p, "w", encoding="utf-8") as fh:
                     fh.write(json.dumps(feature, indent=1, ensure_ascii=False, sort_keys=True))
         except Exception as e:
-            die("Couldn't save Can I Use database to disk.\n{0}", e)
+            die(f"Couldn't save Can I Use database to disk.\n{e}")
             return
     say("Success!")
     return writtenPaths
diff --git a/bikeshed/update/updateCrossRefs.py b/bikeshed/update/updateCrossRefs.py
index 4616872944..62f3d3e030 100644
--- a/bikeshed/update/updateCrossRefs.py
+++ b/bikeshed/update/updateCrossRefs.py
@@ -75,7 +75,7 @@ def setStatus(obj, status):
             with open(p, "w", encoding="utf-8") as f:
                 f.write(json.dumps(specs, ensure_ascii=False, indent=2, sort_keys=True))
         except Exception as e:
-            die("Couldn't save spec database to disk.\n{0}", e)
+            die(f"Couldn't save spec database to disk.\n{e}")
             return
         try:
             for spec, specHeadings in headings.items():
@@ -84,12 +84,12 @@ def setStatus(obj, status):
                 with open(p, "w", encoding="utf-8") as f:
                     f.write(json.dumps(specHeadings, ensure_ascii=False, indent=2, sort_keys=True))
         except Exception as e:
-            die("Couldn't save headings database to disk.\n{0}", e)
+            die(f"Couldn't save headings database to disk.\n{e}")
             return
         try:
             writtenPaths.update(writeAnchorsFile(anchors, path))
         except Exception as e:
-            die("Couldn't save anchor database to disk.\n{0}", e)
+            die(f"Couldn't save anchor database to disk.\n{e}")
             return
         try:
             p = os.path.join(path, "methods.json")
@@ -97,7 +97,7 @@ def setStatus(obj, status):
             with open(p, "w", encoding="utf-8") as f:
                 f.write(json.dumps(methods, ensure_ascii=False, indent=2, sort_keys=True))
         except Exception as e:
-            die("Couldn't save methods database to disk.\n{0}", e)
+            die(f"Couldn't save methods database to disk.\n{e}")
             return
         try:
             p = os.path.join(path, "fors.json")
@@ -105,7 +105,7 @@ def setStatus(obj, status):
             with open(p, "w", encoding="utf-8") as f:
                 f.write(json.dumps(fors, ensure_ascii=False, indent=2, sort_keys=True))
         except Exception as e:
-            die("Couldn't save fors database to disk.\n{0}", e)
+            die(f"Couldn't save fors database to disk.\n{e}")
             return
 
     say("Success!")
@@ -241,9 +241,7 @@ def addToHeadings(rawAnchor, specHeadings, spec):
             match = re.match(r"([\w-]+).*?(#.*)", uri)
             if not match:
                 die(
-                    "Unexpected URI pattern '{0}' for spec '{1}'. Please report this to the Bikeshed maintainer.",
-                    uri,
-                    spec["vshortname"],
+                    f"Unexpected URI pattern '{uri}' for spec '{spec['vshortname']}'. Please report this to the Bikeshed maintainer.",
                 )
                 return
             page, fragment = match.groups()
diff --git a/bikeshed/update/updateLanguages.py b/bikeshed/update/updateLanguages.py
index db67ed8bec..6d0ccb674f 100644
--- a/bikeshed/update/updateLanguages.py
+++ b/bikeshed/update/updateLanguages.py
@@ -12,7 +12,7 @@ def update(path, dryRun=False):
             "https://raw.githubusercontent.com/tabatkins/bikeshed/master/bikeshed/spec-data/readonly/languages.json"
         ).text
     except Exception as e:
-        die("Couldn't download languages data.\n{0}", e)
+        die(f"Couldn't download languages data.\n{e}")
         return
 
     if not dryRun:
@@ -20,6 +20,6 @@ def update(path, dryRun=False):
             with open(os.path.join(path, "languages.json"), "w", encoding="utf-8") as f:
                 f.write(data)
         except Exception as e:
-            die("Couldn't save languages database to disk.\n{0}", e)
+            die(f"Couldn't save languages database to disk.\n{e}")
             return
     say("Success!")
diff --git a/bikeshed/update/updateLinkDefaults.py b/bikeshed/update/updateLinkDefaults.py
index 3f4f2064b0..abc584ff8f 100644
--- a/bikeshed/update/updateLinkDefaults.py
+++ b/bikeshed/update/updateLinkDefaults.py
@@ -12,7 +12,7 @@ def update(path, dryRun=False):
             "https://raw.githubusercontent.com/tabatkins/bikeshed/main/bikeshed/spec-data/readonly/link-defaults.infotree"
         ).text
     except Exception as e:
-        die("Couldn't download link defaults data.\n{0}", e)
+        die(f"Couldn't download link defaults data.\n{e}")
         return
 
     if not dryRun:
@@ -20,6 +20,6 @@ def update(path, dryRun=False):
             with open(os.path.join(path, "link-defaults.infotree"), "w", encoding="utf-8") as f:
                 f.write(data)
         except Exception as e:
-            die("Couldn't save link-defaults database to disk.\n{0}", e)
+            die(f"Couldn't save link-defaults database to disk.\n{e}")
             return
     say("Success!")
diff --git a/bikeshed/update/updateMdn.py b/bikeshed/update/updateMdn.py
index e4ab88de64..96e991162a 100644
--- a/bikeshed/update/updateMdn.py
+++ b/bikeshed/update/updateMdn.py
@@ -13,16 +13,13 @@ def update(path, dryRun=False):
     try:
         response = requests.get(specMapURL)
     except Exception as e:
-        die("Couldn't download the MDN Spec Links data.\n{0}", e)
+        die(f"Couldn't download the MDN Spec Links data.\n{e}")
         return
 
     try:
         data = response.json(encoding="utf-8", object_pairs_hook=OrderedDict)
     except Exception as e:
-        die(
-            "The MDN Spec Links data wasn't valid JSON for some reason." + " Try downloading again?\n{0}",
-            e,
-        )
+        die(f"The MDN Spec Links data wasn't valid JSON for some reason. Try downloading again?\n{e}")
         return
     writtenPaths = set()
     if not dryRun:
@@ -48,15 +45,12 @@ def update(path, dryRun=False):
                 try:
                     fileContents = requests.get(mdnSpecLinksBaseURL + specFilename).text
                 except Exception as e:
-                    die(
-                        "Couldn't download the MDN Spec Links " + specFilename + " file.\n{0}",
-                        e,
-                    )
+                    die(f"Couldn't download the MDN Spec Links {specFilename} file.\n{e}")
                     return
                 with open(p, "w", encoding="utf-8") as fh:
                     fh.write(fileContents)
         except Exception as e:
-            die("Couldn't save MDN Spec Links data to disk.\n{0}", e)
+            die(f"Couldn't save MDN Spec Links data to disk.\n{e}")
             return
     say("Success!")
     return writtenPaths
diff --git a/bikeshed/update/updateTestSuites.py b/bikeshed/update/updateTestSuites.py
index 4aa88b716a..fbe4348432 100644
--- a/bikeshed/update/updateTestSuites.py
+++ b/bikeshed/update/updateTestSuites.py
@@ -25,11 +25,11 @@ def update(path, dryRun=False):
             die("This version of the test suite API is no longer supported. Please update Bikeshed.")
             return
         if res.content_type not in testSuiteDataContentTypes:
-            die("Unrecognized test suite content-type '{0}'.", res.content_type)
+            die(f"Unrecognized test suite content-type '{res.content_type}'.")
             return
         rawTestSuiteData = res.data
     except Exception as e:
-        die("Couldn't download test suite data.  Error was:\n{0}", str(e))
+        die(f"Couldn't download test suite data.  Error was:\n{e}")
         return
 
     testSuites = dict()
@@ -53,5 +53,5 @@ def update(path, dryRun=False):
             with open(os.path.join(path, "test-suites.json"), "w", encoding="utf-8") as f:
                 f.write(json.dumps(testSuites, ensure_ascii=False, indent=2, sort_keys=True))
         except Exception as e:
-            die("Couldn't save test-suite database to disk.\n{0}", e)
+            die(f"Couldn't save test-suite database to disk.\n{e}")
     say("Success!")
diff --git a/bikeshed/update/updateWpt.py b/bikeshed/update/updateWpt.py
index 6f2b6a09b3..2a47186323 100644
--- a/bikeshed/update/updateWpt.py
+++ b/bikeshed/update/updateWpt.py
@@ -12,7 +12,7 @@ def update(path, dryRun=False):
         sha = response.headers["x-wpt-sha"]
         jsonData = response.json()
     except Exception as e:
-        die("Couldn't download web-platform-tests data.\n{0}", e)
+        die(f"Couldn't download web-platform-tests data.\n{e}")
         return
 
     if "version" not in jsonData:
@@ -21,8 +21,7 @@ def update(path, dryRun=False):
 
     if jsonData["version"] != 8:
         die(
-            "Bikeshed currently only knows how to handle WPT v8 manifest data, but got v{0}. Please report this to the maintainer!",
-            jsonData["version"],
+            f"Bikeshed currently only knows how to handle WPT v8 manifest data, but got v{jsonData['version']}. Please report this to the maintainer!"
         )
         return
 
@@ -45,7 +44,7 @@ def update(path, dryRun=False):
                 for ordered_path in sorted(paths):
                     f.write(ordered_path + "\n")
         except Exception as e:
-            die("Couldn't save web-platform-tests data to disk.\n{0}", e)
+            die(f"Couldn't save web-platform-tests data to disk.\n{e}")
             return
     say("Success!")
 
diff --git a/bikeshed/wpt/wptElement.py b/bikeshed/wpt/wptElement.py
index 30ac8b0838..984755be7b 100644
--- a/bikeshed/wpt/wptElement.py
+++ b/bikeshed/wpt/wptElement.py
@@ -30,7 +30,7 @@ def processWptElements(doc):
         for testName in testNames:
             if testName not in testData:
                 die(
-                    "Couldn't find WPT test '{0}' - did you misspell something?",
+                    f"Couldn't find WPT test '{testName}' - did you misspell something?",
                     testName,
                     el=el,
                 )
@@ -54,10 +54,7 @@ def processWptElements(doc):
     if wptRestElements and testData is None:
         testData = loadTestData(doc)
     if len(wptRestElements) > 1:
-        die(
-            "Only one  element allowed per document, you have {0}.",
-            len(wptRestElements),
-        )
+        die(f"Only one  element allowed per document, you have {len(wptRestElements)}.")
         wptRestElements = wptRestElements[0:1]
     elif len(wptRestElements) == 1:
         localPrefix = wptRestElements[0].get("pathprefix")
@@ -69,7 +66,7 @@ def processWptElements(doc):
         atLeastOneElement = True
         prefixedNames = [p for p in testData if prefixInPath(pathPrefix, p) and p not in seenTestNames]
         if len(prefixedNames) == 0:
-            die("Couldn't find any tests with the path prefix '{0}'.", pathPrefix)
+            die(f"Couldn't find any tests with the path prefix '{pathPrefix}'.")
             return
         createHTML(doc, wptRestElements[0], prefixedNames, testData)
         warn(
@@ -249,10 +246,8 @@ def checkForOmittedTests(pathPrefix, testData, seenTestNames):
         numTests = len(unseenTests)
         if numTests < 10:
             warn(
-                "There are {} WPT tests underneath your path prefix '{}' that aren't in your document and must be added:\n{}",
-                numTests,
-                pathPrefix,
-                "\n".join("  " + path for path in sorted(unseenTests)),
+                f"There are {numTests} WPT tests underneath your path prefix '{pathPrefix}' that aren't in your document and must be added:\n"
+                + "\n".join("  " + path for path in sorted(unseenTests)),
             )
         else:
             warn(