Making Safe API Calls With Python
Tags: python, returns, api
Published: 2019-08-28 18:23:04

---
title: Making Safe API Calls With Python
published: true
description: A tutorial on using returns library to make safe API calls
tags: python, returns, APIs
---

Making REST API calls in python is quite common and easy especially due to existince of libraries such as [requests](http://docs.python-requests.org/en/master/).

Take the following example:
```python
def get_user(user_id):
    res = requests.get(f'https://jsonplaceholder.typicode.com/users/{user_id}')
    if res.ok:
        return res.json()
    else:
        print(f'{res.status_code}: {res.text}')
        return {}
```

The code is pretty straightforward. We are defining a function to fetch a user's details from an [API](https://jsonplaceholder.typicode.com) (A very handy web app for testing out web apps).

Simple. Right?

However, what happens when there is no connectivity? Exceptions are raised. 
This can easily be wrapped in a `try...except` clause but it makes our code messy, especially when we have multiple endpoints to call.

Enter [returns](https://returns.readthedocs.io/en/latest/index.html).

![Grand entrance](https://media.giphy.com/media/3oFzmpW5fjL3P45CXC/giphy.gif)

Returns is a library written with safety in mind. It's quite powerful and has a lot to offer. Now let's quickly fix our _unsafe_ code.
```python
class GetUser(object):

    @pipeline
    def __call__(self, user_id: int) -> Result[dict, Exception]:
        res = self._make_request(user_id).unwrap()
        return self._parse_json(res)

    @safe
    def _make_request(self, user_id: int) -> requests.Response:
        res = requests.get(f'https://jsonplaceholder.typicode.com/users/{user_id}')
        res.raise_for_status()
        return res

    @safe
    def _parse_json(self, res: requests.Response) -> dict:
        user = res.json()
        return user


get_user = GetUser()

```

We have defined a class which can be invoked as a function once instantiated. We need to take note of the `@safe` decorator, a very handy decorator which wraps any exception raised into a `Failure` object.
The `Failure`class is a subclass of the `Result` type which is basically a simple container.
Now, our code changes from:
```python
try:
    user = get_user(1)
except Exception:
    print('An error occurred')
```
to:
```python
user = get_user(1) # returns Success(dict) or Failure(Exception)
user = user.value_or({})
```
Isn't that just elegant?

I know what you are thinking. It's too much code! 
That's the same thing I thought at first. But after playing around with the library and a bit of customization, I started to notice that my code looked cleaner. I could quickly scan through a file and get the gist of what is going without have to follow references. This is just the surface. Returns has way more to offer. Check it [out](https://returns.readthedocs.io/en/latest/index.html).

Here's a decorator I came up with to automatically unwrap `Result` inner values or provide a default value instead:
```python
def unwrap(success=None, failure=None):
    def outer_wrapper(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            if is_successful(result):
                if success is not None:
                    return success
            else:
                if failure is not None:
                    return failure
            return result._inner_value

        return wrapper

    return outer_wrapper

```
Usage:
```python
    @unwrap(failure={}) # If a failure occurs an empty dict is returnd
    @pipeline
    def __call__(self, user_id: int) -> Result[dict, Exception]:
        ...
        
user = get_user(1) # returns dict or empty dict      
```


Cheers!