A flaky function — fails the first two calls, succeeds on the third. Naive code crashes immediately. With retry, it succeeds.
import time
attempts = {"n": 0}
def flaky():
attempts["n"] += 1
if attempts["n"] < 3:
raise RuntimeError("transient")
return "ok"
last_error = None
for attempt in range(1, 4):
try:
result = flaky()
break
except Exception as e:
last_error = e
time.sleep(2 ** attempt) # 2, 4, 8 seconds
else:
raise last_error
print(result) # 'ok'The wrapper attempted 3 times, slept exponentially between attempts, returned the result.
Why exponential — 2, 4, 8 — instead of 1 second flat?
Backoff. If the service is overloaded, hammering it every second makes it worse. Doubling gives the service room to recover. The first retry is fast (most blips clear quickly); successive retries get patient.
And for/else — what's the else doing?
Python quirk: an else on a for loop runs only if the loop completed without a break. Inside the try, success calls break. If no attempt succeeds, no break — else fires and re-raises the last error. Idiomatic for retry loops.
What about real Composio calls — same shape?
Same shape. Wrap the toolset.execute_action(...) in the same retry pattern. We use a fake function today so we can guarantee failure on attempts 1-2 — real Gmail rarely flakes on demand. The pattern is identical.
import time
last_error = None
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
result = SOMETHING_FLAKY()
break
except Exception as e:
last_error = e
time.sleep(2 ** attempt)
else:
raise last_error
use(result)for attempt in range(1, MAX+1) — bounded loop; never infinite.try/except — catch the failure; don't re-raise inside the loop.break on success — exit early when it works.time.sleep(2 ** attempt) — exponential backoff: 2, 4, 8, 16 ...for/else: raise last_error — if no attempt succeeded (no break), re-raise.Retry only on transient failures — failures that should clear up on their own.
| Error | Retry? | Why |
|---|---|---|
ConnectionError, TimeoutError | yes | Network blip — usually clears |
| 429 / Too Many Requests | yes (with longer backoff) | Rate limited — wait and try |
| 5xx server errors | yes | Service problem — usually clears |
KeyError, ValueError | no | Bug in your code — won't fix itself |
| 401 / 403 | no | Credentials wrong — escalate, don't loop |
Day 7's lesson on error classes is the foundation here. Catch specific transient classes and re-raise everything else immediately:
try:
SOMETHING()
except (ConnectionError, TimeoutError) as e:
last_error = e
time.sleep(2 ** attempt)For v1 lessons we use the broad except Exception so the lesson stays small. Production scripts narrow it.
Three is a sensible default. Beyond that, you're masking a real problem — escalate to a human alert (day 18) instead of looping forever.
If 1000 clients all retry at exactly 2 ** attempt, they synchronize and overload the service in waves. Add a random offset:
import random
time.sleep(2 ** attempt + random.uniform(0, 1))Not needed for one-script-on-one-machine. Useful when you have many automation scripts running simultaneously.
A flaky function — fails the first two calls, succeeds on the third. Naive code crashes immediately. With retry, it succeeds.
import time
attempts = {"n": 0}
def flaky():
attempts["n"] += 1
if attempts["n"] < 3:
raise RuntimeError("transient")
return "ok"
last_error = None
for attempt in range(1, 4):
try:
result = flaky()
break
except Exception as e:
last_error = e
time.sleep(2 ** attempt) # 2, 4, 8 seconds
else:
raise last_error
print(result) # 'ok'The wrapper attempted 3 times, slept exponentially between attempts, returned the result.
Why exponential — 2, 4, 8 — instead of 1 second flat?
Backoff. If the service is overloaded, hammering it every second makes it worse. Doubling gives the service room to recover. The first retry is fast (most blips clear quickly); successive retries get patient.
And for/else — what's the else doing?
Python quirk: an else on a for loop runs only if the loop completed without a break. Inside the try, success calls break. If no attempt succeeds, no break — else fires and re-raises the last error. Idiomatic for retry loops.
What about real Composio calls — same shape?
Same shape. Wrap the toolset.execute_action(...) in the same retry pattern. We use a fake function today so we can guarantee failure on attempts 1-2 — real Gmail rarely flakes on demand. The pattern is identical.
import time
last_error = None
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
result = SOMETHING_FLAKY()
break
except Exception as e:
last_error = e
time.sleep(2 ** attempt)
else:
raise last_error
use(result)for attempt in range(1, MAX+1) — bounded loop; never infinite.try/except — catch the failure; don't re-raise inside the loop.break on success — exit early when it works.time.sleep(2 ** attempt) — exponential backoff: 2, 4, 8, 16 ...for/else: raise last_error — if no attempt succeeded (no break), re-raise.Retry only on transient failures — failures that should clear up on their own.
| Error | Retry? | Why |
|---|---|---|
ConnectionError, TimeoutError | yes | Network blip — usually clears |
| 429 / Too Many Requests | yes (with longer backoff) | Rate limited — wait and try |
| 5xx server errors | yes | Service problem — usually clears |
KeyError, ValueError | no | Bug in your code — won't fix itself |
| 401 / 403 | no | Credentials wrong — escalate, don't loop |
Day 7's lesson on error classes is the foundation here. Catch specific transient classes and re-raise everything else immediately:
try:
SOMETHING()
except (ConnectionError, TimeoutError) as e:
last_error = e
time.sleep(2 ** attempt)For v1 lessons we use the broad except Exception so the lesson stays small. Production scripts narrow it.
Three is a sensible default. Beyond that, you're masking a real problem — escalate to a human alert (day 18) instead of looping forever.
If 1000 clients all retry at exactly 2 ** attempt, they synchronize and overload the service in waves. Add a random offset:
import random
time.sleep(2 ** attempt + random.uniform(0, 1))Not needed for one-script-on-one-machine. Useful when you have many automation scripts running simultaneously.
Create a free account to get started. Paid plans unlock all tracks.