Centralized user messages in Python apps#

Helpful errors with error codes. Do you love it too? During the development of the Snippets, an online preview tool for reStructuredText (and Markdown coming soon), I tried to produce actually helpful messages to the API user. I started with collecting text strings that could appear to users across the codebase and put them into a single file.

Requirements#

I want

  • have a single central file for all messages

  • messages are not strictly error but all messages that could ever appear to the user

  • every message will need at least a title, a brief summary, and, optionally, a longer description

  • assign codes to messages so the user and developer can quickly “tell” the message even if the wording has changed

  • consume message as str primitive and as dict

The Message class#

In the project root, I create messages.py.

Instead of plain constants, I create a simple class with a title, description, and code.

Python 3.7 has introduced dataclasses, which are ideal for the purpose. I want also some nice string representation.

from dataclasses import asdict, dataclass
from typing import Optional

@dataclass
class Message:
    code: int
    title: str
    description: Optional[str] = None

    def to_dict(self):
        return asdict(self)

    def __str__(self):
        return (
            f"{self.title}: {self.description} (code #{self.code})"
            if self.description
            else f"{self.title}: (code #{self.code})"
        )

Message collection#

Continuing in the messages.py, the collected strings are now Message instances:

BookNotFoundMsg = Message(100, "No such book found")

RouteNotFoundMsg = Message(
    101,
    "No such route",
    "The API doesn't know endpoint of such URI."
)

ShareNotFoundMsg = Message(
    102,
    "Share not found",
    "Share with specified digest has not been found."
)

Thanks to Dataclass, we get the appropriate __init__() constructor for free.

Usage#

String primitives#

Thanks to customized __str__(), a message will become beautiful string:

from messages import BookNotFoundMsg

# No such book found: (code #100)
print(BookNotFoundMsg)

# No such route: The API doesn't know endpoint of such URI. (code #101)
print(RouteNotFoundMsg)

Or accessing individual fields:

# No such route
print(RouteNotFoundMsg.title)

Exceptions#

Also, passing to the exception plays nicely:

# ValueError: No such book found: (code #100)
raise ValueError(BookNotFoundMsg)

# ValueError: No such route: The API doesn't know endpoint of such URI. (code #101)
raise ValueError(RouteNotFoundMsg)

Falcon Web framework#

Our backend technology stack uses amazing Falcon, a minimalist WSGI/ASGI framework for REST APIs.

In Falcon, raising falcon.HTTPError (and children) gives API users beautiful error messages out of the box. These exceptions accept title, description, and code kwargs.

For example,

 raise falcon.HTTPNotFound(title="Book not found")

refactored to

 raise falcon.HTTPNotFound(**BookNotFoundMsg.to_dict())

produces

HTTP/1.1 404 Not Found
Server: gunicorn/20.0.4
Date: Fri, 14 Jul 2023 12:01:19 GMT
Connection: close
content-type: application/json
vary: Accept
content-length: 37

{"title: "Share not found", "description": "Share with specified digest has not been found.", "code": 102}

Falcon even support adding a link to help page with link kwarg.

Comments

comments powered by Disqus