Organizing Flask Views with Automatic Discovery
How to organize class-based Flask views with automatic discovery and registration.
Series: How I Use Flask
9 min
Class-based views are awesome
I’m a huge fan of class-based views in Flask. For a really simple project, a handful of view functions may be just fine, but as soon as you’re building a proper web application, you’re going to want to reach for view classes. Originally, Flask-Classy
was the de facto way to handle class-based views in a Flask app. These days, however, it seems Flask-Classy
has ceased updates and has been replaced with a fork named Flask-Classful
.
Class-based views with Flask-Classful
brings a number of quality-of-developer-life improvements to your Flask apps—simple route bases and prefixes, automatic HTTP method-based functions, before
and after
methods, define-once decorator support, and more. And it’s easy to extend Flask-Classful
with your own additions that make sense for your apps—something I’m sure I’ll touch on in future posts in the series.
Manual import & registration is a pain
There’s one consistent pain point for me when dealing with class-based views in larger Flask apps—the need to import & register your views with your Flask application during init. As your app grows, your views will likely grow. The need to import and register views with your Flask application really ruins developer flow, as adding additional views to your application can’t be handled as simply as adding a new file in a views
folder and seeing it automatically picked up by your application.
Organizing Flask view classes
I like to keep things simple on myself and other developers working on a Flask project. To that end, even though I organize my app code in a more domain-driven than MVC-driven way, I like to keep my views straightforward to find. This means they all sit in a proj/views
folder. That’s the easy part.
I’m big on keeping view module and class names as intuitive as possible. It’s important to ensure views are easily understood and reasoned about when looking for them—or finding them—within a codebase. What I mean by this is that if a new developer is coming into a project, I want them to be able to look at the organization of view modules and be able to relatively quickly get a correct feeling about what a specific view module is tasked with handling—even without reading any of the actual code. As a developer starts learning the domains and boundaries of the application, I want to ensure they can easily start mapping their growing knowledge of what the app does with how the app is organized.
Match model names first
The first rule when I’m working on a new project that is still having its domain-driven boundaries figured out is to always use the same name as model counterparts when a view class is written to encapsulate functionality that operates on a distinct app model.
The goal here is to free developers (including myself) from having to think too much about what to name things once a model has been named. Initial app prototyping and development should move as fast as possible, and this is a nice, simple rule that helps realize that goal by knowing that every model that needs to be interacted with via requests and UI will have a view class defined in a view module that matches its model module name.
Example: If the app has a model defined at
proj/models/user.py
, which definesclass User(Model)
to the application, there should be a matchingproj/views/user.py
with aclass UserView(View)
defined to operate on modelUser
.
Replace model-based names with domain-based names
As the application develops, its various domain boundaries will be defined and solidified. Sometimes this is a natural byproduct of how we talk about the product—calling certain parts of the product specific names (like accounts, clients, reports) coalesces into what those parts of the product eventually wind up being called.
What may start out as a User
model with accompanying UserView
in the beginning of the app development process may eventually be organized under an accounts
domain. As this happens, the app’s various interfaces will be organized under proj/accounts
. To keep things as intuitive as possible, the initial proj/views/user.py
will be renamed to proj/views/accounts.py
. Similarly, I’d rename UserView
to AccountsView
.
Optionally replace domain-based names with productized names
Sometimes, naming significant parts of an application is a bit more intentional—that is, you give them specific names to differentiate them from each other, and to somewhat lightly brand/identify them for users. As the application progresses, it could be that the accounts
domain winds up being provided in the UI under a name like Directory
. In this case, I sometimes wind up renaming a view to match the productized name—here, that’d mean proj/views/accounts.py
would become proj/views/directory.py
.
However, that doesn’t mean the actual domain boundaries in your app layout need to (or even should) be renamed to match the UI name of that portion of the app. Where internal app code is concerned, at least for me, I can make a case for having a proj/views/directory.py
that acts on the proj/accounts
domain. I would feel weird renaming proj/accounts > proj/directory
because all the backend code cares that it is acting on accounts, not on a directory. But a DirectoryView
would naturally feel like it is responsible for delivering the Directory
UX to users.
Note: This requires a bit of intentional thinking about your own application layout. It may feel really natural to talk about something like a
Directory
when it comes to the UI, and rename the responsible view to be inline with the UI/product naming—after all, views are only supposed to handle accepting requests and returning responses. They shouldn’t have a bunch of business logic in them.
Automatic view discovery
Now that we have a bit of guidance on how to organize our Flask views, it’s time to turn our attention to making adding, updating, and moving views around as effortless as possible.
We covered most of the particulars of automatically discovering Python modules by walking a directory in our last post on organizing Flask models with automatic discovery. If you haven’t already checked that post out, I encourage you to do so to be familiar with what we’ll be extending in this post to make automatic view discovery work.
Write a comparator function to identify views
In the last post, we created a dynamic_loader
function that accepted a module
directory and a compare
function to identify and load classes that matched a desired rule. We’re going to reuse that function, and automatically find and load all views defined in our application’s views
folder with a simple comparator that knows how to pick out views when it finds them.
Here’s our dead-simple view-finding comparator:
# proj/lib/loaders.py
def get_views():
"""Dynamic view finder."""
return dynamic_loader('views', is_view)
def is_view(item):
"""Determine if `item` is a `FlaskView` subclass
(because we don't want to register `FlaskView` itself).
"""
return item is not FlaskView and isclass(item) and issubclass(item, FlaskView) # and not item.__ignore__()
As with my models, I often define my own base View
and AdminView
base classes that my app views will subclass. This base class typically defines some standard attributes, endpoints, and private methods that power a variety of functionality across my app views. I could easily switch the check here from FlaskView
to MyBaseView
and get the same result. For this reason, I my base view classes often specify a simple __ignore__
method that contain simple rules to tell my dynamic loader they are abstract base classes which do not supply any actual endpoints the application expects to be present. You may not need this in your application, but I’m leaving the commented out bit in case you find you have similar needs.
Our is_view
comparator is pretty simple—as our dynamic_loader
inspects all the exports present inside the __all__
attribute defined in each view module, it will check if the item is not the FlaskView
class itself, verify it is a class, and ensure the class it has found is a subclass of FlaskView
. Whenever this passes, dynamic_loader
will add that view class to its list of view classes found for the app to register.
Automatic view registration
Now that we know how to automatically find all of our app view classes, we need a simple way to register them all when our application starts up inside our create_app
factory function. For this, we’ll create an importable init_views
function that will be used by create_app
to discover and register every view found at app startup.
# proj/views/__init__.py
from proj.lib.loaders import get_views
def init_views(app=None):
"""Initializes application views."""
if app is None:
raise ValueError('cannot init views without app object')
# register defined views
for view in get_views():
view.register(app)
# Handle HTTP errors
register_error_handlers(app)
def register_error_handlers(app=None):
"""Register app error handlers.
Raises error if app is not provided.
"""
if app is None:
raise ValueError('cannot register error handlers on an empty app')
@app.errorhandler(404)
def error404(self):
return render_template('errors/404.html', title='Page Not Found')
@app.errorhandler(500)
def error500(self):
return render_template('errors/500.html', title='Server Error')
Our init_views
function is pretty straightforward here. We will be calling this from create_app
, so we’ll pass along our app
instance. If init_views
is ever called without an app
argument, we’ll raise a ValueError
to let a developer (or myself) know that something went wrong. You’ll notice we import our get_views
function from proj.lib.loaders
. As long as we have an app
to act on, init_views
will iterate over all the automatically discovered views found by get_views
, and will call view.register(app)
on each one—the register
method is supplied by Flask-Classful
, which means we don’t have to worry about what it’s doing here. We then finish up by calling register_error_handlers
to add in our custom error pages for specific error codes.
Init views in create_app
Now, all that is left is to add init_views
to our create_app
factory function.
# proj/__init__.py
def create_app(config=None):
# other app-init stuff here
from proj.views import init_views
init_views(app)
return app
We init our views as the final step in our create_app
function before returning our ready-to-go app instance. Once we’ve returned, our app will know about all of the views found inside our views
directory, and every view will be properly registered. As we develop our app, we’ll notice that any changes to our views will automatically reload the app and pick up those changes immediately.
Up next
Automatically discovering and registering views is one of the easiest things to implement on top of our existing dynamic_loader
. It’s also one of the biggest productivity wins we can add to a sizeable Flask project. No more manual view importing and registration in our create_app
factory function. Adding views is as simple as adding a new .py
module to our views
folder and subclassing FlaskView
for it to be picked up.
In the next post (or maybe two), we’re going to jump back to models for a bit and take a look at how I handle my base Model
class—including default columns and attributes, as well as some simple database-focused helper methods that are a part of every model in my Flask applications. Check out the next post now.