Skip to content

Configuration

Starlette encourages strict separation of configuration and code according to the twelve-element pattern.

Configuration should be stored in environment variables or in .env files that are not committed to source control.

from sqlalchemy import create_engine
from starlette.applications import Starlette
from starlette.config import Config
from starlette.datastructures import CommaSeparatedStrings, Secret

# Config will be read from environment variables and/or ".env" files.
config = Config(".env")

DEBUG = config('DEBUG', cast=bool, default=False)
DATABASE_URL = config('DATABASE_URL')
SECRET_KEY = config('SECRET_KEY', cast=Secret)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=CommaSeparatedStrings)

app = Starlette(debug=DEBUG)
engine = create_engine(DATABASE_URL)
...


# Don't commit this to source control.
# Eg. Include ".env" in your `.gitignore` file.
DEBUG=True
DATABASE_URL=postgresql://user:password@localhost:5432/database
SECRET_KEY=43n080musdfjt54t-09sdgr
ALLOWED_HOSTS=127.0.0.1, localhost

configuration priority level

Configuration values are read in the following order:

  • From environmental variables.
  • from .env file.
  • Default values given in config.

If none of these match,config(...) will raise an error.

secret

For sensitive keys, the Secret class is useful because it helps minimize situations where its held values might leak into backtracking or other code introspection.

To get the value of an Secret instance, you must explicitly convert it to a string. You should do this only where the value is used.

>>> from myproject import settings
>>> settings.SECRET_KEY
Secret('**********')
>>> str(settings.SECRET_KEY)
'98n349$%8b8-7yjn0n8y93T$23r'

Hint

You can use DatabaseURL from databases package here to store database URLs and avoid leaking them in the logs.

comma-separated string

The CommaSeparatedStrings type is useful for accommodating multiple content in a single configuration key.

>>> from myproject import settings
>>> print(settings.ALLOWED_HOSTS)
CommaSeparatedStrings(['127.0.0.1', 'localhost'])
>>> print(list(settings.ALLOWED_HOSTS))
['127.0.0.1', 'localhost']
>>> print(len(settings.ALLOWED_HOSTS))
2
>>> print(settings.ALLOWED_HOSTS[0])
'127.0.0.1'

Read or modify environment

In some cases, you may want to programmatically read or modify environment variables. This is especially useful in tests where you may want to override specific keys in your environment.

Instead of reading or writing from os.environ, you should use Starlette's environ instance. This instance maps to the standard os.environ, which gives you extra protection by raising errors if you set any environment variables after the configuration has read them.

If you are using pytest, you can set up any initial environment in tests/conftest.py.

from starlette.config import environ

environ['DEBUG'] = 'TRUE'

Read Prefixed Environment Variables

You can namespace environment variables by setting the env_prefix parameter.

import os

from starlette.config import Config

os.environ['APP_DEBUG'] = 'yes'
os.environ['ENVIRONMENT'] = 'dev'

config = Config(env_prefix='APP_')

DEBUG = config('DEBUG') # lookups APP_DEBUG, returns "yes"
ENVIRONMENT = config('ENVIRONMENT') # lookups APP_ENVIRONMENT, raises KeyError as variable is not defined

a complete example

Building large applications can be complex. You need to properly separate configuration and code, isolate databases during testing, use separate test and production databases, and so on.

Here, we'll look at a complete example that shows how we can start building an application.

First, let's separate our settings, our database table definitions, and our application logic:

from starlette.config import Config
from starlette.datastructures import Secret

config = Config(".env")

DEBUG = config('DEBUG', cast=bool, default=False)
SECRET_KEY = config('SECRET_KEY', cast=Secret)

DATABASE_URL = config('DATABASE_URL')


import sqlalchemy

# Database table definitions.
metadata = sqlalchemy.MetaData()

organisations = sqlalchemy.Table(
    ...
)


from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Route

from myproject import settings


async def homepage(request):
    ...

routes = [
    Route("/", endpoint=homepage)
]

middleware = [
    Middleware(
        SessionMiddleware,
        secret_key=settings.SECRET_KEY,
    )
]

app = Starlette(debug=settings.DEBUG, routes=routes, middleware=middleware)

Now let's deal with our test configuration. We want to create a new test database every time the test suite runs and delete it after the test is complete. We also want to make sure

from starlette.config import environ
from starlette.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy_utils import create_database, database_exists, drop_database

# This line would raise an error if we use it after 'settings' has been imported.
environ['DEBUG'] = 'TRUE'

from myproject import settings
from myproject.app import app
from myproject.tables import metadata


@pytest.fixture(autouse=True, scope="session")
def setup_test_database():
    """
    Create a clean test database every time the tests are run.
    """
    url = settings.DATABASE_URL
    engine = create_engine(url)
    assert not database_exists(url), 'Test database already exists. Aborting tests.'
    create_database(url)             # Create the test database.
    metadata.create_all(engine)      # Create the tables.
    yield                            # Run the tests.
    drop_database(url)               # Drop the test database.


@pytest.fixture()
def client():
    """
    Make a 'client' fixture available to test cases.
    """
    # Our fixture is created within a context manager. This ensures that
    # application lifespan runs for every test case.
    with TestClient(app) as test_client:
        yield test_client