Cómo crear una API en Python

Como científico o ingenieros de datos, muchas veces tenemos que compartir nuestro trabajo para que cualquier otra persona de la empresa pueda utilizar los procesos o modelos que hemos creado. Claramente, compartir un script no es una opción, ya que todo el mundo necesitaría tener esos mismos programas que tú. Aquí es cuando entran en juego las APIs y hoy, vamos a ver qué son y cómo crear una API en Python. ¿Suena interesante? ¡Pues vamos a ello!

Conceptos básicos sobre APIs

Una API (Application Programming Interface) permite que dos sistemas informáticos interactuen entre sí. Por ejemplo, si creamos una automatización que genere un informe y lo mande por email, el envío de ese email no se hace de forma manual, lo hará el propio script. Para ello, Python (o el lenguaje que usemos), deberá pedirle a Gmail que mande ese correo, con ese informe adjunto a ciertas personas. La forma de hacerlo es mediante una API, en este caso la API de Gmail.

Bien, ahora que sabes lo que es una API, veamos cuáles son las partes principales de una API:

  • Protocolo de transferencia HTTP: es la forma principal de comunicar información en la web. Existen diferentes métodos, cada uno de ellos utilizados para diferentes cuestiones:
    • GET: este método permite obtener información de la base de datos o de un proceso.
    • POST: permite mandar información, ya sea para añadir información a una base de datos o para pasar el input de un modelo de machine learning, por ejemplo.
    • PUT: permite actualizar información. Generalmente se usa para gestionar información en base de datos.
    • DELETE: este método se utiliza para eliminar información de la base de datos.
  • Url: es la dirección en la que podremos encontrar a nuestra API. Básicamente esta URL constará de tres partes:
    • Protocolo: como toda dirección, puede ser http:// o https://
    • Dominio: el host en el que está alojado, que va desde el protocolo hasta el final del .es o .com, o la terminación que sea. En mi web, por ejemplo, el dominio es anderfernandez.com.
    • Endpoint: al igual que una web tiene varias páginas (/blog), (/legal), una misma API puede incluir varios puntos y que cada uno haga cosas diferentes. Al crear nuestra API en Python nosotros indicaremos los endpoints, por lo que debemos asegurarnos de que cada enpoint sea representativo de lo que haga la API que está por detrás.

Bien, ahora que ya sabemos qué es una API y cuáles son sus partes principales, veamos cómo podemos crear una API en Python. ¡Vamos a ello!

Cómo crear una API en Python

Existen diferentes formas de crear una API en Python, siendo las más utilizadas FastAPI y Flask. Así pues, explicaré cómo funcionan las dos, de tal forma que puedas usar la forma de crear APIs en Python que más te guste. Empecemos con FastAPI.

Cómo crear una API en Python con FastAPI

Requisitos para usar FastAPI

FastAPI es una forma de crear APIs en Python que surgió a finales de 2018. Es muy rápida, aunque solo puede usarse con Python 3.6+ (en mi opinión esto no debería ser un problema, pero es importante.

Para utilizarlo deberás instalar dos librerías: fastapi y uvicorn.

pip install fastapi
pip install uvicorn

Tu primera API en Python con FastAPI

Ahora que ya hemos instalado los paquetes, simplemente debemos crear un fichero en Python donde definiremos nuestra API. En este fichero, deberemos crear una app, en donde iremos incluyendo las APIs, con sus endpoints, parámetros, etc.

Una vez tenemos la app, es ahí donde definimos la información que requiere la API: endpoint, método HTTP, argumentos de entrada y lo que hará la API por detrás.

from fastapi import FastAPI
app = FastAPI()

@app.get("/my-first-api")
def hello():
  return {"Hello world!"}

Con esto ya tendríamos una API muy sencilla creada, que simplemente devuelve “Hello world!”. Como verás, en muy pocas líneas hemos definido: el método (get), el endpoint (“/”) y la función que debe correr esta API.

Incluso, podríamos pasar argumentos a nuestra API, para que los utilice en su función. Importante: siempre que pasemos un argumento a nuestra función debemos indicar el tipo de dato que debe ser (número, texto, etc.).

Importante: FastAPI realiza un check de que el tipo de dato que le pasamos en la llamada es el que hemos indicado que debe ser. Esto es algo fundamental para asegurarnos de que nuestra API funciona adecuadamente y, es algo, que otros frameworks de creación de APIs no incluyen.

Veamos cómo funciona en un ejemplo:

from fastapi import FastAPI
app = FastAPI()

@app.get("/my-first-api")
def hello(name: str):
  return {'Hello ' + name + '!'}

Ahora, cuando hagamos la petición a esta API, tendremos que pasarle el parámetro name para que funcione. Es decir, que si previamente bastara con que fuéramos a http://127.0.0.1:8000/my-first-api, ahora tendremos que pasarle el parámetro name, de tal forma que la petición será a: http://127.0.0.1:8000/my-first-api?name=Ander.

Tal como hemos incluido el argumento name, este argumento es obligatorio: si no se incluye en la petición, no funcionará. Sin embargo, puede que nos interese pasar argumentos opcionales. Para ello, tendremos que usar Asimismo, puede darse el caso de que haya argumentos que sean opcionales

Para ello, tenemos que indicar el argumento como None y FastAPI lo interpretará adecuadamente. Ejemplo: vamos a crear una API a la cual le puedes pasar, o no, la variable name. Si se la pasas, devolverá “Hello {name}!” y, sino, simplemente “Hello!”.

from fastapi import FastAPI

app = FastAPI()

@app.get(("/my-first-api")
def hello(name = None):

    if name is None:
        text = 'Hello!'

    else:
        text = 'Hello ' + name + '!'

    return text

En este caso, si vamos a http://127.0.0.1:8000/my-first-api la API se ejecutará correctamente y nos devolverá “Hello!”, mientras que, si le pasamos el parámetro, por ejemplo, http://127.0.0.1:8000/my-first-api?name=Ander, lo utilizará y seguirá funcionando correctamente, devolviendo Hello Ander!.

Devolver diferentes tipos de datos con FastAPI

La gran mayoría de veces una API suele devolver texto (una predicción, un dato, etc.), aunque muchas veces puede devolver otros tipos de datos, como un DataFrame o una imagen, por ejemplo.

Cuando se trata de objetos “normales”, como un DataFrame, FastAPI lo convertirá directamente a un fichero JSON. Ejemplo:

from fastapi import FastAPI

app = FastAPI()

@app.get("/get-iris")
def get_iris():

    import pandas as pd
    url ='https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/raw/0e7a9b0a5d22642a06d3d5b9bcbad9890c8ee534/iris.csv'
    iris = pd.read_csv(url)

    return iris

Si hacemos una petición a este endpoint, recibiremos la siguiente respuesta:

resp.text
'{"sepal_length":{"0":5.1,"1":4.9,"2":4.7,"3":4.6,"4":5.0,"5":5.4,"6":4.6,"7":5.0,"8":4.4,"9":4.9,"10":5.4,"11":4.8,"12":4.8,"13":4.3,"14":5.8,"15":5.7,"16":5.4,"17":5.1,"18":5.7,"19":5.1,"20":5.4,"21":5.1,"22":4.6,"23":5.1,"24":4.8,"25":5.0,"26":5.0,"27":5.2,"28":5.2,"29":4.7,"30":4.8,"31":5.4,"32":5.2,"33":5.5,"34":4.9,"35":5.0,"36":5.5,"37":4.9,"38":4.4,"39":5.1,"40":5.0,"41":4.5,"42":4.4,"43":5.0,"44":5.1,"45":4.8,"46":5.1,"47":4.6,"48":5.3,"49":5.0,"50":7.0,...

Como ves, Fast API nos convierte el Data Frame directamente en un objeto JSON. Sin embargo, ¿qué ocurre con las imágenes o los vídeos?

FastAPI se basa en starlette, por lo que, para poder responder con imágenes o iconos, podemos usar tanto StreamingResponse como FileResponse. En cualquier caso, necesitaremos instalar aiofiles.

Así pues, si queremos mostrar una imagen en nuestra API, nuestra aplicación de FastAPI será así:


@app.get("/plot-iris")
def plot_iris():

    import pandas as pd
    import matplotlib.pyplot as plt

    url ='https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/raw/0e7a9b0a5d22642a06d3d5b9bcbad9890c8ee534/iris.csv'
    iris = pd.read_csv(url)

    plt.scatter(iris['sepal_length'], iris['sepal_width'])
    plt.savefig('iris.png')
    file = open('iris.png', mode="rb")

    return StreamingResponse(file, media_type="image/png")

Como vemos, crear una API en Python con FastAPI es muy sencillo y muy versatil. Pero, ¿cómo puedo comprobar que mi API funciona correctamente? ¡Veámoslo!

Comprobar el funcionamiento de una API de FastAPI

Como decía al comienzo de esta sección, para crear una API en FastAPI debemos incluir nuestro código en un fichero de Python, preferiblemente main.py. Asimismo, debemos tener instalado uvicorn. Teniendo en cuenta esto, podemos correr nuestra API de una forma muy sencilla, con el siguiente código:

uvicorn main:app --reload

Esto ejecutará nuestra aplicación y ya podremos acceder a nuestra API, tanto a nivel de navegador como haciendo llamadas desde nuestro ordenador. La API aún no se ha subido a ningún sitio, por lo que será accesible desde el localhost. Puedes acceder al localhost tanto en http://127.0.0.1/ como en http://localhost/, aunque tendrás que hacerlo en el mismo puerto en el que se esté ejecutando la API. Para conocerlo, en mi opinión, lo más fácil es usar el enlace que el propio uvicorn te dará al ejecutar la API:

Cómo lanzar una API en Python hecha en FastAPI

Veamos por ejemplo cómo hago peticiones una API que incluye todos los endpoints que he explicado previamente

import requests
from PIL import Image
import io

resp = requests.get('http://127.0.0.1:8000/plot-iris')
file = io.BytesIO(resp.content)
im = Image.open(file)
im.show()

Como vemos, nuestra API nos está devolviendo la imagen correctamente. Probemos ahora con el primer endpoint que hemos creado:

resp = requests.get('http://127.0.0.1:8000/my-first-api?name=Ander')
resp.text
'"Hello Ander!"'

Además, para que no exista ninguna API el propio FastAPI nos crea la documentación en swagger, a la cual podemos acceder desde el path, docs. En mi caso, como la API está expuesta en , puedo acceder a http://127.0.0.1:8000/docs, como se ve en la siguiente imagen:

Cómo ver la documentación Swagger de nuestra API en FastAPI

Además, también otra forma de documentar la API, que es mediante el endpoint /redoc, como vemos a continuación:

Cómo ver la documentación de OpenAPI de nuestra API en FastAPI

Como ves, crear una API con FastAPI es muy muy sencillo, muy rápido e intuitivo. Sin embargo, FastAPI no es la única forma de crear una API en Python. Otra forma de hacerlo es usando flask. ¡Veamos cómo funciona!

Cómo crear una API en Python con Flask

Cómo crear una API con Flask

Lo primero de todo, para poder crear una API en Python usando Flask deberemos instalar los paquetes flask y flask-restful . Una vez tenemos las librerías instaladas, es crear nuestro servidor, al igual que en FastAPI. Esto lo podemos hacer con el siguiente comando:

from flask import Flask

app = Flask()

Además, tendremos que indicar que lance el servidor y el puerto en el que se debe lanzar. Para ello, al final de nuestra app incluiremos lo siguiente:

if __name__ == '__main__':
    app.run(debug=True, port=8000)

Una vez tenemos nuestro servidor creado y le hemos indicado en qué puerto debe ejecutarse, ya podemos empezar a crear nuestras APIs. Para crear una API en Python con Flask, tenemos que indicar: el endpoint, el método y la función que deberá ejecutarse en ese endpoint. Veamos un ejemplo con una API que simplemente nos devuelva el texto “Hello world!”.

from flask import Flask, jsonify, request,send_file

app = Flask()

@app.route('/my-first-api', method = ['GET'])

def hello():

    return "Hello world!"

Como ves, en este caso definimos tanto el endpoint como el método cuando definimos la ruta del servidor con @app.route . En mi caso he definido el método GET, aunque no haría falta, ya que, por defecto, Flask usa el método GET. Por tanto, solo es obligatorio definir el método si vamos a usar un método que no sea GET, es decir, si usamos un POST, PUT o DELETE.

Cómo pasar parámetros a la API en Flask

Muchas veces, nuestras APIs requieren de parámetros. Un claro ejemplo es cuando ponemos un modelo en producción: cada input del modelo deberá ser un parámetro de nuestra API.

En ese sentido, podemos pasar argumentos a nuestra API en Flask al hacer la petición. En este caso, para poder utilizarlos en la función primero deberemos extraerlos de la petición, lo cual lo haremos con request.args.get. Sí, esto es algo diferente a cómo se hace en FastAPI y, en mi opinión, es más engorroso en Flask que en FastApi.

Veamos, por ejemplo, como crear una API que reciba un parámetro name y que nos haga un print del “Hello {name}!”.

@app.route('/my-first-api', methods = ['GET'])
def hello():

    name = request.args.get('name')

    if name is None:
        text = 'Hello!'

    else:
        text = 'Hello ' + name + '!'

    return text

En este caso, nuestra API está cogiendo el parámetro name de la URL de la petición, habiendo dos opciones:

  1. Si no se ha pasado el parámetro name, ejemplo: http://127.0.0.1:8000/my-first-api el parámetro name será None, por lo que la API devolverá Hello!.
  2. Si se ha pasado un parámetro name, ejemplo http://127.0.0.1:8000/my-first-api?name=Ander , la API usará el parámetro para crear la respuesta, que en este caso será Hello Ander! .

Importante: Flask no hace una evaluación de los parámetros de la petición, por lo que tendremos que hacer una evaluación nosotros mismos, ya que, sino, se usará en la función. Por ejemplo, si hacemos la petición a la URL: http://127.0.0.1:8000/my-first-api?name=2, la API devolverá Hello 2! , ya que no ha comprobado que el valor del parámetro debe ser un string y no un número o cualquier otro tipo de objeto.

Devolver diferentes tipos de datos con Flask

Como vemos, hasta ahora hemos devuelto cadenas de texto. Sin embargo, las APIs suelen devolver los datos en formato JSON. Para ello, deberemos usar el módulo jsonify. Veamos un ejemplo de cómo devolver el string anterior, pero esta vez en formato JSON:

@app.route('/my-first-api', methods = ['GET'])
def hello():

    name = request.args.get('name')

    if name is None:
        text = 'Hello!'

    else:
        text = 'Hello ' + name + '!'

    return jsonify({"message": text})

De esta misma forma podemos devolver otro tipo de objetos, como un dataset. Veamos cómo sería un ejemplo, creando un nuevo endpoint que devulva el dataset iris mediante una petición GET:

@app.route("/get-iris")
def get_iris():

    import pandas as pd
    url ='https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/raw/0e7a9b0a5d22642a06d3d5b9bcbad9890c8ee534/iris.csv'
    iris = pd.read_csv(url)

    return jsonify({
        "message": "Iris Dataset",
        "data": iris.to_dict()
        })

Para pasar un DataFrame, tenemos primero que convertilor a un objeto que se pueda convertir en u JSON, como un diccionario. Con esto, tendremos una respuesta como la siguiente:

resp.json()
{'data': {'petal_length': {'0': 1.4, '1': 1.4, '2': 1.3, '3': 1.5, '4': 1.4, '5': 1.7, '6': 1.4, '7': 1.5, '8': 1.4, '9': 1.5, '10': 1.5, '11': 1.6, '12': 1.4, '13': 1.1, '14': 1.2, '15': 1.5, '16': 1.3, '17': 1.4, '18': 1.7, '19': 1.5, '20': 1.7, '21': 1.5, '22': 1.0, '23': 1.7, '24': 1.9, '25': 1.6, '26': 1.6, '27': 1.5, '28': 1.4, '29': 1.6, '30': 1.6, '31': 1.5, '32': 1.5, '33': 1.4, '34': 1.5, '35': 1.2, '36': 1.3, '37': 1.5, '38': 1.3, '39': 1.5, '40': 1.3, '41': 1.3, '42': 1.3, '43': 1.6, '44': 1.9, '45': 1.4, '46': 1.6, '47': 1.4, '48': 1.5, '49': 1.4, '50': 4.7, ...

Por último, ¿se pueden mandar imágenes mediante una API en Python echa en Flask? En FastAPI hemos visto que se puede y, cómo no, Flask no iba a ser menos. De hecho, la forma de hacerlo es muy similar, solo que en este caso, deberemos guardar el contenido dentro del propio flask (o el directorio que sea) y mandarlo usando la función send_file .

@app.route("/plot-iris")
def plot_iris():

    import pandas as pd
    import matplotlib.pyplot as plt
    import os

    url ='https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/raw/0e7a9b0a5d22642a06d3d5b9bcbad9890c8ee534/iris.csv'
    iris = pd.read_csv(url)

    plt.scatter(iris['sepal_length'], iris['sepal_width'])
    plt.savefig('flask/iris.png')

    return send_file('iris.png')

Ahora que ya sabemos cómo mandar diferentes tipos de contenido con en una API en Python creada con Flask, veámos cómo comprobar que nuestra API funciona correctamente.

Comprobar el funcionamiento de una API de Flask

Antes de ejecutar una API de Python hecha en Flask tenemos que comprobar asegurarnos de que la estamos exponiendo en un puerto. Para ello deberemos incluir el código que he mencionado y que pongo a continuación:

if __name__ == '__main__':
    app.run(debug=True, port=8000)

Así pues, para ejecutar la aplicación simplemente deberemos ejecutar nuestro fichero de Python donde hayamos creado la API. En mi caso el comando es el siguiente:

python flask/main.py

Esto nos generará un código como el siguiente:

Cómo lanzar una API en Python hecha en FastAPI

Como ves, nuestra API se estará ejecutando en nuestro local host, en el puerto que le hayamos indicado (en mi caso el puerto 8000).

Así pues, para probar la API simplemente podemos ir a nuestro navegador e ir a los endpoints o, directamente, hacer las peticiones desde Python:

resp.json()
{'message': 'Hello Ander!'}

Como vemos, la API nos está devolviendo el contenido de forma adecuada.

Asimismo, podemos añadir documentación de Swagger y OpenApi a nuestra API en Flask, aunque hacerlo va a ser algo más tedioso que como se hace en FastAPI. De hecho, esto daría para un post en sí mismo, así que os enlazo a este post escrito por Sean Bradley, por si estáis interesados.

Conclusión

Como ves, tanto usando FastApi como Flask la creación de APIs es muy sencilla, aunque tienen ciertas diferencias:

  • Flask es un framework que permite crear aplicaciones web enteras.
  • FastAPI realiza la validación de los tipos de datos que realizamos, así como todo el proceso de documentación, ahorrándonos mucho tiempo respecto a si lo hacemos en Flask.

Así pues, mi recomendación sería que, si únicamente quieres crear la API, lo hagas en FastAPI, ya que seguramente se te haga más rápido. Sin embargo, en el caso de que quieras crear una interfaz de usuario completa (front y back) y que la API sea parte del ecosistema, recomendaría usar Flask, ya que todo quedará bajo el mismo framework y te será mucho más sencillo.

En cualquier caso, crear APIs en Python es algo muy sencillo y fundamental para cualquier persona dedicada en el mundo de los datos. Ya sea para la ejecución de procesos o para la puesta en producción de algoritmos de Machine Learning, seguramente las APIs formen parte de ese proceso.

Como siempre, espero que te haya resultado interesante el post. Si es así, te recomiendo que te suscribas para estar informado cada vez que subo un nuevo post. En cualquier caso, ¡nos vemos en el próximo!