diff --git a/CHANGES.rst b/CHANGES.rst
index c44b413..421388f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -14,6 +14,8 @@
 
 - Fix the ``zpasswd`` console script on Python 3.
 
+- Update the ``zpasswd`` script to use ``argparse`` instead of ``optparse.``
+
 4.2.0 (2016-07-07)
 ==================
 
diff --git a/src/zope/password/zpasswd.py b/src/zope/password/zpasswd.py
index 70b6f9c..25d2433 100644
--- a/src/zope/password/zpasswd.py
+++ b/src/zope/password/zpasswd.py
@@ -14,7 +14,7 @@
 """Implementation of the zpasswd script.
 """
 from __future__ import print_function
-import optparse
+import argparse
 import os
 import sys
 from xml.sax.saxutils import quoteattr
@@ -156,7 +156,7 @@ class Application(object):
     def process(self):
         options = self.options
 
-        destination = sys.stdout if not options.destination else open(options.destination, 'w')
+        destination = options.destination
         try:
             principal = self.get_principal()
 
@@ -269,20 +269,18 @@ def get_password_managers(config_path=None):
 
 def parse_args(argv):
     """Parse the command line, returning an object representing the input."""
-    _path, prog = os.path.split(os.path.realpath(argv[0]))
-    p = optparse.OptionParser(prog=prog,
-                              usage="%prog [options]",
-                              version=VERSION)
-    p.add_option("-c", "--config", dest="config", metavar="FILE",
-                 help=("path to the site.zcml configuration file"
-                       " (more accurate but slow password managers registry creation)"))
-    p.add_option("-o", "--output", dest="destination", metavar="FILE",
-                 help=("the file in which the output will be saved"
-                       " (STDOUT by default)"))
-    options, args = p.parse_args(argv[1:])
+    prog = os.path.split(os.path.realpath(argv[0]))[1]
+    p = argparse.ArgumentParser(prog=prog)
+    p.add_argument("-c", "--config", dest="config", metavar="FILE",
+                   help=("path to the site.zcml configuration file"
+                         " (more accurate but slow password managers registry creation)"))
+    p.add_argument("-o", "--output", dest="destination", metavar="FILE",
+                   help=("the file in which the output will be saved"
+                         " (STDOUT by default)"),
+                   default=sys.stdout,
+                   type=argparse.FileType('w'))
+    p.add_argument("--version", action="version", version=VERSION)
+    options = p.parse_args(argv[1:])
     options.managers = get_password_managers(options.config)
     options.program = prog
-    options.version = VERSION
-    if args:
-        p.error("too many arguments")
     return options

Convert usage of optparse to argparse
optparse is deprecated.
diff --git a/src/zope/password/tests/test_zpasswd.py b/src/zope/password/tests/test_zpasswd.py
index b471a8c..3c33f66 100644
--- a/src/zope/password/tests/test_zpasswd.py
+++ b/src/zope/password/tests/test_zpasswd.py
@@ -17,6 +17,7 @@ import contextlib
 import doctest
 import os
 import sys
+import tempfile
 import unittest
 
 import io
@@ -36,13 +37,13 @@ class TestBase(unittest.TestCase):
 
     def setUp(self):
         # Create a minimal site.zcml file
-        with open('testsite.zcml', 'wb') as file:
-            file.write(
+        with tempfile.NamedTemporaryFile(prefix="testsite",
+                                         suffix=".zcml",
+                                         delete=False) as f:
+            f.write(
                 b'<configure xmlns="http://namespaces.zope.org/zope"/>\n')
-
-    def tearDown(self):
-        # Clean up
-        os.unlink('testsite.zcml')
+            self.config = f.name
+            self.addCleanup(os.remove, f.name)
 
     @contextlib.contextmanager
     def patched_stdio(self, input_data=None):
@@ -81,32 +82,36 @@ class TestBase(unittest.TestCase):
 
 class ArgumentParsingTestCase(TestBase):
 
-    config = "testsite.zcml"
-
     def parse_args(self, args):
         argv = ["foo/bar.py"] + args
         with self.patched_stdio():
             options = zpasswd.parse_args(argv)
+
         self.assertEqual(options.program, "bar.py")
-        self.assertTrue(options.version)
         return options
 
-    def check_stdout_content(self, args):
+    def check_stdout_content(self, args, stderr=False):
         with self.assertRaises(SystemExit) as e:
             self.parse_args(args)
 
         e = e.exception
         self.assertEqual(e.code, 0)
-        self.assertTrue(self.stdout.getvalue())
-        self.assertFalse(self.stderr.getvalue())
+        full = self.stdout
+        empty = self.stderr
+        if stderr:
+            full = self.stderr
+            empty = self.stdout
+        self.assertTrue(full.getvalue())
+        self.assertFalse(empty.getvalue())
 
     def test_no_arguments(self):
         options = self.parse_args([])
         self.assertTrue(options.managers)
-        self.assertFalse(options.destination)
+        self.assertIs(options.destination, self.stdout)
 
     def test_version_long(self):
-        self.check_stdout_content(["--version"])
+        self.check_stdout_content(["--version"],
+                                  stderr=sys.version_info[0] == 2)
 
     def test_help_long(self):
         self.check_stdout_content(["--help"])
@@ -114,13 +119,15 @@ class ArgumentParsingTestCase(TestBase):
     def test_help_short(self):
         self.check_stdout_content(["-h"])
 
-    def test_destination_short(self):
-        options = self.parse_args(["-o", "filename"])
-        self.assertEqual(options.destination, "filename")
+    def test_destination_short(self, option="-o"):
+        handle, path = tempfile.mkstemp()
+        os.close(handle)
+        self.addCleanup(os.remove, path)
+        options = self.parse_args([option, path])
+        self.assertEqual(options.destination.name, path)
 
     def test_destination_long(self):
-        options = self.parse_args(["--output", "filename"])
-        self.assertEqual(options.destination, "filename")
+        self.test_destination_short("--output")
 
     def test_config_short(self):
         options = self.parse_args(["-c", self.config])
@@ -134,7 +141,7 @@ class ArgumentParsingTestCase(TestBase):
         with self.assertRaises(SystemExit):
             self.parse_args(["--config", self.config, "extra stuff"])
 
-        self.assertIn("too many arguments",
+        self.assertIn("unrecognized arguments",
                       self.stderr.getvalue())
 
     def test_main(self):
@@ -163,11 +170,12 @@ class ControlledInputApplication(zpasswd.Application):
 class Options(object):
 
     config = None
-    destination = None
-    version = "[test-version]"
     program = "[test-program]"
     managers = password.managers
 
+    def __init__(self):
+        self.destination = sys.stdout
+
 class InputCollectionTestCase(TestBase):
 
     def createOptions(self):
@@ -185,7 +193,6 @@ class InputCollectionTestCase(TestBase):
             self.assertEqual(line.strip(), expline)
 
     def test_principal_information(self):
-        options = self.createOptions()
         apps = []
         def factory(options):
             app = ControlledInputApplication(
@@ -195,6 +202,7 @@ class InputCollectionTestCase(TestBase):
             apps.append(app)
             return app
         with self.patched_stdio():
+            options = self.createOptions()
             zpasswd.run_app_with_options(options, factory)
         self.assertFalse(self.stderr.getvalue())
         self.assertTrue(apps[0].all_input_consumed())
@@ -214,16 +222,16 @@ class TestDestination(InputCollectionTestCase):
     destination = None
 
     def createOptions(self):
-        import tempfile
         opts = Options()
-        handle, destination = tempfile.mkstemp('.test_zpasswd')
-        self.addCleanup(lambda: os.remove(destination))
-        os.close(handle)
+        destination = tempfile.NamedTemporaryFile(mode='w',
+                                                  suffix=".test_zpasswd",
+                                                  delete=False)
+        self.addCleanup(os.remove, destination.name)
         self.destination = opts.destination = destination
         return opts
 
     def _get_output(self):
-        with open(self.destination) as f:
+        with open(self.destination.name) as f:
             return f.read()
 
 

pip install -e .[test]
parse_log_pytest
pytest
coverage pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
bcrypt==4.3.0
coverage==7.8.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
zope.component==6.0
zope.configuration==6.0
zope.event==5.0
zope.exceptions==5.2
zope.hookable==7.0
zope.i18nmessageid==7.0
zope.interface==7.2
-e git+https://github.com/zopefoundation/zope.password.git@d517a5a165a28336ae51c5e2dabea8a65390e586#egg=zope.password
zope.schema==7.0.1
zope.testing==5.1
zope.testrunner==7.2

name: zope.password
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - bcrypt==4.3.0
      - coverage==7.8.0
      - zope-component==6.0
      - zope-configuration==6.0
      - zope-event==5.0
      - zope-exceptions==5.2
      - zope-hookable==7.0
      - zope-i18nmessageid==7.0
      - zope-interface==7.2
      - zope-schema==7.0.1
      - zope-testing==5.1
      - zope-testrunner==7.2
prefix: /opt/conda/envs/zope.password

src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_destination_long src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_destination_short src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_no_arguments src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_too_many_arguments src/zope/password/tests/test_zpasswd.py::InputCollectionTestCase::test_principal_information src/zope/password/tests/test_zpasswd.py::TestDestination::test_principal_information
src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_config_long src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_config_short src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_help_long src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_help_short src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_main src/zope/password/tests/test_zpasswd.py::ArgumentParsingTestCase::test_version_long src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_exit src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_get_passwd_empty src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_get_passwd_spaces src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_get_passwd_verify_fail src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_get_password_manager_bad src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_get_password_manager_default src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_get_value src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_keyboard_interrupt src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_read_input src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_read_password src/zope/password/tests/test_zpasswd.py::TestRunAndApplication::test_read_password_cancel src/zope/password/tests/test_zpasswd.py::test_suite
src/zope/password/zpasswd.py CHANGES.rst
src/zope/password/zpasswd.py CHANGES.rst