Skip to content

Marcelo's Blog

The FastAPI Expert

I'm Marcelo, nice to meet you! 👋


I'm a software engineer from Brazil. 🇧🇷

I'm currently living in Utrecht, Netherlands. 🇳🇱

I'm a maintainer of Starlette and Uvicorn.

I'm also a FastAPI Expert.

If my open source work is useful to you, consider sponsoring me. 🙏

FastAPI Escape Character

Today, we'll talk about a small feature of FastAPI that might be useful for you: the escape character. 🤓

What is the escape character?

The escape character \f is a character that can be used to tell to FastAPI to truncate what should go to the endpoint description on the OpenAPI.

Let's see it in practice. Consider we have the following endpoint:

main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def home():
    """This is home.
    \f
    This is not on the OpenAPI.
    """

Install the dependencies:

python -m pip install uvicorn fastapi

Then run uvicorn:

uvicorn main:app

When we call the /openapi.json endpoint:

http GET :8000/openapi.json  # (1)!
  1. The HTTP client used is called HTTPie, but you can use curl, or just go to the browser, and access http://localhost:8000/openapi.json.

You'll see the following OpenAPI JSON on the response:

{
    "info": {
        "title": "FastAPI",
        "version": "0.1.0"
    },
    "openapi": "3.0.2",
    "paths": {
        "/": {
            "get": {
                "description": "This is home.",
                "operationId": "home__get",
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {}
                            }
                        },
                        "description": "Successful Response"
                    }
                },
                "summary": "Home"
            }
        }
    }
}

Observe the "description" field does not contain the "This is not on OpenAPI" part of the docstring. The reason is the escape character we used. Everything after the \f will not appear on that field.

This feature may be useful if you are using a docstring linter tool, like darglint.

What's new?

If you are a FastAPI veteran (😎), you are probably familiar with the above. What you probably don't know, is that now (since FastAPI 0.82.0) it's possible to use it on the Pydantic models you use on your FastAPI application.

Let's see another example.

As most of my examples, we'll use potatoes:

main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class PotatoOutput(BaseModel):
    """Super potato.
    \f
    This is not on the OpenAPI.
    """

@app.get("/", response_model=PotatoOutput)
def get_potato():
    ...

Install the dependencies:

python -m pip install uvicorn fastapi

Then run uvicorn:

uvicorn main:app

When we call /openapi.json, as we did above, we'll get the following OpenAPI JSON as response:

{
    "components": {
        "schemas": {
            "PotatoOutput": {
                "description": "Super potato.\n",
                "properties": {},
                "title": "PotatoOutput",
                "type": "object"
            }
        }
    },
    "info": {
        "title": "FastAPI",
        "version": "0.1.0"
    },
    "openapi": "3.0.2",
    "paths": {
        "/": {
            "get": {
                "operationId": "get_potato__get",
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PotatoOutput"
                                }
                            }
                        },
                        "description": "Successful Response"
                    }
                },
                "summary": "Get Potato"
            }
        }
    }
}

Tip

We can also use jq to get the part of the JSON that we are interested.

http GET :8000/openapi.json | jq .components.schemas
{
    "PotatoOutput": {
        "title": "PotatoOutput",
        "type": "object",
        "properties": {},
        "description": "Super potato.\n"
    }
}

As we can see, the description of PotatoOutput doesn't contain the "This is not on the OpenAPI." part as well.

Yey! Now you can use those docstring linter tools as you want with FastAPI! 🙌


Thanks for reading this blog post! 🥳

If you have any suggestions on what I can write about, please feel free to suggest below. 🙏

FastAPI's Test Client

This is my first blog post! 🥳

Please enjoy, and let me know if you have any feedback. 🤓

Abstract

If you are new to FastAPI, you might benefit from reading the following:

If you already know stuff about FastAPI, you might jump to:

Today, we'll talk about the main tool for testing FastAPI applications: the TestClient.

TestClient origin and features

The TestClient is a feature from Starlette (one of the two main dependencies of FastAPI). On which, FastAPI only does a reimport on the testclient module, as we can see here.

We can use the TestClient to test our WebSocket and HTTP endpoints.

The TestClient weird behavior

Although documented on both FastAPI and Starlette's documentation, most of the people are not aware of the TestClient's behavior when it comes to events. To put it simple, there are two ways of creating a TestClient object, and in one of those ways, the events are not executed.

Let's see the behavior with the following FastAPI application:

main.py
from fastapi import FastAPI

app = FastAPI()
started = False

@app.on_event("startup") # (1)!
def startup():
    global started
    started = True

@app.get("/")
def home():
    if started:
        return {"message": "STARTED"}
    else:
        return {"message": "NOT STARTED"}
  1. There are only two events available: startup and shutdown.

    Read more about it on the ASGI documentation.

As you can see, there's a single endpoint, which gives us a different message depending on the value of the started variable. The started variable is set to True on the startup event.

Now, let's test it with the TestClient:

test.py
1
2
3
4
5
6
7
8
9
from fastapi.testclient import TestClient

from main import app

def test_home():
    client = TestClient(app)
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "NOT STARTED"}

Install the dependencies:

python -m pip install "fastapi[all]" pytest

Then run pytest:

pytest test.py


As you can see, the above test passes. Which means the startup event was not triggered. 😱

On the other hand, if we run the following test, we'll get a different result:

test.py
1
2
3
4
5
6
7
8
9
from fastapi.testclient import TestClient

from main import app

def test_home():
    with TestClient(app):
        response = client.get("/")
        assert response.status_code == 200
        assert response.json() == {"message": "STARTED"}

Install the dependencies:

python -m pip install "fastapi[all]" pytest

Then run pytest:

pytest test.py


When used as context manager, the TestClient will trigger the startup event.

The Future of the TestClient

By the moment I'm writing this blog, the latest FastAPI version is 0.83.0 with Starlette pinned on 0.19.1. Starlette is already on version 0.20.3, and the next release will change the internals of the TestClient. To be more specific, the HTTP client will be changed from requests to httpx.

As there are some differences between the two clients, the TestClient will reflect the same differences.

This change will be in Starlette on version 0.21.0, and I'm unsure when it will land on FastAPI.

Let's see the changes you should be aware:

  1. allow_redirects will be now called follow_redirects.
  2. cookies parameter will be deprecated under method calls (it should be used on the client instantiation).
  3. data parameter will be called content when sending bytes or text.
  4. content_type will default to "text/plain" when sending file instead of empty string.
  5. The HTTP methods DELETE, GET, HEAD and OPTIONS will not accept content, data, json and files parameters.
  6. data parameter doesn't accept list of tuples, instead it should be a dictionary.

    client.post(..., data=[("key1", "1"), ("key1", "2"), ("key2", "3")])
    
    client.post(..., data={"key1": ["1", "2"], "key2": "3"})
    

Those changes will likely impact your test suite. Having this in mind, I've created a codemod that will help you to migrate your tests: bump-testclient. 🎉

Here is the list of what the codemod will do:

  1. Replace allow_redirects with follow_redirects.
  2. Replace data with content when sending bytes or text.
  3. Replace client.<method>(..., <parameter>=...) by client.request("<method>", ..., <parameter>=...) when parameter is either content, data, json or files.

In case you want to read more about the differences between the underneath clients, you can check the httpx documentation.

Thanks for reading till here! 🤓