tseaver: @icemac I'm not sure what test would be needed:  the externally-visible behavior of the method has not changed.  Or is your point that `sort_sequence` doesn't have appropriate coverage?

And likewise, a changelog entry shouldn't be needed since no externally-visible change has occurred.
diff --git a/CHANGES.rst b/CHANGES.rst
index 18c8924..209e47d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,6 +9,8 @@ Changelog
 
 - No longer use icons which got deleted in Zope 4.
 
+- Fix sorting in <dtml-in> for duplicate entries in Python 3.
+
 
 3.0b2 (2017-11-03)
 ------------------
diff --git a/src/DocumentTemplate/DT_In.py b/src/DocumentTemplate/DT_In.py
index f9b8150..4c6872d 100644
--- a/src/DocumentTemplate/DT_In.py
+++ b/src/DocumentTemplate/DT_In.py
@@ -330,6 +330,7 @@
 
 '''
 
+from operator import itemgetter
 import sys
 import re
 
@@ -837,7 +838,10 @@ class InClass(object):
             by = SortBy(multsort, sf_list)
             s.sort(by)
         else:
-            s.sort()
+            # In python 3 a key is required when tuples in the list have
+            # the same sort key to prevent attempting to compare the second
+            # item which is dict.
+            s.sort(key=itemgetter(0))
 
         sequence = []
         for k, client in s:
diff --git a/src/TreeDisplay/TreeTag.py b/src/TreeDisplay/TreeTag.py
index 69e28b8..414200c 100644
--- a/src/TreeDisplay/TreeTag.py
+++ b/src/TreeDisplay/TreeTag.py
@@ -395,10 +395,10 @@ def tpRenderTABLE(self, id, root_url, url, state, substate, diff, data,
             if exp:
                 ptreeData['tree-item-expanded'] = 1
                 output('<a name="%s" href="%s?%stree-c=%s#%s">-</a>' %
-                       (id, root_url, param, s, id, script))
+                       (id, root_url, param, s, id))
             else:
                 output('<a name="%s" href="%s?%stree-e=%s#%s">+</a>' %
-                       (id, root_url, param, s, id, script))
+                       (id, root_url, param, s, id))
             output('</td>\n')
 
         else:

<dtml-in list_of_dicts mapping sort=name> breaks for duplicate names
Scenario:

```python
list_of_dicts = [{'name': 'a'}, {'name': 'b'}, {'name': 'a'}]
```

In DTML call:

```
<dtml-in list_of_dicts mapping sort=name>
```

It breaks because internally `dtml-in` changes the list to

```python
[('a', {'name': 'a'}), ('b', {'name': 'b'}), ('a', {'name': 'a'})]
```

There is no problem as long as the list does not contain duplicates of the sort key. If there are duplicates, Python has to compare the dicts when sorting. This works fine on Python 2 but it is no loner defined on Python 3.
diff --git a/src/DocumentTemplate/tests/test_DT_In.py b/src/DocumentTemplate/tests/test_DT_In.py
new file mode 100644
index 0000000..ba517d2
--- /dev/null
+++ b/src/DocumentTemplate/tests/test_DT_In.py
@@ -0,0 +1,34 @@
+import unittest
+
+
+class DummySection(object):
+    blocks = ['dummy']
+
+
+class TestIn(unittest.TestCase):
+    """Testing ..DT_in.InClass."""
+
+    def _getTargetClass(self):
+        from DocumentTemplate.DT_In import InClass
+        return InClass
+
+    def _makeOne(self, *args):
+        blocks = [('in', ' '.join(args), DummySection())]
+        return self._getTargetClass()(blocks)
+
+    def test_sort_sequence(self):
+        """It does not break on duplicate sort keys at a list of dicts."""
+        stmt = self._makeOne('seq', 'mapping', 'sort=key')
+        seq = [
+            {'key': 'c', 'data': '3'},
+            {'key': 'a', 'data': '1'},
+            {'key': 'b', 'data': '2'},
+            {'key': 'a', 'data': '2'},
+        ]
+        result = stmt.sort_sequence(seq, 'key')
+        self.assertEqual([
+            {'key': 'a', 'data': '1'},
+            {'key': 'a', 'data': '2'},
+            {'key': 'b', 'data': '2'},
+            {'key': 'c', 'data': '3'},
+        ], result)
diff --git a/src/DocumentTemplate/tests/test_DT_Var.py b/src/DocumentTemplate/tests/test_DT_Var.py
index 3270824..c81051f 100644
--- a/src/DocumentTemplate/tests/test_DT_Var.py
+++ b/src/DocumentTemplate/tests/test_DT_Var.py
@@ -73,11 +73,11 @@ class TestUrlQuoting(unittest.TestCase):
         utf8_value = unicode_value.encode('UTF-8')
         quoted_utf8_value = b'G%C3%BCnther%20M%C3%BCller'
 
-        self.assertEquals(url_quote(unicode_value), quoted_unicode_value)
-        self.assertEquals(url_quote(utf8_value), quoted_utf8_value)
+        self.assertEqual(url_quote(unicode_value), quoted_unicode_value)
+        self.assertEqual(url_quote(utf8_value), quoted_utf8_value)
 
-        self.assertEquals(url_unquote(quoted_unicode_value), unicode_value)
-        self.assertEquals(url_unquote(quoted_utf8_value), utf8_value)
+        self.assertEqual(url_unquote(quoted_unicode_value), unicode_value)
+        self.assertEqual(url_unquote(quoted_utf8_value), utf8_value)
 
     def test_url_quoting_plus(self):
         from DocumentTemplate.DT_Var import url_quote_plus
@@ -87,10 +87,10 @@ class TestUrlQuoting(unittest.TestCase):
         utf8_value = unicode_value.encode('UTF-8')
         quoted_utf8_value = b'G%C3%BCnther+M%C3%BCller'
 
-        self.assertEquals(url_quote_plus(unicode_value), quoted_unicode_value)
-        self.assertEquals(url_quote_plus(utf8_value), quoted_utf8_value)
+        self.assertEqual(url_quote_plus(unicode_value), quoted_unicode_value)
+        self.assertEqual(url_quote_plus(utf8_value), quoted_utf8_value)
 
-        self.assertEquals(
+        self.assertEqual(
             url_unquote_plus(quoted_unicode_value), unicode_value)
-        self.assertEquals(
+        self.assertEqual(
             url_unquote_plus(quoted_utf8_value), utf8_value)

pip install -e .
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
AccessControl==5.7
Acquisition==4.13
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
AuthEncoding==4.3
BTrees==4.11.3
certifi==2021.5.30
cffi==1.15.1
DateTime==4.9
-e git+https://github.com/zopefoundation/DocumentTemplate.git@6af41a0957407a40e02210926c63810d7bbe53ff#egg=DocumentTemplate
ExtensionClass==4.9
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
multipart==1.1.0
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
Persistence==3.6
persistent==4.9.3
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycparser==2.21
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
python-gettext==4.1
pytz==2025.2
RestrictedPython==6.2
six==1.17.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
transaction==3.1.0
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
zExceptions==4.3
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work
zope.browser==2.4
zope.component==5.1.0
zope.configuration==4.4.1
zope.contenttype==4.6
zope.deferredimport==4.4
zope.deprecation==4.4.0
zope.event==4.6
zope.exceptions==4.6
zope.hookable==5.4
zope.i18n==4.9.0
zope.i18nmessageid==5.1.1
zope.interface==5.5.2
zope.location==4.3
zope.proxy==4.6.1
zope.publisher==6.1.0
zope.schema==6.2.1
zope.security==5.8
zope.sequencesort==4.2
zope.structuredtext==4.4
zope.testing==5.0.1

name: DocumentTemplate
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
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - accesscontrol==5.7
      - acquisition==4.13
      - authencoding==4.3
      - btrees==4.11.3
      - cffi==1.15.1
      - datetime==4.9
      - extensionclass==4.9
      - multipart==1.1.0
      - persistence==3.6
      - persistent==4.9.3
      - pycparser==2.21
      - python-gettext==4.1
      - pytz==2025.2
      - restrictedpython==6.2
      - six==1.17.0
      - transaction==3.1.0
      - zexceptions==4.3
      - zope-browser==2.4
      - zope-component==5.1.0
      - zope-configuration==4.4.1
      - zope-contenttype==4.6
      - zope-deferredimport==4.4
      - zope-deprecation==4.4.0
      - zope-event==4.6
      - zope-exceptions==4.6
      - zope-hookable==5.4
      - zope-i18n==4.9.0
      - zope-i18nmessageid==5.1.1
      - zope-interface==5.5.2
      - zope-location==4.3
      - zope-proxy==4.6.1
      - zope-publisher==6.1.0
      - zope-schema==6.2.1
      - zope-security==5.8
      - zope-sequencesort==4.2
      - zope-structuredtext==4.4
      - zope-testing==5.0.1
prefix: /opt/conda/envs/DocumentTemplate

src/DocumentTemplate/tests/test_DT_In.py::TestIn::test_sort_sequence
src/DocumentTemplate/tests/test_DT_Var.py::TestNewlineToBr::test_newline_to_br src/DocumentTemplate/tests/test_DT_Var.py::TestNewlineToBr::test_newline_to_br_tainted src/DocumentTemplate/tests/test_DT_Var.py::TestUrlQuoting::test_url_quoting src/DocumentTemplate/tests/test_DT_Var.py::TestUrlQuoting::test_url_quoting_plus
src/TreeDisplay/TreeTag.py src/DocumentTemplate/DT_In.py CHANGES.rst
src/TreeDisplay/TreeTag.py src/DocumentTemplate/DT_In.py CHANGES.rst