diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 4ef741c36..7cee0cd64 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -88,9 +88,12 @@ jobs:
 
     - name: Check types with mypy
       run: |
-        mypy -p git
-      # With new versions of mypy new issues might arise. This is a problem if there is nobody able to fix them,
-      # so we have to ignore errors until that changes.
+        mypy --python-version=${{ matrix.python-version }} -p git
+      env:
+        MYPY_FORCE_COLOR: "1"
+        TERM: "xterm-256color"  # For color: https://github.com/python/mypy/issues/13817
+      # With new versions of mypy new issues might arise. This is a problem if there is
+      # nobody able to fix them, so we have to ignore errors until that changes.
       continue-on-error: true
 
     - name: Test with pytest
diff --git a/git/__init__.py b/git/__init__.py
index ed8a88d4b..ca5bed7a3 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -5,38 +5,6 @@
 
 # @PydevCodeAnalysisIgnore
 
-__version__ = "git"
-
-from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
-
-from gitdb.util import to_hex_sha
-from git.exc import *  # noqa: F403  # @NoMove @IgnorePep8
-from git.types import PathLike
-
-try:
-    from git.compat import safe_decode  # @NoMove @IgnorePep8
-    from git.config import GitConfigParser  # @NoMove @IgnorePep8
-    from git.objects import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.refs import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.diff import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.db import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.cmd import Git  # @NoMove @IgnorePep8
-    from git.repo import Repo  # @NoMove @IgnorePep8
-    from git.remote import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.index import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.util import (  # @NoMove @IgnorePep8
-        LockFile,
-        BlockingLockFile,
-        Stats,
-        Actor,
-        remove_password_if_present,
-        rmtree,
-    )
-except GitError as _exc:  # noqa: F405
-    raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
-
-# __all__ must be statically defined by py.typed support
-# __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
 __all__ = [  # noqa: F405
     "Actor",
     "AmbiguousObjectName",
@@ -52,6 +20,7 @@
     "CommandError",
     "Commit",
     "Diff",
+    "DiffConstants",
     "DiffIndex",
     "Diffable",
     "FetchInfo",
@@ -65,18 +34,19 @@
     "HEAD",
     "Head",
     "HookExecutionError",
+    "INDEX",
     "IndexEntry",
     "IndexFile",
     "IndexObject",
     "InvalidDBRoot",
     "InvalidGitRepositoryError",
-    "List",
+    "List",  # Deprecated - import this from `typing` instead.
     "LockFile",
     "NULL_TREE",
     "NoSuchPathError",
     "ODBError",
     "Object",
-    "Optional",
+    "Optional",  # Deprecated - import this from `typing` instead.
     "ParseError",
     "PathLike",
     "PushInfo",
@@ -90,31 +60,62 @@
     "RepositoryDirtyError",
     "RootModule",
     "RootUpdateProgress",
-    "Sequence",
+    "Sequence",  # Deprecated - import from `typing`, or `collections.abc` in 3.9+.
     "StageType",
     "Stats",
     "Submodule",
     "SymbolicReference",
-    "TYPE_CHECKING",
+    "TYPE_CHECKING",  # Deprecated - import this from `typing` instead.
     "Tag",
     "TagObject",
     "TagReference",
     "Tree",
     "TreeModifier",
-    "Tuple",
-    "Union",
+    "Tuple",  # Deprecated - import this from `typing` instead.
+    "Union",  # Deprecated - import this from `typing` instead.
     "UnmergedEntriesError",
     "UnsafeOptionError",
     "UnsafeProtocolError",
     "UnsupportedOperation",
     "UpdateProgress",
     "WorkTreeRepositoryUnsupported",
+    "refresh",
     "remove_password_if_present",
     "rmtree",
     "safe_decode",
     "to_hex_sha",
 ]
 
+__version__ = "git"
+
+from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
+
+from gitdb.util import to_hex_sha
+from git.exc import *  # noqa: F403  # @NoMove @IgnorePep8
+from git.types import PathLike
+
+try:
+    from git.compat import safe_decode  # @NoMove @IgnorePep8
+    from git.config import GitConfigParser  # @NoMove @IgnorePep8
+    from git.objects import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.refs import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.diff import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.db import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.cmd import Git  # @NoMove @IgnorePep8
+    from git.repo import Repo  # @NoMove @IgnorePep8
+    from git.remote import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.index import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.util import (  # @NoMove @IgnorePep8
+        LockFile,
+        BlockingLockFile,
+        Stats,
+        Actor,
+        remove_password_if_present,
+        rmtree,
+    )
+except GitError as _exc:  # noqa: F405
+    raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
+
 # { Initialize git executable path
 GIT_OK = None
 
@@ -146,7 +147,7 @@ def refresh(path: Optional[PathLike] = None) -> None:
     if not Git.refresh(path=path):
         return
     if not FetchInfo.refresh():  # noqa: F405
-        return  # type: ignore [unreachable]
+        return  # type: ignore[unreachable]
 
     GIT_OK = True
 
diff --git a/git/cmd.py b/git/cmd.py
index 915f46a05..ab2688a25 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -14,6 +14,7 @@
 import signal
 from subprocess import Popen, PIPE, DEVNULL
 import subprocess
+import sys
 import threading
 from textwrap import dedent
 
@@ -171,7 +172,7 @@ def pump_stream(
         p_stdout = process.proc.stdout if process.proc else None
         p_stderr = process.proc.stderr if process.proc else None
     else:
-        process = cast(Popen, process)  # type: ignore [redundant-cast]
+        process = cast(Popen, process)  # type: ignore[redundant-cast]
         cmdline = getattr(process, "args", "")
         p_stdout = process.stdout
         p_stderr = process.stderr
@@ -214,72 +215,77 @@ def pump_stream(
                     error_str = error_str.encode()
                 # We ignore typing on the next line because mypy does not like the way
                 # we inferred that stderr takes str or bytes.
-                stderr_handler(error_str)  # type: ignore
+                stderr_handler(error_str)  # type: ignore[arg-type]
 
     if finalizer:
         finalizer(process)
 
 
-def _safer_popen_windows(
-    command: Union[str, Sequence[Any]],
-    *,
-    shell: bool = False,
-    env: Optional[Mapping[str, str]] = None,
-    **kwargs: Any,
-) -> Popen:
-    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
-
-    This avoids an untrusted search path condition where a file like ``git.exe`` in a
-    malicious repository would be run when GitPython operates on the repository. The
-    process using GitPython may have an untrusted repository's working tree as its
-    current working directory. Some operations may temporarily change to that directory
-    before running a subprocess. In addition, while by default GitPython does not run
-    external commands with a shell, it can be made to do so, in which case the CWD of
-    the subprocess, which GitPython usually sets to a repository working tree, can
-    itself be searched automatically by the shell. This wrapper covers all those cases.
+safer_popen: Callable[..., Popen]
 
-    :note:
-        This currently works by setting the :envvar:`NoDefaultCurrentDirectoryInExePath`
-        environment variable during subprocess creation. It also takes care of passing
-        Windows-specific process creation flags, but that is unrelated to path search.
+if sys.platform == "win32":
 
-    :note:
-        The current implementation contains a race condition on :attr:`os.environ`.
-        GitPython isn't thread-safe, but a program using it on one thread should ideally
-        be able to mutate :attr:`os.environ` on another, without unpredictable results.
-        See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
-    """
-    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
-    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
-    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
-    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
-
-    # When using a shell, the shell is the direct subprocess, so the variable must be
-    # set in its environment, to affect its search behavior. (The "1" can be any value.)
-    if shell:
-        safer_env = {} if env is None else dict(env)
-        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
-    else:
-        safer_env = env
-
-    # When not using a shell, the current process does the search in a CreateProcessW
-    # API call, so the variable must be set in our environment. With a shell, this is
-    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
-    # patched. If that is unpatched, then in the rare case the ComSpec environment
-    # variable is unset, the search for the shell itself is unsafe. Setting
-    # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler and
-    # protects against that. (As above, the "1" can be any value.)
-    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
-        return Popen(
-            command,
-            shell=shell,
-            env=safer_env,
-            creationflags=creationflags,
-            **kwargs,
-        )
+    def _safer_popen_windows(
+        command: Union[str, Sequence[Any]],
+        *,
+        shell: bool = False,
+        env: Optional[Mapping[str, str]] = None,
+        **kwargs: Any,
+    ) -> Popen:
+        """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the
+        search.
+
+        This avoids an untrusted search path condition where a file like ``git.exe`` in
+        a malicious repository would be run when GitPython operates on the repository.
+        The process using GitPython may have an untrusted repository's working tree as
+        its current working directory. Some operations may temporarily change to that
+        directory before running a subprocess. In addition, while by default GitPython
+        does not run external commands with a shell, it can be made to do so, in which
+        case the CWD of the subprocess, which GitPython usually sets to a repository
+        working tree, can itself be searched automatically by the shell. This wrapper
+        covers all those cases.
 
+        :note:
+            This currently works by setting the
+            :envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during
+            subprocess creation. It also takes care of passing Windows-specific process
+            creation flags, but that is unrelated to path search.
+
+        :note:
+            The current implementation contains a race condition on :attr:`os.environ`.
+            GitPython isn't thread-safe, but a program using it on one thread should
+            ideally be able to mutate :attr:`os.environ` on another, without
+            unpredictable results. See comments in:
+            https://github.com/gitpython-developers/GitPython/pull/1650
+        """
+        # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards.
+        # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
+        # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
+        creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
+
+        # When using a shell, the shell is the direct subprocess, so the variable must
+        # be set in its environment, to affect its search behavior.
+        if shell:
+            # The original may be immutable, or the caller may reuse it. Mutate a copy.
+            env = {} if env is None else dict(env)
+            env["NoDefaultCurrentDirectoryInExePath"] = "1"  # The "1" can be an value.
+
+        # When not using a shell, the current process does the search in a
+        # CreateProcessW API call, so the variable must be set in our environment. With
+        # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
+        # is patched. In Python versions where it is unpatched, and in the rare case the
+        # ComSpec environment variable is unset, the search for the shell itself is
+        # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
+        # is simpler and protects against that. (As above, the "1" can be any value.)
+        with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
+            return Popen(
+                command,
+                shell=shell,
+                env=env,
+                creationflags=creationflags,
+                **kwargs,
+            )
 
-if os.name == "nt":
     safer_popen = _safer_popen_windows
 else:
     safer_popen = Popen
@@ -1119,13 +1125,13 @@ def execute(
         if inline_env is not None:
             env.update(inline_env)
 
-        if os.name == "nt":
-            cmd_not_found_exception = OSError
+        if sys.platform == "win32":
             if kill_after_timeout is not None:
                 raise GitCommandError(
                     redacted_command,
                     '"kill_after_timeout" feature is not supported on Windows.',
                 )
+            cmd_not_found_exception = OSError
         else:
             cmd_not_found_exception = FileNotFoundError
         # END handle
@@ -1164,37 +1170,57 @@ def execute(
         if as_process:
             return self.AutoInterrupt(proc, command)
 
-        def kill_process(pid: int) -> None:
-            """Callback to kill a process."""
-            if os.name == "nt":
-                raise AssertionError("Bug: This callback would be ineffective and unsafe on Windows, stopping.")
-            p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
-            child_pids = []
-            if p.stdout is not None:
-                for line in p.stdout:
-                    if len(line.split()) > 0:
-                        local_pid = (line.split())[0]
-                        if local_pid.isdigit():
-                            child_pids.append(int(local_pid))
-            try:
-                os.kill(pid, signal.SIGKILL)
-                for child_pid in child_pids:
-                    try:
-                        os.kill(child_pid, signal.SIGKILL)
-                    except OSError:
-                        pass
-                kill_check.set()  # Tell the main routine that the process was killed.
-            except OSError:
-                # It is possible that the process gets completed in the duration after
-                # timeout happens and before we try to kill the process.
-                pass
-            return
-
-        # END kill_process
-
-        if kill_after_timeout is not None:
+        if sys.platform != "win32" and kill_after_timeout is not None:
+            # Help mypy figure out this is not None even when used inside communicate().
+            timeout = kill_after_timeout
+
+            def kill_process(pid: int) -> None:
+                """Callback to kill a process.
+
+                This callback implementation would be ineffective and unsafe on Windows.
+                """
+                p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
+                child_pids = []
+                if p.stdout is not None:
+                    for line in p.stdout:
+                        if len(line.split()) > 0:
+                            local_pid = (line.split())[0]
+                            if local_pid.isdigit():
+                                child_pids.append(int(local_pid))
+                try:
+                    os.kill(pid, signal.SIGKILL)
+                    for child_pid in child_pids:
+                        try:
+                            os.kill(child_pid, signal.SIGKILL)
+                        except OSError:
+                            pass
+                    # Tell the main routine that the process was killed.
+                    kill_check.set()
+                except OSError:
+                    # It is possible that the process gets completed in the duration
+                    # after timeout happens and before we try to kill the process.
+                    pass
+                return
+
+            def communicate() -> Tuple[AnyStr, AnyStr]:
+                watchdog.start()
+                out, err = proc.communicate()
+                watchdog.cancel()
+                if kill_check.is_set():
+                    err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
+                        " ".join(redacted_command),
+                        timeout,
+                    )
+                    if not universal_newlines:
+                        err = err.encode(defenc)
+                return out, err
+
+            # END helpers
+
             kill_check = threading.Event()
-            watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,))
+            watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
+        else:
+            communicate = proc.communicate
 
         # Wait for the process to return.
         status = 0
@@ -1203,22 +1229,11 @@ def kill_process(pid: int) -> None:
         newline = "\n" if universal_newlines else b"\n"
         try:
             if output_stream is None:
-                if kill_after_timeout is not None:
-                    watchdog.start()
-                stdout_value, stderr_value = proc.communicate()
-                if kill_after_timeout is not None:
-                    watchdog.cancel()
-                    if kill_check.is_set():
-                        stderr_value = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
-                            " ".join(redacted_command),
-                            kill_after_timeout,
-                        )
-                        if not universal_newlines:
-                            stderr_value = stderr_value.encode(defenc)
+                stdout_value, stderr_value = communicate()
                 # Strip trailing "\n".
-                if stdout_value.endswith(newline) and strip_newline_in_stdout:  # type: ignore
+                if stdout_value.endswith(newline) and strip_newline_in_stdout:  # type: ignore[arg-type]
                     stdout_value = stdout_value[:-1]
-                if stderr_value.endswith(newline):  # type: ignore
+                if stderr_value.endswith(newline):  # type: ignore[arg-type]
                     stderr_value = stderr_value[:-1]
 
                 status = proc.returncode
@@ -1228,7 +1243,7 @@ def kill_process(pid: int) -> None:
                 stdout_value = proc.stdout.read()
                 stderr_value = proc.stderr.read()
                 # Strip trailing "\n".
-                if stderr_value.endswith(newline):  # type: ignore
+                if stderr_value.endswith(newline):  # type: ignore[arg-type]
                     stderr_value = stderr_value[:-1]
                 status = proc.wait()
             # END stdout handling
diff --git a/git/compat.py b/git/compat.py
index e64c645c7..6f5376c9d 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -3,7 +3,12 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Utilities to help provide compatibility with Python 3."""
+"""Utilities to help provide compatibility with Python 3.
+
+This module exists for historical reasons. Code outside GitPython may make use of public
+members of this module, but is unlikely to benefit from doing so. GitPython continues to
+use some of these utilities, in some cases for compatibility across different platforms.
+"""
 
 import locale
 import os
diff --git a/git/config.py b/git/config.py
index 2164f67dc..4441c2187 100644
--- a/git/config.py
+++ b/git/config.py
@@ -246,7 +246,7 @@ def items_all(self) -> List[Tuple[str, List[_T]]]:
 def get_config_path(config_level: Lit_config_levels) -> str:
     # We do not support an absolute path of the gitconfig on Windows.
     # Use the global config instead.
-    if os.name == "nt" and config_level == "system":
+    if sys.platform == "win32" and config_level == "system":
         config_level = "global"
 
     if config_level == "system":
@@ -344,9 +344,9 @@ def __init__(
             configuration files.
         """
         cp.RawConfigParser.__init__(self, dict_type=_OMD)
-        self._dict: Callable[..., _OMD]  # type: ignore   # mypy/typeshed bug?
+        self._dict: Callable[..., _OMD]
         self._defaults: _OMD
-        self._sections: _OMD  # type: ignore  # mypy/typeshed bug?
+        self._sections: _OMD
 
         # Used in Python 3. Needs to stay in sync with sections for underlying
         # implementation to work.
diff --git a/git/diff.py b/git/diff.py
index 966b73154..06935f87e 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import enum
 import re
 
 from git.cmd import handle_process_output
@@ -22,13 +23,12 @@
     Match,
     Optional,
     Tuple,
-    Type,
     TypeVar,
     Union,
     TYPE_CHECKING,
     cast,
 )
-from git.types import PathLike, Literal
+from git.types import Literal, PathLike
 
 if TYPE_CHECKING:
     from .objects.tree import Tree
@@ -48,10 +48,55 @@
 # ------------------------------------------------------------------------
 
 
-__all__ = ("Diffable", "DiffIndex", "Diff", "NULL_TREE")
+__all__ = ("DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff")
 
-NULL_TREE = object()
-"""Special object to compare against the empty tree in diffs."""
+
+@enum.unique
+class DiffConstants(enum.Enum):
+    """Special objects for :meth:`Diffable.diff`.
+
+    See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various
+    values including these.
+
+    :note:
+        These constants are also available as attributes of the :mod:`git.diff` module,
+        the :class:`Diffable` class and its subclasses and instances, and the top-level
+        :mod:`git` module.
+    """
+
+    NULL_TREE = enum.auto()
+    """Stand-in indicating you want to compare against the empty tree in diffs.
+
+    Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and
+    :const:`Diffable.NULL_TREE`.
+    """
+
+    INDEX = enum.auto()
+    """Stand-in indicating you want to diff against the index.
+
+    Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and
+    :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been
+    kept for backward compatibility and made an alias of this, so it may still be used.
+    """
+
+
+NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE
+"""Stand-in indicating you want to compare against the empty tree in diffs.
+
+See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
+
+This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as
+:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
+"""
+
+INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
+"""Stand-in indicating you want to diff against the index.
+
+See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
+
+This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
+:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
+"""
 
 _octal_byte_re = re.compile(rb"\\([0-9]{3})")
 
@@ -84,19 +129,56 @@ class Diffable:
     compatible type.
 
     :note:
-        Subclasses require a repo member as it is the case for
-        :class:`~git.objects.base.Object` instances, for practical reasons we do not
+        Subclasses require a :attr:`repo` member, as it is the case for
+        :class:`~git.objects.base.Object` instances. For practical reasons we do not
         derive from :class:`~git.objects.base.Object`.
     """
 
     __slots__ = ()
 
-    class Index:
-        """Stand-in indicating you want to diff against the index."""
+    repo: "Repo"
+    """Repository to operate on. Must be provided by subclass or sibling class."""
+
+    NULL_TREE = NULL_TREE
+    """Stand-in indicating you want to compare against the empty tree in diffs.
+
+    See the :meth:`diff` method, which accepts this as a value of its ``other``
+    parameter.
+
+    This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as
+    :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
+    """
+
+    INDEX = INDEX
+    """Stand-in indicating you want to diff against the index.
+
+    See the :meth:`diff` method, which accepts this as a value of its ``other``
+    parameter.
+
+    This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
+    :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`,
+    which is kept for backward compatibility (it is now defined an alias of this).
+    """
+
+    Index = INDEX
+    """Stand-in indicating you want to diff against the index
+    (same as :const:`~Diffable.INDEX`).
+
+    This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See
+    :const:`~Diffable.INDEX` and :meth:`diff` for details.
+
+    :note:
+        Although always meant for use as an opaque constant, this was formerly defined
+        as a class. Its usage is unchanged, but static type annotations that attempt
+        to permit only this object must be changed to avoid new mypy errors. This was
+        previously not possible to do, though ``Type[Diffable.Index]`` approximated it.
+        It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``.
+    """
 
     def _process_diff_args(
-        self, args: List[Union[str, "Diffable", Type["Diffable.Index"], object]]
-    ) -> List[Union[str, "Diffable", Type["Diffable.Index"], object]]:
+        self,
+        args: List[Union[PathLike, "Diffable"]],
+    ) -> List[Union[PathLike, "Diffable"]]:
         """
         :return:
             Possibly altered version of the given args list.
@@ -107,7 +189,7 @@ def _process_diff_args(
 
     def diff(
         self,
-        other: Union[Type["Index"], "Tree", "Commit", None, str, object] = Index,
+        other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
@@ -119,12 +201,16 @@ def diff(
             This the item to compare us with.
 
             * If ``None``, we will be compared to the working tree.
-            * If :class:`~git.index.base.Treeish`, it will be compared against the
-              respective tree.
-            * If :class:`Diffable.Index`, it will be compared against the index.
-            * If :attr:`git.NULL_TREE`, it will compare against the empty tree.
-            * It defaults to :class:`Diffable.Index` so that the method will not by
-              default fail on bare repositories.
+
+            * If a :class:`~git.types.Tree_ish` or string, it will be compared against
+              the respective tree.
+
+            * If :const:`INDEX`, it will be compared against the index.
+
+            * If :const:`NULL_TREE`, it will compare against the empty tree.
+
+            This parameter defaults to :const:`INDEX` (rather than ``None``) so that the
+            method will not by default fail on bare repositories.
 
         :param paths:
             This a list of paths or a single path to limit the diff to. It will only
@@ -140,14 +226,14 @@ def diff(
             sides of the diff.
 
         :return:
-            :class:`DiffIndex`
+            A :class:`DiffIndex` representing the computed diff.
 
         :note:
-            On a bare repository, `other` needs to be provided as
-            :class:`~Diffable.Index`, or as :class:`~git.objects.tree.Tree` or
+            On a bare repository, `other` needs to be provided as :const:`INDEX`, or as
+            an instance of :class:`~git.objects.tree.Tree` or
             :class:`~git.objects.commit.Commit`, or a git command error will occur.
         """
-        args: List[Union[PathLike, Diffable, Type["Diffable.Index"], object]] = []
+        args: List[Union[PathLike, Diffable]] = []
         args.append("--abbrev=40")  # We need full shas.
         args.append("--full-index")  # Get full index paths, not only filenames.
 
@@ -169,11 +255,8 @@ def diff(
         if paths is not None and not isinstance(paths, (tuple, list)):
             paths = [paths]
 
-        if hasattr(self, "Has_Repo"):
-            self.repo: "Repo" = self.repo
-
         diff_cmd = self.repo.git.diff
-        if other is self.Index:
+        if other is INDEX:
             args.insert(0, "--cached")
         elif other is NULL_TREE:
             args.insert(0, "-r")  # Recursive diff-tree.
@@ -186,7 +269,7 @@ def diff(
 
         args.insert(0, self)
 
-        # paths is a list here, or None.
+        # paths is a list or tuple here, or None.
         if paths:
             args.append("--")
             args.extend(paths)
@@ -206,7 +289,7 @@ def diff(
 
 
 class DiffIndex(List[T_Diff]):
-    R"""An Index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
+    R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
     properties.
 
     The class improves the diff handling convenience.
diff --git a/git/index/base.py b/git/index/base.py
index 985b1bccf..59b019f0f 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -11,22 +11,16 @@
 import glob
 from io import BytesIO
 import os
+import os.path as osp
 from stat import S_ISLNK
 import subprocess
+import sys
 import tempfile
 
-from git.compat import (
-    force_bytes,
-    defenc,
-)
-from git.exc import GitCommandError, CheckoutError, GitError, InvalidGitRepositoryError
-from git.objects import (
-    Blob,
-    Submodule,
-    Tree,
-    Object,
-    Commit,
-)
+from git.compat import defenc, force_bytes
+import git.diff as git_diff
+from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
+from git.objects import Blob, Commit, Object, Submodule, Tree
 from git.objects.util import Serializable
 from git.util import (
     LazyMixin,
@@ -40,24 +34,17 @@
 from gitdb.base import IStream
 from gitdb.db import MemoryDB
 
-import git.diff as git_diff
-import os.path as osp
-
 from .fun import (
+    S_IFGITLINK,
+    aggressive_tree_merge,
     entry_key,
-    write_cache,
     read_cache,
-    aggressive_tree_merge,
-    write_tree_from_cache,
-    stat_mode_to_index_mode,
-    S_IFGITLINK,
     run_commit_hook,
+    stat_mode_to_index_mode,
+    write_cache,
+    write_tree_from_cache,
 )
-from .typ import (
-    BaseIndexEntry,
-    IndexEntry,
-    StageType,
-)
+from .typ import BaseIndexEntry, IndexEntry, StageType
 from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
 
 # typing -----------------------------------------------------------------------------
@@ -76,16 +63,16 @@
     Sequence,
     TYPE_CHECKING,
     Tuple,
-    Type,
     Union,
 )
 
-from git.types import Commit_ish, PathLike
+from git.types import Literal, PathLike
 
 if TYPE_CHECKING:
     from subprocess import Popen
-    from git.repo import Repo
+
     from git.refs.reference import Reference
+    from git.repo import Repo
     from git.util import Actor
 
 
@@ -108,7 +95,7 @@ def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str,
         A context manager object that creates the file and provides its name on entry,
         and deletes it on exit.
     """
-    if os.name == "nt":
+    if sys.platform == "win32":
         fd, name = tempfile.mkstemp(dir=directory)
         os.close(fd)
         try:
@@ -644,9 +631,9 @@ def write_tree(self) -> Tree:
         return root_tree
 
     def _process_diff_args(
-        self,  # type: ignore[override]
-        args: List[Union[str, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]],
-    ) -> List[Union[str, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]:
+        self,
+        args: List[Union[PathLike, "git_diff.Diffable"]],
+    ) -> List[Union[PathLike, "git_diff.Diffable"]]:
         try:
             args.pop(args.index(self))
         except IndexError:
@@ -1127,7 +1114,7 @@ def move(
     def commit(
         self,
         message: str,
-        parent_commits: Union[Commit_ish, None] = None,
+        parent_commits: Union[List[Commit], None] = None,
         head: bool = True,
         author: Union[None, "Actor"] = None,
         committer: Union[None, "Actor"] = None,
@@ -1476,10 +1463,17 @@ def reset(
 
         return self
 
-    # @ default_index, breaks typing for some reason, copied into function
+    # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
+    # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
     def diff(
-        self,  # type: ignore[override]
-        other: Union[Type["git_diff.Diffable.Index"], "Tree", "Commit", str, None] = git_diff.Diffable.Index,
+        self,
+        other: Union[  # type: ignore[override]
+            Literal[git_diff.DiffConstants.INDEX],
+            "Tree",
+            "Commit",
+            str,
+            None,
+        ] = git_diff.INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
@@ -1494,12 +1488,11 @@ def diff(
             Will only work with indices that represent the default git index as they
             have not been initialized with a stream.
         """
-
         # Only run if we are the default repository index.
         if self._file_path != self._index_path():
             raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
         # Index against index is always empty.
-        if other is self.Index:
+        if other is self.INDEX:
             return git_diff.DiffIndex()
 
         # Index against anything but None is a reverse diff with the respective item.
@@ -1513,12 +1506,12 @@ def diff(
             # Invert the existing R flag.
             cur_val = kwargs.get("R", False)
             kwargs["R"] = not cur_val
-            return other.diff(self.Index, paths, create_patch, **kwargs)
+            return other.diff(self.INDEX, paths, create_patch, **kwargs)
         # END diff against other item handling
 
         # If other is not None here, something is wrong.
         if other is not None:
-            raise ValueError("other must be None, Diffable.Index, a Tree or Commit, was %r" % other)
+            raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
 
         # Diff against working copy - can be handled by superclass natively.
         return super().diff(other, paths, create_patch, **kwargs)
diff --git a/git/index/fun.py b/git/index/fun.py
index beca67d3f..001e8f6f2 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -18,6 +18,7 @@
     S_IXUSR,
 )
 import subprocess
+import sys
 
 from git.cmd import handle_process_output, safer_popen
 from git.compat import defenc, force_bytes, force_text, safe_decode
@@ -99,7 +100,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     env["GIT_EDITOR"] = ":"
     cmd = [hp]
     try:
-        if os.name == "nt" and not _has_file_extension(hp):
+        if sys.platform == "win32" and not _has_file_extension(hp):
             # Windows only uses extensions to determine how to open files
             # (doesn't understand shebangs). Try using bash to run the hook.
             relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
diff --git a/git/index/typ.py b/git/index/typ.py
index a7d2ad47a..c247fab99 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -12,7 +12,7 @@
 
 # typing ----------------------------------------------------------------------
 
-from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast, List
+from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast
 
 from git.types import PathLike
 
@@ -60,8 +60,8 @@ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
             path: Path = pathlike if isinstance(pathlike, Path) else Path(pathlike)
             # TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no
             # longer supported.
-            filter_parts: List[str] = path.parts
-            blob_parts: List[str] = blob_path.parts
+            filter_parts = path.parts
+            blob_parts = blob_path.parts
             if len(filter_parts) > len(blob_parts):
                 continue
             if all(i == j for i, j in zip(filter_parts, blob_parts)):
diff --git a/git/objects/base.py b/git/objects/base.py
index 2b8dd0ff6..f568a4bc5 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -3,12 +3,12 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from git.exc import WorkTreeRepositoryUnsupported
-from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex
-
 import gitdb.typ as dbtyp
 import os.path as osp
 
+from git.exc import WorkTreeRepositoryUnsupported
+from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex
+
 from .util import get_object_type_by_name
 
 
@@ -16,30 +16,58 @@
 
 from typing import Any, TYPE_CHECKING, Union
 
-from git.types import PathLike, Commit_ish, Lit_commit_ish
+from git.types import AnyGitObject, GitObjectTypeString, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from gitdb.base import OStream
+
+    from git.refs.reference import Reference
+    from git.repo import Repo
+
     from .tree import Tree
     from .blob import Blob
     from .submodule.base import Submodule
-    from git.refs.reference import Reference
 
 IndexObjUnion = Union["Tree", "Blob", "Submodule"]
 
 # --------------------------------------------------------------------------
 
-
-_assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r"
-
 __all__ = ("Object", "IndexObject")
 
 
 class Object(LazyMixin):
-    """An Object which may be :class:`~git.objects.blob.Blob`,
-    :class:`~git.objects.tree.Tree`, :class:`~git.objects.commit.Commit` or
-    `~git.objects.tag.TagObject`."""
+    """Base class for classes representing git object types.
+
+    The following four leaf classes represent specific kinds of git objects:
+
+    * :class:`Blob <git.objects.blob.Blob>`
+    * :class:`Tree <git.objects.tree.Tree>`
+    * :class:`Commit <git.objects.commit.Commit>`
+    * :class:`TagObject <git.objects.tag.TagObject>`
+
+    See gitglossary(7) on:
+
+    * "object": https://git-scm.com/docs/gitglossary#def_object
+    * "object type": https://git-scm.com/docs/gitglossary#def_object_type
+    * "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+    * "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
+    * "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
+    * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
+
+    :note:
+        See the :class:`~git.types.AnyGitObject` union type of the four leaf subclasses
+        that represent actual git object types.
+
+    :note:
+        :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
+        rooted at this :class:`Object` class, even though submodules are not really a
+        type of git object. (This also applies to its
+        :class:`~git.objects.submodule.root.RootModule` subclass.)
+
+    :note:
+        This :class:`Object` class should not be confused with :class:`object` (the root
+        of the class hierarchy in Python).
+    """
 
     NULL_HEX_SHA = "0" * 40
     NULL_BIN_SHA = b"\0" * 20
@@ -53,7 +81,21 @@ class Object(LazyMixin):
 
     __slots__ = ("repo", "binsha", "size")
 
-    type: Union[Lit_commit_ish, None] = None
+    type: Union[GitObjectTypeString, None] = None
+    """String identifying (a concrete :class:`Object` subtype for) a git object type.
+
+    The subtypes that this may name correspond to the kinds of git objects that exist,
+    i.e., the objects that may be present in a git repository.
+
+    :note:
+        Most subclasses represent specific types of git objects and override this class
+        attribute accordingly. This attribute is ``None`` in the :class:`Object` base
+        class, as well as the :class:`IndexObject` intermediate subclass, but never
+        ``None`` in concrete leaf subclasses representing specific git object types.
+
+    :note:
+        See also :class:`~git.types.GitObjectTypeString`.
+    """
 
     def __init__(self, repo: "Repo", binsha: bytes) -> None:
         """Initialize an object by identifying it by its binary sha.
@@ -75,7 +117,7 @@ def __init__(self, repo: "Repo", binsha: bytes) -> None:
         )
 
     @classmethod
-    def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
+    def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject:
         """
         :return:
             New :class:`Object` instance of a type appropriate to the object type behind
@@ -92,7 +134,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
         return repo.rev_parse(str(id))
 
     @classmethod
-    def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish:
+    def new_from_sha(cls, repo: "Repo", sha1: bytes) -> AnyGitObject:
         """
         :return:
             New object instance of a type appropriate to represent the given binary sha1
@@ -113,8 +155,7 @@ def _set_cache_(self, attr: str) -> None:
         """Retrieve object information."""
         if attr == "size":
             oinfo = self.repo.odb.info(self.binsha)
-            self.size = oinfo.size  # type:  int
-            # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type)
+            self.size = oinfo.size  # type: int
         else:
             super()._set_cache_(attr)
 
@@ -174,9 +215,13 @@ def stream_data(self, ostream: "OStream") -> "Object":
 
 
 class IndexObject(Object):
-    """Base for all objects that can be part of the index file, namely
-    :class:`~git.objects.tree.Tree`, :class:`~git.objects.blob.Blob` and
-    :class:`~git.objects.submodule.base.Submodule` objects."""
+    """Base for all objects that can be part of the index file.
+
+    The classes representing git object types that can be part of the index file are
+    :class:`~git.objects.tree.Tree and :class:`~git.objects.blob.Blob`. In addition,
+    :class:`~git.objects.submodule.base.Submodule`, which is not really a git object
+    type but can be part of an index file, is also a subclass.
+    """
 
     __slots__ = ("path", "mode")
 
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 4035c3e7c..b49930edf 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -4,19 +4,23 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from mimetypes import guess_type
-from . import base
+import sys
 
+from . import base
 
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
 __all__ = ("Blob",)
 
 
 class Blob(base.IndexObject):
-    """A Blob encapsulates a git blob object."""
+    """A Blob encapsulates a git blob object.
+
+    See gitglossary(7) on "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+    """
 
     DEFAULT_MIME_TYPE = "text/plain"
     type: Literal["blob"] = "blob"
diff --git a/git/objects/commit.py b/git/objects/commit.py
index dcb3be695..473eae8cc 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -3,57 +3,57 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+from collections import defaultdict
 import datetime
+from io import BytesIO
+import logging
+import os
 import re
 from subprocess import Popen, PIPE
+import sys
+from time import altzone, daylight, localtime, time, timezone
+
 from gitdb import IStream
-from git.util import hex_to_bin, Actor, Stats, finalize_process
-from git.diff import Diffable
 from git.cmd import Git
+from git.diff import Diffable
+from git.util import hex_to_bin, Actor, Stats, finalize_process
 
 from .tree import Tree
-from . import base
 from .util import (
     Serializable,
     TraversableIterableObj,
-    parse_date,
     altz_to_utctz_str,
-    parse_actor_and_date,
     from_timestamp,
+    parse_actor_and_date,
+    parse_date,
 )
-
-from time import time, daylight, altzone, timezone, localtime
-import os
-from io import BytesIO
-import logging
-from collections import defaultdict
-
+from . import base
 
 # typing ------------------------------------------------------------------
 
 from typing import (
     Any,
+    Dict,
     IO,
     Iterator,
     List,
     Sequence,
     Tuple,
-    Union,
     TYPE_CHECKING,
+    Union,
     cast,
-    Dict,
 )
 
-from git.types import PathLike
-
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
+from git.types import PathLike
+
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.refs import SymbolicReference
+    from git.repo import Repo
 
 # ------------------------------------------------------------------------
 
@@ -65,8 +65,12 @@
 class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     """Wraps a git commit object.
 
-    This class will act lazily on some of its attributes and will query the value on
-    demand only if it involves calling the git binary.
+    See gitglossary(7) on "commit object":
+    https://git-scm.com/docs/gitglossary#def_commit_object
+
+    :note:
+        This class will act lazily on some of its attributes and will query the value on
+        demand only if it involves calling the git binary.
     """
 
     # ENVIRONMENT VARIABLES
@@ -80,8 +84,8 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     # INVARIANTS
     default_encoding = "UTF-8"
 
-    # object configuration
     type: Literal["commit"] = "commit"
+
     __slots__ = (
         "tree",
         "author",
@@ -95,8 +99,11 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
         "encoding",
         "gpgsig",
     )
+
     _id_attribute_ = "hexsha"
 
+    parents: Sequence["Commit"]
+
     def __init__(
         self,
         repo: "Repo",
@@ -113,15 +120,12 @@ def __init__(
         encoding: Union[str, None] = None,
         gpgsig: Union[str, None] = None,
     ) -> None:
-        R"""Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
+        """Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
         default will be implicitly set on first query.
 
         :param binsha:
             20 byte sha1.
 
-        :param parents: tuple(Commit, ...)
-            A tuple of commit ids or actual :class:`Commit`\s.
-
         :param tree:
             A :class:`~git.objects.tree.Tree` object.
 
@@ -293,7 +297,7 @@ def name_rev(self) -> str:
     def iter_items(
         cls,
         repo: "Repo",
-        rev: Union[str, "Commit", "SymbolicReference"],  # type: ignore
+        rev: Union[str, "Commit", "SymbolicReference"],
         paths: Union[PathLike, Sequence[PathLike]] = "",
         **kwargs: Any,
     ) -> Iterator["Commit"]:
@@ -429,7 +433,11 @@ def trailers_list(self) -> List[Tuple[str, str]]:
             List containing key-value tuples of whitespace stripped trailer information.
         """
         cmd = ["git", "interpret-trailers", "--parse"]
-        proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE)  # type: ignore
+        proc: Git.AutoInterrupt = self.repo.git.execute(  # type: ignore[call-overload]
+            cmd,
+            as_process=True,
+            istream=PIPE,
+        )
         trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
         trailer = trailer.strip()
 
@@ -508,7 +516,7 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen,
             if proc_or_stream.stdout is not None:
                 stream = proc_or_stream.stdout
         elif hasattr(proc_or_stream, "readline"):
-            proc_or_stream = cast(IO, proc_or_stream)  # type: ignore [redundant-cast]
+            proc_or_stream = cast(IO, proc_or_stream)  # type: ignore[redundant-cast]
             stream = proc_or_stream
 
         readline = stream.readline
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index e5933b116..4e5a2a964 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -7,6 +7,7 @@
 import os
 import os.path as osp
 import stat
+import sys
 import uuid
 
 import git
@@ -38,23 +39,32 @@
     sm_section,
 )
 
-
 # typing ----------------------------------------------------------------------
 
-from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
-from typing import Any, Iterator, Union
-
-from git.types import Commit_ish, PathLike, TBD
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Iterator,
+    Mapping,
+    Sequence,
+    TYPE_CHECKING,
+    Union,
+    cast,
+)
 
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
+from git.types import Commit_ish, PathLike, TBD
+
 if TYPE_CHECKING:
     from git.index import IndexFile
-    from git.repo import Repo
+    from git.objects.commit import Commit
     from git.refs import Head
+    from git.repo import Repo
 
 # -----------------------------------------------------------------------------
 
@@ -102,8 +112,8 @@ class Submodule(IndexObject, TraversableIterableObj):
     k_default_mode = stat.S_IFDIR | stat.S_IFLNK
     """Submodule flags. Submodules are directories with link-status."""
 
-    type: Literal["submodule"] = "submodule"  # type: ignore
-    """This is a bogus type for base class compatibility."""
+    type: Literal["submodule"] = "submodule"  # type: ignore[assignment]
+    """This is a bogus type string for base class compatibility."""
 
     __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
 
@@ -116,7 +126,7 @@ def __init__(
         mode: Union[int, None] = None,
         path: Union[PathLike, None] = None,
         name: Union[str, None] = None,
-        parent_commit: Union[Commit_ish, None] = None,
+        parent_commit: Union["Commit", None] = None,
         url: Union[str, None] = None,
         branch_path: Union[PathLike, None] = None,
     ) -> None:
@@ -148,7 +158,6 @@ def __init__(
         if url is not None:
             self._url = url
         if branch_path is not None:
-            # assert isinstance(branch_path, str)
             self._branch_path = branch_path
         if name is not None:
             self._name = name
@@ -217,7 +226,7 @@ def __repr__(self) -> str:
 
     @classmethod
     def _config_parser(
-        cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool
+        cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool
     ) -> SubmoduleConfigParser:
         """
         :return:
@@ -268,7 +277,7 @@ def _clear_cache(self) -> None:
         # END for each name to delete
 
     @classmethod
-    def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO:
+    def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
         """
         :return:
             Configuration file as :class:`~io.BytesIO` - we only access it through the
@@ -281,7 +290,7 @@ def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO:
     def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
         """:return: Config parser constrained to our submodule in read or write mode"""
         try:
-            pc: Union["Commit_ish", None] = self.parent_commit
+            pc = self.parent_commit
         except ValueError:
             pc = None
         # END handle empty parent repository
@@ -406,7 +415,7 @@ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_ab
         """
         git_file = osp.join(working_tree_dir, ".git")
         rela_path = osp.relpath(module_abspath, start=working_tree_dir)
-        if os.name == "nt" and osp.isfile(git_file):
+        if sys.platform == "win32" and osp.isfile(git_file):
             os.remove(git_file)
         with open(git_file, "wb") as fp:
             fp.write(("gitdir: %s" % rela_path).encode(defenc))
@@ -1246,7 +1255,7 @@ def remove(
 
         return self
 
-    def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) -> "Submodule":
+    def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule":
         """Set this instance to use the given commit whose tree is supposed to
         contain the ``.gitmodules`` blob.
 
@@ -1499,7 +1508,7 @@ def url(self) -> str:
         return self._url
 
     @property
-    def parent_commit(self) -> "Commit_ish":
+    def parent_commit(self) -> "Commit":
         """
         :return:
             :class:`~git.objects.commit.Commit` instance with the tree containing the
@@ -1562,7 +1571,7 @@ def iter_items(
         cls,
         repo: "Repo",
         parent_commit: Union[Commit_ish, str] = "HEAD",
-        *Args: Any,
+        *args: Any,
         **kwargs: Any,
     ) -> Iterator["Submodule"]:
         """
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index 3268d73a4..ae56e5ef4 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -1,12 +1,12 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import logging
+
+import git
+from git.exc import InvalidGitRepositoryError
 from .base import Submodule, UpdateProgress
 from .util import find_first_remote_branch
-from git.exc import InvalidGitRepositoryError
-import git
-
-import logging
 
 # typing -------------------------------------------------------------------
 
@@ -75,9 +75,9 @@ def _clear_cache(self) -> None:
 
     # { Interface
 
-    def update(
+    def update(  # type: ignore[override]
         self,
-        previous_commit: Union[Commit_ish, None] = None,  # type: ignore[override]
+        previous_commit: Union[Commit_ish, str, None] = None,
         recursive: bool = True,
         force_remove: bool = False,
         init: bool = True,
diff --git a/git/objects/tag.py b/git/objects/tag.py
index d8815e436..e7ecfa62b 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -5,10 +5,12 @@
 
 """Provides an :class:`~git.objects.base.Object`-based type for annotated tags.
 
-This defines the :class:`TagReference` class, which represents annotated tags.
+This defines the :class:`TagObject` class, which represents annotated tags.
 For lightweight tags, see the :mod:`git.refs.tag` module.
 """
 
+import sys
+
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
 from ..util import hex_to_bin
@@ -16,9 +18,9 @@
 
 from typing import List, TYPE_CHECKING, Union
 
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
 if TYPE_CHECKING:
@@ -33,7 +35,11 @@
 
 class TagObject(base.Object):
     """Annotated (i.e. non-lightweight) tag carrying additional information about an
-    object we are pointing to."""
+    object we are pointing to.
+
+    See gitglossary(7) on "tag object":
+    https://git-scm.com/docs/gitglossary#def_tag_object
+    """
 
     type: Literal["tag"] = "tag"
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 3964b016c..308dd47a0 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -3,17 +3,16 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from git.util import IterableList, join_path
+import sys
+
 import git.diff as git_diff
-from git.util import to_bin_sha
+from git.util import IterableList, join_path, to_bin_sha
 
-from . import util
-from .base import IndexObject, IndexObjUnion
+from .base import IndexObjUnion, IndexObject
 from .blob import Blob
-from .submodule.base import Submodule
-
 from .fun import tree_entries_from_data, tree_to_stream
-
+from .submodule.base import Submodule
+from . import util
 
 # typing -------------------------------------------------
 
@@ -25,22 +24,22 @@
     Iterator,
     List,
     Tuple,
+    TYPE_CHECKING,
     Type,
     Union,
     cast,
-    TYPE_CHECKING,
 )
 
-from git.types import PathLike
-
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
+from git.types import PathLike
+
 if TYPE_CHECKING:
-    from git.repo import Repo
     from io import BytesIO
+    from git.repo import Repo
 
 TreeCacheTup = Tuple[bytes, int, str]
 
@@ -167,9 +166,12 @@ def __delitem__(self, name: str) -> None:
 
 class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
-    other :class:`~git.objects.tree.Tree`\s.
+    other :class:`Tree`\s.
 
-    Tree as a list:
+    See gitglossary(7) on "tree object":
+    https://git-scm.com/docs/gitglossary#def_tree_object
+
+    Subscripting is supported, as with a list or dict:
 
     * Access a specific blob using the ``tree["filename"]`` notation.
     * You may likewise access by index, like ``blob = tree[0]``.
@@ -235,8 +237,8 @@ def join(self, file: str) -> IndexObjUnion:
         """Find the named object in this tree's contents.
 
         :return:
-            :class:`~git.objects.blob.Blob`, :class:`~git.objects.tree.Tree`,
-            or :class:`~git.objects.submodule.base.Submodule`
+            :class:`~git.objects.blob.Blob`, :class:`Tree`, or
+            :class:`~git.objects.submodule.base.Submodule`
 
         :raise KeyError:
             If the given file or tree does not exist in this tree.
@@ -302,7 +304,7 @@ def cache(self) -> TreeModifier:
         return TreeModifier(self._cache)
 
     def traverse(
-        self,  # type: ignore[override]
+        self,
         predicate: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: True,
         prune: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: False,
         depth: int = -1,
@@ -331,9 +333,9 @@ def traverse(
         return cast(
             Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
             super()._traverse(
-                predicate,
-                prune,
-                depth,  # type: ignore
+                predicate,  # type: ignore[arg-type]
+                prune,  # type: ignore[arg-type]
+                depth,
                 branch_first,
                 visit_once,
                 ignore_self,
@@ -393,7 +395,7 @@ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
         return False
 
     def __reversed__(self) -> Iterator[IndexObjUnion]:
-        return reversed(self._iter_convert_to_object(self._cache))  # type: ignore
+        return reversed(self._iter_convert_to_object(self._cache))  # type: ignore[call-overload]
 
     def _serialize(self, stream: "BytesIO") -> "Tree":
         """Serialize this tree into the stream. Assumes sorted tree data.
diff --git a/git/objects/util.py b/git/objects/util.py
index 26a34f94c..7cca05134 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -619,7 +619,7 @@ class TraversableIterableObj(IterableObj, Traversable):
     def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TIobj]:
         return super()._list_traverse(*args, **kwargs)
 
-    @overload  # type: ignore
+    @overload
     def traverse(self: T_TIobj) -> Iterator[T_TIobj]: ...
 
     @overload
@@ -688,5 +688,13 @@ def traverse(
 
         return cast(
             Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]],
-            super()._traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge),  # type: ignore
+            super()._traverse(
+                predicate,  # type: ignore[arg-type]
+                prune,  # type: ignore[arg-type]
+                depth,
+                branch_first,
+                visit_once,
+                ignore_self,
+                as_edge,
+            ),
         )
diff --git a/git/refs/head.py b/git/refs/head.py
index f6020f461..aae5767d4 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -15,14 +15,14 @@
 
 # typing ---------------------------------------------------
 
-from typing import Any, Sequence, Union, TYPE_CHECKING
+from typing import Any, Sequence, TYPE_CHECKING, Union
 
-from git.types import PathLike, Commit_ish
+from git.types import Commit_ish, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.objects import Commit
     from git.refs import RemoteReference
+    from git.repo import Repo
 
 # -------------------------------------------------------------------
 
@@ -44,11 +44,13 @@ class HEAD(SymbolicReference):
 
     __slots__ = ()
 
+    # TODO: This can be removed once SymbolicReference.commit has static type hints.
+    commit: "Commit"
+
     def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None:
         if path != self._HEAD_NAME:
             raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path))
         super().__init__(repo, path)
-        self.commit: "Commit"
 
     def orig_head(self) -> SymbolicReference:
         """
@@ -97,7 +99,7 @@ def reset(
         if index:
             mode = "--mixed"
 
-            # Tt appears some git versions declare mixed and paths deprecated.
+            # It appears some git versions declare mixed and paths deprecated.
             # See http://github.com/Byron/GitPython/issues#issue/2.
             if paths:
                 mode = None
diff --git a/git/refs/reference.py b/git/refs/reference.py
index a7b545fed..cf418aa5d 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -1,24 +1,20 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from git.util import (
-    LazyMixin,
-    IterableObj,
-)
+from git.util import IterableObj, LazyMixin
 from .symbolic import SymbolicReference, T_References
 
-
 # typing ------------------------------------------------------------------
 
-from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING
-from git.types import Commit_ish, PathLike, _T
+from typing import Any, Callable, Iterator, TYPE_CHECKING, Type, Union
+
+from git.types import AnyGitObject, PathLike, _T
 
 if TYPE_CHECKING:
     from git.repo import Repo
 
 # ------------------------------------------------------------------------------
 
-
 __all__ = ["Reference"]
 
 # { Utilities
@@ -81,7 +77,7 @@ def __str__(self) -> str:
     # @ReservedAssignment
     def set_object(
         self,
-        object: Union[Commit_ish, "SymbolicReference", str],
+        object: Union[AnyGitObject, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "Reference":
         """Special version which checks if the head-log needs an update as well.
@@ -150,7 +146,7 @@ def iter_items(
 
     # { Remote Interface
 
-    @property  # type: ignore  # mypy cannot deal with properties with an extra decorator (2021-04-21).
+    @property
     @require_remote_ref_path
     def remote_name(self) -> str:
         """
@@ -162,7 +158,7 @@ def remote_name(self) -> str:
         # /refs/remotes/<remote name>/<branch_name>
         return tokens[2]
 
-    @property  # type: ignore  # mypy cannot deal with properties with an extra decorator (2021-04-21).
+    @property
     @require_remote_ref_path
     def remote_head(self) -> str:
         """
diff --git a/git/refs/remote.py b/git/refs/remote.py
index bb2a4e438..5cbd1b81b 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -52,7 +52,7 @@ def iter_items(
     # subclasses and recommends Any or "type: ignore".
     # (See: https://github.com/python/typing/issues/241)
     @classmethod
-    def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:  # type: ignore
+    def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:  # type: ignore[override]
         """Delete the given remote references.
 
         :note:
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 465acf872..754e90089 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -6,17 +6,16 @@
 from git.compat import defenc
 from git.objects import Object
 from git.objects.commit import Commit
+from git.refs.log import RefLog
 from git.util import (
+    LockedFD,
+    assure_directory_exists,
+    hex_to_bin,
     join_path,
     join_path_native,
     to_native_path_linux,
-    assure_directory_exists,
-    hex_to_bin,
-    LockedFD,
 )
-from gitdb.exc import BadObject, BadName
-
-from .log import RefLog
+from gitdb.exc import BadName, BadObject
 
 # typing ------------------------------------------------------------------
 
@@ -24,21 +23,21 @@
     Any,
     Iterator,
     List,
+    TYPE_CHECKING,
     Tuple,
     Type,
     TypeVar,
     Union,
-    TYPE_CHECKING,
     cast,
 )
-from git.types import Commit_ish, PathLike
+from git.types import AnyGitObject, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
-    from git.refs import Head, TagReference, RemoteReference, Reference
-    from .log import RefLogEntry
     from git.config import GitConfigParser
     from git.objects.commit import Actor
+    from git.refs import Head, TagReference, RemoteReference, Reference
+    from git.refs.log import RefLogEntry
+    from git.repo import Repo
 
 
 T_References = TypeVar("T_References", bound="SymbolicReference")
@@ -278,7 +277,7 @@ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[T
         """
         return cls._get_ref_info_helper(repo, ref_path)
 
-    def _get_object(self) -> Commit_ish:
+    def _get_object(self) -> AnyGitObject:
         """
         :return:
             The object our ref currently refers to. Refs can be cached, they will always
@@ -345,7 +344,7 @@ def set_commit(
 
     def set_object(
         self,
-        object: Union[Commit_ish, "SymbolicReference", str],
+        object: Union[AnyGitObject, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set the object we point to, possibly dereference our symbolic reference
@@ -353,9 +352,12 @@ def set_object(
 
         :param object:
             A refspec, a :class:`SymbolicReference` or an
-            :class:`~git.objects.base.Object` instance. :class:`SymbolicReference`
-            instances will be dereferenced beforehand to obtain the object they point
-            to.
+            :class:`~git.objects.base.Object` instance.
+
+            * :class:`SymbolicReference` instances will be dereferenced beforehand to
+              obtain the git object they point to.
+            * :class:`~git.objects.base.Object` instances must represent git objects
+              (:class:`~git.types.AnyGitObject`).
 
         :param logmsg:
             If not ``None``, the message will be used in the reflog entry to be written.
@@ -385,8 +387,17 @@ def set_object(
         # set the commit on our reference
         return self._get_reference().set_object(object, logmsg)
 
-    commit = property(_get_commit, set_commit, doc="Query or set commits directly")  # type: ignore
-    object = property(_get_object, set_object, doc="Return the object our ref currently refers to")  # type: ignore
+    commit = property(
+        _get_commit,
+        set_commit,  # type: ignore[arg-type]
+        doc="Query or set commits directly",
+    )
+
+    object = property(
+        _get_object,
+        set_object,  # type: ignore[arg-type]
+        doc="Return the object our ref currently refers to",
+    )
 
     def _get_reference(self) -> "SymbolicReference":
         """
@@ -404,22 +415,22 @@ def _get_reference(self) -> "SymbolicReference":
 
     def set_reference(
         self,
-        ref: Union[Commit_ish, "SymbolicReference", str],
+        ref: Union[AnyGitObject, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set ourselves to the given `ref`.
 
-        It will stay a symbol if the ref is a :class:`~git.refs.reference.Reference`.
+        It will stay a symbol if the `ref` is a :class:`~git.refs.reference.Reference`.
 
-        Otherwise an Object, given as :class:`~git.objects.base.Object` instance or
-        refspec, is assumed and if valid, will be set which effectively detaches the
-        reference if it was a purely symbolic one.
+        Otherwise a git object, specified as a :class:`~git.objects.base.Object`
+        instance or refspec, is assumed. If it is valid, this reference will be set to
+        it, which effectively detaches the reference if it was a purely symbolic one.
 
         :param ref:
             A :class:`SymbolicReference` instance, an :class:`~git.objects.base.Object`
-            instance, or a refspec string. Only if the ref is a
-            :class:`SymbolicReference` instance, we will point to it. Everything else is
-            dereferenced to obtain the actual object.
+            instance (specifically an :class:`~git.types.AnyGitObject`), or a refspec
+            string. Only if the ref is a :class:`SymbolicReference` instance, we will
+            point to it. Everything else is dereferenced to obtain the actual object.
 
         :param logmsg:
             If set to a string, the message will be used in the reflog.
@@ -486,7 +497,11 @@ def set_reference(
 
     # Aliased reference
     reference: Union["Head", "TagReference", "RemoteReference", "Reference"]
-    reference = property(_get_reference, set_reference, doc="Returns the Reference we point to")  # type: ignore
+    reference = property(  # type: ignore[assignment]
+        _get_reference,
+        set_reference,  # type: ignore[arg-type]
+        doc="Returns the Reference we point to",
+    )
     ref = reference
 
     def is_valid(self) -> bool:
diff --git a/git/refs/tag.py b/git/refs/tag.py
index 6a6dad09a..a1d0b470f 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -14,14 +14,15 @@
 
 # typing ------------------------------------------------------------------
 
-from typing import Any, Type, Union, TYPE_CHECKING
-from git.types import Commit_ish, PathLike
+from typing import Any, TYPE_CHECKING, Type, Union
+
+from git.types import AnyGitObject, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.objects import Commit
     from git.objects import TagObject
     from git.refs import SymbolicReference
+    from git.repo import Repo
 
 
 # ------------------------------------------------------------------------------
@@ -82,7 +83,7 @@ def tag(self) -> Union["TagObject", None]:
 
     # Make object read-only. It should be reasonably hard to adjust an existing tag.
     @property
-    def object(self) -> Commit_ish:  # type: ignore[override]
+    def object(self) -> AnyGitObject:  # type: ignore[override]
         return Reference._get_object(self)
 
     @classmethod
diff --git a/git/remote.py b/git/remote.py
index b63cfc208..1723216a4 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -41,14 +41,12 @@
     overload,
 )
 
-from git.types import PathLike, Literal, Commit_ish
+from git.types import AnyGitObject, Literal, PathLike
 
 if TYPE_CHECKING:
-    from git.repo.base import Repo
+    from git.objects.commit import Commit
     from git.objects.submodule.base import UpdateProgress
-
-    # from git.objects.commit import Commit
-    # from git.objects import Blob, Tree, TagObject
+    from git.repo.base import Repo
 
 flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"]
 
@@ -193,7 +191,7 @@ def __init__(
         self.summary = summary
 
     @property
-    def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]:
+    def old_commit(self) -> Union["Commit", None]:
         return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
 
     @property
@@ -359,7 +357,7 @@ def __init__(
         ref: SymbolicReference,
         flags: int,
         note: str = "",
-        old_commit: Union[Commit_ish, None] = None,
+        old_commit: Union[AnyGitObject, None] = None,
         remote_ref_path: Optional[PathLike] = None,
     ) -> None:
         """Initialize a new instance."""
@@ -378,7 +376,7 @@ def name(self) -> str:
         return self.ref.name
 
     @property
-    def commit(self) -> Commit_ish:
+    def commit(self) -> "Commit":
         """:return: Commit of our remote ref"""
         return self.ref.commit
 
@@ -435,7 +433,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
 
         # Parse operation string for more info.
         # This makes no sense for symbolic refs, but we parse it anyway.
-        old_commit: Union[Commit_ish, None] = None
+        old_commit: Union[AnyGitObject, None] = None
         is_tag_operation = False
         if "rejected" in operation:
             flags |= cls.REJECTED
@@ -556,6 +554,9 @@ class Remote(LazyMixin, IterableObj):
         "--exec",
     ]
 
+    url: str  # Obtained dynamically from _config_reader. See __getattr__ below.
+    """The URL configured for the remote."""
+
     def __init__(self, repo: "Repo", name: str) -> None:
         """Initialize a remote instance.
 
@@ -567,7 +568,6 @@ def __init__(self, repo: "Repo", name: str) -> None:
         """
         self.repo = repo
         self.name = name
-        self.url: str
 
     def __getattr__(self, attr: str) -> Any:
         """Allows to call this instance like ``remote.special(*args, **kwargs)`` to
diff --git a/git/repo/base.py b/git/repo/base.py
index a54591746..fe01a9279 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -12,6 +12,7 @@
 from pathlib import Path
 import re
 import shlex
+import sys
 import warnings
 
 import gitdb
@@ -33,29 +34,29 @@
 from git.remote import Remote, add_progress, to_progress_instance
 from git.util import (
     Actor,
-    finalize_process,
     cygpath,
-    hex_to_bin,
     expand_path,
+    finalize_process,
+    hex_to_bin,
     remove_password_if_present,
 )
 
 from .fun import (
-    rev_parse,
-    is_git_dir,
     find_submodule_git_dir,
-    touch,
     find_worktree_git_dir,
+    is_git_dir,
+    rev_parse,
+    touch,
 )
 
 # typing ------------------------------------------------------
 
 from git.types import (
-    TBD,
-    PathLike,
-    Lit_config_levels,
-    Commit_ish,
     CallableProgress,
+    Commit_ish,
+    Lit_config_levels,
+    PathLike,
+    TBD,
     Tree_ish,
     assert_never,
 )
@@ -67,25 +68,25 @@
     Iterator,
     List,
     Mapping,
+    NamedTuple,
     Optional,
     Sequence,
+    TYPE_CHECKING,
     TextIO,
     Tuple,
     Type,
     Union,
-    NamedTuple,
     cast,
-    TYPE_CHECKING,
 )
 
 from git.types import ConfigLevels_Tup, TypedDict
 
 if TYPE_CHECKING:
-    from git.util import IterableList
-    from git.refs.symbolic import SymbolicReference
     from git.objects import Tree
     from git.objects.submodule.base import UpdateProgress
+    from git.refs.symbolic import SymbolicReference
     from git.remote import RemoteProgress
+    from git.util import IterableList
 
 # -----------------------------------------------------------
 
@@ -95,7 +96,7 @@
 
 
 class BlameEntry(NamedTuple):
-    commit: Dict[str, "Commit"]
+    commit: Dict[str, Commit]
     linenos: range
     orig_path: Optional[str]
     orig_linenos: range
@@ -218,7 +219,7 @@ def __init__(
             # Given how the tests are written, this seems more likely to catch Cygwin
             # git used from Windows than Windows git used from Cygwin. Therefore
             # changing to Cygwin-style paths is the relevant operation.
-            epath = cygpath(epath)
+            epath = cygpath(str(epath))
 
         epath = epath or path or os.getcwd()
         if not isinstance(epath, str):
@@ -336,10 +337,10 @@ def close(self) -> None:
             # they are collected by the garbage collector, thus preventing deletion.
             # TODO: Find these references and ensure they are closed and deleted
             # synchronously rather than forcing a gc collection.
-            if os.name == "nt":
+            if sys.platform == "win32":
                 gc.collect()
             gitdb.util.mman.collect()
-            if os.name == "nt":
+            if sys.platform == "win32":
                 gc.collect()
 
     def __eq__(self, rhs: object) -> bool:
@@ -618,7 +619,7 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa
             git_dir = self.git_dir
         # We do not support an absolute path of the gitconfig on Windows.
         # Use the global config instead.
-        if os.name == "nt" and config_level == "system":
+        if sys.platform == "win32" and config_level == "system":
             config_level = "global"
 
         if config_level == "system":
@@ -635,7 +636,7 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa
             else:
                 return osp.normpath(osp.join(repo_dir, "config"))
         else:
-            assert_never(  # type:ignore[unreachable]
+            assert_never(  # type: ignore[unreachable]
                 config_level,
                 ValueError(f"Invalid configuration level: {config_level!r}"),
             )
@@ -771,7 +772,7 @@ def iter_commits(
 
         return Commit.iter_items(self, rev, paths, **kwargs)
 
-    def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
+    def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
         R"""Find the closest common ancestor for the given revision
         (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
         :class:`~git.refs.reference.Reference`\s, etc.).
@@ -796,9 +797,9 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
             raise ValueError("Please specify at least two revs, got only %i" % len(rev))
         # END handle input
 
-        res: List[Union[Commit_ish, None]] = []
+        res: List[Commit] = []
         try:
-            lines = self.git.merge_base(*rev, **kwargs).splitlines()  # List[str]
+            lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines()
         except GitCommandError as err:
             if err.status == 128:
                 raise
@@ -814,7 +815,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
 
         return res
 
-    def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool:
+    def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool:
         """Check if a commit is an ancestor of another.
 
         :param ancestor_rev:
diff --git a/git/repo/fun.py b/git/repo/fun.py
index e3c69c68c..0ac481206 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -6,34 +6,30 @@
 from __future__ import annotations
 
 import os
-import stat
+import os.path as osp
 from pathlib import Path
+import stat
 from string import digits
 
+from git.cmd import Git
 from git.exc import WorkTreeRepositoryUnsupported
 from git.objects import Object
 from git.refs import SymbolicReference
 from git.util import hex_to_bin, bin_to_hex, cygpath
-from gitdb.exc import (
-    BadObject,
-    BadName,
-)
-
-import os.path as osp
-from git.cmd import Git
+from gitdb.exc import BadName, BadObject
 
 # Typing ----------------------------------------------------------------------
 
-from typing import Union, Optional, cast, TYPE_CHECKING
-from git.types import Commit_ish
+from typing import Optional, TYPE_CHECKING, Union, cast, overload
+
+from git.types import AnyGitObject, Literal, PathLike
 
 if TYPE_CHECKING:
-    from git.types import PathLike
-    from .base import Repo
     from git.db import GitCmdObjectDB
+    from git.objects import Commit, TagObject
     from git.refs.reference import Reference
-    from git.objects import Commit, TagObject, Blob, Tree
     from git.refs.tag import Tag
+    from .base import Repo
 
 # ----------------------------------------------------------------------------
 
@@ -56,7 +52,7 @@ def touch(filename: str) -> str:
     return filename
 
 
-def is_git_dir(d: "PathLike") -> bool:
+def is_git_dir(d: PathLike) -> bool:
     """This is taken from the git setup.c:is_git_directory function.
 
     :raise git.exc.WorkTreeRepositoryUnsupported:
@@ -79,7 +75,7 @@ def is_git_dir(d: "PathLike") -> bool:
     return False
 
 
-def find_worktree_git_dir(dotgit: "PathLike") -> Optional[str]:
+def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
     """Search for a gitdir for this worktree."""
     try:
         statbuf = os.stat(dotgit)
@@ -98,7 +94,7 @@ def find_worktree_git_dir(dotgit: "PathLike") -> Optional[str]:
     return None
 
 
-def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]:
+def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
     """Search for a submodule repo."""
     if is_git_dir(d):
         return d
@@ -141,9 +137,15 @@ def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
     # END exception handling
 
 
-def name_to_object(
-    repo: "Repo", name: str, return_ref: bool = False
-) -> Union[SymbolicReference, "Commit", "TagObject", "Blob", "Tree"]:
+@overload
+def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ...
+
+
+@overload
+def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ...
+
+
+def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]:
     """
     :return:
         Object specified by the given name - hexshas (short and long) as well as
@@ -151,8 +153,8 @@ def name_to_object(
 
     :param return_ref:
         If ``True``, and name specifies a reference, we will return the reference
-        instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` or
-        `~gitdb.exc.BadName`.
+        instead of the object. Otherwise it will raise :class:`~gitdb.exc.BadObject` or
+        :class:`~gitdb.exc.BadName`.
     """
     hexsha: Union[None, str, bytes] = None
 
@@ -201,7 +203,7 @@ def name_to_object(
     return Object.new_from_sha(repo, hex_to_bin(hexsha))
 
 
-def deref_tag(tag: "Tag") -> "TagObject":
+def deref_tag(tag: "Tag") -> AnyGitObject:
     """Recursively dereference a tag and return the resulting object."""
     while True:
         try:
@@ -212,7 +214,7 @@ def deref_tag(tag: "Tag") -> "TagObject":
     return tag
 
 
-def to_commit(obj: Object) -> Union["Commit", "TagObject"]:
+def to_commit(obj: Object) -> "Commit":
     """Convert the given object to a commit if possible and return it."""
     if obj.type == "tag":
         obj = deref_tag(obj)
@@ -223,12 +225,18 @@ def to_commit(obj: Object) -> Union["Commit", "TagObject"]:
     return obj
 
 
-def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
-    """
+def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
+    """Parse a revision string. Like ``git rev-parse``.
+
     :return:
-        `~git.objects.base.Object` at the given revision, either
-        `~git.objects.commit.Commit`, `~git.refs.tag.Tag`, `~git.objects.tree.Tree` or
-        `~git.objects.blob.Blob`.
+        `~git.objects.base.Object` at the given revision.
+
+        This may be any type of git object:
+
+        * :class:`Commit <git.objects.commit.Commit>`
+        * :class:`TagObject <git.objects.tag.TagObject>`
+        * :class:`Tree <git.objects.tree.Tree>`
+        * :class:`Blob <git.objects.blob.Blob>`
 
     :param rev:
         ``git rev-parse``-compatible revision specification as string. Please see
@@ -249,7 +257,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         raise NotImplementedError("commit by message search (regex)")
     # END handle search
 
-    obj: Union[Commit_ish, "Reference", None] = None
+    obj: Optional[AnyGitObject] = None
     ref = None
     output_type = "commit"
     start = 0
@@ -271,12 +279,10 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 if token == "@":
                     ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True))
                 else:
-                    obj = cast(Commit_ish, name_to_object(repo, rev[:start]))
+                    obj = name_to_object(repo, rev[:start])
                 # END handle token
             # END handle refname
         else:
-            assert obj is not None
-
             if ref is not None:
                 obj = cast("Commit", ref.commit)
             # END handle ref
@@ -296,7 +302,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 pass  # Default.
             elif output_type == "tree":
                 try:
-                    obj = cast(Commit_ish, obj)
+                    obj = cast(AnyGitObject, obj)
                     obj = to_commit(obj).tree
                 except (AttributeError, ValueError):
                     pass  # Error raised later.
@@ -369,7 +375,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         parsed_to = start
         # Handle hierarchy walk.
         try:
-            obj = cast(Commit_ish, obj)
+            obj = cast(AnyGitObject, obj)
             if token == "~":
                 obj = to_commit(obj)
                 for _ in range(num):
@@ -398,7 +404,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
 
     # Still no obj? It's probably a simple name.
     if obj is None:
-        obj = cast(Commit_ish, name_to_object(repo, rev))
+        obj = name_to_object(repo, rev)
         parsed_to = lr
     # END handle simple name
 
diff --git a/git/types.py b/git/types.py
index efb393471..336f49082 100644
--- a/git/types.py
+++ b/git/types.py
@@ -4,98 +4,226 @@
 import os
 import sys
 from typing import (  # noqa: F401
+    Any,
+    Callable,
     Dict,
     NoReturn,
+    Optional,
     Sequence as Sequence,
     Tuple,
-    Union,
-    Any,
-    Optional,
-    Callable,
     TYPE_CHECKING,
     TypeVar,
+    Union,
 )
 
 if sys.version_info >= (3, 8):
     from typing import (  # noqa: F401
         Literal,
-        TypedDict,
         Protocol,
         SupportsIndex as SupportsIndex,
+        TypedDict,
         runtime_checkable,
     )
 else:
     from typing_extensions import (  # noqa: F401
         Literal,
+        Protocol,
         SupportsIndex as SupportsIndex,
         TypedDict,
-        Protocol,
         runtime_checkable,
     )
 
-# if sys.version_info >= (3, 10):
-#     from typing import TypeGuard  # noqa: F401
-# else:
-#     from typing_extensions import TypeGuard  # noqa: F401
-
-PathLike = Union[str, "os.PathLike[str]"]
-
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.objects import Commit, Tree, TagObject, Blob
+    from git.repo import Repo
 
-    # from git.refs import SymbolicReference
+PathLike = Union[str, "os.PathLike[str]"]
+"""A :class:`str` (Unicode) based file or directory path."""
 
 TBD = Any
+"""Alias of :class:`~typing.Any`, when a type hint is meant to become more specific."""
+
 _T = TypeVar("_T")
+"""Type variable used internally in GitPython."""
+
+AnyGitObject = Union["Commit", "Tree", "TagObject", "Blob"]
+"""Union of the :class:`~git.objects.base.Object`-based types that represent actual git
+object types.
+
+As noted in :class:`~git.objects.base.Object`, which has further details, these are:
+
+* :class:`Blob <git.objects.blob.Blob>`
+* :class:`Tree <git.objects.tree.Tree>`
+* :class:`Commit <git.objects.commit.Commit>`
+* :class:`TagObject <git.objects.tag.TagObject>`
+
+Those GitPython classes represent the four git object types, per gitglossary(7):
+
+* "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+* "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
+* "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
+* "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
+
+For more general information on git objects and their types as git understands them:
+
+* "object": https://git-scm.com/docs/gitglossary#def_object
+* "object type": https://git-scm.com/docs/gitglossary#def_object_type
+
+:note:
+    See also the :class:`Tree_ish` and :class:`Commit_ish` unions.
+"""
 
 Tree_ish = Union["Commit", "Tree"]
-Commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
-Lit_commit_ish = Literal["commit", "tag", "blob", "tree"]
+"""Union of :class:`~git.objects.base.Object`-based types that are inherently tree-ish.
+
+See gitglossary(7) on "tree-ish": https://git-scm.com/docs/gitglossary#def_tree-ish
+
+:note:
+    This union comprises **only** the :class:`~git.objects.commit.Commit` and
+    :class:`~git.objects.tree.Tree` classes, **all** of whose instances are tree-ish.
+    This has been done because of the way GitPython uses it as a static type annotation.
+
+    :class:`~git.objects.tag.TagObject`, some but not all of whose instances are
+    tree-ish (those representing git tag objects that ultimately resolve to a tree or
+    commit), is not covered as part of this union type.
+
+:note:
+    See also the :class:`AnyGitObject` union of all four classes corresponding to git
+    object types.
+"""
+
+Commit_ish = Union["Commit", "TagObject"]
+"""Union of :class:`~git.objects.base.Object`-based types that are sometimes commit-ish.
+
+See gitglossary(7) on "commit-ish": https://git-scm.com/docs/gitglossary#def_commit-ish
+
+:note:
+    :class:`~git.objects.commit.Commit` is the only class whose instances are all
+    commit-ish. This union type includes :class:`~git.objects.commit.Commit`, but also
+    :class:`~git.objects.tag.TagObject`, only **some** of whose instances are
+    commit-ish. Whether a particular :class:`~git.objects.tag.TagObject` peels
+    (recursively dereferences) to a commit can in general only be known at runtime.
+
+:note:
+    This is an inversion of the situation with :class:`Tree_ish`. This union is broader
+    than all commit-ish objects, while :class:`Tree_ish` is narrower than all tree-ish
+    objects.
+
+:note:
+    See also the :class:`AnyGitObject` union of all four classes corresponding to git
+    object types.
+"""
+
+GitObjectTypeString = Literal["commit", "tag", "blob", "tree"]
+"""Literal strings identifying git object types and the
+:class:`~git.objects.base.Object`-based types that represent them.
+
+See the :attr:`Object.type <git.objects.base.Object.type>` attribute. These are its
+values in :class:`~git.objects.base.Object` subclasses that represent git objects. These
+literals therefore correspond to the types in the :class:`AnyGitObject` union.
+
+These are the same strings git itself uses to identify its four object types. See
+gitglossary(7) on "object type": https://git-scm.com/docs/gitglossary#def_object_type
+"""
+
+Lit_commit_ish = Literal["commit", "tag"]
+"""Deprecated. Type of literal strings identifying sometimes-commitish git object types.
+
+Prior to a bugfix, this type had been defined more broadly. Any usage is in practice
+ambiguous and likely to be incorrect. Instead of this type:
+
+* For the type of the string literals associated with :class:`Commit_ish`, use
+  ``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to
+  this type as currently defined.
+
+* For the type of all four string literals associated with :class:`AnyGitObject`, use
+  :class:`GitObjectTypeString`. That is equivalent to the old definition of this type
+  prior to the bugfix.
+"""
 
 # Config_levels ---------------------------------------------------------
 
 Lit_config_levels = Literal["system", "global", "user", "repository"]
+"""Type of literal strings naming git configuration levels.
+
+These strings relate to which file a git configuration variable is in.
+"""
+
+ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]
+"""Static type of a tuple of the four strings representing configuration levels."""
 
 # Progress parameter type alias -----------------------------------------
 
 CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]]
+"""General type of a function or other callable used as a progress reporter for cloning.
 
-# def is_config_level(inp: str) -> TypeGuard[Lit_config_levels]:
-#     # return inp in get_args(Lit_config_level)  # only py >= 3.8
-#     return inp in ("system", "user", "global", "repository")
+This is the type of a function or other callable that reports the progress of a clone,
+when passed as a ``progress`` argument to :meth:`Repo.clone <git.repo.base.Repo.clone>`
+or :meth:`Repo.clone_from <git.repo.base.Repo.clone_from>`.
 
+:note:
+    Those :meth:`~git.repo.base.Repo.clone` and :meth:`~git.repo.base.Repo.clone_from`
+    methods also accept :meth:`~git.util.RemoteProgress` instances, including instances
+    of its :meth:`~git.util.CallableRemoteProgress` subclass.
 
-ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]
+:note:
+    Unlike objects that match this type, :meth:`~git.util.RemoteProgress` instances are
+    not directly callable, not even when they are instances of
+    :meth:`~git.util.CallableRemoteProgress`, which wraps a callable and forwards
+    information to it but is not itself callable.
+
+:note:
+    This type also allows ``None``, for cloning without reporting progress.
+"""
 
 # -----------------------------------------------------------------------------------
 
 
 def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None:
-    """For use in exhaustive checking of literal or Enum in if/else chain.
+    """For use in exhaustive checking of a literal or enum in if/else chains.
 
-    Should only be reached if all members not handled OR attempt to pass non-members through chain.
+    A call to this function should only be reached if not all members are handled, or if
+    an attempt is made to pass non-members through the chain.
 
-    If all members handled, type is Empty. Otherwise, will cause mypy error.
+    :param inp:
+        If all members are handled, the argument for `inp` will have the
+        :class:`~typing.Never`/:class:`~typing.NoReturn` type. Otherwise, the type will
+        mismatch and cause a mypy error.
 
-    If non-members given, should cause mypy error at variable creation.
+    :param raise_error:
+        If ``True``, will also raise :class:`ValueError` with a general "unhandled
+        literal" message, or the exception object passed as `exc`.
 
-    If raise_error is True, will also raise AssertionError or the Exception passed to exc.
+    :param exc:
+        It not ``None``, this should be an already-constructed exception object, to be
+        raised if `raise_error` is ``True``.
     """
     if raise_error:
         if exc is None:
-            raise ValueError(f"An unhandled Literal ({inp}) in an if/else chain was found")
+            raise ValueError(f"An unhandled literal ({inp!r}) in an if/else chain was found")
         else:
             raise exc
 
 
 class Files_TD(TypedDict):
+    """Dictionary with stat counts for the diff of a particular file.
+
+    For the :class:`~git.util.Stats.files` attribute of :class:`~git.util.Stats`
+    objects.
+    """
+
     insertions: int
     deletions: int
     lines: int
 
 
 class Total_TD(TypedDict):
+    """Dictionary with total stats from any number of files.
+
+    For the :class:`~git.util.Stats.total` attribute of :class:`~git.util.Stats`
+    objects.
+    """
+
     insertions: int
     deletions: int
     lines: int
@@ -103,15 +231,21 @@ class Total_TD(TypedDict):
 
 
 class HSH_TD(TypedDict):
+    """Dictionary carrying the same information as a :class:`~git.util.Stats` object."""
+
     total: Total_TD
     files: Dict[PathLike, Files_TD]
 
 
 @runtime_checkable
 class Has_Repo(Protocol):
+    """Protocol for having a :attr:`repo` attribute, the repository to operate on."""
+
     repo: "Repo"
 
 
 @runtime_checkable
 class Has_id_attribute(Protocol):
+    """Protocol for having :attr:`_id_attribute_` used in iteration and traversal."""
+
     _id_attribute_: str
diff --git a/git/util.py b/git/util.py
index 27751f687..2a9dd10a9 100644
--- a/git/util.py
+++ b/git/util.py
@@ -107,19 +107,12 @@
 _logger = logging.getLogger(__name__)
 
 
-def _read_win_env_flag(name: str, default: bool) -> bool:
-    """Read a boolean flag from an environment variable on Windows.
+def _read_env_flag(name: str, default: bool) -> bool:
+    """Read a boolean flag from an environment variable.
 
     :return:
-        On Windows, the flag, or the `default` value if absent or ambiguous.
-        On all other operating systems, ``False``.
-
-    :note:
-        This only accesses the environment on Windows.
+        The flag, or the `default` value if absent or ambiguous.
     """
-    if os.name != "nt":
-        return False
-
     try:
         value = os.environ[name]
     except KeyError:
@@ -140,6 +133,19 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
     return default
 
 
+def _read_win_env_flag(name: str, default: bool) -> bool:
+    """Read a boolean flag from an environment variable on Windows.
+
+    :return:
+        On Windows, the flag, or the `default` value if absent or ambiguous.
+        On all other operating systems, ``False``.
+
+    :note:
+        This only accesses the environment on Windows.
+    """
+    return sys.platform == "win32" and _read_env_flag(name, default)
+
+
 #: We need an easy way to see if Appveyor TCs start failing,
 #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
 #: till then, we wish to hide them.
@@ -223,7 +229,7 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
                 raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
             raise
 
-    if os.name != "nt":
+    if sys.platform != "win32":
         shutil.rmtree(path)
     elif sys.version_info >= (3, 12):
         shutil.rmtree(path, onexc=handler)
@@ -235,7 +241,7 @@ def rmfile(path: PathLike) -> None:
     """Ensure file deleted also on *Windows* where read-only files need special
     treatment."""
     if osp.isfile(path):
-        if os.name == "nt":
+        if sys.platform == "win32":
             os.chmod(path, 0o777)
         os.remove(path)
 
@@ -276,7 +282,7 @@ def join_path(a: PathLike, *p: PathLike) -> PathLike:
     return path
 
 
-if os.name == "nt":
+if sys.platform == "win32":
 
     def to_native_path_windows(path: PathLike) -> PathLike:
         path = str(path)
@@ -328,7 +334,7 @@ def _get_exe_extensions() -> Sequence[str]:
     PATHEXT = os.environ.get("PATHEXT", None)
     if PATHEXT:
         return tuple(p.upper() for p in PATHEXT.split(os.pathsep))
-    elif os.name == "nt":
+    elif sys.platform == "win32":
         return (".BAT", "COM", ".EXE")
     else:
         return ()
@@ -354,7 +360,9 @@ def is_exec(fpath: str) -> bool:
         return (
             osp.isfile(fpath)
             and os.access(fpath, os.X_OK)
-            and (os.name != "nt" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts))
+            and (
+                sys.platform != "win32" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts)
+            )
         )
 
     progs = []
@@ -440,23 +448,7 @@ def decygpath(path: PathLike) -> str:
 _is_cygwin_cache: Dict[str, Optional[bool]] = {}
 
 
-@overload
-def is_cygwin_git(git_executable: None) -> Literal[False]: ...
-
-
-@overload
-def is_cygwin_git(git_executable: PathLike) -> bool: ...
-
-
-def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    if os.name == "nt":
-        # This is Windows-native Python, since Cygwin has os.name == "posix".
-        return False
-
-    if git_executable is None:
-        return False
-
-    git_executable = str(git_executable)
+def _is_cygwin_git(git_executable: str) -> bool:
     is_cygwin = _is_cygwin_cache.get(git_executable)  # type: Optional[bool]
     if is_cygwin is None:
         is_cygwin = False
@@ -479,6 +471,23 @@ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
     return is_cygwin
 
 
+@overload
+def is_cygwin_git(git_executable: None) -> Literal[False]: ...
+
+
+@overload
+def is_cygwin_git(git_executable: PathLike) -> bool: ...
+
+
+def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
+    if sys.platform == "win32":  # TODO: See if we can use `sys.platform != "cygwin"`.
+        return False
+    elif git_executable is None:
+        return False
+    else:
+        return _is_cygwin_git(str(git_executable))
+
+
 def get_user_id() -> str:
     """:return: String identifying the currently active system user as ``name@node``"""
     return "%s@%s" % (getpass.getuser(), platform.node())
@@ -505,10 +514,10 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[
     if isinstance(p, pathlib.Path):
         return p.resolve()
     try:
-        p = osp.expanduser(p)  # type: ignore
+        p = osp.expanduser(p)  # type: ignore[arg-type]
         if expand_vars:
-            p = osp.expandvars(p)  # type: ignore
-        return osp.normpath(osp.abspath(p))  # type: ignore
+            p = osp.expandvars(p)
+        return osp.normpath(osp.abspath(p))
     except Exception:
         return None
 
@@ -732,7 +741,14 @@ def update(
 
 
 class CallableRemoteProgress(RemoteProgress):
-    """An implementation forwarding updates to any callable."""
+    """A :class:`RemoteProgress` implementation forwarding updates to any callable.
+
+    :note:
+        Like direct instances of :class:`RemoteProgress`, instances of this
+        :class:`CallableRemoteProgress` class are not themselves directly callable.
+        Rather, instances of this class wrap a callable and forward to it. This should
+        therefore not be confused with :class:`git.types.CallableProgress`.
+    """
 
     __slots__ = ("_callable",)
 
@@ -1176,7 +1192,7 @@ def __getattr__(self, attr: str) -> T_IterableObj:
         # END for each item
         return list.__getattribute__(self, attr)
 
-    def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj:  # type: ignore
+    def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj:  # type: ignore[override]
         assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str"
 
         if isinstance(index, int):
diff --git a/pyproject.toml b/pyproject.toml
index 57ae01058..1dc1e6aed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,13 +20,12 @@ testpaths = "test"  # Space separated list of paths from root e.g test tests doc
 # filterwarnings ignore::WarningType  # ignores those warnings
 
 [tool.mypy]
-python_version = "3.7"
+python_version = "3.8"
 disallow_untyped_defs = true
 no_implicit_optional = true
 warn_redundant_casts = true
-# warn_unused_ignores = true
+warn_unused_ignores = true
 warn_unreachable = true
-show_error_codes = true
 implicit_reexport = true
 # strict = true
 # TODO: Remove when 'gitdb' is fully annotated.
diff --git a/test/lib/helper.py b/test/lib/helper.py
index 27586c2b0..45a778b7d 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -178,7 +178,7 @@ def git_daemon_launched(base_path, ip, port):
 
     gd = None
     try:
-        if os.name == "nt":
+        if sys.platform == "win32":
             # On MINGW-git, daemon exists in Git\mingw64\libexec\git-core\,
             # but if invoked as 'git daemon', it detaches from parent `git` cmd,
             # and then CANNOT DIE!
@@ -202,7 +202,7 @@ def git_daemon_launched(base_path, ip, port):
                 as_process=True,
             )
         # Yes, I know... fortunately, this is always going to work if sleep time is just large enough.
-        time.sleep(1.0 if os.name == "nt" else 0.5)
+        time.sleep(1.0 if sys.platform == "win32" else 0.5)
     except Exception as ex:
         msg = textwrap.dedent(
             """
@@ -406,7 +406,7 @@ class VirtualEnvironment:
     __slots__ = ("_env_dir",)
 
     def __init__(self, env_dir, *, with_pip):
-        if os.name == "nt":
+        if sys.platform == "win32":
             self._env_dir = osp.realpath(env_dir)
             venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
         else:
@@ -441,7 +441,7 @@ def sources(self):
         return os.path.join(self.env_dir, "src")
 
     def _executable(self, basename):
-        if os.name == "nt":
+        if sys.platform == "win32":
             path = osp.join(self.env_dir, "Scripts", basename + ".exe")
         else:
             path = osp.join(self.env_dir, "bin", basename)
diff --git a/test/test_base.py b/test/test_base.py
index ef7486e86..e477b4837 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -130,7 +130,7 @@ def test_add_unicode(self, rw_repo):
         with open(file_path, "wb") as fp:
             fp.write(b"something")
 
-        if os.name == "nt":
+        if sys.platform == "win32":
             # On Windows, there is no way this works, see images on:
             # https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
             # Therefore, it must be added using the Python implementation.
diff --git a/test/test_config.py b/test/test_config.py
index 4843d91eb..ac19a7fa8 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -7,6 +7,7 @@
 import io
 import os
 import os.path as osp
+import sys
 from unittest import mock
 
 import pytest
@@ -238,7 +239,7 @@ def check_test_value(cr, value):
             check_test_value(cr, tv)
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")',
         raises=AssertionError,
     )
diff --git a/test/test_diff.py b/test/test_diff.py
index ed82b1bbd..96fbc60e3 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -4,15 +4,15 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import gc
-import os
 import os.path as osp
 import shutil
+import sys
 import tempfile
 
 import ddt
 import pytest
 
-from git import NULL_TREE, Diff, DiffIndex, GitCommandError, Repo, Submodule
+from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
 from git.cmd import Git
 from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
 
@@ -309,7 +309,7 @@ def test_diff_with_spaces(self):
         self.assertEqual(diff_index[0].b_path, "file with spaces", repr(diff_index[0].b_path))
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason='"Access is denied" when tearDown calls shutil.rmtree',
         raises=PermissionError,
     )
@@ -352,7 +352,7 @@ def test_diff_submodule(self):
         self.assertIsInstance(diff.b_blob.size, int)
 
     def test_diff_interface(self):
-        # Test a few variations of the main diff routine.
+        """Test a few variations of the main diff routine."""
         assertion_map = {}
         for i, commit in enumerate(self.rorepo.iter_commits("0.1.6", max_count=2)):
             diff_item = commit
@@ -360,7 +360,7 @@ def test_diff_interface(self):
                 diff_item = commit.tree
             # END use tree every second item
 
-            for other in (None, NULL_TREE, commit.Index, commit.parents[0]):
+            for other in (None, NULL_TREE, commit.INDEX, commit.parents[0]):
                 for paths in (None, "CHANGES", ("CHANGES", "lib")):
                     for create_patch in range(2):
                         diff_index = diff_item.diff(other=other, paths=paths, create_patch=create_patch)
@@ -406,10 +406,22 @@ def test_diff_interface(self):
         diff_index = c.diff(cp, ["does/not/exist"])
         self.assertEqual(len(diff_index), 0)
 
+    def test_diff_interface_stability(self):
+        """Test that the Diffable.Index redefinition should not break compatibility."""
+        self.assertIs(
+            Diffable.Index,
+            Diffable.INDEX,
+            "The old and new class attribute names must be aliases.",
+        )
+        self.assertIs(
+            type(Diffable.INDEX).__eq__,
+            object.__eq__,
+            "Equality comparison must be reference-based.",
+        )
+
     @with_rw_directory
     def test_rename_override(self, rw_dir):
-        """Test disabling of diff rename detection"""
-
+        """Test disabling of diff rename detection."""
         # Create and commit file_a.txt.
         repo = Repo.init(rw_dir)
         file_a = osp.join(rw_dir, "file_a.txt")
diff --git a/test/test_git.py b/test/test_git.py
index e1a8bda5e..dae0f6a39 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -74,7 +74,7 @@ def _fake_git(*version_info):
     fake_output = f"git version {fake_version} (fake)"
 
     with tempfile.TemporaryDirectory() as tdir:
-        if os.name == "nt":
+        if sys.platform == "win32":
             fake_git = Path(tdir, "fake-git.cmd")
             script = f"@echo {fake_output}\n"
             fake_git.write_text(script, encoding="utf-8")
@@ -215,7 +215,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
 
         repo = Repo.init(rw_dir)
 
-        if os.name == "nt":
+        if sys.platform == "win32":
             # Copy an actual binary executable that is not git. (On Windows, running
             # "hostname" only displays the hostname, it never tries to change it.)
             other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe")
@@ -228,7 +228,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
             os.chmod(impostor_path, 0o755)
 
         if use_shell_impostor:
-            shell_name = "cmd.exe" if os.name == "nt" else "sh"
+            shell_name = "cmd.exe" if sys.platform == "win32" else "sh"
             shutil.copy(impostor_path, Path(rw_dir, shell_name))
 
         with contextlib.ExitStack() as stack:
@@ -245,7 +245,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
         self.assertRegex(output, r"^git version\b")
 
     @skipUnless(
-        os.name == "nt",
+        sys.platform == "win32",
         "The regression only affected Windows, and this test logic is OS-specific.",
     )
     def test_it_avoids_upcasing_unrelated_environment_variable_names(self):
@@ -667,7 +667,7 @@ def test_successful_default_refresh_invalidates_cached_version_info(self):
             stack.enter_context(mock.patch.dict(os.environ, {"PATH": new_path_var}))
             stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
 
-            if os.name == "nt":
+            if sys.platform == "win32":
                 # On Windows, use a shell so "git" finds "git.cmd". (In the infrequent
                 # case that this effect is desired in production code, it should not be
                 # done with this technique. USE_SHELL=True is less secure and reliable,
diff --git a/test/test_index.py b/test/test_index.py
index fa64b82a2..622e7ca9a 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -14,6 +14,7 @@
 import shutil
 from stat import S_ISLNK, ST_MODE
 import subprocess
+import sys
 import tempfile
 
 import ddt
@@ -124,7 +125,7 @@ def check(cls):
         in System32; Popen finds it even if a shell would run another one, as on CI.
         (Without WSL, System32 may still have bash.exe; users sometimes put it there.)
         """
-        if os.name != "nt":
+        if sys.platform != "win32":
             return cls.Inapplicable()
 
         try:
@@ -561,7 +562,7 @@ def _count_existing(self, repo, files):
     # END num existing helper
 
     @pytest.mark.xfail(
-        os.name == "nt" and Git().config("core.symlinks") == "true",
+        sys.platform == "win32" and Git().config("core.symlinks") == "true",
         reason="Assumes symlinks are not created on Windows and opens a symlink to a nonexistent target.",
         raises=FileNotFoundError,
     )
@@ -754,7 +755,7 @@ def mixed_iterator():
         self.assertNotEqual(entries[0].hexsha, null_hex_sha)
 
         # Add symlink.
-        if os.name != "nt":
+        if sys.platform != "win32":
             for target in ("/etc/nonexisting", "/etc/passwd", "/etc"):
                 basename = "my_real_symlink"
 
@@ -812,7 +813,7 @@ def mixed_iterator():
         index.checkout(fake_symlink_path)
 
         # On Windows, we currently assume we will never get symlinks.
-        if os.name == "nt":
+        if sys.platform == "win32":
             # Symlinks should contain the link as text (which is what a
             # symlink actually is).
             with open(fake_symlink_path, "rt") as fd:
@@ -1043,7 +1044,7 @@ def test_run_commit_hook(self, rw_repo):
     def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         (chdir_to_repo,) = case
 
-        shell_name = "bash.exe" if os.name == "nt" else "sh"
+        shell_name = "bash.exe" if sys.platform == "win32" else "sh"
         maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext()
         repo = Repo.init(rw_dir)
 
diff --git a/test/test_remote.py b/test/test_remote.py
index 35af8172d..f84452deb 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -4,10 +4,10 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import gc
-import os
 import os.path as osp
 from pathlib import Path
 import random
+import sys
 import tempfile
 from unittest import skipIf
 
@@ -769,7 +769,7 @@ def test_create_remote_unsafe_url(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=R"Multiple '\' instead of '/' in remote.url make it differ from expected value",
         raises=AssertionError,
     )
@@ -832,7 +832,7 @@ def test_fetch_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_fetch_unsafe_options must be adjusted in the "
@@ -900,7 +900,7 @@ def test_pull_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_pull_unsafe_options must be adjusted in the "
@@ -974,7 +974,7 @@ def test_push_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_push_unsafe_options must be adjusted in the "
diff --git a/test/test_repo.py b/test/test_repo.py
index 30a44b6c1..238f94712 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -309,7 +309,7 @@ def test_clone_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_clone_unsafe_options must be adjusted in the "
@@ -388,7 +388,7 @@ def test_clone_from_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_clone_from_unsafe_options must be adjusted in the "
@@ -1389,7 +1389,7 @@ def test_do_not_strip_newline_in_stdout(self, rw_dir):
         self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), "hello\n")
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=R"fatal: could not create leading directories of '--upload-pack=touch C:\Users\ek\AppData\Local\Temp\tmpnantqizc\pwn': Invalid argument",  # noqa: E501
         raises=GitCommandError,
     )
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 68164729b..ee7795dbb 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -987,7 +987,7 @@ def test_rename(self, rwdir):
         # This is needed to work around a PermissionError on Windows, resembling others,
         # except new in Python 3.12. (*Maybe* this could be due to changes in CPython's
         # garbage collector detailed in https://github.com/python/cpython/issues/97922.)
-        if os.name == "nt" and sys.version_info >= (3, 12):
+        if sys.platform == "win32" and sys.version_info >= (3, 12):
             gc.collect()
 
         new_path = "renamed/myname"
@@ -1071,7 +1071,7 @@ def test_branch_renames(self, rw_dir):
         assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset"
         assert sm_mod.head.ref.name == sm_pfb.name
 
-    @skipUnless(os.name == "nt", "Specifically for Windows.")
+    @skipUnless(sys.platform == "win32", "Specifically for Windows.")
     def test_to_relative_path_with_super_at_root_drive(self):
         class Repo:
             working_tree_dir = "D:\\"
diff --git a/test/test_util.py b/test/test_util.py
index 824b3ab3d..369896581 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -149,7 +149,7 @@ def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
         mocker.patch.object(pathlib.Path, "chmod")
 
     @pytest.mark.skipif(
-        os.name != "nt",
+        sys.platform != "win32",
         reason="PermissionError is only ever wrapped on Windows",
     )
     def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
@@ -168,7 +168,7 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         "hide_windows_known_errors",
         [
             pytest.param(False),
-            pytest.param(True, marks=pytest.mark.skipif(os.name == "nt", reason="We would wrap on Windows")),
+            pytest.param(True, marks=pytest.mark.skipif(sys.platform == "win32", reason="We would wrap on Windows")),
         ],
     )
     def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors):
@@ -214,7 +214,7 @@ def _run_parse(name, value):
         return ast.literal_eval(output)
 
     @pytest.mark.skipif(
-        os.name != "nt",
+        sys.platform != "win32",
         reason="These environment variables are only used on Windows.",
     )
     @pytest.mark.parametrize(
@@ -410,7 +410,7 @@ def test_blocking_lock_file(self):
             elapsed = time.time() - start
 
         extra_time = 0.02
-        if os.name == "nt" or sys.platform == "cygwin":
+        if sys.platform in {"win32", "cygwin"}:
             extra_time *= 6  # Without this, we get indeterministic failures on Windows.
         elif sys.platform == "darwin":
             extra_time *= 18  # The situation on macOS is similar, but with more delay.