A flexible approach to Python API client development
Tags: python, webdev, opensource, showdev
Published: 2019-04-12 01:13:05

---
title: A flexible approach to Python API client development
published: true
description: Learn how the apiron Python package eases API client development and maintenance using a declarative approach.
tags: python,webdev,opensource,showdev
canonical_url: https://dane.engineering/post/flexible-approach-python-api-client-development
---

![Ditto Pokémon](https://thepracticaldev.s3.amazonaws.com/i/0mlhix21sasx9lzqlfdf.png)

Are you working in a microservice-oriented architecture? Is there an API you want to use that doesn't have a Python SDK? Although high-level HTTP libraries like `requests` aid development by reducing the work you need to do to get up and running, they don't provide much structure around APIs specifically. This can lead to several pieces of similar calling code peppered throughout your system, in the worst cases leading to divergent approaches.

Let's take a look at what a set of API calls could look like using `requests`. I'll use the PokéAPI, a fantastic (and free!) API for querying Pokémon metadata. The most useful endpoint is `/pokemon/:pokemon`, which is the main entrypoint for getting information about a particular Pokémon. You can call this endpoint using `requests`:

```python
>>> import requests
>>> ditto = requests.get('https://pokeapi.co/api/v2/pokemon/ditto')
```

This returns a bevy of information, much of which links to other endpoints. To inspect the data more easily, you'll want to get the returned JSON as a Python dictionary:

```python
>>> ditto = requests.get('https://pokeapi.co/api/v2/pokemon/ditto').json()
```

You can verify you've received a response about Ditto by checking its name:

```python
>>> ditto['name']
'ditto'
```

You can also see that Ditto has one move, named "transform":

```python
>>> ditto['moves'][0]['move']['name']
'transform'
```

You can also get the endpoint for learning more about the transform move:

```python
>>> ditto['moves'][0]['move']['url']
'https://pokeapi.co/api/v2/move/144/'
```

You can then call the `/move/:move_id` endpoint using `requests` to get its info. That will lead to another response which links to yet further endpoints. **There are so many endpoints!** They're all under the same PokéAPI umbrella, but there's not much structure in the code which reflects this fact. If some of these endpoints return a single string value instead of JSON, the code to interact with the data will need to change as well. Is there a better way to keep this all straight?

At scale, it's important to understand all the APIs and endpoints your code is calling, in part so that you don't spend effort creating code that duplicates existing functionality. It also makes it easier to survey usage to understand if you've migrated fully away from a deprecated API or endpoint. To do this you need a single source of truth for an API and its endpoints, and ideally a homogeneous way of interacting with them.

[`apiron`](https://github.com/ithaka/apiron) is a Python package that addresses these desires by providing a declarative approach that produces SDK-like interaction for you to use. Through introspection of your declared configuration, `apiron` allows you to start talking to an API quickly and leads you toward a centralized configuration for all of your API dependencies. Let's look at how you might set up the PokéAPI using `apiron`.

{% github ithaka/apiron %}


With `apiron` you can define a `Service`, which has a `domain` and a collection of `Endpoint`s. The Pokémon and move endpoints happen to return JSON, so using a `JsonEndpoint` will end up returning the response as a dictionary by default. Each endpoint has placeholders that can be filled in dynamically:

```python
from apiron import Service, JsonEndpoint


class PokeAPI(Service):
    domain = 'https://pokeapi.co'

    pokemon = JsonEndpoint(path='/api/v2/pokemon/{pokemon}')
    move = JsonEndpoint(path='/api/v2/move/{move_id}')
```

This is all fine, but how do you interact with it? `apiron` provides SDK-like interaction for calling your configured service. That is, `apiron` tries to feel like something purpose-built for the API you need to use while being flexible enough to do this for any number of APIs. Here's how to make the call to get Ditto's information and information about the transform move:

```python
ditto = PokeAPI.pokemon(path_kwargs={'pokemon': 'ditto'})
transform = PokeAPI.move(path_kwargs={'move_id': 144})
```

From this code, it's easier to see that you're calling the PokéAPI and using the Pokémon and move endpoints. It's also easier to see what data is being plugged into the calls. If you're using an IDE, you can also jump to those endpoint definitions to see that they return JSON, so you get an idea of what to expect when using the responses. As you look through the `PokeAPI` class, you can also see which endpoints are already implemented at a glance, and quickly add any that aren't available yet if you know the right path and returned data type.

If the PokéAPI moves to a new domain, or if an endpoint's path changes, there's one clear place to react to that change. You can also call an endpoint with different headers, cookies, and more without having to duplicate much boilerplate. The readability and inspectability of this approach have improved my experience dealing with numerous services with numerous endpoints. In addition to helping users develop clients for APIs they want to use, this package can potentially reduce the effort needed for API providers to give their consumers a Python SDK. I really hope you'll try it out and tell me all the ways you can think of to break it!