diff --git a/.travis.yml b/.travis.yml
index d9fd26a..11bc8dc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,6 +18,8 @@ matrix:
       env: TOXENV=pypy
     - python: pypy3
       env: TOXENV=pypy3
+    - python: 3.8
+      env: TOXENV=w_deps
     - python: 3.8
       env: TOXENV=cover
 
diff --git a/CHANGES.rst b/CHANGES.rst
index febccc4..aacba50 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -10,6 +10,8 @@
 
 - Sleep for delay on retrying using the delay parameter.
 
+- Retry ``transaction.interfaces.TransientError`` (if importable) by default.
+
 1.4 (2016-06-03)
 ----------------
 
diff --git a/repoze/retry/__init__.py b/repoze/retry/__init__.py
index fb03934..11effe0 100644
--- a/repoze/retry/__init__.py
+++ b/repoze/retry/__init__.py
@@ -6,6 +6,13 @@ import traceback
 from io import BytesIO
 from time import sleep
 
+# Avoid hard dependency on transaction.
+try:
+    from transaction.interfaces import TransientError
+except ImportError:
+    class TransientError(Exception):
+        pass
+
 # Avoid hard dependency on ZODB.
 try:
     from ZODB.POSException import ConflictError
@@ -43,7 +50,7 @@ class Retry:
         self.tries = tries
 
         if retryable is None:
-            retryable = (ConflictError, RetryException,)
+            retryable = (TransientError, ConflictError, RetryException,)
 
         if not isinstance(retryable, (list, tuple)):
             retryable = [retryable]
diff --git a/tox.ini b/tox.ini
index bc506a9..0dbc936 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,21 @@
 [tox]
 envlist = 
-    py27,pypy,py35,py36,py37,py38,pypy3,cover,docs
+    py27,pypy,py35,py36,py37,py38,pypy3,w_deps,cover,docs
 
 [testenv]
 commands = 
     python setup.py -q test -q
 
+[testenv:w_deps]
+basepython =
+    python3.8
+deps =
+    transaction
+    ZODB
+    Zope
+commands = 
+    python setup.py -q test -q
+
 [testenv:cover]
 basepython =
     python3.8

Use ``transaction.interfaces.TransientError`` rather than ``ZODB.POSException.ConflictError``
`transaction.interfaces.TransientError` indicates that a transactions should be retried.  It's a generalization of ConflictError provided so that tools like `repoze.retry` don't have to depend on ZODB.

I'm in the process of making ZEO `ClientDisconnected` errors transient errors so that short disconnection due to server restart don't necessarily result in client errors.

(Someone should have fixed `ZPublisher.Publish.Retry` to subclass `TransientError` a long time ago. :( )

diff --git a/repoze/retry/tests.py b/repoze/retry/tests.py
index f9e3281..3166a4b 100644
--- a/repoze/retry/tests.py
+++ b/repoze/retry/tests.py
@@ -1,7 +1,35 @@
 import unittest
 
+try:
+    import transaction
+except ImportError:
+    _HAVE_TRANSACTION = False
+else:
+    _HAVE_TRANSACTION = True
+
+try:
+    import ZODB
+except ImportError:
+    _HAVE_ZODB = False
+else:
+    _HAVE_ZODB = True
+
+try:
+    import ZPublisher
+except ImportError:
+    _HAVE_ZPUBLISHER = False
+else:
+    _HAVE_ZPUBLISHER = True
+
+
 class CEBase:
 
+    def _getTransientError(self):
+        from repoze.retry import TransientError
+        return TransientError
+
+    TransientError = property(_getTransientError,)
+
     def _getConflictError(self):
         from repoze.retry import ConflictError
         return ConflictError
@@ -53,6 +81,30 @@ class RetryTests(unittest.TestCase, CEBase):
         env['wsgi.errors'] = StringIO()
         return env
 
+    @unittest.skipUnless(_HAVE_TRANSACTION, "Needs transaction")
+    def test_ctor_defaults_w_transaction(self):
+        from transaction.interfaces import TransientError
+        application = DummyApplication(conflicts=0)
+        retry = self._makeOne(application, tries=4)
+        if hasattr(retry, 'retryable'):
+            self.assertIn(TransientError, retry.retryable)
+
+    @unittest.skipUnless(_HAVE_ZODB, "Needs ZODB")
+    def test_ctor_defaults_w_zodb(self):
+        from ZODB.POSException import ConflictError
+        application = DummyApplication(conflicts=0)
+        retry = self._makeOne(application, tries=4)
+        if hasattr(retry, 'retryable'):
+            self.assertIn(ConflictError, retry.retryable)
+
+    @unittest.skipUnless(_HAVE_ZPUBLISHER, "Needs ZPublisher")
+    def test_ctor_defaults_w_zpublisher(self):
+        from ZPublisher.Publish import Retry as RetryException
+        application = DummyApplication(conflicts=0)
+        retry = self._makeOne(application, tries=4)
+        if hasattr(retry, 'retryable'):
+            self.assertIn(RetryException, retry.retryable)
+
     def test_retryable_is_not_sequence(self):
         application = DummyApplication(conflicts=1)
         retry = self._makeOne(application, tries=4,
@@ -367,8 +419,12 @@ class FactoryTests(unittest.TestCase, CEBase):
         self.assertEqual(middleware.tries, 3)
         self.assertEqual(middleware.tries, 3)
         self.assertEqual(middleware.log_after_try_count, 1)
-        self.assertEqual(middleware.retryable,
-                         (self.ConflictError, self.RetryException))
+        expected = [
+            self.TransientError,
+            self.ConflictError,
+            self.RetryException,
+        ]
+        self.assertEqual(middleware.retryable, tuple(expected))
 
     def test_make_retry_override_tries(self):
         from repoze.retry import make_retry #FUT
@@ -376,8 +432,12 @@ class FactoryTests(unittest.TestCase, CEBase):
         middleware = make_retry(app, {}, tries=4)
         self.assertTrue(middleware.application is app)
         self.assertEqual(middleware.tries, 4)
-        self.assertEqual(middleware.retryable,
-                         (self.ConflictError, self.RetryException))
+        expected = [
+            self.TransientError,
+            self.ConflictError,
+            self.RetryException,
+        ]
+        self.assertEqual(middleware.retryable, tuple(expected))
 
     def test_make_retry_override_tries_write_error(self):
         from repoze.retry import make_retry #FUT

pip install -e .[testing]
parse_log_pytest
pytest
nose coverage pytest
3.8
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.6.1
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
nose==1.3.7
packaging @ file:///croot/packaging_1720101850331/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042571233/work
pytest @ file:///croot/pytest_1717793244625/work
-e git+https://github.com/repoze/repoze.retry.git@f538d407852f3afbbf591aaecf9e543f986261e5#egg=repoze.retry
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: repoze.retry
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=py38h06a4308_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.1=py38h06a4308_0
  - pip=24.2=py38h06a4308_0
  - pluggy=1.0.0=py38h06a4308_1
  - pytest=7.4.4=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py38h06a4308_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.6.1
      - nose==1.3.7
prefix: /opt/conda/envs/repoze.retry

repoze/retry/tests.py::FactoryTests::test_make_retry_defaults repoze/retry/tests.py::FactoryTests::test_make_retry_override_tries
repoze/retry/tests.py::RetryTests::test_alternate_retryble_exception repoze/retry/tests.py::RetryTests::test_alternate_retryble_exceptions repoze/retry/tests.py::RetryTests::test_broken_pipe repoze/retry/tests.py::RetryTests::test_conflict_not_raised_start_response_called repoze/retry/tests.py::RetryTests::test_conflict_not_raised_start_response_not_called repoze/retry/tests.py::RetryTests::test_conflict_raised_start_response_called repoze/retry/tests.py::RetryTests::test_conflict_raised_start_response_not_called repoze/retry/tests.py::RetryTests::test_delay repoze/retry/tests.py::RetryTests::test_empty_content_length repoze/retry/tests.py::RetryTests::test_errors_written_after_2nd_try_when_set repoze/retry/tests.py::RetryTests::test_errors_written_after_first_retry_by_default repoze/retry/tests.py::RetryTests::test_io_error repoze/retry/tests.py::RetryTests::test_io_timeout_error_chunked_read repoze/retry/tests.py::RetryTests::test_largechunksize repoze/retry/tests.py::RetryTests::test_no_errors_written_on_first_retry_when_set repoze/retry/tests.py::RetryTests::test_over_highwater repoze/retry/tests.py::RetryTests::test_retryable_is_not_sequence repoze/retry/tests.py::RetryTests::test_socket_timeout_error repoze/retry/tests.py::RetryTests::test_socket_timeout_error_chunked_read repoze/retry/tests.py::RetryTests::test_wsgi_input_seeked_to_zero_on_conflict_withcontentlen repoze/retry/tests.py::WSGIConformanceTests::test_alternate_retryble_exception repoze/retry/tests.py::WSGIConformanceTests::test_alternate_retryble_exceptions repoze/retry/tests.py::WSGIConformanceTests::test_broken_pipe repoze/retry/tests.py::WSGIConformanceTests::test_conflict_not_raised_start_response_called repoze/retry/tests.py::WSGIConformanceTests::test_conflict_not_raised_start_response_not_called repoze/retry/tests.py::WSGIConformanceTests::test_conflict_raised_start_response_called repoze/retry/tests.py::WSGIConformanceTests::test_conflict_raised_start_response_not_called repoze/retry/tests.py::WSGIConformanceTests::test_delay repoze/retry/tests.py::WSGIConformanceTests::test_empty_content_length repoze/retry/tests.py::WSGIConformanceTests::test_errors_written_after_2nd_try_when_set repoze/retry/tests.py::WSGIConformanceTests::test_errors_written_after_first_retry_by_default repoze/retry/tests.py::WSGIConformanceTests::test_io_error repoze/retry/tests.py::WSGIConformanceTests::test_io_timeout_error_chunked_read repoze/retry/tests.py::WSGIConformanceTests::test_largechunksize repoze/retry/tests.py::WSGIConformanceTests::test_no_errors_written_on_first_retry_when_set repoze/retry/tests.py::WSGIConformanceTests::test_over_highwater repoze/retry/tests.py::WSGIConformanceTests::test_retryable_is_not_sequence repoze/retry/tests.py::WSGIConformanceTests::test_socket_timeout_error repoze/retry/tests.py::WSGIConformanceTests::test_socket_timeout_error_chunked_read repoze/retry/tests.py::WSGIConformanceTests::test_wsgi_input_seeked_to_zero_on_conflict_withcontentlen repoze/retry/tests.py::FactoryTests::test_make_retry_override_retryable_multiple repoze/retry/tests.py::FactoryTests::test_make_retry_override_retryable_one repoze/retry/tests.py::FactoryTests::test_make_retry_override_tries_write_error
repoze/retry/__init__.py .travis.yml tox.ini CHANGES.rst
repoze/retry/__init__.py .travis.yml tox.ini CHANGES.rst