Python Friday #220: Manage To-Dos With FastAPI

After covering the basics of FastAPI, it is now time to do something more elaborate. For that purpose, we create a minimalistic to-do task tracker that let us work against an in-memory data store. To save us the time to constantly click in the browser, we will do this exercise by first writing a test and then create the implementation using the test-first approach.

This post is part of my journey to learn Python. You find the code for this post in my PythonFriday repository on GitHub.

 

The Pydantic models

Four our little application we split the input models from those we send back to the API client. That allows us to be specific on what data we expect from the users and show the difference to what we keep inside the API.

Our input model has these four fields:

What we send back to the client contains a bit more details:

 

A failing test to start

We want to work with our API by sending a task to the API and get the data with some additional details back:

Our test currently fails and will so for the next few steps that we need to get done so that we get enough code to run our API.

 

A new API application

Our minimalistic first attempt to make our test pass can look like this:

Make sure that you create the empty __init__.py files inside the models and tests folder and put one next to main.py. Otherwise, you end up with an error message like this one:

ImportError while importing test module ‘….\tests\test_todo.py’.
Hint: make sure your test modules/packages have valid Python names.
Traceback:

tests\test_todo.py:3: in
from ..main import app
E ImportError: attempted relative import with no known parent package

If we now run our test, we should see it passing.

 

A failing test to add more functionality

With our first test passing, we can go for the next feature: we want to get the details of a newly created task. With this test we can check if our API gives us the expected data:

This test also fails at first, then currently there is no endpoint and no way to store the data between the requests to get the result we expect.

 

Add a mock data store

To make our second test pass, we need to keep the data around for two (or more) requests. We can do that with a variable in the API that works as a mock data store:

We need to change the create_task() method to put the newly created task into our data store and then read from there when someone asks for the item in the show_task() method:

We can now run our two tests, and both will pass.

 

Add some error handling

What happens if we ask for a task that does not exist? Will our application crash or will it produce a useful message? Let us write a test to find out what happens:

Our current implementation will throw an exception because there is no element at position 0 in an empty list. We can make our test pass by adding this check:

The HTTPException allows us to specify a status code and set a message that we can send to the client. With that addition, our API can now handle the case where the requested task does not exist.

 

Update a task

We currently can create a task and get its details. When we want to update our task, we need a new test to drive our functionality:

In our API, we need to find the requested task, update the fields, and then return the changed task:

Attention: Since we work on the element of the list and not on a copy, we do not need to update the list. That will need to change when we use a database.

We can now run our tests and they all should pass. We can spot an opportunity for refactoring when it comes to the duplicated code to create an event to have some data to work on in our tests. We write that down to a notepad but postpone that refactoring for a moment.

 

Delete a task

When we want to delete a task, we start once more with a failing test:

We can now implement the delete_task() method with this code:

When we do not specify a response, we get the default response with status code 200. That currently is good enough for our API. We will return to status codes in a future post.

 

Run with uvcorn

If we run our to-do app like we did the minimalistic example, we run into this error:

File “…\minimal_todo\main.py”, line 3, in
from .models.todo import *
ImportError: attempted relative import with no known parent package

Instead, we need to go one folder up of our main.py and run uvicorn with this command:

If we go to http://127.0.0.1:8000/docs, we should see our Swagger documentation:

Our To-Do API now runs with uvicorn and shows us the documentation for our endpoints

 

Next

With our test-first approach we got our ToDo API in a state that it supports the basic CRUD operations. The tests help us to make sure that the application keeps working when we add more features or change the behaviour. However, there is now a lot of redundancy in our tests that we can refactor next week.