Skip to content

Responses

Lilya, by design, furnishes specific response classes that serve a dual purpose. They offer utility and are tasked with sending the appropriate ASGI messages through the send channel.

Lilya automatically includes the Content-Length and Content-Type headers.

How does it work

There are a few ways of using the responses within a Lylia application.

Available responses

All the responses from Lilya inherit from the parent object Response and that same class can also be used directly.

All the responses are considered ASGI applications, which means you can treat them as such in your application if necessary.

Example

from lilya.responses import PlaiText
from lilya.types import Scope, Receive, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send):
    assert scope['type'] == 'http'
    response = PlaiText('Welcome')
    await response(scope, receive, send)

Response

from lilya.responses import Response

Example

from lilya.apps import Lilya
from lilya.responses import Response
from lilya.routing import Path


def home():
    return Response("Welcome home")


app = Lilya(routes=[Path("/", home)])

Lilya provides the set_cookie that allows settings a cookie on a given response. All the responses available in Lilya have access to this functionality.

from lilya.responses import Response
from lilya.types import Scope, Receive, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send):
    assert scope['type'] == 'http'
    response = Response('Welcome', media_type='text/plain')

    response.set_cookie(key=..., value=..., max_age=..., expires=...,)
    await response(scope, receive, send)
Parameters

The available parameters of the set_cookie are as follow:

  • key - A string representing the cookie's key.
  • value - A string representing the cookie's value.
  • max_age - An integer defining the cookie's lifetime in seconds. A negative value or 0 discards the cookie immediately. (Optional)
  • expires - Either an integer indicating the seconds until the cookie expires or a datetime. (Optional)
  • path - A string specifying the subset of routes to which the cookie applies. (Optional)
  • domain - A string specifying the valid domain for the cookie. (Optional)
  • secure - A boolean indicating that the cookie is sent to the server only if the request uses SSL and the HTTPS protocol. (Optional)
  • httponly - A boolean indicating that the cookie is inaccessible via JavaScript through Document.cookie, the XMLHttpRequest, or Request APIs. (Optional)
  • samesite - A string specifying the samesite strategy for the cookie, with valid values of 'lax', 'strict', and 'none'. Defaults to 'lax'. (Optional)

In the same fashion as the set cookie, this function is available on every response provided by Lilya.

from lilya.responses import Response
from lilya.types import Scope, Receive, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send):
    assert scope['type'] == 'http'
    response = Response('Welcome', media_type='text/plain')

    response.delete_cookie(key=..., path=..., domain=...)
    await response(scope, receive, send)
Parameters

The available parameters of the set_cookie are as follow:

  • key - A string representing the cookie's key.
  • path - A string specifying the subset of routes to which the cookie applies. (Optional)
  • domain - A string specifying the valid domain for the cookie. (Optional)

HTMLResponse

Returning an html response.

from lilya.responses import HTMLResponse

Example

from lilya.apps import Lilya
from lilya.responses import HTMLResponse
from lilya.routing import Path


def home():
    return HTMLResponse("<html><body><p>Welcome!</p></body></html>")


app = Lilya(routes=[Path("/", home)])

Error

Response that can be used when throwing a 500 error. Defaults to return an html response.

from lilya.responses import Error

Example

from lilya.apps import Lilya
from lilya.responses import Error
from lilya.routing import Path


def home():
    return Error("<html><body><p>Error!</p></body></html>")


app = Lilya(routes=[Path("/", home)])

PlainText

Response that can be used to return text/plain.

from lilya.responses import PlainText

Example

from lilya.apps import Lilya
from lilya.responses import PlainText
from lilya.routing import Path


def home():
    return PlainText("Welcome home")


app = Lilya(routes=[Path("/", home)])

JSONResponse

Response that can be used to return application/json.

from lilya.responses import JSONResponse

Example

from lilya.apps import Lilya
from lilya.responses import JSONResponse
from lilya.routing import Path


def home():
    return JSONResponse({"message": "Welcome home"})


app = Lilya(routes=[Path("/", home)])

Ok

Response that can be used to return application/json as well. You can see this as an alternative to JSONResponse.

from lilya.responses import Ok

Example

from lilya.apps import Lilya
from lilya.responses import Ok
from lilya.routing import Path


def home():
    return Ok({"message": "Welcome home"})


app = Lilya(routes=[Path("/", home)])

RedirectResponse

Used for redirecting the responses.

from lilya.responses import RedirectResponse

Example

from lilya.apps import Lilya
from lilya.responses import RedirectResponse
from lilya.routing import Path


def home():
    return RedirectResponse(url="/another-url")


app = Lilya(routes=[Path("/", home)])

StreamingResponse

from lilya.responses import StreamingResponse

Example

from collections.abc import Generator

from lilya.apps import Lilya
from lilya.responses import StreamingResponse
from lilya.routing import Path


def my_generator() -> Generator[str, None, None]:
    count = 0
    while True:
        count += 1
        yield str(count)


def home():
    return StreamingResponse(my_generator(), media_type="text/html")


app = Lilya(routes=[Path("/", home)])

FileResponse

from lilya.responses import FileResponse

Streams a file asynchronously as the response, employing a distinct set of arguments for instantiation compared to other response types:

  • path - The filepath to the file to stream.
  • status_code - The Status code to return.
  • headers - Custom headers to include, provided as a dictionary.
  • media_type - A string specifying the media type. If unspecified, the filename or path is used to deduce the media type.
  • filename - If specified, included in the response Content-Disposition.
  • content_disposition_type - Included in the response Content-Disposition. Can be set to attachment (default) or inline.
  • background - A task instance.

Example

from lilya.apps import Lilya
from lilya.responses import FileResponse
from lilya.routing import Path


def home():
    return FileResponse(
        "files/something.csv",
        filename="something",
    )


app = Lilya(routes=[Path("/", home)])

Importing the appropriate class

This is the classic most used way of using the responses. The available responses contains a list of available responses of Lilya but you are also free to design your own and apply them.

Example

from lilya.apps import Lilya
from lilya.responses import JSONResponse
from lilya.routing import Path


def home():
    return JSONResponse({"message": "Welcome home"})


app = Lilya(routes=[Path("/", home)])

Build the Response

This is where the things get great. Lilya provides a make_response function that automatically will build the response for you.

from lilya.responses import make_response

Example

from lilya.apps import Lilya
from lilya.responses import make_response
from lilya.routing import Path


def home():
    return make_response({{"message": "Hello"}}, status_code=201)


app = Lilya(routes=[Path("/", home)])

By default, the make_response returns a JSONResponse but that can be also changed if the response_class parameter is set to something else.

So, why is this make_response different from the other responses? Well, here its where Lilya shines.

Lilya is pure Python, which means that it does not rely or depend on external libraries like Pydantic, msgspec, attrs or any other but allows you to build a custom encoder that can later be used to serialise your response automatically and then passed to the make_response.

Check the build a custom encoder and custom encoders with make_response for more details and how to leverage the power of Lilya.

Delegate to Lilya

Delegating to Lilya means that if no response is specified, Lilya will go through the internal encoders and will try to jsonify the response for you.

Let us see an example.

from lilya.apps import Lilya
from lilya.routing import Path


def home():
    return {"message": "Welcome home"}


app = Lilya(routes=[Path("/", home)])

As you can see, no response was specified but instead a python dict was returned. What Lilya internally does is to guess and understand the type of response parse the result into json and returning a JSONResponse automatically,

If the type of response is not json serialisable, then a ValueError is raised.

Let us see some more examples.

from lilya.apps import Lilya
from lilya.routing import Path


def home_dict():
    return {"message": "Welcome home"}


def home_frozen_set():
    return frozenset({"message": "Welcome home"})


def home_set():
    return set({"message": "Welcome home"})


def home_list():
    return ["Welcome", "home"]


def home_str():
    return "Welcome home"


def home_int():
    return 1


def home_float():
    return 2.0


app = Lilya(
    routes=[
        Path("/dict", home_dict),
        Path("/fronzenset", home_frozen_set),
        Path("/set", home_set),
        Path("/list", home_list),
        Path("/str", home_str),
        Path("/int", home_int),
        Path("/float", home_float),
    ]
)

And the list goes on and on. Lilya by design understands almost every single datastructure of Python by default, including Enum, deque, dataclasses, PurePath, generators and tuple.

Default Encoders

In order to understand how to serialise a specific object into json, Lilya has some default encoders that evaluates when tries to guess the response type.

  • DataclassEncoder - Serialises dataclass objects.
  • EnumEncoder - Serialises Enum objects.
  • PurePathEncoder - Serializes PurePath objects.
  • PrimitiveEncoder - Serializes python primitive types. str, int, float and None.
  • DictEncoder - Serializes dict types.
  • StructureEncoder - Serializes more complex data types. list, set, frozenset, GeneratorType, tuple, deque.

What a brand new encoder is needed and it is not natively supported by Lilya? Well, building a custom encoder is extremly easy and possible.

Build a custom encoder

As mentioned before, Lilya has default encoders that are used to transform a response into a json serialisable response.

To build a custom encoder you must use the Encoder class from Lilya and override the serialize() function where it applies the serialisation process of the encoder type.

Then you must register the encoder for Lilya to use it.

When defining an encoder the __type__ or def is_type(self, value: Any) -> bool: must be declared or overridden.

When the __type__ is properly declared, the default is_type will evaluate the object against the type and return True or False.

This is used internally to understand the type of encoder that will be applied to a given object.

Warning

If you are not able to provide the __type__ for any reason and you just want to override the default evaluation process, simple override the is_type() and apply your custom logic there.

E.g.: In Python 3.8, for a Pydantic BaseModel if passed in the __type__, it will throw an error due to Pydantic internals, so to workaround this issue, you can simply override the is_type() and apply the logic that validates the type of the object and returns a boolean.

from lilya.encoders import Encoder, register_encoder

Example

Create and register an encoder that handles msgspec.Struct types.

from typing import Any

import msgspec
from msgspec import Struct

from lilya.encoders import Encoder, register_encoder


class MsgSpecEncoder(Encoder):
    __type__ = Struct

    def serialize(self, obj: Any) -> Any:
        """
        When a `msgspec.Struct` is serialised,
        it will call this function.
        """
        return msgspec.json.decode(msgspec.json.encode(obj))


# A normal way
register_encoder(MsgSpecEncoder())

# As alternative
register_encoder(MsgSpecEncoder)

Simple right? Because now the MsgSpecEncoder is registered, you can simply do this in your handlers and return directly the msgspec.Struct object type.

from msgspec import Struct

from lilya.routing import Path


class User(Struct):
    name: str
    email: str


def msgspec_struct():
    return User(name="lilya", url="example@lilya.dev")

Design specific custom encoders

Lilya being 100% pure python and not tight to any particular validation library allows you to design custom encoders that are later used by Lilya responses.

Ok, this sounds a bit confusing right? I bet it does so let us go slowly.

Imagine you want to use a particular validation library such as Pydantic, msgspec or even attrs or something else at your choice.

You want to make sure that if you return a pydantic model or a msgspec Struct or even a define attr class.

Let us see how it would look like for all of them.

For Pydantic BaseModel

from __future__ import annotations

from typing import Any

from pydantic import BaseModel

from lilya.encoders import Encoder, register_encoder


class PydanticEncoder(Encoder):
    __type__ = BaseModel

    def serialize(self, obj: BaseModel) -> dict[str, Any]:
        return obj.model_dump()


# A normal way
register_encoder(PydanticEncoder())

# As alternative
register_encoder(PydanticEncoder)

For msgspec Struct

from typing import Any

import msgspec
from msgspec import Struct

from lilya.encoders import Encoder, register_encoder


class MsgSpecEncoder(Encoder):
    __type__ = Struct

    def serialize(self, obj: Any) -> Any:
        """
        When a `msgspec.Struct` is serialised,
        it will call this function.
        """
        return msgspec.json.decode(msgspec.json.encode(obj))


# A normal way
register_encoder(MsgSpecEncoder())

# As alternative
register_encoder(MsgSpecEncoder)

For attrs

from typing import Any

from attrs import asdict, has

from lilya.encoders import Encoder, register_encoder


class AttrsEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        """
        You can use this function instead of declaring
        the `__type__`.
        """
        return has(value)

    def serialize(self, obj: Any) -> Any:
        return asdict(obj)


# A normal way
register_encoder(AttrsEncoder())

# As alternative
register_encoder(AttrsEncoder)

Easy and poweful, right? Yes.

Do you understand what does this mean? Means you can design any encoder at your choice using also any library of your choice as well.

The flexibility of Lilya allows you to be free and for Lilya not to be tight to any particular library.

Custom encoders and responses

After the custom encoders in the examples are created, this allows to do something like this directly.

from attrs import define
from msgspec import Struct
from pydantic import BaseModel

from lilya.apps import Lilya
from lilya.routing import Path


class User(BaseModel):
    name: str
    age: int


class Item(Struct):
    name: str
    age: int


@define
class AttrItem:
    name: str
    age: int


def pydantic_response():
    return User(name="lilya", age=24)


def pydantic_response_list():
    return [User(name="lilya", age=24)]


def msgspec_struct():
    return Item(name="lilya", age=24)


def msgspec_struct_list():
    return [Item(name="lilya", age=24)]


def attrs_response():
    return AttrItem(name="lilya", age=24)


def attrs_response_list():
    return [AttrItem(name="lilya", age=24)]


app = Lilya(
    routes=[
        Path("/pydantic", pydantic_response),
        Path("/pydantic-list", pydantic_response_list),
        Path("/msgspec", msgspec_struct),
        Path("/msgspec-list", pydantic_response_list),
        Path("/attrs", attrs_response),
        Path("/attrs-list", attrs_response_list),
    ]
)

Custom encoders and the make_response

Well, here its where the make_response helps you. The make_response will generate a JSONResponse by default and when you return a custom encoder type, there are some limitations to it.

For example, what if you want to return with a different status_code? Or even attach a task to it?

The custom encoder does not handle that for you but the make_response does!

Let us see how it would look like now using the make_response.

from attrs import define
from msgspec import Struct
from pydantic import BaseModel

from lilya import status
from lilya.apps import Lilya
from lilya.responses import make_response
from lilya.routing import Path


class User(BaseModel):
    name: str
    age: int


class Item(Struct):
    name: str
    age: int


@define
class AttrItem:
    name: str
    age: int


def pydantic_response():
    data = User(name="lilya", age=24)
    return make_response(
        data,
        status_code=status.HTTP_200_OK,
    )


def pydantic_response_list():
    data = [User(name="lilya", age=24)]
    return make_response(
        data,
        status_code=status.HTTP_201_CREATED,
        background=...,
        headers=...,
    )


def msgspec_struct():
    return make_response(Item(name="lilya", age=24))


def msgspec_struct_list():
    return make_response(
        [Item(name="lilya", age=24)],
        status_code=...,
    )


def attrs_response():
    return make_response(
        AttrItem(name="lilya", age=24),
        status_code=...,
    )


def attrs_response_list():
    return make_response(
        [AttrItem(name="lilya", age=24)],
        status_code=...,
    )


app = Lilya(
    routes=[
        Path("/pydantic", pydantic_response),
        Path("/pydantic-list", pydantic_response_list),
        Path("/msgspec", msgspec_struct),
        Path("/msgspec-list", pydantic_response_list),
        Path("/attrs", attrs_response),
        Path("/attrs-list", attrs_response_list),
    ]
)