diff --git a/reviewboard/codesafety/checkers/base.py b/reviewboard/codesafety/checkers/base.py
index 27e531b316b7ee0a1e21f82cd4c8d7f22a1a5873..c7e80205623513d93f79d3370fe2a1a05f886df6 100644
--- a/reviewboard/codesafety/checkers/base.py
+++ b/reviewboard/codesafety/checkers/base.py
@@ -4,7 +4,78 @@ Version Added:
     5.0
 """
 
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional, Sequence, Set, TYPE_CHECKING
+
 from django.template.loader import render_to_string
+from django.utils.safestring import SafeString, mark_safe
+from typing_extensions import NotRequired, TypedDict
+
+from reviewboard.scmtools.models import Repository
+
+if TYPE_CHECKING:
+    # This is available only in django-stubs.
+    from django.utils.functional import _StrOrPromise
+
+
+class CodeSafetyContentItem(TypedDict):
+    """An item of content in a file to check.
+
+    Version Added:
+        5.0.2
+    """
+
+    #: The path to the file in a diff to check.
+    #:
+    #: This can be used by checkers to perform checks on only certain kinds
+    #: of files, or to change checking behavior based on the file type.
+    #:
+    #: This can just be a filename, rather than a full path, if that's all
+    #: that's available.
+    #:
+    #: Type:
+    #:     str
+    path: str
+
+    #: A list of one or more lines within the file to check.
+    #:
+    #: The checker cannot assume anything about the range of lines within the
+    #: file.
+    #:
+    #: Type:
+    #:     list of str
+    lines: List[str]
+
+    #: The repository the file is on, if any.
+    #:
+    #: Type:
+    #:     rbtools.scmtools.models.Repository
+    repository: NotRequired[Repository]
+
+
+class CodeSafetyCheckResults(TypedDict):
+    """The results of a code safety check.
+
+    Version Added:
+        5.0.2
+    """
+
+    #: A set of error IDs found by a code safety checker.
+    #:
+    #: The IDs are local to the code safety checker.
+    #:
+    #: Type:
+    #:     list of str
+    errors: NotRequired[Set[str]]
+
+    #: A set of warning IDs found by a code safety checker.
+    #:
+    #: The IDs are local to the code safety checker.
+    #:
+    #: Type:
+    #:     list of str
+    warnings: NotRequired[Set[str]]
 
 
 class BaseCodeSafetyChecker(object):
@@ -30,13 +101,13 @@ class BaseCodeSafetyChecker(object):
     #:
     #: Type:
     #:     str
-    checker_id = None
+    checker_id: Optional[str] = None
 
     #: The summary shown by the code safety checker for results.
     #:
     #: Type:
     #:    str
-    summary = None
+    summary: Optional[_StrOrPromise] = None
 
     #: The HTML template name for the alert at the top of a file.
     #:
@@ -45,7 +116,7 @@ class BaseCodeSafetyChecker(object):
     #:
     #: Type:
     #:     str
-    file_alert_html_template_name = None
+    file_alert_html_template_name: Optional[str] = None
 
     #: A mapping of warning IDs to human-readable labels.
     #:
@@ -56,9 +127,13 @@ class BaseCodeSafetyChecker(object):
     #:
     #: Type:
     #:     dict
-    result_labels = {}
+    result_labels: Dict[str, _StrOrPromise] = {}
 
-    def check_content(self, content_items, **kwargs):
+    def check_content(
+        self,
+        content_items: List[CodeSafetyContentItem],
+        **kwargs,
+    ) -> CodeSafetyCheckResults:
         """Check content for safety issues.
 
         One or more files may be checked at once, and one or more lines
@@ -69,45 +144,37 @@ class BaseCodeSafetyChecker(object):
         multiple file's contents, or specific ranges from multiple files are
         checked, and to handle the results appropriately.
 
+        Subclasses can extend this with custom arguments. These should all
+        be specified as keyword-only arguments. Review Board may set these
+        based on stored configuration, depending on the code safety checker.
+
+        Version Changed:
+            5.0.2:
+            Added explicit support for subclass-defined custom arguments.
+
         Args:
             content_items (list of dict):
                 A list of dictionaries containing files and lines to check.
-                Each dictionary contains:
-
-                Keys:
-                    path (str):
-                        The path to the file (or just a filename, if that's all
-                        that's available).
-
-                        This can be used to perform checks on only certain
-                        types of files.
 
-                    lines (list of str):
-                        A list of one or more lines within the file to check.
-                        The checker cannot assume anything about the range of
-                        lines within the file.
-
-                    repository (reviewboard.scmtools.models.Repository,
-                                optional):
-                        The repository the file is on, if any.
+                See :py:class:`CodeSafetyContentItem` for the contents of
+                each item.
 
             **kwargs (dict, unused):
                 Additional keyword arguments, for future expansion.
 
         Returns:
             dict:
-            Results from the checks, which may contain:
-
-            Keys:
-                errors (list of str):
-                    A list of error IDs, local to this safety checker.
-
-                warnings (list of str):
-                    A list of warning IDs, local to this safety checker.
+            Results from the checks. See :py:class:`CodeSafetyCheckResults`
+            for details.
         """
         return {}
 
-    def update_line_html(self, line_html, result_ids, **kwargs):
+    def update_line_html(
+        self,
+        line_html: str,
+        result_ids: Sequence[str] = [],
+        **kwargs,
+    ) -> SafeString:
         """Update the rendered diff HTML for a line.
 
         This can update the HTML for a line to highlight any content that
@@ -117,6 +184,14 @@ class BaseCodeSafetyChecker(object):
         Callers should take care to ensure that the updates don't themselves
         cause any unsafe HTML to be generated.
 
+        Subclasses can extend this with custom arguments. These should all
+        be specified as keyword-only arguments. Review Board may set these
+        based on stored configuration, depending on the code safety checker.
+
+        Version Changed:
+            5.0.2:
+            Added explicit support for subclass-defined custom arguments.
+
         Args:
             line_html (str):
                 The HTML of the line.
@@ -131,9 +206,13 @@ class BaseCodeSafetyChecker(object):
             django.utils.safestring.SafeString:
             The updated HTML.
         """
-        return line_html
+        return mark_safe(line_html)
 
-    def get_result_labels(self, result_ids, **kwargs):
+    def get_result_labels(
+        self,
+        result_ids: Sequence[str],
+        **kwargs,
+    ) -> List[str]:
         """Return a list of result labels for the given IDs for display.
 
         By default, this will generate a list based off
@@ -161,7 +240,12 @@ class BaseCodeSafetyChecker(object):
             for _result_id in result_ids
         ]
 
-    def render_file_alert_html(self, error_ids, warning_ids, **kwargs):
+    def render_file_alert_html(
+        self,
+        error_ids: Sequence[str],
+        warning_ids: Sequence[str],
+        **kwargs,
+    ) -> Optional[SafeString]:
         """Render an alert for the top of a file.
 
         This is responsible for rendering an alert that explains the warnings
@@ -200,7 +284,12 @@ class BaseCodeSafetyChecker(object):
         return render_to_string(self.file_alert_html_template_name,
                                 context=context_data)
 
-    def get_file_alert_context_data(self, error_ids, warning_ids, **kwargs):
+    def get_file_alert_context_data(
+        self,
+        error_ids: Sequence[str],
+        warning_ids: Sequence[str],
+        **kwargs,
+    ) -> Dict[str, Any]:
         """Return context variables for the file alert template.
 
         By default, this returns:
diff --git a/reviewboard/codesafety/checkers/registry.py b/reviewboard/codesafety/checkers/registry.py
index bab3ea14d0d6b6f8e4db2fcad0c057fe20cbc764..cc0d09fb6e5e8f63626d2f5609bd7f3a9cfcb1a7 100644
--- a/reviewboard/codesafety/checkers/registry.py
+++ b/reviewboard/codesafety/checkers/registry.py
@@ -4,12 +4,17 @@ Version Added:
     5.0
 """
 
+from __future__ import annotations
+
+from typing import Iterable, Optional
+
+from reviewboard.codesafety.checkers.base import BaseCodeSafetyChecker
 from reviewboard.codesafety.checkers.trojan_source import \
     TrojanSourceCodeSafetyChecker
 from reviewboard.registries.registry import OrderedRegistry
 
 
-class CodeSafetyCheckerRegistry(OrderedRegistry):
+class CodeSafetyCheckerRegistry(OrderedRegistry[BaseCodeSafetyChecker]):
     """Registry for managing code safety checkers.
 
     Version Added:
@@ -18,7 +23,10 @@ class CodeSafetyCheckerRegistry(OrderedRegistry):
 
     lookup_attrs = ['checker_id']
 
-    def get_checker(self, checker_id):
+    def get_checker(
+        self,
+        checker_id: str,
+    ) -> Optional[BaseCodeSafetyChecker]:
         """Return a code checker with the specified ID.
 
         Args:
@@ -30,7 +38,7 @@ class CodeSafetyCheckerRegistry(OrderedRegistry):
         """
         return self.get('checker_id', checker_id)
 
-    def get_defaults(self):
+    def get_defaults(self) -> Iterable[BaseCodeSafetyChecker]:
         """Return the default code safety checkers.
 
         Returns:
diff --git a/reviewboard/codesafety/checkers/trojan_source.py b/reviewboard/codesafety/checkers/trojan_source.py
index ee51867ef9fe66a37e3db5b18f1ada982e4f7486..bdef18bcae20192dbf2b35ef85d50a24d2241c03 100644
--- a/reviewboard/codesafety/checkers/trojan_source.py
+++ b/reviewboard/codesafety/checkers/trojan_source.py
@@ -6,11 +6,20 @@ Version Added:
 
 import unicodedata
 from itertools import chain
+from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Tuple
 
-from django.utils.html import format_html, mark_safe
+from django.utils.html import format_html
+from django.utils.safestring import SafeString, mark_safe
 from django.utils.translation import gettext_lazy as _
+from typing_extensions import TypeAlias
 
-from reviewboard.codesafety.checkers.base import BaseCodeSafetyChecker
+from reviewboard.codesafety.checkers.base import (BaseCodeSafetyChecker,
+                                                  CodeSafetyCheckResults,
+                                                  CodeSafetyContentItem)
+
+
+_UnicodeRange: TypeAlias = Tuple[int, int]
+_UnicodeRanges: TypeAlias = Tuple[_UnicodeRange, ...]
 
 
 #: Zero-width Unicode characters.
@@ -25,7 +34,7 @@ from reviewboard.codesafety.checkers.base import BaseCodeSafetyChecker
 #:
 #: Version Added:
 #:     5.0
-ZERO_WIDTH_UNICODE_CHAR_RANGES = (
+ZERO_WIDTH_UNICODE_CHAR_RANGES: _UnicodeRanges = (
     (0x200B, 0x200C),
 )
 
@@ -50,7 +59,7 @@ ZERO_WIDTH_UNICODE_CHAR_RANGES = (
 #:
 #: Version Added:
 #:     5.0
-BIDI_UNICODE_RANGES = (
+BIDI_UNICODE_RANGES: _UnicodeRanges = (
     (0x202A, 0x202E),
     (0x2066, 0x2069),
 )
@@ -87,14 +96,18 @@ class TrojanSourceCodeSafetyChecker(BaseCodeSafetyChecker):
         'zws': _('Zero-width space characters (CVE-2021-42574)'),
     }
 
-    _unsafe_unicode_check_map = None
+    _unsafe_unicode_check_map: Optional[Dict[_UnicodeRange, str]] = None
 
     _check_unicode_ranges = {
         'bidi': BIDI_UNICODE_RANGES,
         'zws': ZERO_WIDTH_UNICODE_CHAR_RANGES,
     }
 
-    def check_content(self, content_items, **kwargs):
+    def check_content(
+        self,
+        content_items: List[CodeSafetyContentItem],
+        **kwargs,
+    ) -> CodeSafetyCheckResults:
         """Check content for possible Trojan Source code.
 
         This will scan the characters of each line, looking for any
@@ -149,7 +162,12 @@ class TrojanSourceCodeSafetyChecker(BaseCodeSafetyChecker):
             'warnings': warnings,
         }
 
-    def update_line_html(self, line_html, **kwargs):
+    def update_line_html(
+        self,
+        line_html: str,
+        result_ids: Sequence[str],
+        **kwargs,
+    ) -> SafeString:
         """Update the rendered diff HTML for a line.
 
         This will highlight any Unicode characters that would have triggered
@@ -193,7 +211,10 @@ class TrojanSourceCodeSafetyChecker(BaseCodeSafetyChecker):
 
         return mark_safe(''.join(result))
 
-    def _iter_unsafe_chars(self, chars):
+    def _iter_unsafe_chars(
+        self,
+        chars: Iterable[str],
+    ) -> Iterator[Tuple[int, str, int, str]]:
         """Iterate through a string, yielding unsafe characters.
 
         Args:
@@ -204,10 +225,18 @@ class TrojanSourceCodeSafetyChecker(BaseCodeSafetyChecker):
             tuple:
             Information on an unsafe character. This contains:
 
-            1. The 0-based index of the character in the provided string.
-            2. The Unicode character.
-            3. The Unicode codepoint.
-            4. The result ID.
+            Tuple:
+                0 (int):
+                    The 0-based index of the character in the provided string.
+
+                1 (str):
+                    The Unicode character.
+
+                2 (int):
+                    The Unicode codepoint.
+
+                3 (str):
+                    The result ID.
         """
         # We're importing this here, rather than at the module level, since
         # we want to avoid taking the hit until we need it the first time.
@@ -228,7 +257,7 @@ class TrojanSourceCodeSafetyChecker(BaseCodeSafetyChecker):
                         break
 
     @classmethod
-    def _get_unsafe_unicode_check_map(cls):
+    def _get_unsafe_unicode_check_map(cls) -> Dict[_UnicodeRange, str]:
         """Return a range check map for matching unsafe Unicode characters.
 
         This is cached for all future instances.
@@ -237,7 +266,7 @@ class TrojanSourceCodeSafetyChecker(BaseCodeSafetyChecker):
             dict:
             The resulting range check map.
         """
-        checks_map = getattr(cls, '_unsafe_unicode_check_map', None)
+        checks_map = cls._unsafe_unicode_check_map
 
         if checks_map is None:
             checks_map = {
diff --git a/reviewboard/codesafety/tests/test_trojan_source_code_safety_checker.py b/reviewboard/codesafety/tests/test_trojan_source_code_safety_checker.py
index f1c08ea65ae59bfa45e1cd113d3e04b88ef7e5f3..faa05eb5c1cee8edd7ede144d12d15e2b03ff31f 100644
--- a/reviewboard/codesafety/tests/test_trojan_source_code_safety_checker.py
+++ b/reviewboard/codesafety/tests/test_trojan_source_code_safety_checker.py
@@ -110,7 +110,8 @@ class TrojanSourceCodeSafetyCheckerTests(TestCase):
         bi-directional characters
         """
         html = self.checker.update_line_html(
-            '<span>/*</span> <span>\u202D admin</span><span>*/</span>:')
+            '<span>/*</span> <span>\u202D admin</span><span>*/</span>:',
+            result_ids=[])
 
         self.assertIsInstance(html, SafeString)
         self.assertEqual(
@@ -167,7 +168,8 @@ class TrojanSourceCodeSafetyCheckerTests(TestCase):
                 '<span>def</span> <span>is_%sadmin</span><span>()</span>:'
                 % dataset['char']
             )
-            html = self.checker.update_line_html(line)
+            html = self.checker.update_line_html(line,
+                                                 result_ids=[])
 
             self.assertIsInstance(html, SafeString)
 
@@ -203,7 +205,8 @@ class TrojanSourceCodeSafetyCheckerTests(TestCase):
         for dataset in datasets:
             html = self.checker.update_line_html(
                 '<span>def</span> <span>is_%sadmin</span><span>()</span>:'
-                % dataset['char'])
+                % dataset['char'],
+                result_ids=[])
 
             self.assertIsInstance(html, SafeString)
             self.assertEqual(
diff --git a/reviewboard/registries/registry.py b/reviewboard/registries/registry.py
index a9ccc28b612dea220012302ef6d8a6ee822f9a81..c32e777f223e67717cb548f703b23d2672c367d5 100644
--- a/reviewboard/registries/registry.py
+++ b/reviewboard/registries/registry.py
@@ -2,16 +2,20 @@ from djblets.registries.mixins import ExceptionFreeGetterMixin
 from djblets.registries.registry import (
     EntryPointRegistry as DjbletsEntryPointRegistry,
     OrderedRegistry as DjbletsOrderedRegistry,
-    Registry as DjbletsRegistry)
+    Registry as DjbletsRegistry,
+    RegistryItemType)
 
 
-class Registry(ExceptionFreeGetterMixin, DjbletsRegistry):
+class Registry(ExceptionFreeGetterMixin[RegistryItemType],
+               DjbletsRegistry[RegistryItemType]):
     """A registry that does not throw exceptions for failed lookups."""
 
 
-class EntryPointRegistry(ExceptionFreeGetterMixin, DjbletsEntryPointRegistry):
+class EntryPointRegistry(ExceptionFreeGetterMixin[RegistryItemType],
+                         DjbletsEntryPointRegistry[RegistryItemType]):
     """A registry that auto-populates from an entry-point."""
 
 
-class OrderedRegistry(ExceptionFreeGetterMixin, DjbletsOrderedRegistry):
+class OrderedRegistry(ExceptionFreeGetterMixin[RegistryItemType],
+                      DjbletsOrderedRegistry[RegistryItemType]):
     """A registry that keeps track of registration order."""
