Accessing the list#

Intro#

The first thing you’ll be setting up for our app is a route handler that returns a single TODO list. A TODO list in this case will be a list of dictionaries representing the items on that TODO list.

app.py#
 1from typing import Dict, List, Union
 2
 3from litestar import Litestar, get
 4
 5TODO_LIST: List[Dict[str, Union[str, bool]]] = [
 6    {"title": "Start writing TODO list", "done": True},
 7    {"title": "???", "done": False},
 8    {"title": "Profit", "done": False},
 9]
10
11
12@get("/")
13async def get_list() -> List[Dict[str, Union[str, bool]]]:
14    return TODO_LIST
15
16
17app = Litestar([get_list])
app.py#
 1from litestar import Litestar, get
 2
 3TODO_LIST: list[dict[str, str | bool]] = [
 4    {"title": "Start writing TODO list", "done": True},
 5    {"title": "???", "done": False},
 6    {"title": "Profit", "done": False},
 7]
 8
 9
10@get("/")
11async def get_list() -> list[dict[str, str | bool]]:
12    return TODO_LIST
13
14
15app = Litestar([get_list])

If you run the app and visit http://127.0.0.1:8000/ in your browser you’ll see the following output:

../../_images/get_todo_list.png

Suddenly, JSON#

Because the get_list function has been annotated with List[Dict[str, Union[str, bool]]], Litestar infers that you want the data returned from it to be serialized as JSON:

13async def get_list() -> List[Dict[str, Union[str, bool]]]:

Cleaning up the example with dataclasses#

To make your life a little easier, you can transform this example by using dataclasses instead of plain dictionaries:

Tip

For an in-depth explanation of dataclasses, you can read this excellent Real Python article: Data Classes in Python 3.7+

app.py#
 1from dataclasses import dataclass
 2from typing import List
 3
 4from litestar import Litestar, get
 5
 6
 7@dataclass
 8class TodoItem:
 9    title: str
10    done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14    TodoItem(title="Start writing TODO list", done=True),
15    TodoItem(title="???", done=False),
16    TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list() -> List[TodoItem]:
22    return TODO_LIST
23
24
25app = Litestar([get_list])
app.py#
 1from dataclasses import dataclass
 2
 3from litestar import Litestar, get
 4
 5
 6@dataclass
 7class TodoItem:
 8    title: str
 9    done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13    TodoItem(title="Start writing TODO list", done=True),
14    TodoItem(title="???", done=False),
15    TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list() -> list[TodoItem]:
21    return TODO_LIST
22
23
24app = Litestar([get_list])

This looks a lot cleaner and has the added benefit of being able to work with dataclasses instead of plain dictionaries. The result will still be the same: Litestar knows how to turn these dataclasses into JSON and will do so for you automatically.

Tip

In addition to dataclasses, Litestar supports many more types such as TypedDict, NamedTuple, Pydantic models, or attrs classes.

Filtering the list using query parameters#

Currently get_list will always return all items on the list, but what if you are interested in only those items with a specific status, for example all items that are not yet marked as done?

For this you can employ query parameters; to define a query parameter, all that’s needed is to add an otherwise unused parameter to the function. Litestar will recognize this and infer that it’s going to be used as a query parameter. When a request is being made, the query parameter will be extracted from the URL, and passed to the function parameter of the same name.

app.py#
 1from dataclasses import dataclass
 2from typing import List
 3
 4from litestar import Litestar, get
 5
 6
 7@dataclass
 8class TodoItem:
 9    title: str
10    done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14    TodoItem(title="Start writing TODO list", done=True),
15    TodoItem(title="???", done=False),
16    TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: str) -> List[TodoItem]:
22    if done == "1":
23        return [item for item in TODO_LIST if item.done]
24    return [item for item in TODO_LIST if not item.done]
25
26
27app = Litestar([get_list])
app.py#
 1from dataclasses import dataclass
 2
 3from litestar import Litestar, get
 4
 5
 6@dataclass
 7class TodoItem:
 8    title: str
 9    done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13    TodoItem(title="Start writing TODO list", done=True),
14    TodoItem(title="???", done=False),
15    TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list(done: str) -> list[TodoItem]:
21    if done == "1":
22        return [item for item in TODO_LIST if item.done]
23    return [item for item in TODO_LIST if not item.done]
24
25
26app = Litestar([get_list])
../../_images/todos-done.png

Visiting http://127.0.0.1:8000?done=1 will give you all the TODOs that have been marked as done#

../../_images/todos-not-done.png

while http://127.0.0.1:8000?done=0 will return only those not yet done#

At first glance this seems to work just fine, but you might be able to spot a problem: If you input anything other than ?done=1, it would still return items not yet marked as done. For example, ?done=john gives the same result as ?done=0.

An easy solution for this would be to simply check if the query parameter is either 1 or 0, and return a response with an HTTP status code that indicates an error if it’s something else:

app.py#
 1from dataclasses import dataclass
 2from typing import List
 3
 4from litestar import Litestar, get
 5from litestar.exceptions import HTTPException
 6
 7
 8@dataclass
 9class TodoItem:
10    title: str
11    done: bool
12
13
14TODO_LIST: List[TodoItem] = [
15    TodoItem(title="Start writing TODO list", done=True),
16    TodoItem(title="???", done=False),
17    TodoItem(title="Profit", done=False),
18]
19
20
21@get("/")
22async def get_list(done: str) -> List[TodoItem]:
23    if done == "1":
24        return [item for item in TODO_LIST if item.done]
25    if done == "0":
26        return [item for item in TODO_LIST if not item.done]
27    raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400)
28
29
30app = Litestar([get_list])
app.py#
 1from dataclasses import dataclass
 2
 3from litestar import Litestar, get
 4from litestar.exceptions import HTTPException
 5
 6
 7@dataclass
 8class TodoItem:
 9    title: str
10    done: bool
11
12
13TODO_LIST: list[TodoItem] = [
14    TodoItem(title="Start writing TODO list", done=True),
15    TodoItem(title="???", done=False),
16    TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: str) -> list[TodoItem]:
22    if done == "1":
23        return [item for item in TODO_LIST if item.done]
24    if done == "0":
25        return [item for item in TODO_LIST if not item.done]
26    raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400)
27
28
29app = Litestar([get_list])

If the query parameter equals 1, return all items that have done=True:

app.py#
23if done == "1":
24    return [item for item in TODO_LIST if item.done]

If the query parameter equals 0, return all items that have done=False:

app.py#
25if done == "0":
26    return [item for item in TODO_LIST if not item.done]

Finally, if the query parameter has any other value, an HTTPException will be raised. Raising an HTTPException tells Litestar that something went wrong, and instead of returning a normal response, it will send a response with the HTTP status code given (400 in this case) and the error message supplied.

app.py#
27raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400)
../../_images/done-john.png

Try to access http://127.0.0.1?done=john now and you will get this error message#

Now we’ve got that out of the way, but your code has grown to be quite complex for such a simple task. You’re probably thinking “there must be a better way!”, and there is! Instead of doing these things manually, you can also just let Litestar handle them for you!

Converting and validating query parameters#

As mentioned earlier, type annotations can be used for more than static type checking in Litestar; they can also define and configure behaviour. In this case, you can get Litestar to convert the query parameter to a boolean value, matching the values of the TodoItem.done attribute, and in the same step validate it, returning error responses for you should the supplied value not be a valid boolean.

app.py#
 1from dataclasses import dataclass
 2from typing import List
 3
 4from litestar import Litestar, get
 5
 6
 7@dataclass
 8class TodoItem:
 9    title: str
10    done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14    TodoItem(title="Start writing TODO list", done=True),
15    TodoItem(title="???", done=False),
16    TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: bool) -> List[TodoItem]:
22    return [item for item in TODO_LIST if item.done == done]
23
24
25app = Litestar([get_list])
app.py#
 1from dataclasses import dataclass
 2
 3from litestar import Litestar, get
 4
 5
 6@dataclass
 7class TodoItem:
 8    title: str
 9    done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13    TodoItem(title="Start writing TODO list", done=True),
14    TodoItem(title="???", done=False),
15    TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list(done: bool) -> list[TodoItem]:
21    return [item for item in TODO_LIST if item.done == done]
22
23
24app = Litestar([get_list])
../../_images/done-john-2.png

Browse to http://127.0.0.1:8000?done=john from our earlier example, and you will see it now results in this descriptive error message#

What’s happening here?

Since bool is being used as the type annotation for the done parameter, Litestar will try to convert the value into a bool first. Since john (arguably) is not a representation of a boolean value, it will return an error response instead.

app.py#
21async def get_list(done: bool) -> List[TodoItem]:

Tip

It is important to note that this conversion is not the result of calling bool on the raw value. bool("john") would be True, since Python considers all non-empty strings to be truthy.

Litestar however supports customary boolean representation commonly used in the HTTP world; true and 1 are both converted to True, while false and 0 are converted to be False.

If the conversion is successful however, done is now a bool, which can then be compared against the TodoItem.done attribute:

app.py#
22return [item for item in TODO_LIST if item.done == done]

Making the query parameter optional#

There is one problem left to solve though, and that is, what happens when you want to get all items, done or not, and omit the query parameter?

../../_images/missing-query.png

Omitting the ?done query parameter will result in an error#

Because the query parameter has been defined as done: bool without giving it a default value, it will be treated as a required parameter - just like a regular function parameter. If instead you want this to be optional, a default value needs to be supplied.

app.py#
 1from dataclasses import dataclass
 2from typing import List, Optional
 3
 4from litestar import Litestar, get
 5
 6
 7@dataclass
 8class TodoItem:
 9    title: str
10    done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14    TodoItem(title="Start writing TODO list", done=True),
15    TodoItem(title="???", done=False),
16    TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: Optional[bool] = None) -> List[TodoItem]:
22    if done is None:
23        return TODO_LIST
24    return [item for item in TODO_LIST if item.done == done]
25
26
27app = Litestar([get_list])
app.py#
 1from dataclasses import dataclass
 2
 3from litestar import Litestar, get
 4
 5
 6@dataclass
 7class TodoItem:
 8    title: str
 9    done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13    TodoItem(title="Start writing TODO list", done=True),
14    TodoItem(title="???", done=False),
15    TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list(done: bool | None = None) -> list[TodoItem]:
21    if done is None:
22        return TODO_LIST
23    return [item for item in TODO_LIST if item.done == done]
24
25
26app = Litestar([get_list])
../../_images/get_todo_list.png

Browsing to http://localhost:8000 once more, you will now see it does not return an error if the query parameter is omitted#

Tip

In this instance, the default has been set to None, since we don’t want to do any filtering if no done status is specified. If instead you wanted to only display not-done items by default, you could set the value to False instead.

Interactive documentation#

So far we have explored our TODO application by navigating to it manually, but there is another way: Litestar comes with interactive API documentation, which is generated for you automatically. All you need to do is run your app (litestar run) and visit http://127.0.0.1:8000/schema/swagger

../../_images/swagger-get.png

The route handler set up earlier will show up in the interactive documentation#

This documentation not only gives an overview of the API you have constructed, but also allows you to send requests to it.

../../_images/swagger-get-example-request.png

Executing the same requests we did earlier#

Note

This is made possible by Swagger and OpenAPI. Litestar generates an OpenAPI schema based on the route handlers, which can then be used by Swagger to set up the interactive documentation.

Tip

In addition to Swagger, Litestar serves the documentation from the generated OpenAPI schema with ReDoc and Stoplight Elements. You can browse to http://127.0.0.1:8000/schema/redoc and http://127.0.0.1:8000/schema/elements to view each, respectively.