diff -pruN 7.20251225/debian/changelog 7.20251227/debian/changelog
--- 7.20251225/debian/changelog	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/debian/changelog	2025-12-28 00:40:29.000000000 +0000
@@ -1,3 +1,9 @@
+dh-python (7.20251227) unstable; urgency=medium
+
+  * Refactor internals to use dataclasses.
+
+ -- Stefano Rivera <stefanor@debian.org>  Sat, 27 Dec 2025 20:40:29 -0400
+
 dh-python (7.20251225) unstable; urgency=medium
 
   [ Stefano Rivera ]
diff -pruN 7.20251225/dh_python3 7.20251227/dh_python3
--- 7.20251225/dh_python3	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dh_python3	2025-12-28 00:40:29.000000000 +0000
@@ -24,7 +24,6 @@
 import logging
 import os
 import sys
-from argparse import ArgumentParser, SUPPRESS
 from os.path import exists, join
 from shutil import copy as fcopy
 from typing import cast
@@ -35,7 +34,7 @@ from dhpython.interpreter import Interpr
 from dhpython.version import supported, default, Version, VersionRange
 from dhpython.pydist import validate as validate_pydist
 from dhpython.fs import fix_locations, Scan
-from dhpython.option import compiled_regex
+from dhpython.options import DHPythonOptions, build_parser
 from dhpython.tools import pyinstall, pyremove
 
 # initialize script
@@ -67,176 +66,10 @@ class Scanner(Scan):
 
 
 def main() -> None:
-    parser = ArgumentParser()
-    parser.add_argument("--version", action="version", version="%(prog)s DEVELV")
-    parser.add_argument(
-        "--no-guessing-deps",
-        action="store_false",
-        dest="guess_deps",
-        help="disable guessing dependencies",
+    parser = build_parser()
+    options = parser.parse_args(
+        os.environ.get("DH_OPTIONS", "").split() + sys.argv[1:], DHPythonOptions()
     )
-    parser.add_argument(
-        "--skip-private", action="store_true", help="don't check private directories"
-    )
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        action="store_true",
-        default=os.environ.get("DH_VERBOSE") == "1",
-        help="turn verbose mode on",
-    )
-    # arch=False->arch:all only, arch=True->arch:any only, None->all of them
-    parser.add_argument(
-        "-i",
-        "--indep",
-        action="store_false",
-        dest="arch",
-        default=None,
-        help="act on architecture independent packages",
-    )
-    parser.add_argument(
-        "-a",
-        "-s",
-        "--arch",
-        action="store_true",
-        dest="arch",
-        help="act on architecture dependent packages",
-    )
-    parser.add_argument(
-        "-q", "--quiet", action="store_false", dest="verbose", help="be quiet"
-    )
-    parser.add_argument(
-        "-p",
-        "--package",
-        action="append",
-        metavar="PACKAGE",
-        help="act on the package named PACKAGE",
-    )
-    parser.add_argument(
-        "-N",
-        "--no-package",
-        action="append",
-        metavar="PACKAGE",
-        help="do not act on the specified package",
-    )
-    parser.add_argument(
-        "--remaining-packages",
-        action="store_true",
-        help="Do not act on the packages which have already been acted on by "
-        "this debhelper command earlier",
-    )
-    parser.add_argument(
-        "--compile-all",
-        action="store_true",
-        help="compile all files from given private directory in postinst, not "
-        "just the ones provided by the package",
-    )
-    parser.add_argument(
-        "-V",
-        type=VersionRange,
-        dest="vrange",
-        metavar="[X.Y][-][A.B]",
-        help="specify list of supported Python versions. See py3compile(1) for "
-        "examples",
-    )
-    parser.add_argument(
-        "-X",
-        "--exclude",
-        action="append",
-        dest="regexpr",
-        type=compiled_regex,
-        metavar="REGEXPR",
-        help="exclude .py files that match given REGEXPR from "
-        "byte-compilation, in a private dir. You may use this option "
-        "multiple times to build up a list of things to exclude.",
-    )
-    parser.add_argument(
-        "--accept-upstream-versions",
-        action="store_true",
-        help="accept upstream versions while translating Python dependencies "
-        "into Debian ones",
-    )
-    parser.add_argument(
-        "--depends",
-        action="append",
-        metavar="REQ",
-        help="translate given requirements into Debian dependencies and add "
-        "them to ${python3:Depends}. Use it for missing items in "
-        "requires.txt.",
-    )
-    parser.add_argument(
-        "--depends-section",
-        action="append",
-        metavar="SECTION",
-        help="translate requirements from given section into Debian "
-        "dependencies and add them to ${python3:Depends}",
-    )
-    parser.add_argument(
-        "--recommends",
-        action="append",
-        metavar="REQ",
-        help="translate given requirements into Debian dependencies and add "
-        "them to ${python3:Recommends}",
-    )
-    parser.add_argument(
-        "--recommends-section",
-        action="append",
-        metavar="SECTION",
-        help="translate requirements from given section into Debian "
-        "dependencies and add them to ${python3:Recommends}",
-    )
-    parser.add_argument(
-        "--suggests",
-        action="append",
-        metavar="REQ",
-        help="translate given requirements into Debian dependencies and add "
-        "them to ${python3:Suggests}",
-    )
-    parser.add_argument(
-        "--suggests-section",
-        action="append",
-        metavar="SECTION",
-        help="translate requirements from given section into Debian "
-        "dependencies and add them to ${python3:Suggests}",
-    )
-    parser.add_argument(
-        "--requires",
-        action="append",
-        metavar="FILE",
-        help="translate requirements from given file into Debian dependencies "
-        "and add them to ${python3:Depends}",
-    )
-    parser.add_argument(
-        "--shebang", metavar="COMMAND", help="use given command as shebang in scripts"
-    )
-    parser.add_argument(
-        "--ignore-shebangs",
-        action="store_true",
-        help="do not translate shebangs into Debian dependencies",
-    )
-    parser.add_argument(
-        "--no-dbg-cleaning",
-        action="store_false",
-        dest="clean_dbg_pkg",
-        help="do not remove files from debug packages",
-    )
-    parser.add_argument(
-        "--no-ext-rename",
-        action="store_true",
-        help="do not add magic tags nor multiarch tuples to extension file " "names)",
-    )
-    parser.add_argument(
-        "--no-shebang-rewrite", action="store_true", help="do not rewrite shebangs"
-    )
-    parser.add_argument(
-        "private_dir",
-        nargs="?",
-        help="Private directory containing Python modules (optional)",
-    )
-    # debhelper options:
-    parser.add_argument("-O", action="append", help=SUPPRESS)
-
-    options = parser.parse_args(os.environ.get("DH_OPTIONS", "").split() + sys.argv[1:])
     if options.O:
         parser.parse_known_args(options.O, options)
 
@@ -247,7 +80,7 @@ def main() -> None:
             private_dir = "/" + private_dir
     # TODO: support more than one private dir at the same time (see :meth:scan)
     if options.skip_private:
-        private_dir = False
+        private_dir = None
 
     if options.verbose:
         log.setLevel(logging.DEBUG)
@@ -329,7 +162,7 @@ def main() -> None:
                 if not options.ignore_shebangs and len(shebang_versions) == 1:
                     # only one version from shebang
                     args += " -V %s" % shebang_versions[0]
-                elif options.vrange and options.vrange != (None, None):
+                elif options.vrange:
                     args += " -V %s" % options.vrange
             elif ext_no_version:
                 # at least one extension's version not detected
diff -pruN 7.20251225/dhpython/build/base.py 7.20251227/dhpython/build/base.py
--- 7.20251225/dhpython/build/base.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/build/base.py	2025-12-28 00:40:29.000000000 +0000
@@ -19,18 +19,15 @@
 # THE SOFTWARE.
 
 import logging
-from argparse import Namespace
 from collections.abc import Callable
 from functools import wraps
 from glob import glob1
 from os import environ, remove, walk, makedirs
 from os.path import exists, isdir, join
 from pathlib import Path
-from shlex import quote
 from shutil import rmtree, copyfile, copytree, which
 from typing import (
     TYPE_CHECKING,
-    Any,
     ClassVar,
     Concatenate,
     Literal,
@@ -43,6 +40,7 @@ from typing import (
 from dhpython.debhelper import DebHelper, build_options
 from dhpython.exceptions import RequiredCommandMissingException
 from dhpython.tools import execute, ExecutionResult, ExecutionResultWithOutput
+from dhpython.build.options import PybuildOptions
 from dhpython.build.types import Args, Context
 
 if TYPE_CHECKING:
@@ -78,10 +76,10 @@ def copy_test_files(
             files_to_copy = {'pyproject.toml', 'pytest.ini', 'test', 'tests'}
             # check debian/pybuild_pythonX.Y.testfiles
             for tpl in ('_{i}{v}', '_{i}{m}', ''):
-                tpl = tpl.format(i=args['interpreter'].name,
-                                 v=args['version'],
-                                 m=args['version'].major)
-                fpath = join(args['dir'], f'debian/pybuild{tpl}.testfiles')
+                tpl = tpl.format(i=args.interpreter.name,
+                                 v=args.version,
+                                 m=args.version.major)
+                fpath = join(args.dir, f'debian/pybuild{tpl}.testfiles')
                 if exists(fpath):
                     with open(fpath, encoding='utf-8') as fp:
                         # overwrite files_to_copy if .testfiles file found
@@ -102,8 +100,8 @@ def copy_test_files(
                 if not dest_suffix or dest_suffix.endswith('/'):
                     dest_suffix = join(dest_suffix, name.rsplit('/', 1)[-1])
 
-                src_dpath = join(args['dir'], name)
-                dst_dpath = join(dest.format(**args), dest_suffix)
+                src_dpath = join(args.dir, name)
+                dst_dpath = join(args.format(dest), dest_suffix)
                 if exists(src_dpath):
                     if not exists(dst_dpath):
                         log.debug("Copying %s to %s for tests", src_dpath, dst_dpath)
@@ -113,12 +111,12 @@ def copy_test_files(
                         else:
                             copyfile(src_dpath, dst_dpath)
                         files_to_remove.add(dst_dpath + '\n')
-                    if not args['args'] and 'PYBUILD_TEST_ARGS' not in context['ENV']\
+                    if not args.args and 'PYBUILD_TEST_ARGS' not in context.ENV\
                        and (self.cfg.test_pytest or self.cfg.test_nose) \
                        and name in add_to_args:
-                        args['args'] = name
+                        args.args = name
             if files_to_remove and filelist:
-                with open(filelist.format(**args), 'a', encoding="UTF-8") as fp:
+                with open(args.format(filelist), 'a', encoding="UTF-8") as fp:
                     fp.writelines(files_to_remove)
 
             return func(self, context, args, *oargs, **kwargs)
@@ -140,6 +138,7 @@ class Base:
     :attr SUPPORTED_INTERPRETERS: set of interpreter templates (with or without
         {version}) supported by given plugin
     """
+    cfg: PybuildOptions
     NAME: ClassVar[str]
     DESCRIPTION = ''
     REQUIRED_COMMANDS: list[str] = []
@@ -154,7 +153,7 @@ class Base:
     # files and directories to remove during clean step (other than .pyc):
     CLEAN_FILES = {'.pytest_cache', '.coverage'}
 
-    def __init__(self, cfg: Namespace) -> None:
+    def __init__(self, cfg: PybuildOptions) -> None:
         self.cfg = cfg
 
     def __repr__(self) -> str:
@@ -184,7 +183,7 @@ class Base:
         for tpl in self.REQUIRED_FILES:
             found = False
             for ftpl in tpl.split('|'):
-                res = glob1(context['dir'], ftpl)
+                res = glob1(context.dir, ftpl)
                 if res:
                     found = True
                     self.DETECTED_REQUIRED_FILES.setdefault(tpl, []).extend(res)
@@ -196,7 +195,7 @@ class Base:
 
         self.DETECTED_OPTIONAL_FILES = {}
         for ftpl, score in self.OPTIONAL_FILES.items():
-            res = glob1(context['dir'], ftpl)
+            res = glob1(context.dir, ftpl)
             if res:
                 result += score
                 self.DETECTED_OPTIONAL_FILES.setdefault(ftpl, []).extend(res)
@@ -205,7 +204,7 @@ class Base:
         return result
 
     def clean(self, context: Context, args: Args) -> None:
-        tox_dir = join(args['dir'], '.tox')
+        tox_dir = join(args.dir, '.tox')
         if isdir(tox_dir):
             try:
                 rmtree(tox_dir)
@@ -213,7 +212,7 @@ class Base:
                 log.debug('cannot remove %s', tox_dir)
 
         for fn in self.CLEAN_FILES:
-            path = join(context['dir'], fn)
+            path = join(context.dir, fn)
             if isdir(path):
                 try:
                     rmtree(path)
@@ -231,7 +230,7 @@ class Base:
             'python3-setuptools-scm', 'python3-setuptools-git'
         }.intersection(set(dh.build_depends))
 
-        for root, dirs, file_names in walk(context['dir']):
+        for root, dirs, file_names in walk(context.dir):
             for name in dirs[:]:
                 if name == '__pycache__' or (
                         clean_sources_txt and name.endswith('.egg-info')):
@@ -283,11 +282,11 @@ class Base:
             return 'cd {build_dir}; {interpreter} -m pytest {args}'
         elif self.cfg.test_tox:
             tox_config = None
-            if exists(join(args['dir'], 'tox.ini')):
+            if exists(join(args.dir, 'tox.ini')):
                 tox_config = '{dir}/tox.ini'
-            elif exists(join(args['dir'], 'pyproject.toml')):
+            elif exists(join(args.dir, 'pyproject.toml')):
                 tox_config = '{dir}/pyproject.toml'
-            elif exists(join(args['dir'], 'setup.cfg')):
+            elif exists(join(args.dir, 'setup.cfg')):
                 tox_config = '{dir}/setup.cfg'
             else:
                 raise Exception("tox config not found. "
@@ -300,16 +299,16 @@ class Base:
                    '-e', 'py{version.major}{version.minor}',
                    '-x', 'testenv.passenv+=_PYTHON_HOST_PLATFORM',
             ]
-            if args['autopkgtest']:
+            if args.autopkgtest:
                 tox_cmd += ['--skip-pkg-install']
 
-            if not args['autopkgtest']:
+            if not args.autopkgtest:
                 wheel = self.built_wheel(context, args)
                 if not wheel:
                     self.build_wheel(context, args)
                     wheel = self.built_wheel(context, args)
                 assert wheel
-                args['wheel'] = wheel
+                args.wheel = wheel
                 tox_cmd += ['--installpkg', '{wheel}']
 
             tox_cmd.append('{args}')
@@ -330,7 +329,7 @@ class Base:
                 "test discovery problems.\nUse --test-unittest to explicitly "
                 "select the unittest runner.")
             # As this is the fallback option, we allow 0 discovered tests.
-            args['ignore_no_tests'] = True
+            args.ignore_no_tests = True
             return 'cd {build_dir}; {interpreter} -m unittest discover -v {args}'
 
     def build_wheel(self, context: Context, args: Args) -> None:
@@ -339,7 +338,7 @@ class Base:
     def built_wheel(self, context: Context, args: Args) -> str | None:
         """Return the path to any built wheels we can find"""
         # pylint: disable=unused-argument
-        wheels = list(Path(args['home_dir']).glob('*.whl'))
+        wheels = list(Path(args.home_dir).glob('*.whl'))
         n_wheels = len(wheels)
         if n_wheels > 1:
             raise Exception(f"Expecting to have built exactly 1 wheel, but found {n_wheels}")
@@ -379,12 +378,11 @@ class Base:
     ) -> ExecutionResult | ExecutionResultWithOutput:
         if log_file is False and self.cfg.really_quiet:
             log_file = None
-        command = command.format(**args)
-        env = dict(context['ENV'])
-        if 'ENV' in args:
-            env.update(args['ENV'])
+        command = args.format(command)
+        env = dict(context.ENV)
+        env.update(args.ENV)
         log.info(command)
-        return execute(command, cwd=context['dir'], env=env, log_output=log_file)
+        return execute(command, cwd=context.dir, env=env, log_output=log_file)
 
     def print_args(self, context: Context, args: Args) -> None:
         # pylint: disable=unused-argument
@@ -392,15 +390,15 @@ class Base:
         if len(cfg.print_args) == 1 and len(cfg.interpreter) == 1 and '{version}' not in cfg.interpreter[0]:
             i = cfg.print_args[0]
             if '{' in i:
-                print(i.format(**args))
+                print(args.format(i))
             else:
-                print(args.get(i, ''))
+                print(args.as_dict().get(i, ''))
         else:
             for i in cfg.print_args:
                 if '{' in i:
-                    print(i.format(**args))
+                    print(args.format(i))
                 else:
-                    print('{} {}: {}'.format(args['interpreter'], i, args.get(i, '')))
+                    print('{} {}: {}'.format(args.interpreter, i, args.as_dict().get(i, '')))
 
 
 def shell_command(
@@ -425,26 +423,19 @@ def shell_command(
             log.warning('missing command '
                      '(plugin=%s, method=%s, interpreter=%s, version=%s)',
                      self.NAME, func.__name__,
-                     args.get('interpreter'), args.get('version'))
+                     args.interpreter, args.version)
             return
 
         log_file: str | Literal[False]
         if self.cfg.quiet:
-            log_file = join(args['home_dir'], f'{func.__name__}_cmd.log')
+            log_file = join(args.home_dir, f'{func.__name__}_cmd.log')
         else:
             log_file = False
 
-        quoted_args: dict[str, Any] = {}
-        for k, v in args.items():
-            if k in ('dir', 'destdir') or k.endswith('_dir'):
-                assert isinstance(v, str)
-                quoted_args[k] = quote(v)
-            else:
-                quoted_args[k] = v
-        command = command.format(**quoted_args)
+        command = args.format_quoted(command)
 
         output = self.execute(context, args, command=command, log_file=log_file)
-        if output.returncode == 5 and args.get('ignore_no_tests', False):
+        if output.returncode == 5 and args.ignore_no_tests:
             # Temporary hack (see Base.test)
             pass
         elif output.returncode != 0:
@@ -502,14 +493,14 @@ def create_setuptools_parallel_cfg(
             *oargs: P.args,
             **kwargs: P.kwargs,
         ) -> T:
-            fpath = join(args['home_dir'], 'setuptools-build_ext-parallel.cfg')
+            fpath = join(args.home_dir, 'setuptools-build_ext-parallel.cfg')
             if not exists(fpath):
                 with open(fpath, 'w', encoding='utf-8') as fp:
                     lines = ['[build_ext]\n',
                              f'parallel={get_parallel()}\n']
                     log.debug('parallel build extension config file:\n%s', ''.join(lines))
                     fp.writelines(lines)
-            context['ENV']['DIST_EXTRA_CONFIG'] = fpath
+            context.ENV['DIST_EXTRA_CONFIG'] = fpath
             return func(self, context, args, *oargs, **kwargs)
 
         if 'DIST_EXTRA_CONFIG' in environ or (
diff -pruN 7.20251225/dhpython/build/options.py 7.20251227/dhpython/build/options.py
--- 7.20251225/dhpython/build/options.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.20251227/dhpython/build/options.py	2025-12-28 00:40:29.000000000 +0000
@@ -0,0 +1,353 @@
+# Copyright © 2025 Stefano Rivera <stefanor@debian.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from argparse import ArgumentParser, Action, Namespace
+from collections.abc import Sequence
+from dataclasses import dataclass, field
+from typing import Any, override
+from os import environ, getcwd
+
+from dhpython.version import Version
+
+DEFAULT_EXT_PATTERN = r"\.so(\.[^/]*)?$"
+DEFAULT_INSTALL_DIR = "/usr/lib/python{version}/dist-packages"
+
+@dataclass
+class PybuildOptions:
+    verbose: bool = environ.get("PYBUILD_VERBOSE") == "1"
+    quiet: bool = environ.get("PYBUILD_QUIET") == "1"
+    really_quiet: bool = environ.get("PYBUILD_RQUIET") == "1"
+    detect_only: bool = False
+    clean_only: bool = False
+    configure_only: bool = False
+    build_only: bool = False
+    install_only: bool = False
+    test_only: bool = False
+    autopkgtest_only: bool = False
+    list_systems: bool = False
+    print_args: list[str] = field(default_factory=list)
+    before_clean: str | None = None
+    clean_args: str | None = None
+    after_clean: str | None = None
+    before_configure: str | None = None
+    configure_args: str | None = None
+    after_configure: str | None = None
+    before_build: str | None = None
+    build_args: str | None = None
+    after_build: str | None = None
+    before_install: str | None = None
+    install_args: str | None = None
+    after_install: str | None = None
+    before_test: str | None = None
+    test_args: str | None = None
+    after_test: str | None = None
+    test_nose: bool = environ.get("PYBUILD_TEST_NOSE") == "1"
+    test_nose2: bool = environ.get("PYBUILD_TEST_NOSE2") == "1"
+    test_pytest: bool = environ.get("PYBUILD_TEST_PYTEST") == "1"
+    test_tox: bool = environ.get("PYBUILD_TEST_TOX") == "1"
+    test_stestr: bool = environ.get("PYBUILD_TEST_STESTR") == "1"
+    test_unittest: bool = environ.get("PYBUILD_TEST_UNITTEST") == "1"
+    test_custom: bool = environ.get("PYBUILD_TEST_CUSTOM") == "1"
+    dir: str = getcwd()
+    # We handle this default manually, because --name affects it
+    destdir: str | None = None
+    ext_destdir: str | None = environ.get("PYBUILD_EXT_DESTDIR")
+    ext_pattern: str = environ.get("PYBUILD_EXT_PATTERN", DEFAULT_EXT_PATTERN)
+    ext_sub_pattern: str | None = environ.get("PYBUILD_EXT_SUB_PATTERN")
+    ext_sub_repl: str | None = environ.get("PYBUILD_EXT_SUB_REPL")
+    install_dir: str = DEFAULT_INSTALL_DIR
+    name: str | None = environ.get("PYBUILD_NAME")
+    system: str | None = environ.get("PYBUILD_SYSTEM")
+    versions: list[Version] = field(default_factory=list)
+    interpreter: list[str] = field(default_factory=list)
+    disable: str | None = None
+    custom_tests: bool = False
+
+
+class AppendVersions(Action):
+    """
+    Append possibly-space-separated versions.
+
+    This allows the use of -p "$(py3versions -rv)".
+    """
+
+    @override
+    def __call__(
+        self,
+        parser: ArgumentParser,
+        namespace: Namespace,
+        values: str | Sequence[Any] | None,
+        option_string: str | None = None,
+    ) -> None:
+        assert isinstance(values, str)
+        for value in values.split():
+            getattr(namespace, self.dest).append(Version(value))
+
+
+def build_parser() -> ArgumentParser:
+    usage = "%(prog)s [ACTION] [BUILD SYSTEM ARGS] [DIRECTORIES] [OPTIONS]"
+    parser = ArgumentParser(usage=usage)
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        help="turn verbose mode on",
+    )
+    parser.add_argument(
+        "-q",
+        "--quiet",
+        action="store_true",
+        help="doesn't show external command's output",
+    )
+    parser.add_argument(
+        "-qq",
+        "--really-quiet",
+        action="store_true",
+        help="be quiet",
+    )
+    parser.add_argument("--version", action="version", version="%(prog)s DEVELV")
+
+    action = parser.add_argument_group(
+        "ACTION",
+        """The default is to build,
+        install and test the library using detected build system version by
+        version. Selecting one of following actions, will invoke given action
+        for all versions - one by one - which (contrary to the default action)
+        in some build systems can overwrite previous results.""",
+    )
+    action.add_argument(
+        "--detect",
+        action="store_true",
+        dest="detect_only",
+        help="return the name of detected build system",
+    )
+    action.add_argument(
+        "--clean",
+        action="store_true",
+        dest="clean_only",
+        help="clean files using auto-detected build system specific methods",
+    )
+    action.add_argument(
+        "--configure",
+        action="store_true",
+        dest="configure_only",
+        help="invoke configure step for all requested Python versions",
+    )
+    action.add_argument(
+        "--build",
+        action="store_true",
+        dest="build_only",
+        help="invoke build step for all requested Python versions",
+    )
+    action.add_argument(
+        "--install",
+        action="store_true",
+        dest="install_only",
+        help="invoke install step for all requested Python versions",
+    )
+    action.add_argument(
+        "--test",
+        action="store_true",
+        dest="test_only",
+        help="invoke tests for auto-detected build system",
+    )
+    action.add_argument(
+        "--autopkgtest",
+        action="store_true",
+        dest="autopkgtest_only",
+        help="invoke autopkgtests for auto-detected build system",
+    )
+    action.add_argument(
+        "--list-systems",
+        action="store_true",
+        help="list available build systems and exit",
+    )
+    action.add_argument(
+        "--print",
+        action="append",
+        dest="print_args",
+        help="print pybuild's internal parameters",
+    )
+
+    arguments = parser.add_argument_group(
+        "BUILD SYSTEM ARGS",
+        """
+        Additional arguments passed to the build system.
+        --system=custom requires complete command.""",
+    )
+    arguments.add_argument(
+        "--before-clean", metavar="CMD", help="invoked before the clean command"
+    )
+    arguments.add_argument("--clean-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-clean", metavar="CMD", help="invoked after the clean command"
+    )
+
+    arguments.add_argument(
+        "--before-configure", metavar="CMD", help="invoked before the configure command"
+    )
+    arguments.add_argument("--configure-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-configure", metavar="CMD", help="invoked after the configure command"
+    )
+
+    arguments.add_argument(
+        "--before-build", metavar="CMD", help="invoked before the build command"
+    )
+    arguments.add_argument("--build-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-build", metavar="CMD", help="invoked after the build command"
+    )
+
+    arguments.add_argument(
+        "--before-install", metavar="CMD", help="invoked before the install command"
+    )
+    arguments.add_argument("--install-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-install", metavar="CMD", help="invoked after the install command"
+    )
+
+    arguments.add_argument(
+        "--before-test", metavar="CMD", help="invoked before the test command"
+    )
+    arguments.add_argument("--test-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-test", metavar="CMD", help="invoked after the test command"
+    )
+
+    tests = parser.add_argument_group(
+        "TESTS",
+        """\
+        unittest\'s discover is used by default (if available)""",
+    )
+    tests.add_argument(
+        "--test-nose",
+        action="store_true",
+        help="use nose module in --test step",
+    )
+    tests.add_argument(
+        "--test-nose2",
+        action="store_true",
+        help="use nose2 module in --test step",
+    )
+    tests.add_argument(
+        "--test-pytest",
+        action="store_true",
+        help="use pytest module in --test step",
+    )
+    tests.add_argument(
+        "--test-tox",
+        action="store_true",
+        help="use tox in --test step",
+    )
+    tests.add_argument(
+        "--test-stestr",
+        action="store_true",
+        help="use stestr in --test step",
+    )
+    tests.add_argument(
+        "--test-unittest",
+        action="store_true",
+        help="use unittest in --test step",
+    )
+    tests.add_argument(
+        "--test-custom",
+        action="store_true",
+        help="use custom command in --test step",
+    )
+
+    dirs = parser.add_argument_group("DIRECTORIES")
+    dirs.add_argument(
+        "-d",
+        "--dir",
+        action="store",
+        metavar="DIR",
+        help="source files directory - base for other relative dirs [default: CWD]",
+    )
+    dirs.add_argument(
+        "--dest-dir",
+        action="store",
+        metavar="DIR",
+        dest="destdir",
+        help="destination directory [default: debian/tmp]",
+    )
+    dirs.add_argument(
+        "--ext-dest-dir",
+        action="store",
+        metavar="DIR",
+        dest="ext_destdir",
+        help="destination directory for .so files",
+    )
+    dirs.add_argument(
+        "--ext-pattern",
+        action="store",
+        metavar="PATTERN",
+        help="regular expression for files that should be moved"
+        " if --ext-dest-dir is set [default: .so files]",
+    )
+    dirs.add_argument(
+        "--ext-sub-pattern",
+        action="store",
+        metavar="PATTERN",
+        help="pattern to change --ext-pattern's filename or path",
+    )
+    dirs.add_argument(
+        "--ext-sub-repl",
+        action="store",
+        metavar="PATTERN",
+        help="replacement for match from --ext-sub-pattern," " empty string by default",
+    )
+    dirs.add_argument(
+        "--install-dir",
+        action="store",
+        metavar="DIR",
+        help="installation directory [default: .../dist-packages]",
+    )
+    dirs.add_argument(
+        "--name",
+        action="store",
+        help="use this name to guess destination directories",
+    )
+
+    limit = parser.add_argument_group("LIMITATIONS")
+    limit.add_argument(
+        "-s",
+        "--system",
+        help="select a build system [default: auto-detection]",
+    )
+    limit.add_argument(
+        "-p",
+        "--pyver",
+        action=AppendVersions,
+        dest="versions",
+        help="""build for Python VERSION.
+                               This option can be used multiple times
+                               [default: all supported Python 3.X versions]""",
+    )
+    limit.add_argument(
+        "-i",
+        "--interpreter",
+        action="append",
+        help="change interpreter [default: python{version}]",
+    )
+    limit.add_argument(
+        "--disable", metavar="ITEMS", help="disable action, interpreter or version"
+    )
+    return parser
diff -pruN 7.20251225/dhpython/build/plugin_custom.py 7.20251227/dhpython/build/plugin_custom.py
--- 7.20251225/dhpython/build/plugin_custom.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/build/plugin_custom.py	2025-12-28 00:40:29.000000000 +0000
@@ -32,25 +32,25 @@ class BuildSystem(Base):
     @override
     def clean(self, context: Context, args: Args) -> str:
         super().clean(context, args)
-        return args['args']
+        return args.args
 
     @shell_command
     @override
     def configure(self, context: Context, args: Args) -> str:
-        return args['args']
+        return args.args
 
     @shell_command
     @override
     def build(self, context: Context, args: Args) -> str:
-        return args['args']
+        return args.args
 
     @shell_command
     @override
     def install(self, context: Context, args: Args) -> str:
-        return args['args']
+        return args.args
 
     @copy_test_files()
     @shell_command
     @override
     def test(self, context: Context, args: Args) -> str:
-        return args['args'] or super().test_cmd(context, args)
+        return args.args or super().test_cmd(context, args)
diff -pruN 7.20251225/dhpython/build/plugin_distutils.py 7.20251227/dhpython/build/plugin_distutils.py
--- 7.20251225/dhpython/build/plugin_distutils.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/build/plugin_distutils.py	2025-12-28 00:40:29.000000000 +0000
@@ -66,22 +66,22 @@ def create_pydistutils_cfg(
         *oargs: P.args,
         **kwargs: P.kwargs
     ) -> T:
-        fpath = join(args['home_dir'], '.pydistutils.cfg')
+        fpath = join(args.home_dir, '.pydistutils.cfg')
         if not exists(fpath):
             with open(fpath, 'w', encoding='utf-8') as fp:
                 lines = ['[clean]\n',
                          'all=1\n',
                          '[build]\n',
-                         'build_lib={}\n'.format(args['build_dir']),
+                         'build_lib={}\n'.format(args.build_dir),
                          '[install]\n',
                          'force=1\n',
                          'install_layout=deb\n',
                          'install_scripts=$base/bin\n',
-                         'install_lib={}\n'.format(args['install_dir']),
+                         'install_lib={}\n'.format(args.install_dir),
                          'prefix=/usr\n']
                 log.debug('pydistutils config file:\n%s', ''.join(lines))
                 fp.writelines(lines)
-        context['ENV']['HOME'] = args['home_dir']
+        context.ENV['HOME'] = args.home_dir
         return func(self, context, args, *oargs, **kwargs)
 
     return wrapped_func
@@ -102,9 +102,9 @@ class BuildSystem(Base):
     def detect(self, context: Context) -> int:
         result = super().detect(context)
         if _setup_tpl in self.DETECTED_REQUIRED_FILES:
-            context['args']['setup_py'] = self.DETECTED_REQUIRED_FILES[_setup_tpl][0]
+            context.args['setup_py'] = self.DETECTED_REQUIRED_FILES[_setup_tpl][0]
         else:
-            context['args']['setup_py'] = 'setup.py'
+            context.args['setup_py'] = 'setup.py'
         return result
 
     @shell_command
@@ -112,7 +112,7 @@ class BuildSystem(Base):
     @override
     def clean(self, context: Context, args: Args) -> str | Literal[0]:
         super().clean(context, args)
-        if exists(args['interpreter'].binary()):
+        if exists(args.interpreter.binary()):
             return '{interpreter} {setup_py} clean {args}'
         return 0  # no need to invoke anything
 
@@ -138,27 +138,27 @@ class BuildSystem(Base):
         except ImportError:
             raise Exception("wheel is required to build wheels for distutils/setuptools packages. Build-Depend on python3-wheel.")
         # Don't build a wheel in the deb install layout
-        fpath = join(args['home_dir'], '.pydistutils.cfg')
+        fpath = join(args.home_dir, '.pydistutils.cfg')
         remove(fpath)
         return '{interpreter.binary_dv} -c "import setuptools, runpy; runpy.run_path(\'{setup_py}\')" bdist_wheel {args}'
 
     @override
     def build_wheel(self, context: Context, args: Args) -> None:
         self._bdist_wheel(context, args)
-        dist_dir = join(args['dir'], 'dist')
+        dist_dir = join(args.dir, 'dist')
         wheels = glob1(dist_dir, '*.whl')
         n_wheels = len(wheels)
         if n_wheels != 1:
             raise Exception(f"Expected 1 wheel, found {n_wheels}")
-        move(join(dist_dir, wheels[0]), args['home_dir'])
+        move(join(dist_dir, wheels[0]), args.home_dir)
 
     @shell_command
     @create_pydistutils_cfg
     @override
     def install(self, context: Context, args: Args) -> str:
         # remove egg-info dirs from build_dir
-        for fname in glob1(args['build_dir'], '*.egg-info'):
-            fpath = join(args['build_dir'], fname)
+        for fname in glob1(args.build_dir, '*.egg-info'):
+            fpath = join(args.build_dir, fname)
             if isdir(fpath):
                 rmtree(fpath)
             else:
diff -pruN 7.20251225/dhpython/build/plugin_meson.py 7.20251227/dhpython/build/plugin_meson.py
--- 7.20251225/dhpython/build/plugin_meson.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/build/plugin_meson.py	2025-12-28 00:40:29.000000000 +0000
@@ -41,9 +41,9 @@ class BuildSystem(Base):
     def configure(self, context: Context, args: Args) -> str:
         # Can't be specified on the command line, directly
         # https://github.com/mesonbuild/meson/issues/9671
-        with open(join(args['build_dir'], 'pybuild-meson-native.ini'), 'w', encoding="UTF-8") as f:
+        with open(join(args.build_dir, 'pybuild-meson-native.ini'), 'w', encoding="UTF-8") as f:
             f.write('[binaries]\n')
-            f.write("python3 = '" + args['interpreter'].binary_dv + "'\n")
+            f.write(f"python3 = '{args.interpreter.binary_dv}'\n")
 
         return ('dh_auto_configure --buildsystem=meson'
                 ' --builddirectory={build_dir} --'
diff -pruN 7.20251225/dhpython/build/plugin_pyproject.py 7.20251227/dhpython/build/plugin_pyproject.py
--- 7.20251225/dhpython/build/plugin_pyproject.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/build/plugin_pyproject.py	2025-12-28 00:40:29.000000000 +0000
@@ -96,11 +96,10 @@ class BuildSystem(Base):
     @override
     def clean(self, context: Context, args: Args) -> None:
         super().clean(context, args)
-        if osp.exists(args['interpreter'].binary()):
-            log.debug("removing '%s' (and everything under it)",
-                      args['build_dir'])
-            if osp.isdir(args['build_dir']):
-                shutil.rmtree(args['build_dir'])
+        if osp.exists(args.interpreter.binary()):
+            log.debug("removing '%s' (and everything under it)", args.build_dir)
+            if osp.isdir(args.build_dir):
+                shutil.rmtree(args.build_dir)
 
     @override
     def configure(self, context: Context, args: Args) -> None:
@@ -121,7 +120,7 @@ class BuildSystem(Base):
             return [
                 # build_dir is where we unpack the wheel, so we need to build
                 # elsewhere.
-                f"build-dir={args['home_dir']}/meson-build",
+                f"build-dir={args.home_dir}/meson-build",
                 "compile-args=--verbose",
                 # From Debhelper's meson.pm
                 "setup-args=--wrap-mode=nodownload",
@@ -137,14 +136,13 @@ class BuildSystem(Base):
     @override
     def build_wheel(self, context: Context, args: Args) -> str:
         """ build a wheel using the PEP517 builder defined by upstream """
-        log.info('Building wheel for %s with "build" module',
-                 args['interpreter'])
+        log.info('Building wheel for %s with "build" module', args.interpreter)
         config_settings = self._backend_config_settings(args)
-        context['ENV']['FLIT_NO_NETWORK'] = '1'
-        context['ENV']['HOME'] = args['home_dir']
+        context.ENV['FLIT_NO_NETWORK'] = '1'
+        context.ENV['HOME'] = args.home_dir
         return ('{interpreter} -m build '
                 '--skip-dependency-check --no-isolation --wheel '
-                '--outdir ' + args['home_dir'] + ' ' +
+                '--outdir ' + args.home_dir + ' ' +
                 ' '.join(('--config-setting ' + setting)
                          for setting in config_settings) +
                 ' {args}'
@@ -164,11 +162,10 @@ class BuildSystem(Base):
 
     def unpack_wheel(self, context: Context, args: Args) -> None:
         """ unpack the wheel into pybuild's normal  """
-        log.info('Unpacking wheel built for %s with "installer" module',
-                 args['interpreter'])
+        log.info('Unpacking wheel built for %s with "installer" module', args.interpreter)
         extras = {}
         for extra in ('scripts', 'data', 'include'):
-            path = Path(args["home_dir"]) / extra
+            path = Path(args.home_dir) / extra
             if path.exists():
                 log.warning('%s directory already exists, skipping unpack. '
                             'Is the Python package being built twice?',
@@ -177,13 +174,13 @@ class BuildSystem(Base):
             extras[extra] = str(path)
         destination = SchemeDictionaryDestination(
             {
-                'platlib': args['build_dir'],
-                'purelib': args['build_dir'],
-                'scripts': extras['scripts'],
-                'data': extras['data'],
-                'headers': extras['include'],
+                'platlib': args.build_dir,
+                'purelib': args.build_dir,
+                'scripts': extras["scripts"],
+                'data': extras["data"],
+                'headers': extras["include"],
             },
-            interpreter=args['interpreter'].binary_dv,
+            interpreter=args.interpreter.binary_dv,
             script_kind='posix',
         )
 
@@ -196,8 +193,7 @@ class BuildSystem(Base):
 
     @override
     def install(self, context: Context, args: Args) -> None:
-        log.info('Copying package built for %s to destdir',
-                 args['interpreter'])
+        log.info('Copying package built for %s to destdir', args.interpreter)
         try:
             paths = sysconfig.get_paths(scheme='deb_system')
         except KeyError:
@@ -207,7 +203,7 @@ class BuildSystem(Base):
 
         # start by copying the data, scripts, and headers
         for extra in ('data', 'scripts', 'include'):
-            src_dir = Path(args['home_dir']) / extra
+            src_dir = Path(args.home_dir) / extra
             if not src_dir.exists():
                 continue
             if extra == 'include':
@@ -216,11 +212,11 @@ class BuildSystem(Base):
                         source.read_dist_info("METADATA")
                     )
                     extra_path = osp.join(
-                        args['interpreter'].include_dir, metadata["Name"]
+                        args.interpreter.include_dir, metadata["Name"]
                     )
             else:
                 extra_path = paths[extra]
-            target_dir = args['destdir'] + extra_path
+            target_dir = args.destdir + extra_path
             log.debug('Copying %s directory contents from %s -> %s',
                       extra, src_dir, target_dir)
             shutil.copytree(
@@ -230,8 +226,8 @@ class BuildSystem(Base):
             )
 
         # then copy the modules
-        module_dir = args['build_dir']
-        target_dir = args['destdir'] + args['install_dir']
+        module_dir = args.build_dir
+        target_dir = args.destdir + args.install_dir
         log.debug('Copying module contents from %s -> %s',
                   module_dir, target_dir)
         shutil.copytree(
@@ -243,8 +239,8 @@ class BuildSystem(Base):
     @shell_command
     @override
     def test(self, context: Context, args: Args) -> str:
-        scripts = Path(args["home_dir"]) / 'scripts'
+        scripts = Path(args.home_dir) / 'scripts'
         if scripts.exists():
-            context['ENV']['PATH'] = f"{scripts}:{context['ENV']['PATH']}"
-        context['ENV']['HOME'] = args['home_dir']
+            context.ENV['PATH'] = f"{scripts}:{context.ENV['PATH']}"
+        context.ENV['HOME'] = args.home_dir
         return super().test_cmd(context, args)
diff -pruN 7.20251225/dhpython/build/types.py 7.20251227/dhpython/build/types.py
--- 7.20251225/dhpython/build/types.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/build/types.py	2025-12-28 00:40:29.000000000 +0000
@@ -18,13 +18,16 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
-from typing import NotRequired, TypedDict
+from dataclasses import asdict, dataclass, field, replace
+from typing import Any
+from shlex import quote
 
 from dhpython.interpreter import Interpreter
 from dhpython.version import Version
 
 
-class Args(TypedDict):
+@dataclass
+class Args:
     ENV: dict[str, str]
     args: str
     autopkgtest: bool
@@ -37,16 +40,34 @@ class Args(TypedDict):
     package: str
     version: Version
     # during tests only:
-    test_dir: NotRequired[str]
+    test_dir: str | None = None
     # Injected by base:
-    ignore_no_tests: NotRequired[bool]
-    wheel: NotRequired[str]
+    ignore_no_tests: bool = False
+    wheel: str | None = None
     # Injected by plugin_distutils:
-    setup_py: NotRequired[str]
+    setup_py: str | None = None
 
+    def format(self, template: str) -> str:
+        return template.format(**self.as_dict())
 
-class Context(TypedDict):
+    def format_quoted(self, template: str) -> str:
+        quoted = self.as_dict()
+        for k, v in quoted.items():
+            if v and (k in ('dir', 'destdir') or k.endswith('_dir')):
+                assert isinstance(v, str)
+                quoted[k] = quote(v)
+        return template.format(**quoted)
+
+    def as_dict(self) -> dict[str, Any]:
+        return asdict(self)
+
+
+@dataclass
+class Context:
     ENV: dict[str, str]
-    args: Args
     dir: str
     destdir: str
+    args: dict[str, str] = field(default_factory=dict)
+
+    def copy(self) -> "Context":
+        return replace(self)
diff -pruN 7.20251225/dhpython/debhelper.py 7.20251227/dhpython/debhelper.py
--- 7.20251225/dhpython/debhelper.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/debhelper.py	2025-12-28 00:40:29.000000000 +0000
@@ -21,10 +21,11 @@
 import errno
 import logging
 import re
+from dataclasses import dataclass
 from os import makedirs, chmod, environ
 from os.path import basename, exists, join, dirname
 from sys import argv
-from typing import NamedTuple, TypeAlias, TypedDict
+from typing import NamedTuple, TypeAlias
 
 from dhpython import DEPENDS_SUBSTVARS, PKG_NAME_TPLS, RT_LOCATIONS, RT_TPLS
 
@@ -42,7 +43,8 @@ parse_dep = re.compile(
 ).match
 
 
-class PackageData(TypedDict):
+@dataclass
+class PackageData:
     substvars: dict[str, list[str]]
     autoscripts: dict[str, dict[str, list[str]]]
     rtupdates: list[tuple[str, str]]
@@ -195,9 +197,9 @@ class DebHelper:
             )
             if (
                 options.arch is False
-                and pkg["arch"] != "all"
+                and pkg.arch != "all"
                 or options.arch is True
-                and pkg["arch"] == "all"
+                and pkg.arch == "all"
             ):
                 # TODO: check also if arch matches current architecture:
                 continue
@@ -238,20 +240,20 @@ class DebHelper:
 
     def addsubstvar(self, package: str, name: str, value: str) -> None:
         """debhelper's addsubstvar"""
-        self.packages[package]["substvars"].setdefault(name, []).append(value)
+        self.packages[package].substvars.setdefault(name, []).append(value)
 
     def autoscript(self, package: str, when: str, template: str, args: str) -> None:
         """debhelper's autoscript"""
-        self.packages[package]["autoscripts"].setdefault(when, {}).setdefault(
+        self.packages[package].autoscripts.setdefault(when, {}).setdefault(
             template, []
         ).append(args)
 
     def add_rtupdate(self, package: str, value: tuple[str, str]) -> None:
-        self.packages[package]["rtupdates"].append(value)
+        self.packages[package].rtupdates.append(value)
 
     def save_autoscripts(self) -> None:
         for package, settings in self.packages.items():
-            autoscripts = settings.get("autoscripts")
+            autoscripts = settings.autoscripts
             if not autoscripts:
                 continue
 
@@ -277,7 +279,7 @@ class DebHelper:
                         if self.options.compile_all and args:
                             # TODO: should args be checked to contain dir name?
                             tpl = tpl.replace("-p #PACKAGE#", "")
-                        elif settings["arch"] == "all":
+                        elif settings.arch == "all":
                             tpl = tpl.replace("#PACKAGE#", package)
                         else:
                             arch = environ["DEB_HOST_ARCH"]
@@ -295,7 +297,7 @@ class DebHelper:
 
     def save_substvars(self) -> None:
         for package, settings in self.packages.items():
-            substvars = settings.get("substvars")
+            substvars = settings.substvars
             if not substvars:
                 continue
             fn = "debian/%s.substvars" % package
@@ -331,7 +333,7 @@ class DebHelper:
     def save_rtupdate(self) -> None:
         for package, settings in self.packages.items():
             pkg_arg = "" if self.options.compile_all else "-p %s" % package
-            values = settings.get("rtupdates")
+            values = settings.rtupdates
             if not values:
                 continue
             d = f"debian/{package}/{RT_LOCATIONS[self.impl]}"
diff -pruN 7.20251225/dhpython/depends.py 7.20251227/dhpython/depends.py
--- 7.20251225/dhpython/depends.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/depends.py	2025-12-28 00:40:29.000000000 +0000
@@ -19,13 +19,13 @@
 # THE SOFTWARE.
 
 import logging
-from argparse import Namespace
 from functools import partial
 from os.path import exists, join
 
 from dhpython import PKG_PREFIX_MAP, MINPYCDEP
 from dhpython.fs import ScanResult
 from dhpython.debhelper import BD, DebHelper
+from dhpython.options import DHPythonOptions
 from dhpython.pydist import (
     NewDependencies,
     guess_dependency,
@@ -144,7 +144,7 @@ class Dependencies:
         for dep in new_dependencies["suggests"]:
             self.suggest(dep)
 
-    def parse(self, stats: ScanResult, options: Namespace) -> None:
+    def parse(self, stats: ScanResult, options: DHPythonOptions) -> None:
         log.debug("generating dependencies for package %s", self.package)
         tpl = self.ipkg_tpl
         vtpl = self.ipkg_vtpl
@@ -261,11 +261,6 @@ class Dependencies:
                     args += " -X '%s'" % regex.pattern.replace("'", r"'\''")
                 self.rtscript((private_dir, args))
 
-        section_options = {
-            "depends_sec": options.depends_section,
-            "recommends_sec": options.recommends_section,
-            "suggests_sec": options.suggests_section,
-        }
         guess_deps = partial(
             guess_dependency,
             impl=self.impl,
@@ -277,7 +272,14 @@ class Dependencies:
                 # TODO: should options.recommends and options.suggests be
                 # removed from requires.txt?
                 self.add_new_dependencies(
-                    parse_pydep(self.impl, fn, bdep=self.bdep, **section_options)
+                    parse_pydep(
+                        self.impl,
+                        fn,
+                        bdep=self.bdep,
+                        depends_sec=options.depends_section,
+                        recommends_sec=options.recommends_section,
+                        suggests_sec=options.suggests_section,
+                    )
                 )
             for fpath in stats["egg-info"]:
                 with open(fpath, encoding="utf-8") as fp:
@@ -288,7 +290,12 @@ class Dependencies:
             for fpath in stats["dist-info"]:
                 self.add_new_dependencies(
                     parse_requires_dist(
-                        self.impl, fpath, bdep=self.bdep, **section_options
+                        self.impl,
+                        fpath,
+                        bdep=self.bdep,
+                        depends_sec=options.depends_section,
+                        recommends_sec=options.recommends_section,
+                        suggests_sec=options.suggests_section,
                     )
                 )
 
@@ -310,7 +317,14 @@ class Dependencies:
                     log.warning("cannot find requirements file: %s", fn)
                     continue
             self.add_new_dependencies(
-                parse_pydep(self.impl, fpath, bdep=self.bdep, **section_options)
+                parse_pydep(
+                    self.impl,
+                    fpath,
+                    bdep=self.bdep,
+                    depends_sec=options.depends_section,
+                    recommends_sec=options.recommends_section,
+                    suggests_sec=options.suggests_section,
+                )
             )
 
         log.debug(self)
diff -pruN 7.20251225/dhpython/fs.py 7.20251227/dhpython/fs.py
--- 7.20251225/dhpython/fs.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/fs.py	2025-12-28 00:40:29.000000000 +0000
@@ -23,7 +23,6 @@ import logging
 import os
 import re
 import sys
-from argparse import Namespace
 from collections.abc import Iterable, Sequence
 from filecmp import cmp as cmpfile
 from os.path import lexists, exists, isdir, islink, join, realpath, split, splitext
@@ -35,6 +34,7 @@ from typing import Literal, TypedDict
 from dhpython import MULTIARCH_DIR_TPL
 from dhpython.tools import fix_shebang, clean_egg_name
 from dhpython.interpreter import EXTFILE_RE, Interpreter
+from dhpython.options import DHPythonOptions
 from dhpython.version import Version
 
 log = logging.getLogger("dhpython")
@@ -44,7 +44,7 @@ def fix_locations(
     package: str,
     interpreter: Interpreter,
     versions: Sequence[Version],
-    options: Namespace,
+    options: DHPythonOptions,
 ) -> None:
     """Move files to the right location."""
     # make a copy since we change version later
@@ -80,7 +80,7 @@ def share_files(
     srcdir: str,
     dstdir: str,
     interpreter: Interpreter,
-    options: Namespace,
+    options: DHPythonOptions,
 ) -> None:
     """Try to move as many files from srcdir to dstdir as possible."""
     for i in os.listdir(srcdir):
@@ -238,7 +238,7 @@ class Scan:
     package: str
     proot: str
     dpath: str | None | Literal[False]
-    options: Namespace
+    options: DHPythonOptions
     result: ScanResult
 
     def __init__(
@@ -247,7 +247,7 @@ class Scan:
         package: str,
         dpath: str | None = None,
         *,
-        options: Namespace,
+        options: DHPythonOptions,
     ) -> None:
         self.interpreter = interpreter
         self.impl = interpreter.impl
diff -pruN 7.20251225/dhpython/option.py 7.20251227/dhpython/option.py
--- 7.20251225/dhpython/option.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/option.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,29 +0,0 @@
-# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-import re
-
-
-def compiled_regex(string: str) -> re.Pattern[str]:
-    """argparse regex type"""
-    try:
-        return re.compile(string)
-    except re.error:
-        raise ValueError("regular expression is not valid")
diff -pruN 7.20251225/dhpython/options.py 7.20251227/dhpython/options.py
--- 7.20251225/dhpython/options.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.20251227/dhpython/options.py	2025-12-28 00:40:29.000000000 +0000
@@ -0,0 +1,235 @@
+# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import re
+from argparse import ArgumentParser, SUPPRESS
+from dataclasses import dataclass, field
+from os import environ
+
+from dhpython.version import VersionRange
+
+
+@dataclass
+class DHPythonOptions:
+    O: list[str] = field(default_factory=list)
+    accept_upstream_versions: bool = False
+    arch: bool | None = None
+    clean_dbg_pkg: bool = True
+    compile_all: bool = False
+    depends: list[str] = field(default_factory=list)
+    depends_section: list[str] = field(default_factory=list)
+    guess_deps: bool = True
+    ignore_shebangs: bool = False
+    no_ext_rename: bool = False
+    no_package: list[str] = field(default_factory=list)
+    no_shebang_rewrite: bool = False
+    package: list[str] = field(default_factory=list)
+    private_dir: str | None = None
+    recommends: list[str] = field(default_factory=list)
+    recommends_section: list[str] = field(default_factory=list)
+    regexpr: list[re.Pattern[str]] = field(default_factory=list)
+    remaining_packages: bool = False
+    requires: list[str] = field(default_factory=list)
+    shebang: str | None = None
+    skip_private: bool = False
+    suggests: list[str] = field(default_factory=list)
+    suggests_section: list[str] = field(default_factory=list)
+    verbose: bool = environ.get("DH_VERBOSE") == "1"
+    vrange: VersionRange | None = None
+    write_log: bool = False
+
+
+def compiled_regex(string: str) -> re.Pattern[str]:
+    """argparse regex type"""
+    try:
+        return re.compile(string)
+    except re.error:
+        raise ValueError("regular expression is not valid")
+
+
+def build_parser() -> ArgumentParser:
+    parser = ArgumentParser()
+    parser.add_argument("--version", action="version", version="%(prog)s DEVELV")
+    parser.add_argument(
+        "--no-guessing-deps",
+        action="store_false",
+        dest="guess_deps",
+        help="disable guessing dependencies",
+    )
+    parser.add_argument(
+        "--skip-private", action="store_true", help="don't check private directories"
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        help="turn verbose mode on",
+    )
+    # arch=False->arch:all only, arch=True->arch:any only, None->all of them
+    parser.add_argument(
+        "-i",
+        "--indep",
+        action="store_false",
+        dest="arch",
+        default=None,
+        help="act on architecture independent packages",
+    )
+    parser.add_argument(
+        "-a",
+        "-s",
+        "--arch",
+        action="store_true",
+        dest="arch",
+        help="act on architecture dependent packages",
+    )
+    parser.add_argument(
+        "-q", "--quiet", action="store_false", dest="verbose", help="be quiet"
+    )
+    parser.add_argument(
+        "-p",
+        "--package",
+        action="append",
+        metavar="PACKAGE",
+        help="act on the package named PACKAGE",
+    )
+    parser.add_argument(
+        "-N",
+        "--no-package",
+        action="append",
+        metavar="PACKAGE",
+        help="do not act on the specified package",
+    )
+    parser.add_argument(
+        "--remaining-packages",
+        action="store_true",
+        help="Do not act on the packages which have already been acted on by "
+        "this debhelper command earlier",
+    )
+    parser.add_argument(
+        "--compile-all",
+        action="store_true",
+        help="compile all files from given private directory in postinst, not "
+        "just the ones provided by the package",
+    )
+    parser.add_argument(
+        "-V",
+        type=VersionRange,
+        dest="vrange",
+        metavar="[X.Y][-][A.B]",
+        help="specify list of supported Python versions. See py3compile(1) for "
+        "examples",
+    )
+    parser.add_argument(
+        "-X",
+        "--exclude",
+        action="append",
+        dest="regexpr",
+        type=compiled_regex,
+        metavar="REGEXPR",
+        help="exclude .py files that match given REGEXPR from "
+        "byte-compilation, in a private dir. You may use this option "
+        "multiple times to build up a list of things to exclude.",
+    )
+    parser.add_argument(
+        "--accept-upstream-versions",
+        action="store_true",
+        help="accept upstream versions while translating Python dependencies "
+        "into Debian ones",
+    )
+    parser.add_argument(
+        "--depends",
+        action="append",
+        metavar="REQ",
+        help="translate given requirements into Debian dependencies and add "
+        "them to ${python3:Depends}. Use it for missing items in "
+        "requires.txt.",
+    )
+    parser.add_argument(
+        "--depends-section",
+        action="append",
+        metavar="SECTION",
+        help="translate requirements from given section into Debian "
+        "dependencies and add them to ${python3:Depends}",
+    )
+    parser.add_argument(
+        "--recommends",
+        action="append",
+        metavar="REQ",
+        help="translate given requirements into Debian dependencies and add "
+        "them to ${python3:Recommends}",
+    )
+    parser.add_argument(
+        "--recommends-section",
+        action="append",
+        metavar="SECTION",
+        help="translate requirements from given section into Debian "
+        "dependencies and add them to ${python3:Recommends}",
+    )
+    parser.add_argument(
+        "--suggests",
+        action="append",
+        metavar="REQ",
+        help="translate given requirements into Debian dependencies and add "
+        "them to ${python3:Suggests}",
+    )
+    parser.add_argument(
+        "--suggests-section",
+        action="append",
+        metavar="SECTION",
+        help="translate requirements from given section into Debian "
+        "dependencies and add them to ${python3:Suggests}",
+    )
+    parser.add_argument(
+        "--requires",
+        action="append",
+        metavar="FILE",
+        help="translate requirements from given file into Debian dependencies "
+        "and add them to ${python3:Depends}",
+    )
+    parser.add_argument(
+        "--shebang", metavar="COMMAND", help="use given command as shebang in scripts"
+    )
+    parser.add_argument(
+        "--ignore-shebangs",
+        action="store_true",
+        help="do not translate shebangs into Debian dependencies",
+    )
+    parser.add_argument(
+        "--no-dbg-cleaning",
+        action="store_false",
+        dest="clean_dbg_pkg",
+        help="do not remove files from debug packages",
+    )
+    parser.add_argument(
+        "--no-ext-rename",
+        action="store_true",
+        help="do not add magic tags nor multiarch tuples to extension file " "names)",
+    )
+    parser.add_argument(
+        "--no-shebang-rewrite", action="store_true", help="do not rewrite shebangs"
+    )
+    parser.add_argument(
+        "private_dir",
+        nargs="?",
+        help="Private directory containing Python modules (optional)",
+    )
+    # debhelper options:
+    parser.add_argument("-O", action="append", help=SUPPRESS)
+    return parser
diff -pruN 7.20251225/dhpython/pydist.py 7.20251227/dhpython/pydist.py
--- 7.20251225/dhpython/pydist.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/pydist.py	2025-12-28 00:40:29.000000000 +0000
@@ -25,8 +25,8 @@ import platform
 import os
 import re
 import subprocess
-from argparse import Namespace
 from collections.abc import Callable
+from dataclasses import dataclass
 from enum import Enum, StrEnum, auto
 from functools import cache, partial
 from os.path import exists, isdir, join
@@ -46,6 +46,7 @@ from dhpython import (
 )
 from dhpython.debhelper import BD
 from dhpython.markers import ComplexEnvironmentMarker, parse_environment_marker
+from dhpython.options import DHPythonOptions
 from dhpython.version import get_requested_versions, Version
 
 log = logging.getLogger("dhpython")
@@ -150,6 +151,15 @@ class RequirementModification(NamedTuple
     alternative: str | None = None
 
 
+@dataclass
+class PyDist:
+    name: str
+    versions: set[Version]
+    dependency: str
+    standard: Standard | None
+    rules: list[str]
+
+
 def validate(fpath: str) -> bool:
     """Check if pydist file looks good."""
     with open(fpath, encoding="utf-8") as fp:
@@ -165,14 +175,6 @@ def validate(fpath: str) -> bool:
     return True
 
 
-class PyDist(TypedDict):
-    name: str
-    versions: set[Version]
-    dependency: str
-    standard: Standard | None
-    rules: list[str]
-
-
 @cache
 def load(impl: str) -> dict[str, list[PyDist]]:
     """Load information about installed Python distributions.
@@ -213,7 +215,7 @@ def load(impl: str) -> dict[str, list[Py
                     rules=data["rules"].split(";") if data["rules"] else [],
                     standard=cast(Standard | None, data["standard"]),
                 )
-                result.setdefault(dist["name"], []).append(dist)
+                result.setdefault(dist.name, []).append(dist)
     return result
 
 
@@ -278,17 +280,17 @@ def guess_dependency(
     if details:
         log.debug("dependency: module %s is known to pydist: %r", name, details)
         for item in details:
-            if version and version not in item.get("versions", version):
+            if version and version not in item.versions:
                 # rule doesn't match version, try next one
                 continue
-            if not item["dependency"]:
+            if not item.dependency:
                 log.debug("dependency: requirement ignored")
                 return None  # this requirement should be ignored
-            if item["dependency"].endswith(")"):
+            if item.dependency.endswith(")"):
                 # no need to translate versions if version is hardcoded in
                 # Debian dependency
                 log.debug("dependency: requirement already has hardcoded version")
-                return merge_marker(item["dependency"])
+                return merge_marker(item.dependency)
             if req_d["operator"] == "==" and req_d["version"].endswith("*"):
                 # Translate "== 1.*" to "~= 1.0"
                 req_d["operator"] = "~="
@@ -296,11 +298,11 @@ def guess_dependency(
                 log.debug("dependency: translated wildcard version to semver limit")
             if (
                 req_d["version"]
-                and (item["standard"] or item["rules"])
+                and (item.standard or item.rules)
                 and req_d["operator"] not in (None, "!=")
             ):
                 o = _translate_op(req_d["operator"])
-                v = _translate(req_d["version"], item["rules"], item["standard"])
+                v = _translate(req_d["version"], item.rules, item.standard)
                 if req_d["operator"] == "==" and req_d["operator2"] is None:
                     # Loosen for Debian revisions
                     m = re.search(r"(.*)(\d+)(\D*)$", v)
@@ -311,24 +313,24 @@ def guess_dependency(
                     else:
                         max_v = v + ".0~"
                     d = (
-                        merge_marker(f"{item['dependency']} (>= {v})")
+                        merge_marker(f"{item.dependency} (>= {v})")
                         + ", "
-                        + merge_marker(f"{item['dependency']} (<< {max_v})")
+                        + merge_marker(f"{item.dependency} (<< {max_v})")
                     )
                 else:
-                    d = merge_marker(f"{item['dependency']} ({o} {v})")
+                    d = merge_marker(f"{item.dependency} ({o} {v})")
                 if req_d["version2"] and req_d["operator2"] not in (None, "!="):
                     o2 = _translate_op(req_d["operator2"])
-                    v2 = _translate(req_d["version2"], item["rules"], item["standard"])
-                    d += ", " + merge_marker(f"{item['dependency']} ({o2} {v2})")
+                    v2 = _translate(req_d["version2"], item.rules, item.standard)
+                    d += ", " + merge_marker(f"{item.dependency} ({o2} {v2})")
                 elif req_d["operator"] == "~=":
                     o2 = "<<"
                     v2 = _translate(
                         _max_compatible(req_d["version"]),
-                        item["rules"],
-                        item["standard"],
+                        item.rules,
+                        item.standard,
                     )
-                    d += ", " + merge_marker(f"{item['dependency']} ({o2} {v2})")
+                    d += ", " + merge_marker(f"{item.dependency} ({o2} {v2})")
                 log.debug("dependency: constructed version")
                 return d
             elif (
@@ -337,36 +339,31 @@ def guess_dependency(
                 and req_d["operator"] not in (None, "!=")
             ):
                 o = _translate_op(req_d["operator"])
-                d = merge_marker(f"{item['dependency']} ({o} {req_d['version']})")
+                d = merge_marker(f"{item.dependency} ({o} {req_d['version']})")
                 if req_d["version2"] and req_d["operator2"] not in (None, "!="):
                     o2 = _translate_op(req_d["operator2"])
                     d += ", " + merge_marker(
-                        f"{item['dependency']} ({o2} {req_d['version2']})"
+                        f"{item.dependency} ({o2} {req_d['version2']})"
                     )
                 elif req_d["operator"] == "~=":
                     o2 = "<<"
                     d += ", " + merge_marker(
-                        f"{item['dependency']} "
-                        f"({o2} {_max_compatible(req_d['version'])})"
+                        f"{item.dependency} ({o2} {_max_compatible(req_d['version'])})"
                     )
                 log.debug("dependency: constructed upstream version")
                 return d
             else:
-                if item["dependency"] in bdep:
-                    if (
-                        None in bdep[item["dependency"]]
-                        and bdep[item["dependency"]][None]
-                    ):
+                if item.dependency in bdep:
+                    if None in bdep[item.dependency] and bdep[item.dependency][None]:
                         log.debug("dependency: included in build-deps with limits ")
                         return merge_marker(
-                            f"{item['dependency']} "
-                            f"({bdep[item['dependency']][None]})"
+                            f"{item.dependency} ({bdep[item.dependency][None]})"
                         )
-                    # if arch in bdep[item['dependency']]:
+                    # if arch in bdep[item.dependency]:
                     # TODO: handle architecture specific dependencies from build depends
                     #       (current architecture is needed here)
                 log.debug("dependency: included in build-deps")
-                return merge_marker(item["dependency"])
+                return merge_marker(item.dependency)
 
     # search for Egg metadata file or directory (using dpkg -S)
     dpkg_query_tpl, regex_filter = PYDIST_DPKG_SEARCH_TPLS[impl]
@@ -619,7 +616,7 @@ def parse_pydep(
     fname: str,
     bdep: BD | None = None,
     *,
-    options: Namespace | None = None,
+    options: DHPythonOptions | None = None,
     depends_sec: list[str] | None = None,
     recommends_sec: list[str] | None = None,
     suggests_sec: list[str] | None = None,
@@ -714,7 +711,7 @@ def parse_requires_dist(
     fname: str,
     bdep: BD | None = None,
     *,
-    options: Namespace | None = None,
+    options: DHPythonOptions | None = None,
     depends_sec: list[str] | None = None,
     recommends_sec: list[str] | None = None,
     suggests_sec: list[str] | None = None,
diff -pruN 7.20251225/dhpython/tools.py 7.20251227/dhpython/tools.py
--- 7.20251225/dhpython/tools.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/tools.py	2025-12-28 00:40:29.000000000 +0000
@@ -30,8 +30,10 @@ from shutil import rmtree
 from os.path import exists, getsize, isdir, islink, join, split
 from pathlib import Path
 from subprocess import Popen, PIPE
-from typing import Literal, TextIO, NamedTuple, overload
+from typing import TYPE_CHECKING, Literal, TextIO, NamedTuple, overload
 
+if TYPE_CHECKING:
+    from dhpython.version import VersionRange
 
 log = logging.getLogger("dhpython")
 EGGnPTH_RE = re.compile(r"(.*?)(-py\d\.\d(?:-[^.]*)?)?(\.egg-info|\.pth)$")
@@ -323,7 +325,9 @@ def dpkg_architecture() -> dict[str, str
     return arch_data
 
 
-def pyinstall(interpreter: "Interpreter", package: str, vrange: str) -> None:
+def pyinstall(
+    interpreter: "Interpreter", package: str, vrange: "VersionRange | None"
+) -> None:
     """Install local files listed in pkg.pyinstall files as public modules."""
     srcfpath = "./debian/%s.pyinstall" % package
     if not exists(srcfpath):
@@ -367,7 +371,9 @@ def pyinstall(interpreter: "Interpreter"
                     os.link(fpath, dstfpath)
 
 
-def pyremove(interpreter: "Interpreter", package: str, vrange: str) -> None:
+def pyremove(
+    interpreter: "Interpreter", package: str, vrange: "VersionRange | None"
+) -> None:
     """Remove public modules listed in pkg.pyremove file."""
     srcfpath = "./debian/%s.pyremove" % package
     if not exists(srcfpath):
@@ -400,7 +406,7 @@ def pyremove(interpreter: "Interpreter",
 
 
 from dhpython.interpreter import Interpreter
-from dhpython.version import Version, get_requested_versions, RANGE_PATTERN
+from dhpython.version import RANGE_PATTERN, Version, get_requested_versions
 
 INSTALL_RE = re.compile(
     r"""
diff -pruN 7.20251225/dhpython/version.py 7.20251227/dhpython/version.py
--- 7.20251225/dhpython/version.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/dhpython/version.py	2025-12-28 00:40:29.000000000 +0000
@@ -435,7 +435,7 @@ def supported(impl: str) -> list[Version
 
 def get_requested_versions(
     impl: str,
-    vrange: str | None = None,
+    vrange: VersionRange | str | None = None,
     available: bool | None = None,
 ) -> set[Version]:
     """Return a set of requested and supported Python versions.
diff -pruN 7.20251225/pybuild 7.20251227/pybuild
--- 7.20251225/pybuild	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/pybuild	2025-12-28 00:40:29.000000000 +0000
@@ -21,16 +21,27 @@
 # THE SOFTWARE.
 
 import logging
-import argparse
 import re
 import sys
 from collections.abc import Callable
 from enum import StrEnum, auto
-from os import environ, getcwd, makedirs, remove
+from os import environ, makedirs, remove
 from os.path import abspath, exists, isdir, join
 from shutil import rmtree
 from tempfile import mkdtemp
-from typing import Literal, overload, cast
+from typing import Literal, overload
+
+from dhpython import build, PKG_PREFIX_MAP
+from dhpython.build.options import (
+    DEFAULT_INSTALL_DIR,
+    PybuildOptions,
+    build_parser,
+)
+from dhpython.build.types import Args, Context
+from dhpython.debhelper import DebHelper, build_options
+from dhpython.version import Version, build_sorted, get_requested_versions
+from dhpython.interpreter import Interpreter
+from dhpython.tools import dpkg_architecture, execute, move_matching_files
 
 INTERP_VERSION_RE = re.compile(r"^python(?P<version>3\.\d+)(?P<dbg>-dbg)?$")
 logging.basicConfig(
@@ -39,14 +50,8 @@ logging.basicConfig(
 log = logging.getLogger("dhpython")
 
 
-def main(cfg: argparse.Namespace) -> None:
+def main(cfg: PybuildOptions) -> None:
     log.debug("cfg: %s", cfg)
-    from dhpython import build, PKG_PREFIX_MAP
-    from dhpython.build.types import Args, Context
-    from dhpython.debhelper import DebHelper, build_options
-    from dhpython.version import Version, build_sorted, get_requested_versions
-    from dhpython.interpreter import Interpreter
-    from dhpython.tools import dpkg_architecture, execute, move_matching_files
 
     if cfg.list_systems:
         for name, Plugin in sorted(build.plugins.items()):
@@ -110,7 +115,6 @@ def main(cfg: argparse.Namespace) -> Non
                 selected_plugin = build_dep.split("-", 2)[2]
                 break
 
-    context: dict[str, str | dict[str, str]]  # proto-context
     if selected_plugin:
         certainty = 99
         Plugin = build.plugins.get(selected_plugin)
@@ -118,7 +122,7 @@ def main(cfg: argparse.Namespace) -> Non
             log.error("unrecognized build system: %s", selected_plugin)
             sys.exit(10)
         plugin = Plugin(cfg)
-        context = {"ENV": env, "args": {}, "dir": cfg.dir}
+        context = Context(ENV=env, destdir="debian/tmp", dir=cfg.dir)
         plugin.detect(context)
     else:
         plugin, certainty = None, 0
@@ -133,7 +137,7 @@ def main(cfg: argparse.Namespace) -> Non
                     exc_info=cfg.verbose,
                 )
                 continue
-            tmp_context = {"ENV": env, "args": {}, "dir": cfg.dir}
+            tmp_context = Context(ENV=env, destdir="debian/tmp", dir=cfg.dir)
             tmp_certainty = tmp_plugin.detect(tmp_context)
             log.debug("Plugin %s: certainty %i", Plugin.NAME, tmp_certainty)
             if tmp_certainty and tmp_certainty > certainty:
@@ -152,8 +156,8 @@ def main(cfg: argparse.Namespace) -> Non
         # with this interpreter
         tpls = {i for i in plugin.SUPPORTED_INTERPRETERS if "{version}" in i}
         if tpls:
-            for ipreter in cfg.interpreter:
-                m = INTERP_VERSION_RE.match(ipreter)
+            for interpreter in cfg.interpreter:
+                m = INTERP_VERSION_RE.match(interpreter)
                 if m:
                     ver = m.group("version")
                     updated = {tpl.format(version=ver) for tpl in tpls}
@@ -181,7 +185,7 @@ def main(cfg: argparse.Namespace) -> Non
             m = INTERP_VERSION_RE.match(i)
             if m:
                 log.debug("defaulting to version hardcoded in interpreter name")
-                versions = [m.group("version")]
+                versions = [Version(v) for v in m.group("version")]
             else:
                 IMAP = {v: k for k, v in PKG_PREFIX_MAP.items()}
                 if i in IMAP:
@@ -195,7 +199,6 @@ def main(cfg: argparse.Namespace) -> Non
             versions = build_sorted(
                 get_requested_versions("cpython3", available=True), impl="cpython3"
             )
-    versions = [Version(v) for v in versions]
 
     @overload
     def get_option(
@@ -273,7 +276,7 @@ def main(cfg: argparse.Namespace) -> Non
             "build_dir", interpreter, version, default=join(home_dir, "build")
         )
 
-        destdir = context["destdir"].format(version=version, interpreter=i)
+        destdir = context.destdir.format(version=version, interpreter=i)
         if cfg.name:
             package = ipreter.suggest_pkg_name(cfg.name)
         else:
@@ -282,40 +285,37 @@ def main(cfg: argparse.Namespace) -> Non
             destdir = f"debian/{package}"
         destdir = abspath(destdir)
 
-        args = cast(Args, dict(context["args"]))
-        args.update(
-            {
-                "autopkgtest": cfg.autopkgtest_only,
-                "package": package,
-                "interpreter": ipreter,
-                "version": version,
-                "args": get_option("%s_args" % step, interpreter, version, ""),
-                "dir": abspath(context["dir"].format(version=version, interpreter=i)),
-                "destdir": destdir,
-                "build_dir": abspath(build_dir.format(version=version, interpreter=i)),
-                # versioned dist-packages even for Python 3.X - dh_python3 will fix it later
-                # (and will have a chance to compare files)
-                "install_dir": get_option(
-                    "install_dir",
-                    interpreter,
-                    version,
-                    "/usr/lib/python{version}/dist-packages",
-                ).format(version=version, interpreter=i),
-                "home_dir": abspath(home_dir),
-            }
+        args = Args(
+            ENV={},
+            autopkgtest=cfg.autopkgtest_only,
+            package=package,
+            interpreter=ipreter,
+            version=version,
+            args=get_option("%s_args" % step, interpreter, version, ""),
+            dir=abspath(context.dir.format(version=version, interpreter=i)),
+            destdir=destdir,
+            build_dir=abspath(build_dir.format(version=version, interpreter=i)),
+            # versioned dist-packages even for Python 3.X - dh_python3 will fix it later
+            # (and will have a chance to compare files)
+            install_dir=get_option(
+                "install_dir",
+                interpreter,
+                version,
+                DEFAULT_INSTALL_DIR,
+            ).format(version=version, interpreter=i),
+            home_dir=abspath(home_dir),
         )
-        env = dict(args.get("ENV", {}))
-        pp_str = env.get("PYTHONPATH", context["ENV"].get("PYTHONPATH"))
+        for k, v in context.args.items():
+            setattr(args, k, v)
+        pp_str = args.ENV.get("PYTHONPATH", context.ENV.get("PYTHONPATH"))
         pp = pp_str.split(":") if pp_str else []
         if step in {"build", "test", "autopkgtest"}:
             if step in {"test", "autopkgtest"}:
-                args["test_dir"] = join(
-                    args["destdir"], args["install_dir"].lstrip("/")
-                )
-                if args["test_dir"] not in pp:
-                    pp.append(args["test_dir"])
-            if args["build_dir"] not in pp:
-                pp.append(args["build_dir"])
+                args.test_dir = join(args.destdir, args.install_dir.lstrip("/"))
+                if args.test_dir not in pp:
+                    pp.append(args.test_dir)
+            if args.build_dir not in pp:
+                pp.append(args.build_dir)
         # cross compilation support for Python 2.x
         if version.major == 2 and arch_data.get("DEB_BUILD_ARCH") != arch_data.get(
             "DEB_HOST_ARCH"
@@ -326,24 +326,23 @@ def main(cfg: argparse.Namespace) -> Non
                     version, arch_data["DEB_HOST_MULTIARCH"]
                 ),
             )
-        env["PYTHONPATH"] = ":".join(pp)
+        args.ENV["PYTHONPATH"] = ":".join(pp)
         # cross compilation support for Python <= 3.8 (see above)
         if version.major == 3:
             name = "_PYTHON_SYSCONFIGDATA_NAME"
-            value = env.get(name, context["ENV"].get(name, ""))
+            value = args.ENV.get(name, context.ENV.get(name, ""))
             if (
                 version << "3.8"
                 and value.startswith("_sysconfigdata_")
                 and not value.startswith("_sysconfigdata_m")
             ):
-                value = env[name] = "_sysconfigdata_m%s" % value[15:]
+                value = args.ENV[name] = "_sysconfigdata_m%s" % value[15:]
             # update default from main() for -dbg interpreter
             if value and ipreter.debug and not value.startswith("_sysconfigdata_d"):
-                env[name] = "_sysconfigdata_d%s" % value[15:]
-        args["ENV"] = env
+                args.ENV[name] = "_sysconfigdata_d%s" % value[15:]
 
-        if not exists(args["build_dir"]):
-            makedirs(args["build_dir"])
+        if not exists(args.build_dir):
+            makedirs(args.build_dir)
 
         return args
 
@@ -375,25 +374,24 @@ def main(cfg: argparse.Namespace) -> Non
     ) -> None:
         step = Step(func.__func__.__name__)  # type: ignore
         args = get_args(context, step, version, interpreter)
-        env = dict(context["ENV"])
-        if "ENV" in args:
-            env.update(args["ENV"])
+        env = dict(context.ENV)
+        env.update(args.ENV)
 
         before_cmd = get_option(f"before_{step}", interpreter, version)
         if before_cmd:
             log_file: str | Literal[False]
             if cfg.quiet:
-                log_file = join(args["home_dir"], f"before_{step}_cmd.log")
+                log_file = join(args.home_dir, f"before_{step}_cmd.log")
             else:
                 log_file = False
-            command = before_cmd.format(**args)
+            command = args.format(before_cmd)
             log.info(command)
-            output = execute(command, cwd=context["dir"], env=env, log_output=log_file)
+            output = execute(command, cwd=context.dir, env=env, log_output=log_file)
             if output.returncode != 0:
                 msg = f"exit code={output.returncode}: {command}"
                 raise Exception(msg)
 
-        fpath = join(args["home_dir"], "testfiles_to_rm_before_install")
+        fpath = join(args.home_dir, "testfiles_to_rm_before_install")
         if step == Step.INSTALL and exists(fpath):
             with open(fpath, encoding="UTF-8") as fp:
                 for line in fp:
@@ -409,18 +407,18 @@ def main(cfg: argparse.Namespace) -> Non
         after_cmd = get_option(f"after_{step}", interpreter, version)
         if after_cmd:
             if cfg.quiet:
-                log_file = join(args["home_dir"], f"after_{step}_cmd.log")
+                log_file = join(args.home_dir, f"after_{step}_cmd.log")
             else:
                 log_file = False
-            command = after_cmd.format(**args)
+            command = args.format(after_cmd)
             log.info(command)
-            output = execute(command, cwd=context["dir"], env=env, log_output=log_file)
+            output = execute(command, cwd=context.dir, env=env, log_output=log_file)
             if output.returncode != 0:
                 msg = f"exit code={output.returncode}: {command}"
                 raise Exception(msg)
 
     def move_to_ext_destdir(
-        i: Step,
+        i: str,
         version: Version,
         context: Context,
     ) -> None:
@@ -431,7 +429,7 @@ def main(cfg: argparse.Namespace) -> Non
         if ext_destdir:
             assert ext_pattern
             move_matching_files(
-                args["destdir"],
+                args.destdir,
                 ext_destdir,
                 ext_pattern,
                 get_option("ext_sub_pattern", i, version),
@@ -476,9 +474,9 @@ def main(cfg: argparse.Namespace) -> Non
             for version in iversions:
                 if is_disabled(step, i, version):
                     continue
-                c = cast(Context, dict(context))
-                c["dir"] = get_option("dir", i, version, cfg.dir)
-                c["destdir"] = get_option("destdir", i, version, default_destdir)
+                c = context.copy()
+                c.dir = get_option("dir", i, version, cfg.dir)
+                c.destdir = get_option("destdir", i, version, default_destdir)
                 try:
                     run(func, i, version, c)
                 except Exception as err:
@@ -519,9 +517,9 @@ def main(cfg: argparse.Namespace) -> Non
                 if key in context_map:
                     c = context_map[key]
                 else:
-                    c = cast(Context, dict(context))
-                    c["dir"] = get_option("dir", i, version, cfg.dir)
-                    c["destdir"] = get_option("destdir", i, version, default_destdir)
+                    context.copy()
+                    c.dir = get_option("dir", i, version, cfg.dir)
+                    c.destdir = get_option("destdir", i, version, default_destdir)
                     context_map[key] = c
 
                 if not is_disabled(Step.CLEAN, i, version):
@@ -540,287 +538,17 @@ def main(cfg: argparse.Namespace) -> Non
         sys.exit(14)
 
 
-def parse_args(argv: list[str]) -> argparse.Namespace:
-    usage = "%(prog)s [ACTION] [BUILD SYSTEM ARGS] [DIRECTORIES] [OPTIONS]"
-    parser = argparse.ArgumentParser(usage=usage)
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        action="store_true",
-        default=environ.get("PYBUILD_VERBOSE") == "1",
-        help="turn verbose mode on",
-    )
-    parser.add_argument(
-        "-q",
-        "--quiet",
-        action="store_true",
-        default=environ.get("PYBUILD_QUIET") == "1",
-        help="doesn't show external command's output",
-    )
-    parser.add_argument(
-        "-qq",
-        "--really-quiet",
-        action="store_true",
-        default=environ.get("PYBUILD_RQUIET") == "1",
-        help="be quiet",
-    )
-    parser.add_argument("--version", action="version", version="%(prog)s DEVELV")
-
-    action = parser.add_argument_group(
-        "ACTION",
-        """The default is to build,
-        install and test the library using detected build system version by
-        version. Selecting one of following actions, will invoke given action
-        for all versions - one by one - which (contrary to the default action)
-        in some build systems can overwrite previous results.""",
-    )
-    action.add_argument(
-        "--detect",
-        action="store_true",
-        dest="detect_only",
-        help="return the name of detected build system",
-    )
-    action.add_argument(
-        "--clean",
-        action="store_true",
-        dest="clean_only",
-        help="clean files using auto-detected build system specific methods",
-    )
-    action.add_argument(
-        "--configure",
-        action="store_true",
-        dest="configure_only",
-        help="invoke configure step for all requested Python versions",
-    )
-    action.add_argument(
-        "--build",
-        action="store_true",
-        dest="build_only",
-        help="invoke build step for all requested Python versions",
-    )
-    action.add_argument(
-        "--install",
-        action="store_true",
-        dest="install_only",
-        help="invoke install step for all requested Python versions",
-    )
-    action.add_argument(
-        "--test",
-        action="store_true",
-        dest="test_only",
-        help="invoke tests for auto-detected build system",
-    )
-    action.add_argument(
-        "--autopkgtest",
-        action="store_true",
-        dest="autopkgtest_only",
-        help="invoke autopkgtests for auto-detected build system",
-    )
-    action.add_argument(
-        "--list-systems",
-        action="store_true",
-        help="list available build systems and exit",
-    )
-    action.add_argument(
-        "--print",
-        action="append",
-        dest="print_args",
-        help="print pybuild's internal parameters",
-    )
-
-    arguments = parser.add_argument_group(
-        "BUILD SYSTEM ARGS",
-        """
-        Additional arguments passed to the build system.
-        --system=custom requires complete command.""",
-    )
-    arguments.add_argument(
-        "--before-clean", metavar="CMD", help="invoked before the clean command"
-    )
-    arguments.add_argument("--clean-args", metavar="ARGS")
-    arguments.add_argument(
-        "--after-clean", metavar="CMD", help="invoked after the clean command"
-    )
-
-    arguments.add_argument(
-        "--before-configure", metavar="CMD", help="invoked before the configure command"
-    )
-    arguments.add_argument("--configure-args", metavar="ARGS")
-    arguments.add_argument(
-        "--after-configure", metavar="CMD", help="invoked after the configure command"
-    )
-
-    arguments.add_argument(
-        "--before-build", metavar="CMD", help="invoked before the build command"
-    )
-    arguments.add_argument("--build-args", metavar="ARGS")
-    arguments.add_argument(
-        "--after-build", metavar="CMD", help="invoked after the build command"
-    )
-
-    arguments.add_argument(
-        "--before-install", metavar="CMD", help="invoked before the install command"
-    )
-    arguments.add_argument("--install-args", metavar="ARGS")
-    arguments.add_argument(
-        "--after-install", metavar="CMD", help="invoked after the install command"
-    )
-
-    arguments.add_argument(
-        "--before-test", metavar="CMD", help="invoked before the test command"
-    )
-    arguments.add_argument("--test-args", metavar="ARGS")
-    arguments.add_argument(
-        "--after-test", metavar="CMD", help="invoked after the test command"
-    )
-
-    tests = parser.add_argument_group(
-        "TESTS",
-        """\
-        unittest\'s discover is used by default (if available)""",
-    )
-    tests.add_argument(
-        "--test-nose",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_NOSE") == "1",
-        help="use nose module in --test step",
-    )
-    tests.add_argument(
-        "--test-nose2",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_NOSE2") == "1",
-        help="use nose2 module in --test step",
-    )
-    tests.add_argument(
-        "--test-pytest",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_PYTEST") == "1",
-        help="use pytest module in --test step",
-    )
-    tests.add_argument(
-        "--test-tox",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_TOX") == "1",
-        help="use tox in --test step",
-    )
-    tests.add_argument(
-        "--test-stestr",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_STESTR") == "1",
-        help="use stestr in --test step",
-    )
-    tests.add_argument(
-        "--test-unittest",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_UNITTEST") == "1",
-        help="use unittest in --test step",
-    )
-    tests.add_argument(
-        "--test-custom",
-        action="store_true",
-        default=environ.get("PYBUILD_TEST_CUSTOM") == "1",
-        help="use custom command in --test step",
-    )
-
-    dirs = parser.add_argument_group("DIRECTORIES")
-    dirs.add_argument(
-        "-d",
-        "--dir",
-        action="store",
-        metavar="DIR",
-        default=environ.get("PYBUILD_DIR", getcwd()),
-        help="source files directory - base for other relative dirs [default: CWD]",
-    )
-    dirs.add_argument(
-        "--dest-dir",
-        action="store",
-        metavar="DIR",
-        dest="destdir",
-        default=None,  # We handle this default manually, because --name affects it
-        help="destination directory [default: debian/tmp]",
-    )
-    dirs.add_argument(
-        "--ext-dest-dir",
-        action="store",
-        metavar="DIR",
-        dest="ext_destdir",
-        default=environ.get("PYBUILD_EXT_DESTDIR"),
-        help="destination directory for .so files",
-    )
-    dirs.add_argument(
-        "--ext-pattern",
-        action="store",
-        metavar="PATTERN",
-        default=environ.get("PYBUILD_EXT_PATTERN", r"\.so(\.[^/]*)?$"),
-        help="regular expression for files that should be moved"
-        " if --ext-dest-dir is set [default: .so files]",
-    )
-    dirs.add_argument(
-        "--ext-sub-pattern",
-        action="store",
-        metavar="PATTERN",
-        default=environ.get("PYBUILD_EXT_SUB_PATTERN"),
-        help="pattern to change --ext-pattern's filename or path",
-    )
-    dirs.add_argument(
-        "--ext-sub-repl",
-        action="store",
-        metavar="PATTERN",
-        default=environ.get("PYBUILD_EXT_SUB_REPL"),
-        help="replacement for match from --ext-sub-pattern," " empty string by default",
-    )
-    dirs.add_argument(
-        "--install-dir",
-        action="store",
-        metavar="DIR",
-        help="installation directory [default: .../dist-packages]",
-    )
-    dirs.add_argument(
-        "--name",
-        action="store",
-        default=environ.get("PYBUILD_NAME"),
-        help="use this name to guess destination directories",
-    )
-
-    limit = parser.add_argument_group("LIMITATIONS")
-    limit.add_argument(
-        "-s",
-        "--system",
-        default=environ.get("PYBUILD_SYSTEM"),
-        help="select a build system [default: auto-detection]",
-    )
-    limit.add_argument(
-        "-p",
-        "--pyver",
-        action="append",
-        dest="versions",
-        help="""build for Python VERSION.
-                               This option can be used multiple times
-                               [default: all supported Python 3.X versions]""",
-    )
-    limit.add_argument(
-        "-i",
-        "--interpreter",
-        action="append",
-        help="change interpreter [default: python{version}]",
-    )
-    limit.add_argument(
-        "--disable", metavar="ITEMS", help="disable action, interpreter or version"
-    )
-
-    args = parser.parse_args(argv)
+def parse_args(argv: list[str]) -> PybuildOptions:
+    parser = build_parser()
+    args = parser.parse_args(argv, PybuildOptions())
     if not args.interpreter:
         args.interpreter = environ.get(
             "PYBUILD_INTERPRETERS", "python{version}"
         ).split()
     if not args.versions:
-        args.versions = environ.get("PYBUILD_VERSIONS", "").split()
-    else:
-        # add support for -p `pyversions -rv`
-        versions = []
-        for version in args.versions:
-            versions.extend(version.split())
-        args.versions = versions
+        args.versions = [
+            Version(version) for version in environ.get("PYBUILD_VERSIONS", "").split()
+        ]
 
     if (
         args.test_nose
diff -pruN 7.20251225/tests/common.py 7.20251227/tests/common.py
--- 7.20251225/tests/common.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/tests/common.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,36 +0,0 @@
-from typing import Any
-
-from dhpython.version import VersionRange
-
-
-class FakeOptions:
-    accept_upstream_versions: bool = False
-    depends: list[str]
-    depends_section: list[str]
-    guess_deps: bool = True
-    no_ext_rename: bool = False
-    recommends: list[str]
-    recommends_section: list[str]
-    remaining_packages: bool = False
-    requires: list[str]
-    suggests: list[str]
-    suggests_section: list[str]
-    vrange: VersionRange | None = None
-
-    def __init__(self, **kwargs: Any) -> None:
-        opts = {
-            "depends": (),
-            "depends_section": (),
-            "guess_deps": False,
-            "no_ext_rename": False,
-            "recommends": (),
-            "recommends_section": (),
-            "requires": (),
-            "suggests": (),
-            "suggests_section": (),
-            "vrange": None,
-            "accept_upstream_versions": False,
-        }
-        opts.update(kwargs)
-        for k, v in opts.items():
-            setattr(self, k, v)
diff -pruN 7.20251225/tests/test_debhelper.py 7.20251227/tests/test_debhelper.py
--- 7.20251225/tests/test_debhelper.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/tests/test_debhelper.py	2025-12-28 00:40:29.000000000 +0000
@@ -92,10 +92,10 @@ class TestControlBlockParsing(DebHelperT
         )
 
     def test_parses_arch(self) -> None:
-        self.assertEqual(self.dh.packages["python3-foo-ext"]["arch"], "any")
+        self.assertEqual(self.dh.packages["python3-foo-ext"].arch, "any")
 
     def test_parses_arch_all(self) -> None:
-        self.assertEqual(self.dh.packages["python3-foo"]["arch"], "all")
+        self.assertEqual(self.dh.packages["python3-foo"].arch, "all")
 
 
 class TestControlSkipIndep(DebHelperTestCase):
diff -pruN 7.20251225/tests/test_depends.py 7.20251227/tests/test_depends.py
--- 7.20251225/tests/test_depends.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/tests/test_depends.py	2025-12-28 00:40:29.000000000 +0000
@@ -2,19 +2,17 @@ import os
 import logging
 import platform
 import unittest
-from argparse import Namespace
 from collections.abc import Callable, Sequence
 from tempfile import TemporaryDirectory
-from typing import Any, cast
+from typing import Any
 from unittest.mock import patch
 
 from dhpython.fs import ScanResult
 from dhpython.pydist import PyDist, Standard
 from dhpython.depends import Dependencies
+from dhpython.options import DHPythonOptions
 from dhpython.version import Version
 
-from .common import FakeOptions
-
 
 def pep386(d: dict[str, str]) -> dict[str, list[PyDist]]:
     """Mark all pydist entries as being PEP386"""
@@ -68,7 +66,7 @@ class DependenciesTestCase(unittest.Test
     prepared_stats: ScanResult
     requires: dict[str, Sequence[str]] = {}
     dist_info_metadata: dict[str, Sequence[str]] = {}
-    options = FakeOptions()
+    options = DHPythonOptions()
     parse = True
 
     def setUp(self) -> None:
@@ -120,7 +118,7 @@ class DependenciesTestCase(unittest.Test
         self.addCleanup(cleanup)
 
         if self.parse:
-            self.d.parse(stats, cast(Namespace, self.options))
+            self.d.parse(stats, self.options)
         else:
             self.prepared_stats = stats
 
@@ -134,7 +132,7 @@ class DependenciesTestCase(unittest.Test
 
 
 class TestRequiresCPython3(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True)
+    options = DHPythonOptions(guess_deps=True)
     pydist = {
         "bar": [pydist(name="bar", dependency="python3-bar")],
         "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
@@ -161,7 +159,7 @@ class TestRequiresCPython3(DependenciesT
 
 
 class TestRequiresCompatible(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True)
+    options = DHPythonOptions(guess_deps=True)
     pydist = {
         "bar": [pydist(name="bar", dependency="python3-bar")],
         "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
@@ -197,7 +195,7 @@ class TestRequiresCompatible(Dependencie
 
 
 class TestRequiresDistPython3(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True)
+    options = DHPythonOptions(guess_deps=True)
     pydist = {
         "bar": [pydist(name="bar", dependency="python3-bar")],
         "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
@@ -236,7 +234,7 @@ class TestRequiresDistPython3(Dependenci
 
 
 class TestEnvironmentMarkersDistInfo(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True, depends_section=["feature"])
+    options = DHPythonOptions(guess_deps=True, depends_section=["feature"])
     pydist = pep386(
         {
             "no_markers": "python3-no-markers",
@@ -733,7 +731,7 @@ class TestEnvironmentMarkersEggInfo(Test
 
 
 class TestIgnoresUnusedModulesDistInfo(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True, depends_section=["feature"])
+    options = DHPythonOptions(guess_deps=True, depends_section=["feature"])
     dist_info_metadata = {
         "debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA": (
             "Requires-Dist: unusued-complex-module ; "
@@ -748,7 +746,7 @@ class TestIgnoresUnusedModulesDistInfo(D
         if not hasattr(self, "assertLogs"):
             raise unittest.SkipTest("Requires Python >= 3.4")
         with self.assertLogs(logger="dhpython", level=logging.INFO) as logs:
-            self.d.parse(self.prepared_stats, cast(Namespace, self.options))
+            self.d.parse(self.prepared_stats, self.options)
         for line in logs.output:
             self.assertTrue(
                 line.startswith("INFO:dhpython:Ignoring complex environment marker"),
@@ -758,7 +756,7 @@ class TestIgnoresUnusedModulesDistInfo(D
 
 
 class TestIgnoresUnusedModulesEggInfo(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True, depends_section=["feature"])
+    options = DHPythonOptions(guess_deps=True, depends_section=["feature"])
     requires = {
         "debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt": (
             "[nativelib:(sys_platform == 'darwin')]",
@@ -775,7 +773,7 @@ class TestIgnoresUnusedModulesEggInfo(De
         if not hasattr(self, "assertNoLogs"):
             raise unittest.SkipTest("Requires Python >= 3.10")
         with self.assertNoLogs(logger="dhpython", level=logging.INFO):
-            self.d.parse(self.prepared_stats, cast(Namespace, self.options))
+            self.d.parse(self.prepared_stats, self.options)
 
 
 class TestCExtensionPython3(DependenciesTestCase):
diff -pruN 7.20251225/tests/test_distutils_extra.py 7.20251227/tests/test_distutils_extra.py
--- 7.20251225/tests/test_distutils_extra.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/tests/test_distutils_extra.py	2025-12-28 00:40:29.000000000 +0000
@@ -1,20 +1,18 @@
 import os
 import unittest
-from argparse import Namespace
 from copy import deepcopy
 from tempfile import TemporaryDirectory
-from typing import cast
 
 from dhpython.depends import Dependencies
 from dhpython.fs import ScanResult
+from dhpython.options import DHPythonOptions
 from dhpython.pydist import Standard
 
-from .common import FakeOptions
 from .test_depends import prime_pydist, pydist
 
 
 class TestDistutilsExtra(unittest.TestCase):
-    options = FakeOptions(guess_deps=True)
+    options = DHPythonOptions(guess_deps=True)
     pydist = {
         "bar": [pydist(name="bar", dependency="python3-bar")],
         "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
@@ -65,5 +63,5 @@ gTranscribe is a software focused on eas
             )
         cleanup = prime_pydist(self.impl, self.pydist)
         self.addCleanup(cleanup)
-        self.d.parse(stats, cast(Namespace, self.options))
+        self.d.parse(stats, self.options)
         self.assertIn("python3-bar", self.d.depends)
diff -pruN 7.20251225/tests/test_fs.py 7.20251227/tests/test_fs.py
--- 7.20251225/tests/test_fs.py	2025-12-25 15:48:54.000000000 +0000
+++ 7.20251227/tests/test_fs.py	2025-12-28 00:40:29.000000000 +0000
@@ -1,17 +1,15 @@
 import os
-from argparse import Namespace
 from collections.abc import Sequence
 from pathlib import Path
 from tempfile import TemporaryDirectory
-from typing import Any, cast
+from typing import Any
 from unittest import TestCase
 
 from dhpython.interpreter import Interpreter
 from dhpython.fs import Scan, merge_WHEEL, share_files
+from dhpython.options import DHPythonOptions
 from dhpython.version import Version
 
-from .common import FakeOptions
-
 
 class FSTestCase(TestCase):
     files: dict[str, Sequence[str]] = {}
@@ -69,7 +67,7 @@ class ShareFilesTestCase(FSTestCase):
             self.tempdir.name,
             self.destdir.name,
             self.interpreter,
-            cast(Namespace, FakeOptions(**options)),
+            DHPythonOptions(**options),
         )
 
     def destPath(self, name: str) -> Path:
@@ -155,7 +153,7 @@ class ScanTestCase(FSTestCase):
             self.scan = Scan(
                 interpreter=Interpreter(self.impl),
                 package=self.package,
-                options=cast(Namespace, FakeOptions(**self.options)),
+                options=DHPythonOptions(**self.options),
             )
         finally:
             os.chdir(pwd)
diff -pruN 7.20251225/tests/test_options.py 7.20251227/tests/test_options.py
--- 7.20251225/tests/test_options.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.20251227/tests/test_options.py	2025-12-28 00:40:29.000000000 +0000
@@ -0,0 +1,20 @@
+from dataclasses import fields
+from unittest import TestCase
+
+from dhpython.options import build_parser, DHPythonOptions
+
+
+class TestPybuildOptions(TestCase):
+    def test_args_match_options(self) -> None:
+        parser = build_parser()
+        parser_options = {action.dest for action in parser._get_optional_actions()} | {
+            action.dest for action in parser._get_positional_actions()
+        }
+        # Built-ins
+        parser_options -= {"help", "version"}
+
+        options = {field.name for field in fields(DHPythonOptions)}
+        # Added in parse_args()
+        options -= {"write_log"}
+
+        self.assertEqual(parser_options, options)
diff -pruN 7.20251225/tests/test_pybuild_options.py 7.20251227/tests/test_pybuild_options.py
--- 7.20251225/tests/test_pybuild_options.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.20251227/tests/test_pybuild_options.py	2025-12-28 00:40:29.000000000 +0000
@@ -0,0 +1,20 @@
+from dataclasses import fields
+from unittest import TestCase
+
+from dhpython.build.options import build_parser, PybuildOptions
+
+
+class TestPybuildOptions(TestCase):
+    def test_args_match_options(self) -> None:
+        parser = build_parser()
+        parser_options = {action.dest for action in parser._get_optional_actions()} | {
+            action.dest for action in parser._get_positional_actions()
+        }
+        # Built-ins
+        parser_options -= {"help", "version"}
+
+        options = {field.name for field in fields(PybuildOptions)}
+        # Added in parse_args()
+        options -= {"custom_tests"}
+
+        self.assertEqual(parser_options, options)
