Calm error handling for Python – decorator-based, zero boilerplate.
Writing repetitive try / except blocks clutters your code and hides the business logic.
tranq gives you declarative error handling with decorators, context managers, and a rich set of retry strategies – so you can focus on what your code does, not how it recovers from failures.
| Feature | Description |
|---|---|
| 🧘 Tranquil | Clean, readable, and maintainable. |
| 🔁 Smart retries | Exponential, linear, Fibonacci backoff, jitter, and max delay. |
| 🚦 Circuit Breaker | Prevent cascading failures (sync & async). |
| 🧪 Conditional retry | On specific exceptions or result values. |
| 📦 Retry groups | All-or-nothing execution for multiple functions. |
| 📊 Built‑in metrics & profiling | Monitor performance and error rates. |
| 📝 Pluggable reporters | Send errors to files, Sentry, Slack, or custom destinations. |
| 🧩 Context manager API | Use tranq.retry(...) when decorators aren't ideal. |
| 🔧 Stateful retry | Persist attempt count across calls. |
| 🎭 Mock error injection | Test your error handling with ease. |
pip install tranqRequires Python 3.9 or later.
import tranq
@tranq.handle(on=ValueError, retry=3, delay=0.5, backoff=2.0)
def risky():
# This will be retried up to 3 times with exponential backoff
...@tranq.handle_async(on=ConnectionError, retry=2, fallback=lambda: "offline")
async def fetch_data():
...cb = tranq.CircuitBreaker(failure_threshold=5, timeout=60)
@tranq.handle(circuit_breaker=cb)
def call_unstable_service():
...with tranq.retry(on=ValueError, retry=2) as ctx:
result = ctx.run(my_function, arg1, arg2)group = tranq.retry_group(step1, step2, step3, on=Exception, retry=1)
results = group.run() # if any step fails, all are retried togetherChoose from exponential, linear, or Fibonacci backoff. Add jitter to avoid thundering herds.
@tranq.handle(
on=TimeoutError,
retry=5,
delay=0.1,
backoff=2.0,
backoff_strategy="exponential", # "linear", "fibonacci", or custom callable
max_delay=10.0,
jitter=True,
)
def fetch():
...retry_if– retry only when the exception matches a condition.retry_on_result– retry if the result is unacceptable (e.g.,None).
@tranq.handle(
on=requests.RequestException,
retry_if=lambda e: e.response.status_code == 429, # rate‑limit
retry=3,
)
def call_api():
...
@tranq.handle(
retry_on_result=lambda result: result is None,
retry=2,
)
def get_data():
...Run different callbacks for different exception types.
def log_warning(e):
print(f"Warning: {e}")
def alert_admin(e):
send_alert(e)
@tranq.handle(
on=(ValueError, ConnectionError),
on_error={ValueError: log_warning, ConnectionError: alert_admin},
)
def process():
...Prevent repeated calls to a failing service. Available as CircuitBreaker (sync) and AsyncCircuitBreaker (async).
from tranq import CircuitBreaker, AsyncCircuitBreaker
# Sync
cb = CircuitBreaker(failure_threshold=3, timeout=30, half_open_requests=1)
@tranq.handle(circuit_breaker=cb)
def sync_call():
...
# Async
acb = AsyncCircuitBreaker(failure_threshold=3, timeout=30)
@tranq.handle_async(circuit_breaker=acb)
async def async_call():
...Persist the attempt counter across multiple invocations – useful for batch processing.
@tranq.handle(on=ValueError, retry=3, stateful=True)
def process_item(item):
# If it fails, the next call continues from the same attempt number
...Send error details to files, Sentry, Slack, or your own reporter.
from tranq import FileReporter, SentryReporter, SlackReporter
reporters = [
FileReporter("/var/log/tranq_errors.log"),
SentryReporter(dsn="..."),
SlackReporter(webhook_url="..."),
]
@tranq.handle(on=Exception, reporters=reporters)
def critical_task():
...Implement your own by subclassing Reporter and defining report(exception, context).
- Metrics – track call count, error count, and total duration (enable with
metrics=True). - Profiling – use the
@profiledecorator to measure function runtime.
@tranq.handle(metrics=True, metric_prefix="myapp")
def expensive_op():
...
from tranq import get_metrics, profile, get_profile
@profile
def heavy_computation():
...
print(get_metrics()) # all metric data
print(get_profile("heavy_computation")) # calls, total_durationInject errors with a given probability to test your error‑handling logic.
from tranq import mock_errors
with mock_errors(ValueError, probability=0.8):
# 80% of the time, ValueError is raised inside this block
result = my_function()Pass runtime dependencies directly into your decorated function.
@tranq.handle(inject={"logger": logging.getLogger("app")})
def do_work(logger=None):
logger.info("Working...")cb = CircuitBreaker(failure_threshold=3, timeout=60)
@tranq.handle(
on=requests.RequestException,
retry=5,
backoff_strategy="fibonacci",
max_delay=30,
jitter=True,
retry_if=lambda e: e.response.status_code in (429, 503),
circuit_breaker=cb,
metrics=True,
metric_prefix="api",
reporters=[FileReporter("api_errors.log")],
fallback=lambda: {"status": "fallback"},
)
def fetch_from_external_api():
...from tranq import retry_group, async_retry_group
def step1(): ...
def step2(): ...
async def step3(): ...
# Sync group (all functions must be sync)
group = retry_group(step1, step2, on=ValueError, retry=2)
results = group.run()
# Async group – mix sync and async functions
async_group = async_retry_group(step1, step3, on=Exception, retry=1)
results = await async_group.run()| Decorator | Description |
|---|---|
handle(...) |
Sync error‑handling decorator |
handle_async(...) |
Async error‑handling decorator |
| API | Description |
|---|---|
retry(...) |
Context manager for retry logic |
| API | Description |
|---|---|
retry_group(*funcs, **kwargs) |
All‑or‑nothing sync retry group |
async_retry_group(*funcs, **kwargs) |
All‑or‑nothing async retry group |
| Class | Description |
|---|---|
CircuitBreaker(failure_threshold, timeout, half_open_requests) |
Sync circuit breaker |
AsyncCircuitBreaker(...) |
Async circuit breaker |
| Function / Class | Description |
|---|---|
Policy |
Dataclass with all configurable parameters |
set_global_policy(policy) |
Set a default policy for all decorators |
get_global_policy() |
Retrieve the global policy |
| Class | Description |
|---|---|
Reporter |
Abstract base class |
FileReporter(file_path) |
Log errors to a file |
SentryReporter(dsn) |
Send errors to Sentry |
SlackReporter(webhook_url) |
Send errors to Slack |
| Function | Description |
|---|---|
get_metrics() / reset_metrics() |
Read / reset metric data |
profile(func) / get_profile(name=None) |
Profile a function and retrieve results |
mock_errors(exception, probability) |
Inject errors for testing |
| Exception | Description |
|---|---|
TranqError |
Base exception |
RetryExhaustedError |
Raised when retries are exhausted and reraise=True |
CircuitBreakerError |
Raised when circuit is open |
ResultNotAcceptedError |
Raised when retry_on_result condition fails |
RetryGroupError |
Raised by retry groups on failure |
Contributions are welcome! Please open an issue or submit a pull request.
- Fork the repository.
- Create a feature branch.
- Install development dependencies:
pip install -e '.[dev]' - Run tests:
pytest - Submit a PR.
For details, see CONTRIBUTING.md.
MIT © RaptorVampire
Inspired by libraries like tenacity and backoff, but built with a focus on simplicity, modern Python features, and a consistent API for both sync and async code.
🧘 Happy error handling! 🧘