diff --git a/examples/bbox_detection/labelme2voc.py b/examples/bbox_detection/labelme2voc.py
index 71ab3b461..7577e71f0 100755
--- a/examples/bbox_detection/labelme2voc.py
+++ b/examples/bbox_detection/labelme2voc.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from __future__ import print_function
 
 import argparse
 import glob
diff --git a/examples/instance_segmentation/labelme2voc.py b/examples/instance_segmentation/labelme2voc.py
index c2edb7f49..70eef07bb 100755
--- a/examples/instance_segmentation/labelme2voc.py
+++ b/examples/instance_segmentation/labelme2voc.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from __future__ import print_function
 
 import argparse
 import glob
diff --git a/examples/tutorial/load_label_png.py b/examples/tutorial/load_label_png.py
index f72c8ce12..8951132be 100644
--- a/examples/tutorial/load_label_png.py
+++ b/examples/tutorial/load_label_png.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from __future__ import print_function
 
 import os.path as osp
 
diff --git a/labelme/__main__.py b/labelme/__main__.py
index 98fe1ac6d..3603b7c71 100644
--- a/labelme/__main__.py
+++ b/labelme/__main__.py
@@ -39,9 +39,7 @@ def main():
     parser.add_argument(
         "--config",
         dest="config",
-        help="config file or yaml-format string (default: {})".format(
-            default_config_file
-        ),
+        help=f"config file or yaml-format string (default: {default_config_file})",
         default=default_config_file,
     )
     # config for the gui
@@ -106,7 +104,7 @@ def main():
     args = parser.parse_args()
 
     if args.version:
-        print("{0} {1}".format(__appname__, __version__))
+        print(f"{__appname__} {__version__}")
         sys.exit(0)
 
     logger.setLevel(getattr(logging, args.logger_level.upper()))
@@ -173,7 +171,7 @@ def main():
     )
 
     if reset_config:
-        logger.info("Resetting Qt config: %s" % win.settings.fileName())
+        logger.info("Resetting Qt config: %s", win.settings.fileName())
         win.settings.clear()
         sys.exit(0)
 
diff --git a/labelme/app.py b/labelme/app.py
index 207a769cf..4706b0324 100644
--- a/labelme/app.py
+++ b/labelme/app.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import functools
 import html
 import math
@@ -88,7 +86,7 @@ def __init__(
         # Set point size from config file
         Shape.point_size = self._config["shape"]["point_size"]
 
-        super(MainWindow, self).__init__()
+        super().__init__()
         self.setWindowTitle(__appname__)
 
         # Whether we need to save or not.
@@ -914,7 +912,7 @@ def menu(self, title, actions=None):
 
     def toolbar(self, title, actions=None):
         toolbar = ToolBar(title)
-        toolbar.setObjectName("%sToolBar" % title)
+        toolbar.setObjectName(f"{title}ToolBar")
         # toolbar.setOrientation(Qt.Vertical)
         toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
         if actions:
@@ -962,7 +960,7 @@ def setDirty(self):
         self.actions.save.setEnabled(True)
         title = __appname__
         if self.filename is not None:
-            title = "{} - {}*".format(title, self.filename)
+            title = f"{title} - {self.filename}*"
         self.setWindowTitle(title)
 
     def setClean(self):
@@ -978,7 +976,7 @@ def setClean(self):
         self.actions.createAiMaskMode.setEnabled(True)
         title = __appname__
         if self.filename is not None:
-            title = "{} - {}".format(title, self.filename)
+            title = f"{title} - {self.filename}"
         self.setWindowTitle(title)
 
         if self.hasLabelFile():
@@ -1251,7 +1249,7 @@ def _edit_label(self, value=None):
                     )
                 )
             else:
-                item.setText("{} ({})".format(shape.label, shape.group_id))
+                item.setText(f"{shape.label} ({shape.group_id})")
             self.setDirty()
             if self.uniqLabelList.findItemByLabel(shape.label) is None:
                 item = self.uniqLabelList.createItemFromLabel(shape.label)
@@ -1304,7 +1302,7 @@ def addLabel(self, shape):
         if shape.group_id is None:
             text = shape.label
         else:
-            text = "{} ({})".format(shape.label, shape.group_id)
+            text = f"{shape.label} ({shape.group_id})"
         label_list_item = LabelListWidgetItem(text, shape)
         self.labelList.addItem(label_list_item)
         if self.uniqLabelList.findItemByLabel(shape.label) is None:
@@ -1693,7 +1691,7 @@ def loadFile(self, filename=None):
 
         if image.isNull():
             formats = [
-                "*.{}".format(fmt.data().decode())
+                f"*.{fmt.data().decode()}"
                 for fmt in QtGui.QImageReader.supportedImageFormats()
             ]
             self.errorMessage(
@@ -1773,7 +1771,7 @@ def resizeEvent(self, event):
             and self.zoomMode != self.MANUAL_ZOOM
         ):
             self.adjustScale()
-        super(MainWindow, self).resizeEvent(event)
+        super().resizeEvent(event)
 
     def paintCanvas(self):
         assert not self.image.isNull(), "cannot paint null image"
@@ -1821,7 +1819,7 @@ def closeEvent(self, event):
 
     def dragEnterEvent(self, event):
         extensions = [
-            ".%s" % fmt.data().decode().lower()
+            f".{fmt.data().decode().lower()}"
             for fmt in QtGui.QImageReader.supportedImageFormats()
         ]
         if event.mimeData().hasUrls():
@@ -1902,11 +1900,11 @@ def openFile(self, _value=False):
             return
         path = osp.dirname(str(self.filename)) if self.filename else "."
         formats = [
-            "*.{}".format(fmt.data().decode())
+            f"*.{fmt.data().decode()}"
             for fmt in QtGui.QImageReader.supportedImageFormats()
         ]
         filters = self.tr("Image & Label files (%s)") % " ".join(
-            formats + ["*%s" % LabelFile.suffix]
+            formats + [f"*{LabelFile.suffix}"]
         )
         fileDialog = FileDialogPreview(self)
         fileDialog.setFileMode(FileDialogPreview.ExistingFile)
@@ -2035,7 +2033,7 @@ def deleteFile(self):
         label_file = self.getLabelFile()
         if osp.exists(label_file):
             os.remove(label_file)
-            logger.info("Label file is removed: {}".format(label_file))
+            logger.info("Label file is removed: %s", label_file)
 
             item = self.fileListWidget.currentItem()
             item.setCheckState(Qt.Unchecked)
@@ -2081,7 +2079,7 @@ def mayContinue(self):
 
     def errorMessage(self, title, message):
         return QtWidgets.QMessageBox.critical(
-            self, title, "<p><b>%s</b></p>%s" % (title, message)
+            self, title, f"<p><b>{title}</b></p>{message}"
         )
 
     def currentPath(self):
@@ -2157,7 +2155,7 @@ def imageList(self):
 
     def importDroppedImageFiles(self, imageFiles):
         extensions = [
-            ".%s" % fmt.data().decode().lower()
+            f".{fmt.data().decode().lower()}"
             for fmt in QtGui.QImageReader.supportedImageFormats()
         ]
 
@@ -2216,7 +2214,7 @@ def importDirImages(self, dirpath, pattern=None, load=True):
 
     def scanAllImages(self, folderPath):
         extensions = [
-            ".%s" % fmt.data().decode().lower()
+            f".{fmt.data().decode().lower()}"
             for fmt in QtGui.QImageReader.supportedImageFormats()
         ]
 
diff --git a/labelme/cli/draw_label_png.py b/labelme/cli/draw_label_png.py
index 6fe080a02..ee28d52e1 100644
--- a/labelme/cli/draw_label_png.py
+++ b/labelme/cli/draw_label_png.py
@@ -41,17 +41,14 @@ def main():
 
     unique_label_values = np.unique(label)
 
-    logger.info("Label image shape: {}".format(label.shape))
-    logger.info("Label values: {}".format(unique_label_values.tolist()))
+    logger.info("Label image shape: %s", label.shape)
+    logger.info("Label values: %s", unique_label_values.tolist())
     if label_names is not None:
-        logger.info(
-            "Label names: {}".format(
-                [
-                    "{}:{}".format(label_value, label_names[label_value])
-                    for label_value in unique_label_values
-                ]
-            )
-        )
+        names = [
+            f"{label_value}:{label_names[label_value]}"
+            for label_value in unique_label_values
+        ]
+        logger.info("Label names: %s", names)
 
     if args.image:
         num_cols = 2
@@ -75,7 +72,7 @@ def main():
             label_names=label_names,
             font_size=label.shape[1] // 30,
         )
-        plt.title("{}\n{}".format(args.label_png, args.image))
+        plt.title(f"{args.label_png}\n{args.image}")
         plt.imshow(label_viz_with_overlay)
 
     plt.tight_layout()
diff --git a/labelme/cli/export_json.py b/labelme/cli/export_json.py
index 4479321e7..8e3cb7dde 100644
--- a/labelme/cli/export_json.py
+++ b/labelme/cli/export_json.py
@@ -63,7 +63,7 @@ def main():
         for lbl_name in label_names:
             f.write(lbl_name + "\n")
 
-    logger.info("Saved to: {}".format(out_dir))
+    logger.info("Saved to: %s", out_dir)
 
 
 if __name__ == "__main__":
diff --git a/labelme/cli/json_to_dataset.py b/labelme/cli/json_to_dataset.py
index ef3749270..cc4eb261e 100644
--- a/labelme/cli/json_to_dataset.py
+++ b/labelme/cli/json_to_dataset.py
@@ -73,7 +73,7 @@ def main():
         for lbl_name in label_names:
             f.write(lbl_name + "\n")
 
-    logger.info("Saved to: {}".format(out_dir))
+    logger.info("Saved to: %s", out_dir)
 
 
 if __name__ == "__main__":
diff --git a/labelme/cli/on_docker.py b/labelme/cli/on_docker.py
index ea2536396..3732b3426 100644
--- a/labelme/cli/on_docker.py
+++ b/labelme/cli/on_docker.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from __future__ import print_function
 
 import argparse
 import distutils.spawn
@@ -35,13 +34,13 @@ def get_ip():
 
 def labelme_on_docker(in_file, out_file):
     ip = get_ip()
-    cmd = "xhost + %s" % ip
+    cmd = f"xhost + {ip}"
     subprocess.check_output(shlex.split(cmd))
 
     if out_file:
         out_file = osp.abspath(out_file)
         if osp.exists(out_file):
-            raise RuntimeError("File exists: %s" % out_file)
+            raise RuntimeError(f"File exists: {out_file}")
         else:
             open(osp.abspath(out_file), "w")
 
@@ -63,10 +62,10 @@ def labelme_on_docker(in_file, out_file):
     if out_file:
         out_file_a = osp.abspath(out_file)
         out_file_b = osp.join("/home/developer", osp.basename(out_file))
-        cmd += " -v {0}:{1}".format(out_file_a, out_file_b)
-    cmd += " wkentaro/labelme labelme {0}".format(in_file_b)
+        cmd += f" -v {out_file_a}:{out_file_b}"
+    cmd += f" wkentaro/labelme labelme {in_file_b}"
     if out_file:
-        cmd += " -O {0}".format(out_file_b)
+        cmd += f" -O {out_file_b}"
     subprocess.call(shlex.split(cmd))
 
     if out_file:
@@ -92,7 +91,7 @@ def main():
     try:
         out_file = labelme_on_docker(args.in_file, args.output)
         if out_file:
-            print("Saved to: %s" % out_file)
+            print(f"Saved to: {out_file}")
     except RuntimeError as e:
         sys.stderr.write(e.__str__() + "\n")
         sys.exit(1)
diff --git a/labelme/config/__init__.py b/labelme/config/__init__.py
index 112a919c7..f4a633505 100644
--- a/labelme/config/__init__.py
+++ b/labelme/config/__init__.py
@@ -13,7 +13,7 @@ def update_dict(target_dict, new_dict, validate_item=None):
         if validate_item:
             validate_item(key, value)
         if key not in target_dict:
-            logger.warn("Skipping unexpected key in config: {}".format(key))
+            logger.warn("Skipping unexpected key in config: %s", key)
             continue
         if isinstance(target_dict[key], dict) and isinstance(value, dict):
             update_dict(target_dict[key], value, validate_item=validate_item)
@@ -35,24 +35,18 @@ def get_default_config():
         try:
             shutil.copy(config_file, user_config_file)
         except Exception:
-            logger.warn("Failed to save config: {}".format(user_config_file))
+            logger.warn("Failed to save config: %s", user_config_file)
 
     return config
 
 
 def validate_config_item(key, value):
     if key == "validate_label" and value not in [None, "exact"]:
-        raise ValueError(
-            "Unexpected value for config key 'validate_label': {}".format(value)
-        )
+        raise ValueError(f"Unexpected value for config key 'validate_label': {value}")
     if key == "shape_color" and value not in [None, "auto", "manual"]:
-        raise ValueError(
-            "Unexpected value for config key 'shape_color': {}".format(value)
-        )
+        raise ValueError(f"Unexpected value for config key 'shape_color': {value}")
     if key == "labels" and value is not None and len(value) != len(set(value)):
-        raise ValueError(
-            "Duplicates are detected for config key 'labels': {}".format(value)
-        )
+        raise ValueError(f"Duplicates are detected for config key 'labels': {value}")
 
 
 def get_config(config_file_or_yaml=None, config_from_args=None):
@@ -64,7 +58,7 @@ def get_config(config_file_or_yaml=None, config_from_args=None):
         config_from_yaml = yaml.safe_load(config_file_or_yaml)
         if not isinstance(config_from_yaml, dict):
             with open(config_from_yaml) as f:
-                logger.info("Loading config file from: {}".format(config_from_yaml))
+                logger.info("Loading config file from: %s", config_from_yaml)
                 config_from_yaml = yaml.safe_load(f)
         update_dict(config, config_from_yaml, validate_item=validate_config_item)
 
diff --git a/labelme/label_file.py b/labelme/label_file.py
index 3c1f31530..f6aba1a6c 100644
--- a/labelme/label_file.py
+++ b/labelme/label_file.py
@@ -1,4 +1,5 @@
 import base64
+import builtins
 import contextlib
 import io
 import json
@@ -23,7 +24,7 @@ def open(name, mode):
         encoding = None
     else:
         encoding = "utf-8"
-    yield io.open(name, mode, encoding=encoding)
+    yield builtins.open(name, mode, encoding=encoding)
     return
 
 
@@ -31,7 +32,7 @@ class LabelFileError(Exception):
     pass
 
 
-class LabelFile(object):
+class LabelFile:
     suffix = ".json"
 
     def __init__(self, filename=None):
@@ -46,8 +47,8 @@ def __init__(self, filename=None):
     def load_image_file(filename):
         try:
             image_pil = PIL.Image.open(filename)
-        except IOError:
-            logger.error("Failed opening image file: {}".format(filename))
+        except OSError:
+            logger.error("Failed opening image file: %r", filename)
             return
 
         # apply orientation to image according to exif
diff --git a/labelme/logger.py b/labelme/logger.py
index 25a5ce91c..faa9336dc 100644
--- a/labelme/logger.py
+++ b/labelme/logger.py
@@ -37,7 +37,7 @@ def colored(text):
                     attrs={"bold": True},
                 )
 
-            record.levelname2 = colored("{:<7}".format(record.levelname))
+            record.levelname2 = colored(f"{record.levelname:<7}")
             record.message2 = colored(record.msg)
 
             asctime2 = datetime.datetime.fromtimestamp(record.created)
diff --git a/labelme/shape.py b/labelme/shape.py
index 0f1fd9fdb..9654c07bb 100644
--- a/labelme/shape.py
+++ b/labelme/shape.py
@@ -12,7 +12,7 @@
 # - [opt] Store paths instead of creating new ones at each paint.
 
 
-class Shape(object):
+class Shape:
     # Render handles as squares
     P_SQUARE = 0
 
@@ -112,7 +112,7 @@ def shape_type(self, value):
             "points",
             "mask",
         ]:
-            raise ValueError("Unexpected shape_type: {}".format(value))
+            raise ValueError(f"Unexpected shape_type: {value}")
         self._shape_type = value
 
     def close(self):
diff --git a/labelme/utils/_io.py b/labelme/utils/_io.py
index 9e97ef558..1b7fa681d 100644
--- a/labelme/utils/_io.py
+++ b/labelme/utils/_io.py
@@ -21,6 +21,6 @@ def lblsave(filename, lbl):
         lbl_pil.save(filename)
     else:
         raise ValueError(
-            "[%s] Cannot save the pixel-wise class label as PNG. "
-            "Please consider using the .npy format." % filename
+            f"[{filename}] Cannot save the pixel-wise class label as PNG. "
+            "Please consider using the .npy format."
         )
diff --git a/labelme/utils/qt.py b/labelme/utils/qt.py
index 7fed3ad18..6db4f7f6d 100644
--- a/labelme/utils/qt.py
+++ b/labelme/utils/qt.py
@@ -11,7 +11,7 @@
 
 def newIcon(icon):
     icons_dir = osp.join(here, "../icons")
-    return QtGui.QIcon(osp.join(":/", icons_dir, "%s.png" % icon))
+    return QtGui.QIcon(osp.join(":/", icons_dir, f"{icon}.png"))
 
 
 def newButton(text, icon=None, slot=None):
@@ -70,7 +70,7 @@ def labelValidator():
     return QtGui.QRegExpValidator(QtCore.QRegExp(r"^[^ \t].+"), None)
 
 
-class struct(object):
+class struct:
     def __init__(self, **kwargs):
         self.__dict__.update(kwargs)
 
@@ -95,4 +95,4 @@ def distancetoline(point, line):
 
 def fmtShortcut(text):
     mod, key = text.split("+", 1)
-    return "<b>%s</b>+<b>%s</b>" % (mod, key)
+    return f"<b>{mod}</b>+<b>{key}</b>"
diff --git a/labelme/utils/shape.py b/labelme/utils/shape.py
index 2aab557c9..35160f42a 100644
--- a/labelme/utils/shape.py
+++ b/labelme/utils/shape.py
@@ -95,11 +95,9 @@ def labelme_shapes_to_label(img_shape, shapes):
 
 def masks_to_bboxes(masks):
     if masks.ndim != 3:
-        raise ValueError("masks.ndim must be 3, but it is {}".format(masks.ndim))
+        raise ValueError(f"masks.ndim must be 3, but it is {masks.ndim}")
     if masks.dtype != bool:
-        raise ValueError(
-            "masks.dtype must be bool type, but it is {}".format(masks.dtype)
-        )
+        raise ValueError(f"masks.dtype must be bool type, but it is {masks.dtype}")
     bboxes = []
     for mask in masks:
         where = np.argwhere(mask)
diff --git a/labelme/widgets/brightness_contrast_dialog.py b/labelme/widgets/brightness_contrast_dialog.py
index 47f5d8ec3..bb5725755 100644
--- a/labelme/widgets/brightness_contrast_dialog.py
+++ b/labelme/widgets/brightness_contrast_dialog.py
@@ -9,7 +9,7 @@ class BrightnessContrastDialog(QtWidgets.QDialog):
     _base_value = 50
 
     def __init__(self, img, callback, parent=None):
-        super(BrightnessContrastDialog, self).__init__(parent)
+        super().__init__(parent)
         self.setModal(True)
         self.setWindowTitle("Brightness/Contrast")
 
diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py
index d71a6125b..fb3e3d3fd 100644
--- a/labelme/widgets/canvas.py
+++ b/labelme/widgets/canvas.py
@@ -44,7 +44,7 @@ def __init__(self, *args, **kwargs):
         self.double_click = kwargs.pop("double_click", "close")
         if self.double_click not in [None, "close"]:
             raise ValueError(
-                "Unexpected value for double_click event: {}".format(self.double_click)
+                f"Unexpected value for double_click event: {self.double_click}"
             )
         self.num_backups = kwargs.pop("num_backups", 10)
         self._crosshair = kwargs.pop(
@@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs):
                 "ai_mask": False,
             },
         )
-        super(Canvas, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         # Initialise local state.
         self.mode = self.EDIT
         self.shapes = []
@@ -125,18 +125,18 @@ def createMode(self, value):
             "ai_polygon",
             "ai_mask",
         ]:
-            raise ValueError("Unsupported createMode: %s" % value)
+            raise ValueError(f"Unsupported createMode: {value}")
         self._createMode = value
 
     def initializeAiModel(self, name):
         if name not in [model.name for model in labelme.ai.MODELS]:
-            raise ValueError("Unsupported ai model: %s" % name)
+            raise ValueError(f"Unsupported ai model: {name}")
         model = [model for model in labelme.ai.MODELS if model.name == name][0]
 
         if self._ai_model is not None and self._ai_model.name == model.name:
-            logger.debug("AI model is already initialized: %r" % model.name)
+            logger.debug("AI model is already initialized: %r", model.name)
         else:
-            logger.debug("Initializing AI model: %r" % model.name)
+            logger.debug("Initializing AI model: %r", model.name)
             self._ai_model = model()
 
         if self.pixmap is None:
@@ -668,7 +668,7 @@ def deleteShape(self, shape):
 
     def paintEvent(self, event):
         if not self.pixmap:
-            return super(Canvas, self).paintEvent(event)
+            return super().paintEvent(event)
 
         p = self._painter
         p.begin(self)
@@ -780,7 +780,7 @@ def transformPos(self, point):
 
     def offsetToCenter(self):
         s = self.scale
-        area = super(Canvas, self).size()
+        area = super().size()
         w, h = self.pixmap.width() * s, self.pixmap.height() * s
         aw, ah = area.width(), area.height()
         x = (aw - w) / (2 * s) if aw > w else 0
@@ -898,7 +898,7 @@ def sizeHint(self):
     def minimumSizeHint(self):
         if self.pixmap:
             return self.scale * self.pixmap.size()
-        return super(Canvas, self).minimumSizeHint()
+        return super().minimumSizeHint()
 
     def wheelEvent(self, ev):
         if QT5:
diff --git a/labelme/widgets/color_dialog.py b/labelme/widgets/color_dialog.py
index f1590a3e1..a53d8cb27 100644
--- a/labelme/widgets/color_dialog.py
+++ b/labelme/widgets/color_dialog.py
@@ -3,7 +3,7 @@
 
 class ColorDialog(QtWidgets.QColorDialog):
     def __init__(self, parent=None):
-        super(ColorDialog, self).__init__(parent)
+        super().__init__(parent)
         self.setOption(QtWidgets.QColorDialog.ShowAlphaChannel)
         # The Mac native dialog does not support our restore button.
         self.setOption(QtWidgets.QColorDialog.DontUseNativeDialog)
diff --git a/labelme/widgets/escapable_qlist_widget.py b/labelme/widgets/escapable_qlist_widget.py
index 546934421..2a033d84e 100644
--- a/labelme/widgets/escapable_qlist_widget.py
+++ b/labelme/widgets/escapable_qlist_widget.py
@@ -4,6 +4,6 @@
 
 class EscapableQListWidget(QtWidgets.QListWidget):
     def keyPressEvent(self, event):
-        super(EscapableQListWidget, self).keyPressEvent(event)
+        super().keyPressEvent(event)
         if event.key() == Qt.Key_Escape:
             self.clearSelection()
diff --git a/labelme/widgets/file_dialog_preview.py b/labelme/widgets/file_dialog_preview.py
index 6110bcb18..f355fdeec 100644
--- a/labelme/widgets/file_dialog_preview.py
+++ b/labelme/widgets/file_dialog_preview.py
@@ -7,7 +7,7 @@
 
 class ScrollAreaPreview(QtWidgets.QScrollArea):
     def __init__(self, *args, **kwargs):
-        super(ScrollAreaPreview, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
         self.setWidgetResizable(True)
 
@@ -33,7 +33,7 @@ def clear(self):
 
 class FileDialogPreview(QtWidgets.QFileDialog):
     def __init__(self, *args, **kwargs):
-        super(FileDialogPreview, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.setOption(self.DontUseNativeDialog, True)
 
         self.labelPreview = ScrollAreaPreview(self)
@@ -50,7 +50,7 @@ def __init__(self, *args, **kwargs):
 
     def onChange(self, path):
         if path.lower().endswith(".json"):
-            with open(path, "r") as f:
+            with open(path) as f:
                 data = json.load(f)
                 self.labelPreview.setText(json.dumps(data, indent=4, sort_keys=False))
             self.labelPreview.label.setAlignment(
diff --git a/labelme/widgets/label_dialog.py b/labelme/widgets/label_dialog.py
index e6c33ba1d..14455180a 100644
--- a/labelme/widgets/label_dialog.py
+++ b/labelme/widgets/label_dialog.py
@@ -23,7 +23,7 @@ def keyPressEvent(self, e):
         if e.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Down]:
             self.list_widget.keyPressEvent(e)
         else:
-            super(LabelQLineEdit, self).keyPressEvent(e)
+            super().keyPressEvent(e)
 
 
 class LabelDialog(QtWidgets.QDialog):
@@ -42,7 +42,7 @@ def __init__(
             fit_to_content = {"row": False, "column": True}
         self._fit_to_content = fit_to_content
 
-        super(LabelDialog, self).__init__(parent)
+        super().__init__(parent)
         self.edit = LabelQLineEdit()
         self.edit.setPlaceholderText(text)
         self.edit.setValidator(labelme.utils.labelValidator())
@@ -119,7 +119,7 @@ def __init__(
             completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
             completer.setFilterMode(QtCore.Qt.MatchContains)
         else:
-            raise ValueError("Unsupported completion: {}".format(completion))
+            raise ValueError(f"Unsupported completion: {completion}")
         completer.setModel(self.labelList.model())
         self.edit.setCompleter(completer)
 
@@ -230,7 +230,7 @@ def popUp(self, text=None, move=True, flags=None, group_id=None, description=Non
         items = self.labelList.findItems(text, QtCore.Qt.MatchFixedString)
         if items:
             if len(items) != 1:
-                logger.warning("Label list has duplicate '{}'".format(text))
+                logger.warning("Label list has duplicate %r", text)
             self.labelList.setCurrentItem(items[0])
             row = self.labelList.row(items[0])
             self.edit.completer().setCurrentRow(row)
diff --git a/labelme/widgets/label_list_widget.py b/labelme/widgets/label_list_widget.py
index 0011a9562..275ee6993 100644
--- a/labelme/widgets/label_list_widget.py
+++ b/labelme/widgets/label_list_widget.py
@@ -9,7 +9,7 @@
 # https://stackoverflow.com/a/2039745/4158863
 class HTMLDelegate(QtWidgets.QStyledItemDelegate):
     def __init__(self, parent=None):
-        super(HTMLDelegate, self).__init__()
+        super().__init__()
         self.doc = QtGui.QTextDocument(self)
 
     def paint(self, painter, option, index):
@@ -67,7 +67,7 @@ def sizeHint(self, option, index):
 
 class LabelListWidgetItem(QtGui.QStandardItem):
     def __init__(self, text=None, shape=None):
-        super(LabelListWidgetItem, self).__init__()
+        super().__init__()
         self.setText(text or "")
         self.setShape(shape)
 
@@ -89,7 +89,7 @@ def __hash__(self):
         return id(self)
 
     def __repr__(self):
-        return '{}("{}")'.format(self.__class__.__name__, self.text())
+        return f'{self.__class__.__name__}("{self.text()}")'
 
 
 class StandardItemModel(QtGui.QStandardItemModel):
@@ -106,7 +106,7 @@ class LabelListWidget(QtWidgets.QListView):
     itemSelectionChanged = QtCore.Signal(list, list)
 
     def __init__(self):
-        super(LabelListWidget, self).__init__()
+        super().__init__()
         self._selectedItems = []
 
         self.setWindowFlags(Qt.Window)
@@ -171,7 +171,7 @@ def findItemByShape(self, shape):
             item = self.model().item(row, 0)
             if item.shape() == shape:
                 return item
-        raise ValueError("cannot find shape: {}".format(shape))
+        raise ValueError(f"cannot find shape: {shape}")
 
     def clear(self):
         self.model().clear()
diff --git a/labelme/widgets/tool_bar.py b/labelme/widgets/tool_bar.py
index a0087242c..75a595fba 100644
--- a/labelme/widgets/tool_bar.py
+++ b/labelme/widgets/tool_bar.py
@@ -4,7 +4,7 @@
 
 class ToolBar(QtWidgets.QToolBar):
     def __init__(self, title):
-        super(ToolBar, self).__init__(title)
+        super().__init__(title)
         layout = self.layout()
         m = (0, 0, 0, 0)
         layout.setSpacing(0)
@@ -14,7 +14,7 @@ def __init__(self, title):
 
     def addAction(self, action):
         if isinstance(action, QtWidgets.QWidgetAction):
-            return super(ToolBar, self).addAction(action)
+            return super().addAction(action)
         btn = QtWidgets.QToolButton()
         btn.setDefaultAction(action)
         btn.setToolButtonStyle(self.toolButtonStyle())
diff --git a/labelme/widgets/unique_label_qlist_widget.py b/labelme/widgets/unique_label_qlist_widget.py
index 19ef74812..5d858397f 100644
--- a/labelme/widgets/unique_label_qlist_widget.py
+++ b/labelme/widgets/unique_label_qlist_widget.py
@@ -1,5 +1,3 @@
-# -*- encoding: utf-8 -*-
-
 import html
 
 from qtpy import QtWidgets
@@ -10,7 +8,7 @@
 
 class UniqueLabelQListWidget(EscapableQListWidget):
     def mousePressEvent(self, event):
-        super(UniqueLabelQListWidget, self).mousePressEvent(event)
+        super().mousePressEvent(event)
         if not self.indexAt(event.pos()).isValid():
             self.clearSelection()
 
@@ -22,7 +20,7 @@ def findItemByLabel(self, label):
 
     def createItemFromLabel(self, label):
         if self.findItemByLabel(label):
-            raise ValueError("Item for label '{}' already exists".format(label))
+            raise ValueError(f"Item for label '{label}' already exists")
 
         item = QtWidgets.QListWidgetItem()
         item.setData(Qt.UserRole, label)
@@ -31,7 +29,7 @@ def createItemFromLabel(self, label):
     def setItemLabel(self, item, label, color=None):
         qlabel = QtWidgets.QLabel()
         if color is None:
-            qlabel.setText("{}".format(label))
+            qlabel.setText(f"{label}")
         else:
             qlabel.setText(
                 '{} <font color="#{:02x}{:02x}{:02x}">●</font>'.format(
diff --git a/labelme/widgets/zoom_widget.py b/labelme/widgets/zoom_widget.py
index 13fb2c253..bd4d8128b 100644
--- a/labelme/widgets/zoom_widget.py
+++ b/labelme/widgets/zoom_widget.py
@@ -5,7 +5,7 @@
 
 class ZoomWidget(QtWidgets.QSpinBox):
     def __init__(self, value=100):
-        super(ZoomWidget, self).__init__()
+        super().__init__()
         self.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
         self.setRange(1, 1000)
         self.setSuffix(" %")
@@ -15,7 +15,7 @@ def __init__(self, value=100):
         self.setAlignment(QtCore.Qt.AlignCenter)
 
     def minimumSizeHint(self):
-        height = super(ZoomWidget, self).minimumSizeHint().height()
+        height = super().minimumSizeHint().height()
         fm = QtGui.QFontMetrics(self.font())
         width = fm.width(str(self.maximum()))
         return QtCore.QSize(width, height)
diff --git a/ruff.toml b/ruff.toml
index 205cc1d18..ba78fe997 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -6,10 +6,15 @@ exclude = [
 
 line-length = 88
 indent-width = 4
+target-version = "py38"
 
 [lint]
-# Enable Pyflakes (`F`), pycodestyle (`E`), isort (`I`).
-select = ["E", "F", "I"]
+select = [
+  "E", # pycodestyle
+  "F", # pyflakes
+  "I", # isort
+  "UP", # pyupgrade
+]
 ignore = []
 
 # Allow fix for all enabled rules (when `--fix`) is provided.
diff --git a/setup.py b/setup.py
index 26222a9ad..ea0e7da7b 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ def get_version():
     with open(filename) as f:
         match = re.search(r"""^__version__ = ['"]([^'"]*)['"]""", f.read(), re.M)
     if not match:
-        raise RuntimeError("{} doesn't contain __version__".format(filename))
+        raise RuntimeError(f"{filename} doesn't contain __version__")
     version = match.groups()[0]
     return version
 
@@ -106,13 +106,13 @@ def main():
 
         commands = [
             "git push origin main",
-            "git tag v{:s}".format(version),
+            f"git tag v{version:s}",
             "git push origin --tags",
             "python setup.py sdist",
-            "twine upload dist/labelme-{:s}.tar.gz".format(version),
+            f"twine upload dist/labelme-{version:s}.tar.gz",
         ]
         for cmd in commands:
-            print("+ {:s}".format(cmd))
+            print(f"+ {cmd:s}")
             subprocess.check_call(shlex.split(cmd))
         sys.exit(0)
 
@@ -128,6 +128,7 @@ def main():
         url="https://github.com/wkentaro/labelme",
         install_requires=get_install_requires(),
         license="GPLv3",
+        python_requires=">=3.8",
         keywords="Image Annotation, Machine Learning",
         classifiers=[
             "Development Status :: 5 - Production/Stable",
@@ -136,11 +137,11 @@ def main():
             "Natural Language :: English",
             "Operating System :: OS Independent",
             "Programming Language :: Python",
-            "Programming Language :: Python :: 3.5",
-            "Programming Language :: Python :: 3.6",
-            "Programming Language :: Python :: 3.7",
             "Programming Language :: Python :: 3.8",
             "Programming Language :: Python :: 3.9",
+            "Programming Language :: Python :: 3.10",
+            "Programming Language :: Python :: 3.11",
+            "Programming Language :: Python :: 3.12",
             "Programming Language :: Python :: 3 :: Only",
         ],
         package_data={"labelme": ["icons/*", "config/*.yaml", "translate/*"]},
diff --git a/tests/labelme_tests/widgets_tests/test_label_list_widget.py b/tests/labelme_tests/widgets_tests/test_label_list_widget.py
index fb840e539..fa9f420e6 100644
--- a/tests/labelme_tests/widgets_tests/test_label_list_widget.py
+++ b/tests/labelme_tests/widgets_tests/test_label_list_widget.py
@@ -1,5 +1,3 @@
-# -*- encoding: utf-8 -*-
-
 import pytest
 
 from labelme.widgets import LabelListWidget