tldr
Tenacitygood. [Docs here]((https://tenacity.readthedocs.io/en/latest/).- This AWS article is necessary reading if you build software that integrates with other software/services.
- The biggest thing missed by the Tenacity docs is the
retry_stateclass and what it gives you. The best place to rectify this is the source code.
Make better API requests
You're writing Python. You're slinging API requests. Maybe to integrate with a vendor, or you're gluing different services together.
Maybe things are smooth sailing and the API is nice and friendly. Maybe you're losing your mind with lack of availability, failed requests, too many retries...and so on, because you're either not retrying, or your for loop keeps failing to help.
Please, please, please, learn to use Tenacity.
This is just to give you a taste of what the library can do and how cool it is. It's not a guide.
What's the problem?
Say you have this kind of thing going on:
from my_integration import hit_api
from random import randint
MAX_TRIES = 3
for try_num in range(MAX_TRIES):
response_data, response_code = hit_api()
if response_code == 200:
break
if try_num == (MAX_TRIES - 1):
print("API failed to return data!")
# do some other stuff. Hopefully.
Or, God forbid, something like this:
response_data, response_code = None, None
while response_code != 200:
hit_api()
You can tell what's wrong with these, right?
The second hits the API over and over again, forever, as quickly as the computer(s) and/or network will allow. Put yourself in the API vendor's shoes: how would you respond to someone making thousands of requests over the course of just a few minutes? The right answer is probably to block them.
The first works a bit better, but it lends itself to a few problems:
- It allows no other success case but 200. This might be valid depending on your context, but from my experience, it probably isn't.
- All errors are treated as equal. For it to make sense for you to retry your request in an automated fashion, you have to actually be sure that your requests are valid; what's the point of retrying requests where it's your request that's bad, not the response?
- You're, again, retrying as quickly as the computer will let you. In other words, you have no wait strategy. You might get 429'd (Too Many Requests) doing that, especially if you need to make more than one call for different parameters or something.
There's a meta-problem here, too: you're being a bad client. Sure, if you're paying for an API with a contract saying "you can bash our servers with as many requests as you can dish out," then let it be. That's between you and your generous vendor. But if you're using a free API, you should really strive to not be such a dick. You should wait your turn, not overload servers, and limit your accesses to their generously free resources to just what you need.
You don't just have to want to be a good person, though
Maybe you don't even care and just, pragmatically, want your requests to return good data. Either way, tenacity will rescue you.
The engineering effort of adding tenacity is just as large as engineering your own retry strategies that actually work. It's also:
- A more easily reproducible pattern across a codebase
- More explicit in intent: the
@retrywrapper makes it super easy to find which functions have this behavior - Very flexible! There are a ton of different retry strategies and options for specific use cases, as well as decent defaults if you don't care to define those.
This is my invitation to you to read the project's README and documentation. But if you're short on time, the best part of the whole package—to me—is the wait strategies, which are really where you choose how to be a good neighbor. Specifically, I think a fantastic default is wait_random_exponential, which is inspired by this AWS technical blogpost that I highly, highly recommend you read if you are dealing with any kind of API/vendor/partner integrations. It's linked in the docs themselves.
On wrappers
Tenacity was my first introduction to wrappers/decorators in Python. I'd seen these funky things, but had never used them myself as a data engineer before Tenacity. When you do something like:
@retry(...)
def hit_api(...)
# stuff
result = hit_api()
This is basically equivalent to:
result = retry(hit_api())
So your hit_api function's return value gets passed through Tenacity's retry and you get access to all the neat tools it provides.
Btw, one of the neatest of those tools is retry_state. The docs aren't actually that great re: what retry_state actually contains. If you want to pull the thread on this—and I highly suggest you do, because it's how you'll access what was actually returned from failures via the retry_state.outcome attribute—check out the source code of the core class, RetryCallState.
Uh, that's it
I'm trying to write more, so I'm focusing on small articles like this one. This is even too long, really.
When I think back on my data engineering career thus far, some of my biggest projects have been integrations. Which is to say: gluing stuff together. Every decent integration has involved some kind of API, and every API has failed me at least once. (And I have failed them countless times.) Tenacity, as soon as I learned about it, became a mainstay in all my Python projects, and will remain so until maybe Claude ruins it.
Happy gluing!