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
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,
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 defines
class User(Model) to the application, there should be a matching
proj/views/user.py with a
class UserView(View) defined to operate on model
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
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
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:
"""Dynamic view finder."""
return dynamic_loader('views', is_view)
"""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
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
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.
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.
from proj.lib.loaders import get_views
"""Initializes application views."""
if app is None:
raise ValueError('cannot init views without app object')
# register defined views
for view in get_views():
# Handle HTTP errors
"""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')
return render_template('errors/404.html', title='Page Not Found')
return render_template('errors/500.html', title='Server Error')
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
Now, all that is left is to add
init_views to our
create_app factory function.
# other app-init stuff here
from proj.views import init_views
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.
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.