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 Messages
  • SQLALCHEMY_DATABASE_URI — the database connection string Flask-SQLAlchemy uses, covered in Flask-SQLAlchemy Basics
  • DEBUG — 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.

Why the fallback value matters too: '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
Note: A .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.

Next up: With configuration sorted, head to Routing Basics to start mapping real Bookshelf URLs — /books, /shelves, and more — to view functions.