diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 9660e28288a..b4b3f71cbb7 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -154,7 +154,7 @@ def test_cmyk(self) -> None:
             assert k > 0.9
 
     def test_rgb(self) -> None:
-        def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
+        def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
             return tuple(v[0] for v in im.layer)
 
         im = hopper()
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 4fdc4179170..d8372789be9 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -99,7 +99,7 @@ def text(self, text: str) -> None:
         im = Image.new("L", (100, 100))
 
         p = Pretty()
-        im._repr_pretty_(p, None)
+        im._repr_pretty_(p, False)
         assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
 
     def test_open_formats(self) -> None:
diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst
index 18cd99cf3a2..454b94d8ce7 100644
--- a/docs/reference/plugins.rst
+++ b/docs/reference/plugins.rst
@@ -185,6 +185,14 @@ Plugin reference
     :undoc-members:
     :show-inheritance:
 
+:mod:`~PIL.MpoImagePlugin` Module
+----------------------------------
+
+.. automodule:: PIL.MpoImagePlugin
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 :mod:`~PIL.MspImagePlugin` Module
 ---------------------------------
 
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 7a73d1f69b4..35915e6520a 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -65,16 +65,24 @@ def has_ghostscript() -> bool:
     return gs_binary is not False
 
 
-def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image:
+def Ghostscript(
+    tile: list[ImageFile._Tile],
+    size: tuple[int, int],
+    fp: IO[bytes],
+    scale: int = 1,
+    transparency: bool = False,
+) -> Image.Image:
     """Render an image using Ghostscript"""
     global gs_binary
     if not has_ghostscript():
         msg = "Unable to locate Ghostscript on paths"
         raise OSError(msg)
+    assert isinstance(gs_binary, str)
 
     # Unpack decoder tile
-    decoder, tile, offset, data = tile[0]
-    length, bbox = data
+    args = tile[0].args
+    assert isinstance(args, tuple)
+    length, bbox = args
 
     # Hack to support hi-res rendering
     scale = int(scale) or 1
@@ -227,7 +235,11 @@ def _read_comment(s: str) -> bool:
                         # put floating point values there anyway.
                         box = [int(float(i)) for i in v.split()]
                         self._size = box[2] - box[0], box[3] - box[1]
-                        self.tile = [("eps", (0, 0) + self.size, offset, (length, box))]
+                        self.tile = [
+                            ImageFile._Tile(
+                                "eps", (0, 0) + self.size, offset, (length, box)
+                            )
+                        ]
                     except Exception:
                         pass
             return True
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index ae8634539e8..ebf4f46c435 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -220,7 +220,7 @@ class Quantize(IntEnum):
 if TYPE_CHECKING:
     from xml.etree.ElementTree import Element
 
-    from . import ImageFile, ImagePalette
+    from . import ImageFile, ImagePalette, TiffImagePlugin
     from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
 ID: list[str] = []
 OPEN: dict[
@@ -676,7 +676,7 @@ def __repr__(self) -> str:
             id(self),
         )
 
-    def _repr_pretty_(self, p, cycle) -> None:
+    def _repr_pretty_(self, p, cycle: bool) -> None:
         """IPython plain text display support"""
 
         # Same as __repr__ but without unpredictable id(self),
@@ -1551,6 +1551,7 @@ def get_child_images(self) -> list[ImageFile.ImageFile]:
                     ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
         ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
         if ifd1 and ifd1.get(513):
+            assert exif._info is not None
             ifds.append((ifd1, exif._info.next))
 
         offset = None
@@ -1560,12 +1561,13 @@ def get_child_images(self) -> list[ImageFile.ImageFile]:
                 offset = current_offset
 
             fp = self.fp
-            thumbnail_offset = ifd.get(513)
-            if thumbnail_offset is not None:
-                thumbnail_offset += getattr(self, "_exif_offset", 0)
-                self.fp.seek(thumbnail_offset)
-                data = self.fp.read(ifd.get(514))
-                fp = io.BytesIO(data)
+            if ifd is not None:
+                thumbnail_offset = ifd.get(513)
+                if thumbnail_offset is not None:
+                    thumbnail_offset += getattr(self, "_exif_offset", 0)
+                    self.fp.seek(thumbnail_offset)
+                    data = self.fp.read(ifd.get(514))
+                    fp = io.BytesIO(data)
 
             with open(fp) as im:
                 from . import TiffImagePlugin
@@ -3869,14 +3871,14 @@ class Exif(_ExifBase):
     bigtiff = False
     _loaded = False
 
-    def __init__(self):
-        self._data = {}
-        self._hidden_data = {}
-        self._ifds = {}
-        self._info = None
-        self._loaded_exif = None
+    def __init__(self) -> None:
+        self._data: dict[int, Any] = {}
+        self._hidden_data: dict[int, Any] = {}
+        self._ifds: dict[int, dict[int, Any]] = {}
+        self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None
+        self._loaded_exif: bytes | None = None
 
-    def _fixup(self, value):
+    def _fixup(self, value: Any) -> Any:
         try:
             if len(value) == 1 and isinstance(value, tuple):
                 return value[0]
@@ -3884,24 +3886,26 @@ def _fixup(self, value):
             pass
         return value
 
-    def _fixup_dict(self, src_dict):
+    def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]:
         # Helper function
         # returns a dict with any single item tuples/lists as individual values
         return {k: self._fixup(v) for k, v in src_dict.items()}
 
-    def _get_ifd_dict(self, offset: int, group: int | None = None):
+    def _get_ifd_dict(
+        self, offset: int, group: int | None = None
+    ) -> dict[int, Any] | None:
         try:
             # an offset pointer to the location of the nested embedded IFD.
             # It should be a long, but may be corrupted.
             self.fp.seek(offset)
         except (KeyError, TypeError):
-            pass
+            return None
         else:
             from . import TiffImagePlugin
 
             info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group)
             info.load(self.fp)
-            return self._fixup_dict(info)
+            return self._fixup_dict(dict(info))
 
     def _get_head(self) -> bytes:
         version = b"\x2B" if self.bigtiff else b"\x2A"
@@ -3966,7 +3970,7 @@ def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None:
         self.fp.seek(offset)
         self._info.load(self.fp)
 
-    def _get_merged_dict(self):
+    def _get_merged_dict(self) -> dict[int, Any]:
         merged_dict = dict(self)
 
         # get EXIF extension
@@ -4124,7 +4128,7 @@ def __len__(self) -> int:
             keys.update(self._info)
         return len(keys)
 
-    def __getitem__(self, tag: int):
+    def __getitem__(self, tag: int) -> Any:
         if self._info is not None and tag not in self._data and tag in self._info:
             self._data[tag] = self._fixup(self._info[tag])
             del self._info[tag]
@@ -4133,7 +4137,7 @@ def __getitem__(self, tag: int):
     def __contains__(self, tag: object) -> bool:
         return tag in self._data or (self._info is not None and tag in self._info)
 
-    def __setitem__(self, tag: int, value) -> None:
+    def __setitem__(self, tag: int, value: Any) -> None:
         if self._info is not None and tag in self._info:
             del self._info[tag]
         self._data[tag] = value
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index e4a7dba446b..2a8846c1afd 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError:
     raise _get_oserror(error, encoder=False)
 
 
-def _tilesort(t) -> int:
+def _tilesort(t: _Tile) -> int:
     # sort on offset
     return t[2]
 
@@ -161,7 +161,7 @@ def get_format_mimetype(self) -> str | None:
             return Image.MIME.get(self.format.upper())
         return None
 
-    def __setstate__(self, state) -> None:
+    def __setstate__(self, state: list[Any]) -> None:
         self.tile = []
         super().__setstate__(state)
 
@@ -525,7 +525,7 @@ def close(self) -> Image.Image:
 # --------------------------------------------------------------------
 
 
-def _save(im, fp, tile, bufsize: int = 0) -> None:
+def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None:
     """Helper to save image based on tile list
 
     :param im: Image object.
@@ -554,7 +554,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None:
 
 
 def _encode_tile(
-    im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None
+    im: Image.Image,
+    fp: IO[bytes],
+    tile: list[_Tile],
+    bufsize: int,
+    fh,
+    exc: BaseException | None = None,
 ) -> None:
     for encoder_name, extents, offset, args in tile:
         if offset > 0:
@@ -664,7 +669,11 @@ def setfd(self, fd: IO[bytes]) -> None:
         """
         self.fd = fd
 
-    def setimage(self, im, extents=None):
+    def setimage(
+        self,
+        im: Image.core.ImagingCore,
+        extents: tuple[int, int, int, int] | None = None,
+    ) -> None:
         """
         Called from ImageFile to set the core output image for the codec
 
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index d83d60b7b1b..3a83f9ca68a 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -42,7 +42,7 @@
 import sys
 import tempfile
 import warnings
-from typing import IO, Any
+from typing import IO, TYPE_CHECKING, Any
 
 from . import Image, ImageFile
 from ._binary import i16be as i16
@@ -51,6 +51,9 @@
 from ._binary import o16be as o16
 from .JpegPresets import presets
 
+if TYPE_CHECKING:
+    from .MpoImagePlugin import MpoImageFile
+
 #
 # Parser
 
@@ -329,7 +332,7 @@ class JpegImageFile(ImageFile.ImageFile):
     format = "JPEG"
     format_description = "JPEG (ISO 10918)"
 
-    def _open(self):
+    def _open(self) -> None:
         s = self.fp.read(3)
 
         if not _accept(s):
@@ -342,13 +345,13 @@ def _open(self):
         self._exif_offset = 0
 
         # JPEG specifics (internal)
-        self.layer = []
-        self.huffman_dc = {}
-        self.huffman_ac = {}
-        self.quantization = {}
-        self.app = {}  # compatibility
-        self.applist = []
-        self.icclist = []
+        self.layer: list[tuple[int, int, int, int]] = []
+        self.huffman_dc: dict[Any, Any] = {}
+        self.huffman_ac: dict[Any, Any] = {}
+        self.quantization: dict[int, list[int]] = {}
+        self.app: dict[str, bytes] = {}  # compatibility
+        self.applist: list[tuple[str, bytes]] = []
+        self.icclist: list[bytes] = []
 
         while True:
             i = s[0]
@@ -831,7 +834,9 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
 
 ##
 # Factory for making JPEG and MPO instances
-def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None):
+def jpeg_factory(
+    fp: IO[bytes] | None = None, filename: str | bytes | None = None
+) -> JpegImageFile | MpoImageFile:
     im = JpegImageFile(fp, filename)
     try:
         mpheader = im._getmp()
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 52cfcd13a8e..910fa9755cd 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -40,7 +40,7 @@
 import zlib
 from collections.abc import Callable
 from enum import IntEnum
-from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn
+from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast
 
 from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
 from ._binary import i16be as i16
@@ -1223,7 +1223,11 @@ def _write_multiple_frames(
     if default_image:
         if im.mode != mode:
             im = im.convert(mode)
-        ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
+        ImageFile._save(
+            im,
+            cast(IO[bytes], _idat(fp, chunk)),
+            [("zip", (0, 0) + im.size, 0, rawmode)],
+        )
 
     seq_num = 0
     for frame, frame_data in enumerate(im_frames):
@@ -1258,14 +1262,14 @@ def _write_multiple_frames(
             # first frame must be in IDAT chunks for backwards compatibility
             ImageFile._save(
                 im_frame,
-                _idat(fp, chunk),
+                cast(IO[bytes], _idat(fp, chunk)),
                 [("zip", (0, 0) + im_frame.size, 0, rawmode)],
             )
         else:
             fdat_chunks = _fdat(fp, chunk, seq_num)
             ImageFile._save(
                 im_frame,
-                fdat_chunks,
+                cast(IO[bytes], fdat_chunks),
                 [("zip", (0, 0) + im_frame.size, 0, rawmode)],
             )
             seq_num = fdat_chunks.seq_num
@@ -1465,7 +1469,9 @@ def _save(
         )
     if single_im:
         ImageFile._save(
-            single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)]
+            single_im,
+            cast(IO[bytes], _idat(fp, chunk)),
+            [("zip", (0, 0) + single_im.size, 0, rawmode)],
         )
 
     if info:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index dd013a0cf45..d9d1bab5ac8 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -288,8 +288,10 @@ def _accept(prefix: bytes) -> bool:
     return prefix[:4] in PREFIXES
 
 
-def _limit_rational(val, max_val):
-    inv = abs(val) > 1
+def _limit_rational(
+    val: float | Fraction | IFDRational, max_val: int
+) -> tuple[float, float]:
+    inv = abs(float(val)) > 1
     n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
     return n_d[::-1] if inv else n_d
 
@@ -792,7 +794,9 @@ def write_string(self, value: str | bytes | int) -> bytes:
         return value + b"\0"
 
     @_register_loader(5, 8)
-    def load_rational(self, data, legacy_api: bool = True):
+    def load_rational(
+        self, data: bytes, legacy_api: bool = True
+    ) -> tuple[tuple[int, int] | IFDRational, ...]:
         vals = self._unpack(f"{len(data) // 4}L", data)
 
         def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
@@ -801,7 +805,7 @@ def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
         return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
 
     @_register_writer(5)
-    def write_rational(self, *values) -> bytes:
+    def write_rational(self, *values: IFDRational) -> bytes:
         return b"".join(
             self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
         )
@@ -828,7 +832,7 @@ def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
         return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
 
     @_register_writer(10)
-    def write_signed_rational(self, *values) -> bytes:
+    def write_signed_rational(self, *values: IFDRational) -> bytes:
         return b"".join(
             self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
             for frac in values
