A 'skill' that's always fascinated me is just how long some engineers can make it in their career while carrying glaringly obvious gaps in their knowledge of the systems they use every day. To my surprise, I've turned corners where I myself have been that engineer all along, and there's perhaps no better example of this then the time I've spent with Flask.
WARNING: highly opinionated statement incoming: Flask is everything a framework should be. /opinion. That is to say, it isn't really a fully-fledged framework at all. Sure, the term microframework might seem like a cute branding term, but that doesn't negate the fact that there's something about Flask that's different. When I write apps in Flask, I feel as though I'm writing apps in Python. On the other hand, when I write apps in Django, it feels more like I'm writing apps in Django. A disciplined programmer might feel that overly structured frameworks damper creativity and they're probably right: these are the backbones of businesses, thus it makes sense to keep people from deviating from the norm.
The upside of Flask is also its downside: there's nearly an infinite number of ways to solve a single problem. Every StackOverflow regular has their own preference, and sometimes, just none of them seem... right. The problem is undoubtedly compounded by some of the phrasing coming from Flask's official documentation itself. Instead of guiding users down the correct path of best practices, Flask's docs sometimes read like a distracted child aimlessly attempting to explain everything in the universe, while giving weight to nothing in particular.
Flask Factories And "The Flask Application Context"
When you hear of the Flask Application Factory, this is simply referring to a common (and preferred) base structure to building a Flask app. The Application Factory pattern is an app structure where our app entry point sits atop all other parts of our application, and pieces together the various modules or Blueprints that might make our app.
The reason why the Application Factory is so important has to do with something called Flask's Application Context. Our app's "context" is what takes the assortment of Python files and modules we call our app, and brings them together so that they see and work with one another. In order for Flask to recognize the data models we have in
models.py, our Blueprints, or anything else, Flask needs to be told that these things exist after the app is "created" with
app = Flask(__name__).
If you are already using packages and blueprints for your application (Modular Applications with Blueprints) there are a couple of really nice ways to further improve the experience. A common pattern is creating the application object when the blueprint is imported.
And here's their description of the Application context:
The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the current_app and g proxies are accessed instead.
That's not entirely useful if we don't understand what the terminology is referring to. Let's fix that.
Top-Down View of an Application Factory App
Are you the type of person to start an app by first creating an app.py file in our base directory? If so, please stop - this simply isn't a realistic way to build production-ready applications. When we create an app which follows the Application Factory pattern, our app should look like this:
app ├── /application │ ├── __init__.py │ ├── auth.py │ ├── forms.py │ ├── models.py │ ├── routes.py │ ├── /static │ │ ├── /dist │ │ │ ├── /css │ │ │ ├── /img │ │ │ └── /js │ │ └── src │ │ ├── /js │ │ └── /less │ └── /templates ├── config.py ├── requirements.txt ├── setup.py ├── start.sh └── wsgi.py
Notice there's no app.py, main.py, or anything of the sort in our base directory. Instead, the entirety of our app lives in the /application folder, with the creation of our app happening in
__init__.py. The init file is where we actually create what's called the Application Factory.
If you're wondering how we deploy an app where the main entry point isn't in the root directory, I'm very proud of you. Yes, our app is being created in application/__init__.py, so a file called wsgi.py simply imports this file for the purpose of serving as our app gateway. More on that another time.
Structuring __init__.py Correctly
Let's dig into it! The example app I'll be creating will use a database and a Redis store for session variables simply to demonstrate.
A properly configured Applciation Factory should do the following for our app:
- Derive our app's config from a class, file, or environment variables.
- Initialize plugins accessible to any part of our app, such as a database or login logic with
- Set any variables we want to be accessible globally.
- Import the logic which makes u our app (such as routes).
- Register Blueprints.
The below does all of those things:
from flask import Flask, g from flask_sqlalchemy import SQLAlchemy from flask_session import Session from flask_redis import FlaskRedis # Globally accessible libraries db = SQLAlchemy() r = FlaskRedis() def create_app(): """Initialize the core application.""" app = Flask(__name__, instance_relative_config=False) app.config.from_object('config.Config') # Initialize Plugins db.init_app(app) r.init_app(app) with app.app_context(): # Set global variables r.set('endpoint', app.config['ENDPOINT']) # Include our Routes from . import routes # Register Blueprints app.register_blueprint(auth.auth_bp) app.register_blueprint(admin.admin_bp) return app
The order of operations here is critical. Let's break it down.
Creating Instances of Plugins
The vast majority of what we do happens in a function called
create_app(). Before we even get to creating our app, we create global instances of
flask_redis. Even though we've set these, nothing has happened until we "initialize" these plugins after our app object is created.
The first two lines of
create_app() should be no surprise: we're creating our Flask app and stating that it should be configured using a class called Config in a file named config.py:
app = Flask(__name__, instance_relative_config=False) app.config.from_object('config.Config')
After the app object is created, we then "initialize" those plugins we mentioned earlier:
# Initialize Plugins db.init_app(app) login_manager.init_app(app)
The Application Context
Next comes the moment of truth: creating the app context. What happens in the app context, stays in the app context... but seriously. Any part of our app which is not imported, initialized, or registered within the
with app.app_context(): block effectively does not exist. This block is the lifeblood of our Flask app - it's essentially saying "here are all the pieces of my program."
The first thing we see is
g.endpoint = app.config['ENDPOINT']. In Flask,
g is a build-in object, where "g" stands for "global". Because we've just associated a value to g before importing any parts of our app, any part of our app that comes thereafter can access the things we save to
g simply by calling
g.[KEY_NAME]. In this example, we saved an endpoint which came from our config file.
Next, we import the base parts of our app (any Python files or logic which aren't Blueprints). I've imported routes.py via
from . import routes. This file contains the route for my app's home page, so now visiting the app will actually resolve to something.
Next, we register Blueprints. Blueprints are "registered" by calling
register_blueprint() on our
app object, and passing the Blueprint module's name, followed by the name of our Blueprint. For example, in
app.register_blueprint(auth.auth_bp), I have a file called auth.py which contains a Blueprint named auth_bp.
We finally wrap things up with
So we return
app, but what are we returning to, exactly? That's where the mysterious
wsgi.py file from earlier comes in. Almost every
wsgi.py file looks like this:
from application import create_app app = create_app() if __name__ == "__main__": app.run(host='0.0.0.0')
Ah ha! Now we have a file in out app's root directory that can serve as our entry point. When setting up a production web server to point to your app, you will almost always configure it to point to wsgi.py, which in turn imports and starts our entire app.