SomberNight: Looks OK, and seems to work for me too. Thanks!
diff --git a/hashin.py b/hashin.py
index cbc998c..a43390e 100755
--- a/hashin.py
+++ b/hashin.py
@@ -246,7 +246,7 @@ def run_packages(
         maybe_restriction = "" if not restriction else "; {0}".format(restriction)
         new_lines = "{0}=={1}{2} \\\n".format(req, data["version"], maybe_restriction)
         padding = " " * 4
-        for i, release in enumerate(data["hashes"]):
+        for i, release in enumerate(sorted(data["hashes"], key=lambda r: r["hash"])):
             new_lines += "{0}--hash={1}:{2}".format(padding, algorithm, release["hash"])
             if i != len(data["hashes"]) - 1:
                 new_lines += " \\"

Order of hashes no longer canonical
Since version 0.14, the order of hashes does not seem to be deterministic.
Before 0.14, the hashes used to be lexicographically ordered.

I am guessing this was changed in https://github.com/peterbe/hashin/pull/95

What is the reason for this?

We use hashin in electrum for deterministic builds.
The [requirements files](https://github.com/spesmilo/electrum/blob/c7f3adb67ea6afd50fec60221ba711ddea110281/contrib/deterministic-build/requirements.txt) are generated by [this script](https://github.com/spesmilo/electrum/blob/c7f3adb67ea6afd50fec60221ba711ddea110281/contrib/freeze_packages.sh).
The `freeze_packages.sh` script is manually rerun to update dependencies (see e.g. https://github.com/spesmilo/electrum/commit/ba4af29bf82f7addb68c3ed996002665313ab223).
If the order of hashes for a given package is not canonical, then rerunning the script can result in changes for packages that have not actually been updated, making the diff considerably less readable.
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 459694f..e494a33 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -461,7 +461,7 @@ autocompeter==1.2.3 \\
     assert result == new_lines[2]
 
 
-def test_amend_requirements_content_replacement_amonst_others():
+def test_amend_requirements_content_replacement_amongst_others():
     previous = (
         """
 otherpackage==1.0.0 --hash=sha256:cHay6ATFKumO3svU3B-8qBMYb-f1_dYlR4OgClWntEI
@@ -489,7 +489,7 @@ autocompeter==1.2.3 \\
     assert result == previous + new_lines[2]
 
 
-def test_amend_requirements_content_replacement_amonst_others_2():
+def test_amend_requirements_content_replacement_amongst_others_2():
     previous = (
         "https://github.com/rhelmer/pyinotify/archive/9ff352f.zip"
         "#egg=pyinotify "
@@ -609,7 +609,6 @@ def test_run(murlopen, tmpfile, capsys):
         # Now check the verbose output
         captured = capsys.readouterr()
         out_lines = captured.out.splitlines()
-        # out_lines = my_stdout.getvalue().splitlines()
         assert "https://pypi.org/pypi/hashin/json" in out_lines[0], out_lines[0]
         # url to download
         assert "hashin-0.10-py2-none-any.whl" in out_lines[1], out_lines[1]
@@ -647,6 +646,60 @@ def test_run(murlopen, tmpfile, capsys):
         ) in lines
 
 
+def test_canonical_list_of_hashes(murlopen, tmpfile, capsys):
+    """When hashes are written down, after the package spec, write down the hashes
+    in a canonical way. Essentially, in lexicographic order. But when comparing
+    existing hashes ignore any order.
+    """
+
+    def mocked_get(url, **options):
+        if url == "https://pypi.org/pypi/hashin/json":
+            return _Response(
+                {
+                    "info": {"version": "0.10", "name": "hashin"},
+                    "releases": {
+                        # NOTE that the order of list is different from
+                        # the order you'd get of `sorted(x.digest for x in releases)`
+                        "0.10": [
+                            {
+                                "url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
+                                "digests": {"sha256": "ccccc"},
+                            },
+                            {
+                                "url": "https://pypi.org/packages/2.7/p/hashin/hashin-0.10-py2-none-any.whl",
+                                "digests": {"sha256": "aaaaa"},
+                            },
+                            {
+                                "url": "https://pypi.org/packages/3.3/p/hashin/hashin-0.10-py3-none-any.whl",
+                                "digests": {"sha256": "bbbbb"},
+                            },
+                        ]
+                    },
+                }
+            )
+        raise NotImplementedError(url)
+
+    murlopen.side_effect = mocked_get
+
+    with tmpfile() as filename:
+        with open(filename, "w") as f:
+            f.write("")
+
+        retcode = hashin.run("hashin==0.10", filename, "sha256")
+
+        assert retcode == 0
+        with open(filename) as f:
+            output = f.read()
+        assert output
+        assert output.endswith("\n")
+        lines = output.splitlines()
+
+        assert lines[0] == "hashin==0.10 \\"
+        assert lines[1] == "    --hash=sha256:aaaaa \\"
+        assert lines[2] == "    --hash=sha256:bbbbb \\"
+        assert lines[3] == "    --hash=sha256:ccccc"
+
+
 def test_run_atomic_not_write_with_error_on_last_package(murlopen, tmpfile):
     def mocked_get(url, **options):
 
@@ -1623,7 +1676,7 @@ def test_run_comments_with_package_spec_patterns(murlopen, tmpfile, capsys):
 
 
 def test_run_dry(murlopen, tmpfile, capsys):
-    """dry run should edit the requirements.txt file and print
+    """dry run should not edit the requirements file and print
     hashes and package name in the console
     """
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest>=6.0.0 mock
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
backports.tarfile==1.2.0
cachetools==5.5.2
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.1
colorama==0.4.6
cryptography==44.0.2
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
filelock==3.18.0
-e git+https://github.com/peterbe/hashin.git@90f2804dc624a52cd9283bd62fead99b837a3c1f#egg=hashin
id==1.5.0
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
keyring==25.6.0
markdown-it-py==3.0.0
mdurl==0.1.2
mock==5.2.0
more-itertools==10.6.0
nh3==0.2.21
packaging==24.2
pip-api==0.0.34
platformdirs==4.3.7
pluggy==1.5.0
pycparser==2.22
Pygments==2.19.1
pyproject-api==1.9.0
pytest==8.3.5
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
tomli==2.2.1
tox==4.25.0
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
zipp==3.21.0

name: hashin
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
  - 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
  - pip=25.0=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
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - backports-tarfile==1.2.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - colorama==0.4.6
      - cryptography==44.0.2
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - id==1.5.0
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - keyring==25.6.0
      - markdown-it-py==3.0.0
      - mdurl==0.1.2
      - mock==5.2.0
      - more-itertools==10.6.0
      - nh3==0.2.21
      - packaging==24.2
      - pip-api==0.0.34
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycparser==2.22
      - pygments==2.19.1
      - pyproject-api==1.9.0
      - pytest==8.3.5
      - readme-renderer==44.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - tomli==2.2.1
      - tox==4.25.0
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - zipp==3.21.0
prefix: /opt/conda/envs/hashin

tests/test_cli.py::test_canonical_list_of_hashes
tests/test_cli.py::test_get_latest_version_simple tests/test_cli.py::test_get_latest_version_only_pre_release tests/test_cli.py::test_get_latest_version_non_pre_release_leading_zeros tests/test_cli.py::test_get_hashes_error tests/test_cli.py::test_non_200_ok_download tests/test_cli.py::test_main_packageerrors_stderr tests/test_cli.py::test_packages_and_update_all tests/test_cli.py::test_no_packages_and_not_update_all tests/test_cli.py::test_interactive_not_update_all tests/test_cli.py::test_main_version tests/test_cli.py::test_amend_requirements_content_new tests/test_cli.py::test_amend_requirements_different_old_name tests/test_cli.py::test_amend_requirements_content_multiple_merge tests/test_cli.py::test_amend_requirements_content_replacement tests/test_cli.py::test_amend_requirements_content_actually_not_replacement tests/test_cli.py::test_amend_requirements_content_replacement_addition tests/test_cli.py::test_amend_requirements_content_replacement_single_to_multi tests/test_cli.py::test_amend_requirements_content_replacement_2 tests/test_cli.py::test_amend_requirements_content_replacement_amongst_others tests/test_cli.py::test_amend_requirements_content_replacement_amongst_others_2 tests/test_cli.py::test_amend_requirements_content_new_similar_name tests/test_cli.py::test_run tests/test_cli.py::test_run_atomic_not_write_with_error_on_last_package tests/test_cli.py::test_run_interactive tests/test_cli.py::test_run_interactive_quit_and_accept_all tests/test_cli.py::test_run_interactive_case_redirect tests/test_cli.py::test_run_without_specific_version tests/test_cli.py::test_run_with_alternate_index_url tests/test_cli.py::test_run_contained_names tests/test_cli.py::test_run_case_insensitive tests/test_cli.py::test_run_update_all tests/test_cli.py::test_run_comments_with_package_spec_patterns tests/test_cli.py::test_run_dry tests/test_cli.py::test_run_dry_multiple_packages tests/test_cli.py::test_run_pep_0496 tests/test_cli.py::test_filter_releases tests/test_cli.py::test_release_url_metadata_python tests/test_cli.py::test_expand_python_version tests/test_cli.py::test_get_package_hashes tests/test_cli.py::test_get_package_hashes_from_alternate_index_url tests/test_cli.py::test_get_package_hashes_package_not_found tests/test_cli.py::test_get_package_hashes_unknown_algorithm tests/test_cli.py::test_get_package_hashes_without_version tests/test_cli.py::test_with_extras_syntax tests/test_cli.py::test_extras_syntax_edit tests/test_cli.py::test_add_extra_extras_syntax_edit tests/test_cli.py::test_change_extra_extras_syntax_edit tests/test_cli.py::test_remove_extra_extras_syntax_edit tests/test_cli.py::test_interactive_upgrade_request tests/test_cli.py::test_interactive_upgrade_request_repeat_question tests/test_cli.py::test_interactive_upgrade_request_help tests/test_cli.py::test_interactive_upgrade_request_force_yes
hashin.py
hashin.py