Configuration & Environments
The previous lesson's create_app() called app.config.from_object(Config)
without explaining what that Config actually holds. This lesson fills that gap:
where Bookshelf's settings live, how to organize them for different environments, and how to keep
the sensitive ones out of your source code entirely.
app.config: Where App-Wide Settings Live
Every Flask application instance carries a dictionary-like object at app.config,
holding every app-wide setting Flask and its extensions read from. A few keys you'll see
constantly through this track:
SECRET_KEY— signs session cookies and other security-sensitive data; needed as soon as we cover Sessions & Cookies and Flash MessagesSQLALCHEMY_DATABASE_URI— the database connection string Flask-SQLAlchemy uses, covered in Flask-SQLAlchemy BasicsDEBUG— whether debug mode (auto-reload, the interactive debugger) is active
You can set these directly — app.config['SECRET_KEY'] = '...' — but doing
that for every setting, in the file that also defines routes, gets messy fast. That's what the
Config class pattern is for.
The Config Class Pattern
Rather than scattering configuration across the codebase, a common Flask convention is a single
config.py file holding a base Config class plus one subclass per
environment, each overriding only the values that differ:
Example: config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-fallback-key')
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///bookshelf-dev.db'
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
DevelopmentConfig points at a local SQLite file, ideal for working on Bookshelf on
your own machine. TestingConfig uses an in-memory SQLite database that vanishes the
moment the test run finishes — exactly the isolation the previous lesson's factory pattern
was built to support. ProductionConfig reads a real database URL from an environment
variable rather than hardcoding one, and keeps DEBUG firmly off.
Loading the Right Config
app.config.from_object() accepts either a class object or a dotted import path as a
string, so create_app() can load whichever environment's configuration it needs:
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object('config.DevelopmentConfig')
return app
Passing which config class to load as an argument to create_app() itself (for
example, create_app('config.TestingConfig')) is a common refinement — it lets
the test suite request TestingConfig explicitly, while the real development server
requests DevelopmentConfig, from the same factory function.
Reading Sensitive Values from Environment Variables
Notice that SECRET_KEY above reads from os.environ.get('SECRET_KEY',
'dev-fallback-key') rather than a hardcoded string. This is a deliberate security habit:
anything genuinely sensitive — secret keys, database passwords, API tokens — should
come from the environment the app runs in, not be typed directly into a file that lives in
version control.
'dev-fallback-key' only ever
gets used if SECRET_KEY isn't set in the environment, which is a reasonable
convenience while developing Bookshelf locally. In production, that environment variable should
always be set to a real, unique, unguessable value — never the fallback. This distinction
gets revisited with concrete guidance in the later
Security Best Practices lesson.
Local Convenience with python-dotenv
Setting environment variables by hand in every new terminal session gets tedious quickly. The
python-dotenv package solves this for local development by loading key-value pairs
from a plain text file named .env into the environment automatically.
pip install python-dotenv
A .env file at the project root holds your local values in simple
KEY=value lines:
SECRET_KEY=a-real-random-value-for-local-dev
DATABASE_URL=sqlite:///bookshelf-dev.db
Then, near the top of run.py (before create_app() is called), load it:
from dotenv import load_dotenv
load_dotenv()
from bookshelf import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)
load_dotenv() reads .env and populates os.environ with its
values before anything else runs, so every os.environ.get(...) call in
config.py sees them exactly as if they'd been set in the terminal directly.
Why .env Must Never Be Committed
Because .env exists specifically to hold real secrets for your local setup, it must
be added to .gitignore immediately — before it ever contains a real value:
# .gitignore
.env
.env file committed to a public (or even private) repository is a
genuine security incident waiting to happen — secret keys and database credentials end up
readable by anyone with repository access, forever, in the commit history. This exact concern
returns with more detail in Security Best
Practices later in the track. Many teams commit a .env.example file instead,
listing the expected variable names with placeholder values, so new contributors know what to
fill in without ever seeing real secrets.
FLASK_ENV and FLASK_DEBUG
Flask also recognizes a couple of its own environment variables that affect its behavior directly,
separate from anything in your own Config classes. FLASK_DEBUG=1 turns
on debug mode (the same auto-reload and interactive debugger covered in the previous lesson)
without needing debug=True hardcoded into app.run(). Older Flask
versions also used a FLASK_ENV variable (set to development or
production) to control this and a few related defaults at once; current Flask
versions favor FLASK_DEBUG directly. Either way, the same rule applies as always:
these should be 1/development locally, and unset or
production-appropriate anywhere Bookshelf is actually deployed.
/books, /shelves, and more — to view functions.