Skip to main content

Tubthumper: Helping you get up ... again!

CI/CD: n/a Docs: n/a Downloads: n/a PyPI: n/a codecov: n/a


What's in a name?

Tubthumper is a Python package of retry utilities named after the English anarcho-communist rock band Chumbawamba's 1997 hit Tubthumping. Yes, really.

I get knocked down, but I get up again. 🎶
You're never gonna keep me down. 🎶
I get knocked down, but I get up again. 🎶
You're never gonna keep me down... 🎶

Getting Started

Installation

tubthumper is a pip-installable package hosted on PyPI. Getting started is as easy as:

$ pip install tubthumper

tubthumper requires Python 3.9 or greater. For Python 3.10 or greater, it has no external dependencies, i.e. standard library only, but earlier versions require typing-extensions.

Usage

Import tubthumper's useful bits:

>>> from tubthumper import retry, retry_decorator, retry_factory

Call a function with retry and jittered exponential backoff:

>>> retry(get_ip, exceptions=ConnectionError)
WARNING: Function threw exception below on try 1, retrying in 0.844422 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Call that same function with positional and keyword arguments, e.g. retry get_ip(42, "test", dev=True):

>>> retry(get_ip,
...     args=(42, "test"), kwargs={"dev": True},
...     exceptions=ConnectionError)
WARNING: Function threw exception below on try 1, retrying in 0.420572 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Bake retry behavior into your function with a decorator:

>>> @retry_decorator(exceptions=ConnectionError)
... def get_ip_retry():
...     return requests.get("http://ip.jsontest.com").json()
>>> get_ip_retry()
WARNING: Function threw exception below on try 1, retrying in 0.511275 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Create a new function with retry behavior from an existing one:

>>> get_ip_retry = retry_factory(get_ip, exceptions=ConnectionError)
>>> get_ip_retry()
WARNING: Function threw exception below on try 1, retrying in 0.783799 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Customization

While tubthumper ships with a set of sensible defaults, its retry behavior is fully customizable.

Exceptions

Because overbroad except clauses are the most diabolical Python antipattern, there is no sensible default for what exception or exceptions to catch and retry. Thus, every tubthumper interface has a required exceptions keyword-only argument, which takes an exception or tuple of exceptions to catch and retry on, i.e. a sensible lack of a default.

>>> retry(get_ip, exceptions=ConnectionError)
WARNING: Function threw exception below on try 1, retrying in 0.476597 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}
>>> retry(get_ip, exceptions=(KeyError, ConnectionError))
WARNING: Function threw exception below on try 1, retrying in 0.908113 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

By default, tubthumper raises a tubthumper.RetryError exception when all retries have been exhausted:

>>> retry(lambda: 1/0, retry_limit=0, exceptions=ZeroDivisionError)
Traceback (most recent call last):
  ...
tubthumper._retry_factory.RetryError: Retry limit 0 reached

You can override this behavior using the reraise flag to reraise the original exception in place of RetryError:

>>> retry(lambda: 1/0, retry_limit=0, reraise=True, exceptions=ZeroDivisionError)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

Retry Limits

By default, tubthumper will retry endlessly, but you have two means of limiting retry behavior. As shown previously, to limit the number of retries attempted, use the retry_limit keyword-only argument:

>>> retry(lambda: 1/0, retry_limit=10, exceptions=ZeroDivisionError)
...  # Warning logs for each failed call
Traceback (most recent call last):
  ...
tubthumper._retry_factory.RetryError: Retry limit 10 reached

Alternatively, you can use the time_limit keyword-only argument to prevent retry attempts after a certain duration:

>>> retry(lambda: 1/0, time_limit=60, exceptions=ZeroDivisionError)
...  # Warning logs for each failed call
Traceback (most recent call last):
  ...
tubthumper._retry_factory.RetryError: Time limit 60 exceeded

Backoff timing

By default, the backoff duration doubles with each retry, starting off at one second. As well, each backoff period is jittered, i.e. scaled by a uniformly distributed random number on the [0.0, 1.0) interval. You can disable jittering using the jitter keyword-only argument:

>>> retry(get_ip, jitter=False, exceptions=ConnectionError)
WARNING: Function threw exception below on try 1, retrying in 1 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

You can set the initial backoff duration using the init_backoff keyword-only argument:

>>> retry(get_ip, jitter=False, init_backoff=10, exceptions=ConnectionError)
WARNING: Function threw exception below on try 1, retrying in 10 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Finally, you can set the factor by which each successive backoff duration is scaled using the exponential keyword-only argument:

>>> retry(get_ip, jitter=False, exponential=3, exceptions=ConnectionError)
WARNING: Function threw exception below on try 1, retrying in 1 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
WARNING: Function threw exception below on try 2, retrying in 3 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Logging

By default, tubthumper logs each caught exception at the logging.WARNING level using a logger named tubthumper, i.e. logging.getLogger("tubthumper"). As described in the Python logging tutorial, for this default logger, "events of severity WARNING and greater will be printed to sys.stderr" if no further logging is configured.

You can set the logging level using the log_level keyword-only argument:

>>> retry(get_ip, log_level=logging.DEBUG, exceptions=ConnectionError) # No warnings
{'ip': '8.8.8.8'}

You can provide your own logger using the logger keyword-only argument. This logger's log method will be called like so:

logger.log(log_level, "Function threw...", exc_info=True)

Features

Compatible with methods

tubthumper's various interfaces are compatible with methods, including classmethods and staticmethods:

>>> class Class:
...     @retry_decorator(exceptions=ConnectionError)
...     def get_ip(self):
...         return requests.get("http://ip.jsontest.com").json()
...
>>> Class().get_ip()
WARNING: Function threw exception below on try 1, retrying in 0.719705 seconds
Traceback (most recent call last):
  ...
requests.exceptions.ConnectionError: http://ip.jsontest.com
{'ip': '8.8.8.8'}

Signature preserving

tubthumper's various interfaces preserve the relevant dunder attributes of your function:

>>> @retry_decorator(exceptions=ConnectionError)
... def func(one: bool, two: float = 3.0) -> complex:
...     """This is a docstring"""
...
>>> func.__name__
'func'
>>> func.__qualname__
'func'
>>> func.__module__
'__main__'
>>> func.__doc__
'This is a docstring'
>>> func.__annotations__
{'one': <class 'bool'>, 'two': <class 'float'>, 'return': <class 'complex'>}

tubthumper also preserves the inspect module's function signature, and is* functions:

>>> import inspect
>>> inspect.signature(func)
<Signature (one: bool, two: float = 3.0) -> complex>
>>> inspect.isfunction(func)
True
>>> inspect.isroutine(func)
True
>>> inspect.ismethod(Class().get_ip)
True

Async support

tubthumper's various interfaces support coroutine functions, including generator-based coroutines, awaiting them while using async.sleep between awaits:

>>> @retry_decorator(exceptions=ConnectionError)
... async def get_ip():
...     return requests.get("http://ip.jsontest.com").json()
...
>>> inspect.iscoroutinefunction(get_ip)
True

Fully type annotated

tubthumper's various interfaces are fully type annotated, passing pyright.

100% Test Coverage

tubthumper achieves 100% test coverage across three supported operating systems (Windows, MacOS, & Linux). You can find the coverage report on Codecov.

Metadata

Release files for tubthumper 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tubthumper 0.3.0
File Size Uploaded Provenance
tubthumper-0.3.0.tar.gz 620.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for tubthumper 0.3.0
File Interpreter ABI Platform
tubthumper-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 633.8 kB

Release files / tubthumper-0.3.0.tar.gz

Download URL tubthumper-0.3.0.tar.gz
Size 620.5 kB
Tags Source
SHA-256 checksum
How to use checksums
3375f400cb089a0c5dc704b39bb9fc48cf1e6bdad09c7450683eb52684302d13
BLAKE2b-256 checksum
How to use checksums
b9de46064fcb23d2b78e3dbd26702e3ff409d36b6e00ebc36669ba8982f50e0b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.4.24

Release files / tubthumper-0.3.0-py3-none-any.whl

Download URL tubthumper-0.3.0-py3-none-any.whl
Size 13.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad65ff179e052ba02d79bd44ad0ceeb30d7cd6b56886cb19e7cbe152fe36f170
BLAKE2b-256 checksum
How to use checksums
ab26ef860bbfda6ec13f7f2cfabf482445f3b2704d1f892db122725f3ac86fcb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.4.24

Security for tubthumper 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page