Skip to content

Handling Configuration (config.py)

Configuration is where many Flask apps go wrong.

Good config:

  • separates dev vs prod
  • uses environment variables for secrets
  • keeps defaults sensible

config.py:

python
import os
 
 
class BaseConfig:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key")
    SQLALCHEMY_TRACK_MODIFICATIONS = False
 
 
class DevelopmentConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
 
 
class TestingConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
 
 
class ProductionConfig(BaseConfig):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
python
app.config.from_object("config.DevelopmentConfig")

Or:

python
app.config.from_object(config_object)

Never commit secrets (SECRET_KEY, DB passwords) to git.

Use:

  • .env files locally (with python-dotenv)
  • CI/hosting environment variables in production

Four ways to load configuration, and the order matters

Section titled “Four ways to load configuration, and the order matters”
diagram Diagram mermaid

Measured: after from_object("config.Dev") set DATABASE = "dev.db", a later from_pyfile("config.py") changed it to instance.db, and from_prefixed_env() then changed it to env.db. Load from least specific to most specific.

config.py
class Base:
    DEBUG = False
    DATABASE = "base.db"
    lowercase_ignored = "not loaded"
 
class Dev(Base):
    DEBUG = True
    DATABASE = "dev.db"
measured
DEBUG             True
DATABASE          'dev.db'
lowercase_ignored None      <- silently skipped
inherited DEBUG   present   <- class inheritance works

A lowercase name is not an error — it simply never arrives, which is a quiet way to lose a setting. The convention exists so a config module can hold helpers and imports without them polluting app.config.

load.py
app = Flask(__name__, instance_relative_config=True)
app.config.from_pyfile("config.py")            # reads instance/config.py
measured
SECRET_KEY  'from-instance-file'

A missing file raises:

silent.py
app.config.from_pyfile("nope.py")                 # FileNotFoundError
app.config.from_pyfile("nope.py", silent=True)    # returns False, carries on

Use silent=True for a genuinely optional local override, and let it raise for a file that must exist.

environment
FLASK_DATABASE=env.db
FLASK_MAX_ITEMS=25
FLASK_FEATURE_ON=true
DATABASE=no-prefix-ignored
measured
DATABASE    'env.db'
MAX_ITEMS   25      (int)     <- parsed as JSON, not left as a string
FEATURE_ON  True    (bool)
unprefixed DATABASE ignored

This is the detail people miss: values are run through a JSON parse, so 25 becomes an int and true becomes a bool rather than the string 'true' — which would have been truthy either way and hidden the bug. A value that is not valid JSON stays a string.

sketch Precedence: the last load wins p5.js
Configuration sources are applied in order. Load defaults first and the most deployment-specific source last.
pch.quizTag pch.quizDefaultTitle
  1. A config class defines DEBUG = True and lowercase_ignored = 'x'. What does from_object load?

    pch.quizShowAnswer

    B — only DEBUG; lowercase names are skipped silently — Measured: lowercase_ignored came back None. The convention lets a config module hold imports and helpers without them landing in app.config — but it also means a mis-cased setting vanishes without an error.

  2. With FLASK_MAX_ITEMS=25 in the environment, what does from_prefixed_env put in app.config['MAX_ITEMS']?

    pch.quizShowAnswer

    B — the integer 25, because values are parsed as JSON — Measured as int. FLASK_FEATURE_ON=true likewise became the bool True rather than the truthy string 'true'. Values that are not valid JSON remain strings.

  3. In what order should configuration sources be loaded?

    pch.quizShowAnswer

    B — defaults first, then instance files, then environment, then explicit overrides — least specific to most — Each load overwrites the previous, so the last write wins. Measured DATABASE moving dev.db to instance.db to env.db as each source was applied.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading