diff --git a/.appveyor.yml b/.appveyor.yml
index e12987a5fa7..f490561cdcd 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -51,7 +51,7 @@ build_script:
 
 test_script:
 - cd c:\pillow
-- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
+- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
 - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
 - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
 - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
diff --git a/.ci/install.sh b/.ci/install.sh
index 8e65f64c447..b2d61586695 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
 python3 -m pip install --upgrade wheel
 python3 -m pip install coverage
 python3 -m pip install defusedxml
+python3 -m pip install ipython
 python3 -m pip install olefile
 python3 -m pip install -U pytest
 python3 -m pip install -U pytest-cov
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 25943776f10..ddb4212301d 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
 
 python3 -m pip install coverage
 python3 -m pip install defusedxml
+python3 -m pip install ipython
 python3 -m pip install olefile
 python3 -m pip install -U pytest
 python3 -m pip install -U pytest-cov
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 8e282709917..0aa79e4235a 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -74,6 +74,7 @@ jobs:
             perl
             python3${{ matrix.python-minor-version }}-cython
             python3${{ matrix.python-minor-version }}-devel
+            python3${{ matrix.python-minor-version }}-ipython
             python3${{ matrix.python-minor-version }}-numpy
             python3${{ matrix.python-minor-version }}-sip
             python3${{ matrix.python-minor-version }}-tkinter
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 8cad252724c..190f83f40fc 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -684,6 +684,13 @@ def test_planar_configuration_save(self, tmp_path: Path) -> None:
             with Image.open(outfile) as reloaded:
                 assert_image_equal_tofile(reloaded, infile)
 
+    def test_invalid_tiled_dimensions(self) -> None:
+        with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
+            data = fp.read()
+        b = BytesIO(data[:144] + b"\x02" + data[145:])
+        with pytest.raises(ValueError):
+            Image.open(b)
+
     @pytest.mark.parametrize("mode", ("P", "PA"))
     def test_palette(self, mode: str, tmp_path: Path) -> None:
         outfile = str(tmp_path / "temp.tif")
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 719732d127b..2afcf2efd72 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -42,6 +42,12 @@
 except ImportError:
     ElementTree = None
 
+PrettyPrinter: type | None
+try:
+    from IPython.lib.pretty import PrettyPrinter
+except ImportError:
+    PrettyPrinter = None
+
 
 # Deprecation helper
 def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@@ -91,16 +97,15 @@ def test_sanity(self) -> None:
         # with pytest.raises(MemoryError):
         #   Image.new("L", (1000000, 1000000))
 
+    @pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
     def test_repr_pretty(self) -> None:
-        class Pretty:
-            def text(self, text: str) -> None:
-                self.pretty_output = text
-
         im = Image.new("L", (100, 100))
 
-        p = Pretty()
+        output = io.StringIO()
+        assert PrettyPrinter is not None
+        p = PrettyPrinter(output)
         im._repr_pretty_(p, False)
-        assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
+        assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
 
     def test_open_formats(self) -> None:
         PNGFILE = "Tests/images/hopper.png"
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index 95e91db839f..a8bd798c12d 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -412,9 +412,8 @@ def test_encode(self) -> None:
         with pytest.raises(NotImplementedError):
             encoder.encode_to_pyfd()
 
-        fh = BytesIO()
         with pytest.raises(NotImplementedError):
-            encoder.encode_to_file(fh, 0)
+            encoder.encode_to_file(0, 0)
 
     def test_zero_height(self) -> None:
         with pytest.raises(UnidentifiedImageError):
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
index c3afa908937..a743d831f5a 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -5,8 +5,6 @@
 from io import BytesIO
 from pathlib import Path
 
-import pytest
-
 from PIL import Image, PSDraw
 
 
@@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None:
     assert os.path.getsize(tempfile) > 0
 
 
-@pytest.mark.parametrize("buffer", (True, False))
-def test_stdout(buffer: bool) -> None:
+def test_stdout() -> None:
     # Temporarily redirect stdout
     old_stdout = sys.stdout
 
     class MyStdOut:
         buffer = BytesIO()
 
-    mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
+    mystdout = MyStdOut()
 
     sys.stdout = mystdout
 
@@ -67,6 +64,4 @@ class MyStdOut:
     # Reset stdout
     sys.stdout = old_stdout
 
-    if isinstance(mystdout, MyStdOut):
-        mystdout = mystdout.buffer
-    assert mystdout.getvalue() != b""
+    assert mystdout.buffer.getvalue() != b""
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index 66c5e5422da..bc3758218db 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -362,6 +362,7 @@ Classes
     :undoc-members:
     :show-inheritance:
 .. autoclass:: PIL.Image.ImagePointHandler
+.. autoclass:: PIL.Image.ImagePointTransform
 .. autoclass:: PIL.Image.ImageTransformHandler
 
 Protocols
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 3bf5d9c0464..92656249716 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -319,11 +319,11 @@ def _open(self) -> None:
         self.load()
 
     @property
-    def size(self):
+    def size(self) -> tuple[int, int]:
         return self._size
 
     @size.setter
-    def size(self, value):
+    def size(self, value: tuple[int, int]) -> None:
         if value not in self.info["sizes"]:
             msg = "This is not one of the allowed sizes of this image"
             raise ValueError(msg)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index a6eefff56fc..5aa49b619c4 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -221,6 +221,8 @@ class Quantize(IntEnum):
     import mmap
     from xml.etree.ElementTree import Element
 
+    from IPython.lib.pretty import PrettyPrinter
+
     from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
     from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
 ID: list[str] = []
@@ -468,43 +470,53 @@ def _getencoder(
 # Simple expression analyzer
 
 
-class _E:
+class ImagePointTransform:
+    """
+    Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than
+    8 bits, this represents an affine transformation, where the value is multiplied by
+    ``scale`` and ``offset`` is added.
+    """
+
     def __init__(self, scale: float, offset: float) -> None:
         self.scale = scale
         self.offset = offset
 
-    def __neg__(self) -> _E:
-        return _E(-self.scale, -self.offset)
+    def __neg__(self) -> ImagePointTransform:
+        return ImagePointTransform(-self.scale, -self.offset)
 
-    def __add__(self, other: _E | float) -> _E:
-        if isinstance(other, _E):
-            return _E(self.scale + other.scale, self.offset + other.offset)
-        return _E(self.scale, self.offset + other)
+    def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
+        if isinstance(other, ImagePointTransform):
+            return ImagePointTransform(
+                self.scale + other.scale, self.offset + other.offset
+            )
+        return ImagePointTransform(self.scale, self.offset + other)
 
     __radd__ = __add__
 
-    def __sub__(self, other: _E | float) -> _E:
+    def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
         return self + -other
 
-    def __rsub__(self, other: _E | float) -> _E:
+    def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
         return other + -self
 
-    def __mul__(self, other: _E | float) -> _E:
-        if isinstance(other, _E):
+    def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
+        if isinstance(other, ImagePointTransform):
             return NotImplemented
-        return _E(self.scale * other, self.offset * other)
+        return ImagePointTransform(self.scale * other, self.offset * other)
 
     __rmul__ = __mul__
 
-    def __truediv__(self, other: _E | float) -> _E:
-        if isinstance(other, _E):
+    def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
+        if isinstance(other, ImagePointTransform):
             return NotImplemented
-        return _E(self.scale / other, self.offset / other)
+        return ImagePointTransform(self.scale / other, self.offset / other)
 
 
-def _getscaleoffset(expr) -> tuple[float, float]:
-    a = expr(_E(1, 0))
-    return (a.scale, a.offset) if isinstance(a, _E) else (0, a)
+def _getscaleoffset(
+    expr: Callable[[ImagePointTransform], ImagePointTransform | float]
+) -> tuple[float, float]:
+    a = expr(ImagePointTransform(1, 0))
+    return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
 
 
 # --------------------------------------------------------------------
@@ -677,7 +689,7 @@ def __repr__(self) -> str:
             id(self),
         )
 
-    def _repr_pretty_(self, p, cycle: bool) -> None:
+    def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
         """IPython plain text display support"""
 
         # Same as __repr__ but without unpredictable id(self),
@@ -1892,7 +1904,13 @@ def alpha_composite(
 
     def point(
         self,
-        lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
+        lut: (
+            Sequence[float]
+            | NumpyArray
+            | Callable[[int], float]
+            | Callable[[ImagePointTransform], ImagePointTransform | float]
+            | ImagePointHandler
+        ),
         mode: str | None = None,
     ) -> Image:
         """
@@ -1909,7 +1927,7 @@ def point(
            object::
 
                class Example(Image.ImagePointHandler):
-                 def point(self, data):
+                 def point(self, im: Image) -> Image:
                    # Return result
         :param mode: Output mode (default is same as input). This can only be used if
            the source image has mode "L" or "P", and the output has mode "1" or the
@@ -1928,10 +1946,10 @@ def point(self, data):
                 # check if the function can be used with point_transform
                 # UNDONE wiredfool -- I think this prevents us from ever doing
                 # a gamma function point transform on > 8bit images.
-                scale, offset = _getscaleoffset(lut)
+                scale, offset = _getscaleoffset(lut)  # type: ignore[arg-type]
                 return self._new(self.im.point_transform(scale, offset))
             # for other modes, convert the function to a table
-            flatLut = [lut(i) for i in range(256)] * self.im.bands
+            flatLut = [lut(i) for i in range(256)] * self.im.bands  # type: ignore[arg-type]
         else:
             flatLut = lut
 
@@ -2867,11 +2885,11 @@ def __transformer(
         self,
         box: tuple[int, int, int, int],
         image: Image,
-        method,
-        data,
+        method: Transform,
+        data: Sequence[float],
         resample: int = Resampling.NEAREST,
         fill: bool = True,
-    ):
+    ) -> None:
         w = box[2] - box[0]
         h = box[3] - box[1]
 
@@ -4006,15 +4024,19 @@ def tobytes(self, offset: int = 8) -> bytes:
             ifd[tag] = value
         return b"Exif\x00\x00" + head + ifd.tobytes(offset)
 
-    def get_ifd(self, tag):
+    def get_ifd(self, tag: int) -> dict[int, Any]:
         if tag not in self._ifds:
             if tag == ExifTags.IFD.IFD1:
                 if self._info is not None and self._info.next != 0:
-                    self._ifds[tag] = self._get_ifd_dict(self._info.next)
+                    ifd = self._get_ifd_dict(self._info.next)
+                    if ifd is not None:
+                        self._ifds[tag] = ifd
             elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
                 offset = self._hidden_data.get(tag, self.get(tag))
                 if offset is not None:
-                    self._ifds[tag] = self._get_ifd_dict(offset, tag)
+                    ifd = self._get_ifd_dict(offset, tag)
+                    if ifd is not None:
+                        self._ifds[tag] = ifd
             elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
                 if ExifTags.IFD.Exif not in self._ifds:
                     self.get_ifd(ExifTags.IFD.Exif)
@@ -4071,7 +4093,9 @@ def get_ifd(self, tag):
                                 (offset,) = struct.unpack(">L", data)
                                 self.fp.seek(offset)
 
-                                camerainfo = {"ModelID": self.fp.read(4)}
+                                camerainfo: dict[str, int | bytes] = {
+                                    "ModelID": self.fp.read(4)
+                                }
 
                                 self.fp.read(4)
                                 # Seconds since 2000
@@ -4087,16 +4111,18 @@ def get_ifd(self, tag):
                                 ][1]
                                 camerainfo["Parallax"] = handler(
                                     ImageFileDirectory_v2(), parallax, False
-                                )
+                                )[0]
 
                                 self.fp.read(4)
                                 camerainfo["Category"] = self.fp.read(2)
 
-                                makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
+                                makernote = {0x1101: camerainfo}
                         self._ifds[tag] = makernote
                 else:
                     # Interop
-                    self._ifds[tag] = self._get_ifd_dict(tag_data, tag)
+                    ifd = self._get_ifd_dict(tag_data, tag)
+                    if ifd is not None:
+                        self._ifds[tag] = ifd
         ifd = self._ifds.get(tag, {})
         if tag == ExifTags.IFD.Exif and self._hidden_data:
             ifd = {
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 73554fa53e3..fdeb81d7a10 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -31,6 +31,7 @@
 import abc
 import io
 import itertools
+import os
 import struct
 import sys
 from typing import IO, Any, NamedTuple
@@ -555,7 +556,7 @@ def _encode_tile(
     fp: IO[bytes],
     tile: list[_Tile],
     bufsize: int,
-    fh,
+    fh: int | None,
     exc: BaseException | None = None,
 ) -> None:
     for encoder_name, extents, offset, args in tile:
@@ -577,6 +578,7 @@ def _encode_tile(
                             break
                 else:
                     # slight speedup: compress to real file object
+                    assert fh is not None
                     errcode = encoder.encode_to_file(fh, bufsize)
             if errcode < 0:
                 raise _get_oserror(errcode, encoder=True) from exc
@@ -801,7 +803,7 @@ def encode_to_pyfd(self) -> tuple[int, int]:
             self.fd.write(data)
         return bytes_consumed, errcode
 
-    def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
+    def encode_to_file(self, fh: int, bufsize: int) -> int:
         """
         :param fh: File handle.
         :param bufsize: Buffer size.
@@ -814,5 +816,5 @@ def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
         while errcode == 0:
             status, errcode, buf = self.encode(bufsize)
             if status > 0:
-                fh.write(buf[status:])
+                os.write(fh, buf[status:])
         return errcode
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 153243519de..6ccf28aa1dd 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -214,7 +214,7 @@ def getiptcinfo(
         # as 4-byte integers, so we cannot use the get method...)
         try:
             data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK]
-        except (AttributeError, KeyError):
+        except KeyError:
             pass
 
     if data is None:
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
index 673eae1d12c..02939d26b5f 100644
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -17,7 +17,7 @@
 from __future__ import annotations
 
 import sys
-from typing import TYPE_CHECKING
+from typing import IO, TYPE_CHECKING
 
 from . import EpsImagePlugin
 
@@ -28,15 +28,12 @@
 class PSDraw:
     """
     Sets up printing to the given file. If ``fp`` is omitted,
-    ``sys.stdout.buffer`` or ``sys.stdout`` is assumed.
+    ``sys.stdout.buffer`` is assumed.
     """
 
-    def __init__(self, fp=None):
+    def __init__(self, fp: IO[bytes] | None = None) -> None:
         if not fp:
-            try:
-                fp = sys.stdout.buffer
-            except AttributeError:
-                fp = sys.stdout
+            fp = sys.stdout.buffer
         self.fp = fp
 
     def begin_document(self, id: str | None = None) -> None:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 7bbbc832e19..5c6996a652c 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -624,12 +624,12 @@ def reset(self) -> None:
         self._tagdata: dict[int, bytes] = {}
         self.tagtype = {}  # added 2008-06-05 by Florian Hoech
         self._next = None
-        self._offset = None
+        self._offset: int | None = None
 
     def __str__(self) -> str:
         return str(dict(self))
 
-    def named(self):
+    def named(self) -> dict[str, Any]:
         """
         :returns: dict of name|key: value
 
@@ -643,7 +643,7 @@ def named(self):
     def __len__(self) -> int:
         return len(set(self._tagdata) | set(self._tags_v2))
 
-    def __getitem__(self, tag):
+    def __getitem__(self, tag: int) -> Any:
         if tag not in self._tags_v2:  # unpack on the fly
             data = self._tagdata[tag]
             typ = self.tagtype[tag]
@@ -855,7 +855,7 @@ def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
             raise OSError(msg)
         return ret
 
-    def load(self, fp):
+    def load(self, fp: IO[bytes]) -> None:
         self.reset()
         self._offset = fp.tell()
 
@@ -1098,7 +1098,7 @@ def __setitem__(self, tag: int, value: Any) -> None:
         for legacy_api in (False, True):
             self._setitem(tag, value, legacy_api)
 
-    def __getitem__(self, tag):
+    def __getitem__(self, tag: int) -> Any:
         if tag not in self._tags_v1:  # unpack on the fly
             data = self._tagdata[tag]
             typ = self.tagtype[tag]
@@ -1124,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile):
     format_description = "Adobe TIFF"
     _close_exclusive_fp_after_loading = False
 
-    def __init__(self, fp=None, filename=None):
-        self.tag_v2 = None
+    def __init__(
+        self,
+        fp: StrOrBytesPath | IO[bytes] | None = None,
+        filename: str | bytes | None = None,
+    ) -> None:
+        self.tag_v2: ImageFileDirectory_v2
         """ Image file directory (tag dictionary) """
 
-        self.tag = None
+        self.tag: ImageFileDirectory_v1
         """ Legacy tag entries """
 
         super().__init__(fp, filename)
@@ -1143,9 +1147,6 @@ def _open(self) -> None:
 
         self.tag_v2 = ImageFileDirectory_v2(ifh)
 
-        # legacy IFD entries will be filled in later
-        self.ifd: ImageFileDirectory_v1 | None = None
-
         # setup frame pointers
         self.__first = self.__next = self.tag_v2.next
         self.__frame = -1
@@ -1396,8 +1397,11 @@ def _setup(self) -> None:
         logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
 
         # size
-        xsize = int(self.tag_v2.get(IMAGEWIDTH))
-        ysize = int(self.tag_v2.get(IMAGELENGTH))
+        xsize = self.tag_v2.get(IMAGEWIDTH)
+        ysize = self.tag_v2.get(IMAGELENGTH)
+        if not isinstance(xsize, int) or not isinstance(ysize, int):
+            msg = "Invalid dimensions"
+            raise ValueError(msg)
         self._size = xsize, ysize
 
         logger.debug("- size: %s", self.size)
@@ -1545,8 +1549,12 @@ def _setup(self) -> None:
             else:
                 # tiled image
                 offsets = self.tag_v2[TILEOFFSETS]
-                w = self.tag_v2.get(TILEWIDTH)
+                tilewidth = self.tag_v2.get(TILEWIDTH)
                 h = self.tag_v2.get(TILELENGTH)
+                if not isinstance(tilewidth, int) or not isinstance(h, int):
+                    msg = "Invalid tile dimensions"
+                    raise ValueError(msg)
+                w = tilewidth
 
             for offset in offsets:
                 if x + w > xsize:
@@ -1624,7 +1632,7 @@ def _setup(self) -> None:
 }
 
 
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp, filename: str | bytes) -> None:
     try:
         rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
     except KeyError as e:
@@ -1760,10 +1768,11 @@ def _save(im, fp, filename):
         if im.mode == "1":
             inverted_im = im.copy()
             px = inverted_im.load()
-            for y in range(inverted_im.height):
-                for x in range(inverted_im.width):
-                    px[x, y] = 0 if px[x, y] == 255 else 255
-            im = inverted_im
+            if px is not None:
+                for y in range(inverted_im.height):
+                    for x in range(inverted_im.width):
+                        px[x, y] = 0 if px[x, y] == 255 else 255
+                im = inverted_im
         else:
             im = ImageOps.invert(im)
 
@@ -1805,11 +1814,11 @@ def _save(im, fp, filename):
     ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
 
     if im.mode == "YCbCr":
-        for tag, value in {
+        for tag, default_value in {
             YCBCRSUBSAMPLING: (1, 1),
             REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
         }.items():
-            ifd.setdefault(tag, value)
+            ifd.setdefault(tag, default_value)
 
     blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
     if libtiff:
@@ -1852,7 +1861,7 @@ def _save(im, fp, filename):
         ]
 
         # bits per sample is a single short in the tiff directory, not a list.
-        atts = {BITSPERSAMPLE: bits[0]}
+        atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
         # Merge the ones that we have with (optional) more bits from
         # the original file, e.g x,y resolution so that we can
         # save(load('')) == original file.
@@ -1923,13 +1932,15 @@ def _save(im, fp, filename):
         offset = ifd.save(fp)
 
         ImageFile._save(
-            im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
+            im,
+            fp,
+            [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
         )
 
     # -- helper for multi-page save --
     if "_debug_multipage" in encoderinfo:
         # just to access o32 and o16 (using correct byte order)
-        im._debug_multipage = ifd
+        setattr(im, "_debug_multipage", ifd)
 
 
 class AppendingTiffWriter:
