# Sanic Application

# Instance

The most basic building block is the Sanic() instance. It is not required, but the custom is to instantiate this in a file called server.py.

# /path/to/server.py
from sanic import Sanic
app = Sanic("MyHelloWorldApp")

# Application context

Most applications will have the need to share/reuse data or objects across different parts of the code base. The most common example is DB connections.

In versions of Sanic prior to v21.3, this was commonly done by attaching an attribute to the application instance

# Raises a warning as deprecated feature in 21.3
app = Sanic("MyApp")
app.db = Database()

Because this can create potential problems with name conflicts, and to be consistent with request context objects, v21.3 introduces application level context object.

# Correct way to attach objects to the application
app = Sanic("MyApp")
app.ctx.db = Database()

# App Registry

When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible.

# ./path/to/server.py
from sanic import Sanic
app = Sanic("my_awesome_server")
# ./path/to/somewhere_else.py
from sanic import Sanic
app = Sanic.get_app("my_awesome_server")

If you call Sanic.get_app("non-existing") on an app that does not exist, it will raise SanicException by default. You can, instead, force the method to return a new instance of Sanic with that name.

app = Sanic.get_app(
    "non-existing",
    force_create=True,
)

If there is only one Sanic instance registered, then calling Sanic.get_app() with no arguments will return that instance

Sanic("My only app")
app = Sanic.get_app()

# Configuration

Sanic holds the configuration in the config attribute of the Sanic instance. Configuration can be modified either using dot-notation OR like a dictionary.

app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config['DB_USER'] = 'appuser'
db_settings = {
    'DB_HOST': 'localhost',
    'DB_NAME': 'appdb',
    'DB_USER': 'appuser'
}
app.config.update(db_settings)

Heads up

Config keys should be uppercase. But, this is mainly by convention, and lowercase will work most of the time.

app.config.GOOD = "yay!"
app.config.bad = "boo"

There is much more detail about configuration later on.

# Customization

The Sanic application instance can be customized for your application needs in a variety of ways at instantiation.

# Custom configuration

This simplest form of custom configuration would be to pass your own object directly into that Sanic application instance

If you create a custom configuration object, it is highly recommended that you subclass the Sanic Config option to inherit its behavior. You could use this option for adding properties, or your own set of custom logic.

Added in v21.6

from sanic.config import Config
class MyConfig(Config):
    FOO = "bar"
app = Sanic(..., config=MyConfig())

A useful example of this feature would be if you wanted to use a config file in a form that differs from what is supported.

from sanic import Sanic, text
from sanic.config import Config
class TomlConfig(Config):
    def __init__(self, *args, path: str, **kwargs):
        super().__init__(*args, **kwargs)
        with open(path, "r") as f:
            self.apply(toml.load(f))
    def apply(self, config):
        self.update(self._to_uppercase(config))
    def _to_uppercase(self, obj: Dict[str, Any]) -> Dict[str, Any]:
        retval: Dict[str, Any] = {}
        for key, value in obj.items():
            upper_key = key.upper()
            if isinstance(value, list):
                retval[upper_key] = [
                    self._to_uppercase(item) for item in value
                ]
            elif isinstance(value, dict):
                retval[upper_key] = self._to_uppercase(value)
            else:
                retval[upper_key] = value
        return retval
toml_config = TomlConfig(path="/path/to/config.toml")
app = Sanic(toml_config.APP_NAME, config=toml_config)

# Custom context

By default, the application context is a SimpleNamespace() (opens new window) that allows you to set any properties you want on it. However, you also have the option of passing any object whatsoever instead.

Added in v21.6

app = Sanic(..., ctx=1)
app = Sanic(..., ctx={})
class MyContext:
    ...
app = Sanic(..., ctx=MyContext())

# Custom requests

It is sometimes helpful to have your own Request class, and tell Sanic to use that instead of the default. One example is if you wanted to modify the default request.id generator.

Important

It is important to remember that you are passing the class not an instance of the class.

import time
from sanic import Request, Sanic, text
class NanoSecondRequest(Request):
    @classmethod
    def generate_id(*_):
        return time.time_ns()
app = Sanic(..., request_class=NanoSecondRequest)
@app.get("/")
async def handler(request):
    return text(str(request.id))

# Custom error handler

See exception handling for more

from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
    def default(self, request, exception):
        ''' handles errors that have no error handlers assigned '''
        # You custom error handling logic...
        return super().default(request, exception)
app = Sanic(..., error_handler=CustomErrorHandler())

# Custom dumps function

It may sometimes be necessary or desirable to provide a custom function that serializes an object to JSON data.

import ujson
dumps = partial(ujson.dumps, escape_forward_slashes=False)
app = Sanic(__name__, dumps=dumps)

Or, perhaps use another library or create your own.

from orjson import dumps
app = Sanic(__name__, dumps=dumps)

# Custom loads function

Similar to dumps, you can also provide a custom function for deserializing data.

Added in v22.9

from orjson import loads
app = Sanic(__name__, loads=loads)

NEW in v23.6

# Custom typed application

The correct, default type of a Sanic application instance is:

sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]

It refers to two generic types:

  1. The first is the type of the configuration object. It defaults to sanic.config.Config, but can be any subclass of that.
  2. The second is the type of the application context. It defaults to types.SimpleNamespace, but can be any object as show above.

Let's look at some examples of how the type will change.

Consider this example where we pass a custom subclass of Config and a custom context object.

from sanic import Sanic
from sanic.config import Config
class CustomConfig(Config):
    pass
app = Sanic("test", config=CustomConfig())
reveal_type(app) # N: Revealed type is "sanic.app.Sanic[main.CustomConfig, types.SimpleNamespace]"
sanic.app.Sanic[main.CustomConfig, types.SimpleNamespace]

Similarly, when passing a custom context object, the type will change to reflect that.

from sanic import Sanic
class Foo:
    pass
app = Sanic("test", ctx=Foo())
reveal_type(app)  # N: Revealed type is "sanic.app.Sanic[sanic.config.Config, main.Foo]"
sanic.app.Sanic[sanic.config.Config, main.Foo]

Of course, you can set both the config and context to custom types.

from sanic import Sanic
from sanic.config import Config
class CustomConfig(Config):
    pass
class Foo:
    pass
app = Sanic("test", config=CustomConfig(), ctx=Foo())
reveal_type(app)  # N: Revealed type is "sanic.app.Sanic[main.CustomConfig, main.Foo]"
sanic.app.Sanic[main.CustomConfig, main.Foo]

This pattern is particularly useful if you create a custom type alias for your application instance so that you can use it to annotate listeners and handlers.

# ./path/to/types.py
from sanic.app import Sanic
from sanic.config import Config
from myapp.context import MyContext
from typing import TypeAlias
MyApp = TypeAlias("MyApp", Sanic[Config, MyContext])
# ./path/to/listeners.py
from myapp.types import MyApp
def add_listeners(app: MyApp):
    @app.before_server_start
    async def before_server_start(app: MyApp):
        # do something with your fully typed app instance
        await app.ctx.db.connect()
# ./path/to/server.py
from myapp.types import MyApp
from myapp.context import MyContext
from myapp.config import MyConfig
from myapp.listeners import add_listeners
app = Sanic("myapp", config=MyConfig(), ctx=MyContext())
add_listeners(app)

Added in v23.6

# Custom typed request

Sanic also allows you to customize the type of the request object. This is useful if you want to add custom properties to the request object, or be able to access your custom properties of a typed application instance.

The correct, default type of a Sanic request instance is:

sanic.request.Request[
    sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace],
    types.SimpleNamespace
]

It refers to two generic types:

  1. The first is the type of the application instance. It defaults to sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace], but can be any subclass of that.
  2. The second is the type of the request context. It defaults to types.SimpleNamespace, but can be any object as show above in custom requests.

Let's look at some examples of how the type will change.

Expanding upon the full example above where there is a type alias for a customized application instance, we can also create a custom request type so that we can access those same type annotations.

Of course, you do not need type aliases for this to work. We are only showing them here to cut down on the amount of code shown.

from sanic import Request
from myapp.types import MyApp
from types import SimpleNamespace
def add_routes(app: MyApp):
    @app.get("/")
    async def handler(request: Request[MyApp, SimpleNamespace]):
        # do something with your fully typed app instance
        results = await request.app.ctx.db.query("SELECT * FROM foo")

Perhaps you have a custom request object that generates a custom context object. You can type annotate it to properly access those properties with your IDE as shown here.

from sanic import Request, Sanic
from sanic.config import Config
class CustomConfig(Config):
    pass
class Foo:
    pass
class RequestContext:
    foo: Foo
class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]):
    @staticmethod
    def make_context() -> RequestContext:
        ctx = RequestContext()
        ctx.foo = Foo()
        return ctx
app = Sanic(
    "test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest
)
@app.get("/")
async def handler(request: CustomRequest):
    # Full access to typed:
    # - custom application configuration object
    # - custom application context object
    # - custom request context object
    pass

See more information in the custom request context section.

Added in v23.6

MIT Licensed
Copyright © 2018-present Sanic Community Organization

~ Made with ❤️ and ☕️ ~