The Flask App Factory Pattern
Using create_app
with sensible defaults for great good.
Series: How I Use Flask
6 min
Starting up a Flask app
As the Flask docs point out, every Flask app is an instance of the Flask
class, and all configuration must be registered with that class at app startup. Flask’s standard way of handling this is with the create_app
factory, whose job it is to instantiate and return an instance of the Flask
class.
There are some pretty standard things I do in all of my Flask create_app
functions. Let’s take a look at those—and point out that some of them may be subjects of further posts to look more specifically at a certain piece of functionality.
Always expect a config file to be present
I always expect a config file to be present at app startup time, even in development. However, I also want to ensure other developers who are just getting started know a config file should be present. On top of that, I also want to support a config file as an option—meaning I want to allow a developer (or myself) to tell the app what config file to use when starting up. Lastly, I want to report a meaningful error if the config file cannot be found, and immediately stop trying to start the app.
Here’s typically how I handle accomplishing all this:
# proj/app/__init__.py
from os.path import abspath, dirname, isfile, join
def create_app(config_file=None):
"""Default application factory.
Default usage:
app = create_app()
Usage with config file:
app = create_app('/path/to/config.yml')
If config_file is not provided, will look for default
config expected at '<proj_root>/config/config.yml'.
Returns Flask app object.
Raises EnvironmentError if config_file cannot be found.
"""
current_dir = dirname(abspath(__file__))
if config_file is None:
# use default config file location
config_file = abspath(join(current_dir, '../config/config.yml'))
else:
config_file = abspath(config_file)
# raise error if config_file doesn't exist
if not isfile(config_file):
raise EnvironmentError('App config file does not exist at %s' % config_file)
This should all look pretty self-explanatory to seasoned Python developers. My create_app
function allows an optional config_file
keyword argument that defaults to None
. If no config_file
is provided when invoking create_app
, startup will look for a config.yml
file to be present in a sibling config
folder to the current_dir
. I then use os.path.isfile
to check the config_file
is in fact present, and raise an EnvironmentError
to inform a developer (or myself) that the required config was not found (whether passed in or the expected default).
The goal here is for the app to inform a developer where the app looked for a config file when it isn’t present, or to proceed when it is.
Make it easy to know (and check) app state
It’s not uncommon to need to know your app’s state somewhere in your codebase—by which I mean knowing if your app is in debug, testing, or live/production mode. I like to make sure my app carries this information at all times. This is a lesson learned from years of particularly annoying checks like this in different parts of app code:
from flask import current_app as app
# inside some code block somewhere
if not app.config.get('DEBUG', True):
# do a thing that should only happen in live mode
It took way too long to one day think this should just live as attributes on the app to make conditionals like this easier to read again and again. Now, I typically wind up doing something like the following, leveraging both knowledge of the DEBUG
config variable, as well as my app’s ENVIRONMENT
settings from my configs:
# proj/app/__init__.py -- inside create_app
# set some helpful attrs to greatly simplify state checks
cfg = app.config
app.env = cfg.get('ENVIRONMENT', 'development')
if app.debug:
app.live = False
if app.env == 'test':
app.testing = True
elif app.env == 'development':
app.dev = True
else:
raise EnvironmentError('Invalid environment for app state.')
else:
if app.env == 'production':
app.live = True
elif app.env == 'development':
# dev.proj runs in development with debug off
app.live = False
app.testing = False
else:
raise EnvironmentError('Invalid environment for app state.')
I make some sanity checks on app state + environment, then set necessary attributes on the Flask
app itself—app.env
, app.live
, and app.dev
are the custom attributes here, as Flask itself supports app.debug
and app.testing
. These sanity checks basically ensure we prevent an app from starting when certain environment + debug settings are mutually exclusive—like running in debug
mode in a production
environment.
Now, it’s possible to make some simple state checks in code that are easy to understand on first read:
from flask import current_app as app
# inside some code block somewhere
if app.live:
# do live-only stuff here
if app.debug:
# do debug stuff
if app.testing:
# do something that should only happen during tests
Why
app.debug
andapp.dev
? I run dev/staging instances withDEBUG
off, but may still want to know that I’m indev
mode. One way in which I leverage this is using this attr to place a colored border on the page to visually signify that I’m looking at a dev instance of an app.
State-dependent feature activation
It’s awfully common to activate certain features and third-party integrations, dependencies, or tools based on what state an app is running under. With the above state and app.env
attributes in place, it becomes pretty easy to do so.
For example, if I only want to setup Rollbar error reporting when my app is in live
mode, it’s this easy:
# proj/app/__init__.py -- inside create_app
# setup rollbar
if app.live:
import rollbar
from rollbar.contrib.flask import report_exception
rollbar.init(
cfg.get('ROLLBAR_ACCESS_TOKEN'),
app.env,
# server root directory, makes tracebacks prettier
root=cfg.get('ROOT_DIR'),
# flask already sets up logging
allow_logging_basic_config=False
)
got_request_exception.connect(report_exception, app)
Now I can rest easy knowing I won’t be reporting errors to Rollbar from localhost
.
Customizing Jinja
The Flask app factory is also the place for customizing how you want to use Jinja in your app—e.g., registering custom filters, template helpers, or customizing the way you want to use Jinja line statements. That last one is a big one for me. I’m simply not a fan of the mustache-percent syntax in my templates—mostly because I find them unpleasant to both type and read.
Here’s how I configure Jinja to recognize my preferred line statements:
# proj/app/__init__.py -- inside create_app
app.jinja_env.line_statement_prefix = '%'
app.jinja_env.line_comment_prefix = '##'
This means my templates wind up looking like this:
<!-- some_template.html -->
% macro scheduled_post(obj, pubtime=None)
% if obj.publish_on
<div class="mb2 pa3 ba br1 b--orange tc">
<h5 class="f5 mt0 mb2">Scheduled to Publish:</h5>
<p class="f6 pv0 ma0">{{ obj.publish_on.strftime('%A, %B %d') }}</p>
% if pubtime
<p class="f6 pv0 ma0">{{ pubtime }}</p>
% endif
</div>
% endif
% endmacro
Using helper functions to initialize models and views
I’m not the biggest fan of the way one must register models and views in a Flask app. I’m going to cover how I automate model/view discovery and registration in a future post, but suffice to say I call out to helper functions for this (as well as a few other tasks). I like to keep my create_app
functions as easy-to-read as I can, so registering models and views winds up looking like this in my apps:
# proj/app/__init__.py
from flask import Flask
from proj.db import db, init_db
def create_app(config_file=None):
# ... all the other create_app stuff
app = Flask(__name__)
# setup db
init_db(app, db)
# register views
from proj.views import init_views
init_views(app)
Up next
Well, I wanted to keep this post right around the 5-minute mark for reading time, and I’ve gone just over that. We’re going to end there.
Up next, we’ll take a look at how I organize and automate discovery and initialization of app models. Stay tuned.