This analysis is a comparison of 2 python web frameworks, Flask and Django. It discusses their features and how their technical philosophies impact software developers. It is based on my experience using both, as well as time spent personally admiring both codebases.
Synopsis
Django is best suited for RDBMS-backed websites. Flask is good for corner cases that wouldn't benefit from Django's deep integration with RDBMS.
When using Flask, it's easy to miss the comforts a full-fledge framework provides. Django's extension community is more active. Django's ORM is superb. Flask developers will be forced to reinvent the wheel to catch up for things that'd be quick wins with Django.
Both excel at prototyping; getting an idea off the ground fast, and leave room for chiseling away fine-grain details after. Python makes both a joy to work with.
Similarities
Request object
Information of user's client request to server
URL routing
Routes HTTP requests (GET, POST, PUT, UPDATE), data payload, and URL path
Views
Invoked when a request matches URL pattern and receives request object
Context information
Passed into HTML template for processing.
django.template.Template.render() (pass dict into Context object)
flask.render_template() (accepts dict)
HTML template engine
Renders template via context information.
Response object
Object with HTTP meta information and content to send to the browser.
Static file-handling
Handles static files like CSS, JS assets, and downloads.
The Django Web Framework
Today, Django is built and maintained by the open source community. The initial release was July 21, 2005, by Lawrence Journal-World.
What Django provides
-
Context processor middleware (global, per-request dict-like object passed into templates)
-
QuerySet (reuseable object used in ORM-backed features)
-
Model Forms (ORM-backed forms)
-
-
DetailView, ListView (ORM-backed views)
-
Administration web interface (ORM-backed CRUD backend)
-
User model
Multi-tenancy via domain
Settings, configurable via DJANGO_SETTINGS_MODULE
Extending Django
Django has a vibrant third-party development community. Apps are installed via appending them to the INSTALLED_APPS in the settings.
Popular Django extensions include:
REST: Django REST Framework, aka "DRF"
Permissions: django-guardian
Asset pipelines: django-compressor, django-webpack-loader
Debugging, Miscellaneous: django-extensions, django-debug-toolbar
Filtering / Search: django-filter
Tabular / paginated output of db: django-tables2
Django extension project names tend to be prefixed django- and lowercase.
Customizing Django
Eventually the included forms, fields and class-based views included in Django aren't going to be enough.
Django's scope
Django is a framework. The aspects django occupies are:
mapping database schemas, their queries, and query results to objects
mapping URL patterns to views containing business logic
providing request information such as GET, PUT, and session stuff to views (HttpRequest)
presenting data, including HTML templates and JSON (HttpResponse)
environmental configuration (settings) and an environment variables (DJANGO_SETTINGS_MODULE) e.g. dev, staging, prod workflows
A tool kit of libraries that abstract the monotony of common tasks in web projects.
If it's difficult to visualize a web app in terms of its database schema and WordPress or Drupal would suffice, Django may not be the strongest pick for that.
Where a CMS will automatically provide a web admin to post content, toggle plugins and settings, and even allow user registration and comments, Django leaves you building blocks of components you customize to the situation. Programming is required.
Django's programming language, python, also gives it a big boost.
Django uses classes right
While python isn't statically typed, its inheritance hierarchy is very straight-forward and navigable.
Code Editors
Free tools in the community such as jedi provide navigation of modules, functions and classes to editors like vim, Visual Studio Code and Atom.
Python classes benefit from many real-world examples being available in the open source community to study. They're a pleasure incorporating in your code. An example for django would be class-based views which shipped in Django 1.3.
OOP + Python
For those seeking a good example of OOP in Python, in addition to class-based views, Django is a sweeping resource. It abstracts out HTTP requests and responses, as well as SQL dialects in a class hierarchy.
See my answer on HN for Ask HN: How often do you use inheritance?
Stretching the batteries
Django isn't preventing custom solutions. It provides a couple of frameworks which complement each other and handles initializing the frameworks being used via project's settings. If a project doesn't leverage a component Django provides, it stays out of the way.
Let's try a few examples of how flexible Django is.
Scenario 1: Displaying a user profile on a website.
URL pattern is r"^profile/(?P<pk>\d+)/$", e.g. /profile/1
Let's begin by using the simplest view possible, and map directly to a function, grab the user model via get_user_model():
from django.contrib.auth import get_user_model
from django.http import HttpResponse
def user_profile(request, **kwargs):
User = get_user_model()
user = User.objects.get(pk=kwargs['pk'])
html = "<html><body>Full Name: %s.</body></html>" % user.get_full_name()
return HttpResponse(html)
urls.py:
from django.conf.urls import url
from .views import user_profile
urlpatterns = [
url(r'^profile/(?P<pk>\d+)/$', user_profile),
]
So where does the request, **kwargs in user_profile come from? Django injects the user's request and any URL group pattern matches to views when the user visits a page matching a URL pattern.
HttpRequest is passed into the view as request.
Since the URL pattern, r'^profile/(?P<pk>\d+)/$', contains a named group, pk, that will be passed via Keyword Arguments **kwargs.
If it was r'^profile/(\d+)/$', it'd be passed in as tuple() argument into the *arg parameter.
Arguments and Parameters
Bring in a high-level view:
Django has an opinionated flow and a shortcut for this. By using the named regular expression group pk, there is a class that will automatically return an object for that key.
So, it looks like a DetailView is best suited. We only want to get information on one core object.
Easy enough, get_object()'s default behavior grabs the PK:
from django.contrib.auth import get_user_model
from django.views.generic.detail import DetailView
class UserProfile(DetailView):
model = get_user_model()
urls.py:
from django.conf.urls import url
from .views import UserProfile
urlpatterns = [
url(r'^profile/(?P<pk>\d+)/$', UserProfile.as_view()),
]
Append as_view() to routes using class-based views.
If profile/1 is missing a template, accessing the page displays an error:
django.template.exceptions.TemplateDoesNotExist: core/myuser_detail.html
The file location and name depends on the app name and model name. Create a new template in the location after TemplateDoesNotExist in any of the projects templates/ directories.
In this circumstance, it needs core/myuser_detail.html. Let's use the app's template directory. So inside core/templates/core/myuser_detail.html, make a file with this HTML:
<html><body>Full name: {{ object.get_full_name }}</body></html>
Custom template paths can be specified via punching out template_name in the view.
That works in any descendent of TemplateView or class mixing in TemplateResponseMixin.
Note
Django doesn't require using DetailView.
A plain-old View could work. Or a TemplateView if there's an HTML template.
As seen above, there are function views.
These creature comforts were put into Django because they represent bread and butter cases. It makes additional sense when factoring in REST.
Harder: Getting the user by a username
Next, let's try usernames instead of user ID's, /profile/yourusername. In the views file:
from django.contrib.auth import get_user_model
from django.http import HttpResponse
def user_profile(request, **kwargs):
User = get_user_model()
user = User.objects.get(username=kwargs['username'])
html = "<html><body>Full Name: %s.</body></html>" % user.get_full_name()
return HttpResponse(html)
urls.py:
from django.conf.urls import url
from .views import user_profile
urlpatterns = [
url(r'^profile/(?P<pk>\w+)/$', user_profile),
]
Notice how we switched the regex to use \w for alphanumeric character and the underscore. Equivalent to [a-zA-Z0-9_].
For the class-based view, the template stays the same. View has an addition:
class UserProfile(DetailView):
model = get_user_model()
slug_field = 'username'
urls.py:
urlpatterns = [
url(r'^profile/(?P<slug>\w+)/$', UserProfile.as_view()),
]
Another "shortcut" DetailView provides; a slug. It's derived from SingleObjectMixin. Since the url pattern has a named group, i.e. (?P<slug>\w+) as opposed to (\w+).
But, let's say the named group "slug" doesn't convey enough meaning. We want to be accurate to what it is, a username:
urlpatterns = [
url(r'^profile/(?P<username>\w+)/$', UserProfile.as_view()),
]
We can specify a slug_url_kwarg:
class UserProfile(DetailView):
model = get_user_model()
slug_field = 'username'
slug_url_kwarg = 'username'
Make it trickier: User's logged in profile
If a user is logged in, /profile should take them to their user page.
So a pattern of r"^profile/$", in urls.py:
urlpatterns = [
url(r'^profile/$', UserProfile.as_view()),
]
Since there's no way to pull up the user's ID from the URL, we need to pull their authentication info to get that profile.
Django thought about that. Django can attach the user's information to the HttpRequest so the view can use it. Via user.
In the project's settings, add AuthenticationMiddleware to MIDDLEWARE:
MIDDLEWARE = [
# ... other middleware
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
In the view file, using the same template:
class UserProfile(DetailView):
def get_object(self):
return self.request.user
This overrides get_object() to pull the User right out of the request.
This page only will work if logged in, so let's use login_required(), in urls.py:
from django.contrib.auth.decorators import login_required
urlpatterns = [
url(r'^profile/$', login_required(UserProfile.as_view())),
]
That will assure only logged-in users can view the page. It will also send the user to a login form which forward them back to the page after login.
Even with high-level reuseable components, there's a lot of versatility and tweaking oppurtunities. This saves time from hacking up solution for common cases. Reducing bugs, making code uniform, and freeing up time for the stuff that will be more specialized.
Retrofit the batteries
Relying on the django's components, such as views and forms, gives developers certainty things will behave with certainty. When customizations needs to happen, it's helpful to see if subclassing a widget or form field would do the trick. This assures the new custom components gets the validation, form state-awareness, and template output of the form framework.
Configuring Django
Django's settings are stored in a python file. This means that the Django configuration can include any python code, including accessing environment variables, importing other modules, checking if a file exists, lists, tuples, arrays, and dicts.
Django relies on an environment variable, DJANGO_SETTINGS_MODULE, to load a module of setting information.
Settings are a lazily-loaded singleton object:
When an attribute of django.conf.settings is accessed, it will do a onetime "setup". The section Django's initialization shows there's a few ways settings get configured.
Singleton, meaning that it can be imported from throughout the application code and still retrieve the same instance of the object.
Reminder
Sometimes global interpreter locks and thread safety are brought up when discussing languages. Web admin interfaces and JSON API's aren't CPU bound. Most web problems are I/O bound.
In other words, issues websites face when scaling are concurrency related. In practice, it's not even limited to the dichotomy of concurrency and parallelism: Websites scale by offloading to infrastructure such as reverse proxies, task queues (e.g. Celery, RQ), and replicated databases. Computational heavy backend services are done elsewhere and use different tools (kafka, hadoop, spark, Elasticsearch, etc).
Django uses import_module() to turn a string into a module. It's kind of like an eval, but strictly for importing. It happens here.
It's available as an environmental variable as projects commonly have multiple settings files. For instance, a base settings file, then other files for local, development, staging, and production. Those 3 will have different database configurations. Production will likely have heavy caching.
To access settings attributes application-wide, import the settings:
from django.conf import settings
From there, attributes can be accessed:
print(settings.DATABASES)
Virtual environments and site packages
When developing via a shell, not being sourced into a virtual enviroment could lead to a settings module (and probably the django package itself) not being found.
The same applies to UWSGI configurations, similar symptoms will arise when deploying. This can be done via the virtualenv option.
This is the single biggest learning barrier python has. It will be a hindrance every step of the way until the concept is internalized.
Django's intialization
Django's initialization is complicated. However, its complexity is proportional to what's required to do the job.
As seen in configuring django, the settings are loaded as a side-effect of accessing the setting object.
In addition to that, django maintains an application registry, apps, also a singleton. It's populated via django.setup().
Finding and loading the settings requires an environmental variable is set. Django's generated manage.py will set a default one if its unspecified.
via command-line / manage.py (development)
User runs ./manage.py (including arguments, e.g. $ ./manage.py collectstatic
settings are lazily loaded upon import of execute_from_command_line of django.core.management.
Accessing an attribute of settings (e.g. if settings.configured) implicitly imports the settings module's information.
execute_from_command_line() accepts sys.argv and passes them to initialize ManagementUtility
ManagementUtility.execute() (source) pulls a settings attribute for the first time, invokes django.setup() (populating the app registry)
ManagementUtility.execute() directs sys.argv command to the appropriate app functions. A list of commands are cached. In addition, these are hard-coded:
autocompletion
runserver
help output (--help)
In addition, upon running, commands will run system checks (since Django 1.7). Any command inheriting from BaseCommand runs checks implicitly. $ ./manage.py check will run checks explicitly.
via WSGI (server)
Point WSGI server wrapper (e.g. UWSGI) to wsgi.py generated by Django
wsgi.py will run get_wsgi_application()
Serves WSGI-compatible response
The Flask Microframework
Flask is also built and maintained in the open source community. The project, as well as its dependencies, Jinja2 and Werkzeug, are Pallets projects. The creator of the software itself is Armin Ronacher. Initial release April 1, 2010.
What Flask provides
URL routing via Werkzeug
Modularity via blueprints
In-browser REPL-powered tracebook debugging via Werkzeug's
Static file handling
Extending Flask
Since Flask doesn't include things like an ORM, authentication and access control, it's up to the user to include libraries to handle those a la carte.
Popular Flask extensions include:
Database: Flask-SQLAlchemy
REST: Flask-RESTful (flask-restful-swagger), Flask API
Admins: Flask-Admin Flask-SuperAdmin
Auth: Flask-Login, Flask-Security
Asset Pipeline: Flask-Assets, Flask-Webpack
Commands: Flask-Script
Flask extension project names tend to be prefixed Flask-, PascalCase, with the first letter of words uppercase.
Used with flask, but not flask-specific (could be used in normal scripts):
Social authentication: authomatic, python-social-auth
Forms: WTForms
RDBMS: SQLAlchemy, peewee
Mongo: MongoEngine
For more, see awesome-flask on github.
Configuring Flask
Configuration is typically added after Flask object is initialized. No server is running at this point:
app = Flask(__name__)
After initialization, configuration available via a dict-like attribute via the Flask.config.
Only uppercase values are stored in the config.
There are a few ways to set configuration options. dict.update():
app.config.update(KEYWORD0='value0', KEYWORD1='value1')
For the future examples, let's assume this:
- website/ - __init__.py - app.py - config/ - __init__.py - dev.py
Inside website/config/dev.py:
class DevConfig(object):
DEBUG = True
TESTING = True
DATABASE_URL = 'sqlite://:memory:'
Creating a class and pointing to it via flask.Config.from_object() also works:
from .config.dev import DevConfig
app.config.from_object(DevConfig)
Another option with from_object() is a string of the config object's location:
app.config.from_object('website.config.dev.DevConfig')
In addition, it'll work with modules (django's style of storing settings). For website/config/dev.py:
DEBUG = True
TESTING = True
DATABASE_URL = 'sqlite://:memory:'
Then:
app.config.from_object('website.config.dev')
So, this sounds strange, but as of Flask 1.12, that's all there is regarding importing classes/modules. The rest is all importing python files.
To import an object (module or class) from an environmental variable, do something like:
app.config.from_object(os.environ.get('FLASK_MODULE', 'web.conf.default'))
flask.Config.from_envvar() is spiritually similar to DJANGO_SETTINGS_MODULE, but looks can be deceiving.
The environmental variable set points to a file, which is interpreted like a module.
Tangent: Confusion with configs
Despite the pythonic use of from_object() and the pattern using classes to store configs for dev/prod setups in official documentation, and the abundance of string to python object importation utilities, environmental variables in Flask don't point to a class, but to files which are interpreted as modules.
There's a potential Chesterton's Fence issue also. I made an issue about it to document my observations. The maintainer's response was they're enhancing the FLASK_APP environmental variable to specify an application factory with arbitrary arguments.)
In the writer's opinion, an API-centric framework like flask introducing the FLASK_APP variable exacerbates the aforementioned confusion. Why add FLASK_APP when from_envvar() is available? Why not allow pointing to a config object and leveraging what flask already has and exemplifies in its documentation?
It's already de facto in the flask community to point to modules and classes when apps bootstrap. There's a reason for that. Maintainer's should harken back on using the tools and gears that originally earned flask its respect. In microframeworks, nonorthogonality sticks out like a sore thumb.
Assuming website/config/dev.py:
DEBUG = True
TESTING = True
DATABASE_URL = 'sqlite://:memory:'
Let's apply a configuration from an environmental variable:
app.config.from_envvars('FLASK_CONFIG')
FLASK_CONFIG should map to a python file:
export FLASK_CONFIG=website/config/dev.py
Here's where Flask's configurations aren't so orthogonal. There's also a flask.Config.from_pyfile():
app.config.from_pyfile('website/config/dev.py')
Flask's Initialization
Flask's initiation is different then Django's.
Before any server is started, the Flask object must be initialized. The Flask object acts a registry URL mappings, view callback code (business logic), hooks, and other configuration data.
The Flask object only requires one argument to initialize, the so-called import_name parameter. This is used as a way to identify what belongs to your application. For more information on this parameter, see About the First Parameter on the Flask API documentation page:
from flask import Flask
app = Flask('myappname')
Above: app, an instantiated Flask object. No server or configuration present (yet).
Dissecting the Flask object
During the initialization, the Flask object hollowed out dict and list attributes to store "hook" functions, such as:
See a pattern above? They're all function callbacks that are triggered upon events occuring. template_context_processors seems a lot like Django's context processor middleware.
blueprints: blueprints
extensions: extensions
url_map: url mappings
view_functions: view callbacks
So why list these? Situational awareness is a key matter when using a micro framework. Understanding what happens under the hood ensures confidence the application is handled by the developer, not the other way around.
Hooking in views
The application object is instantiated relatively early because it's used to decorate views.
Still, at this point, you don't have a server running yet. Just a Flask object. Most examples will show the object instantiated as app, you can of course use any name.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World'
The flask.Flask.route() decorator is just a fancy way of doing flask.Flask.add_url_rule():
from flask import Flask
app = Flask(__name__)
def hello_world():
return 'Hello, World'
app.add_url_rule('/', 'hello_world', hello_world)
Configure the Flask object
Here's an interesting one: Generally configuration isn't added until after the after initializing the Python object.
You could make a function to act as a factory/bootstrapper for flask objects. There's nothing magical here, nothing's tying you down - it's python. Unlike with django, which controls initialization, a Flask project has to handle minutiae of initialization on its own.
In this situation, let's wrap it in a pure function:
from flask import Flask
class DevConfig(object):
DEBUG = True
TESTING = True
DATABASE_URL = 'sqlite://:memory:'
def get_app():
app = Flask(__name__)
app.config.from_object(DevConfig)
return app
Flask and Databases
Unlike Django, Flask doesn't tie project's to a database.
There's no rules saying a Flask app has to connect to a database. It's python, flask could used to make a proxy/abstraction of a thirdparty REST API. Or a quick web front-end to a pure-python program. Another possiblity, generating a purely static website with no SQL backend a la NPR.
If a website is using RDBMS, which is often true, a popular choice is SQLAlchemy. Flask-SQLAlchemy helps assist in gluing them together.
SQLAlchemy is mature (a decade older than this writing), battle-tested with a massive test suite, dialects for many SQL solutions. It also provides something called "core" underneath the hood that allows building SQL queries via python objects.
SQLAlchemy is also active. Innovation keeps happening. The change log keeps showing good things happening. Like Django's ORM, SQLAlchemy's documentation is top notch. Not to mention, Alembic, a project by the same author, harnesses SQLAlchemy to power migrations.
Interpretations
Software development best practices form over time. Decisions should be made by those with familiarity with their product or service's needs.
Over the last 10 years, the fundamentals of web projects haven't shifted. None of Rails' or Django's MVC workflows were thrown out the window. On the contrary, they thrived. At the end of the day, the basics still boils down to JSON, HTML templates, CSS, and JS assets.
Flask is pure, easy to master, but can lend to reinventing the wheel
Flask is meant to stay out of the way and put the developer into control. Even over things as granular as piecing together the Flask object, registering blueprints and starting the web server.
The API is, much like this website, is documented using sphinx. The reference will become a goto. To add to it, a smaller codebase means a developer can realistically wrap their brain around the internals.
Developers that find implicit behavior to be a hindrance and thrive in explicitness will feel comfortable using Flask.
However, this comes at the cost of omitting niceties many web projects would actually find helpful, not an encumbrance. It'll also leave developer's relying on third party extensions. To think of a few that'd come up for many:
What about authentication?
There's no way to store the users. So grab SQLAlchemy, peewee, or MongoEngine. There's the database back-end.
Now to building the user schema. Should the website accept email addresses as usernames? What about password hashing? Maybe Flask-Security or Flask-Login will do here.
Meanwhile, Django would have the ORM, User Model, authentication decorators for views, and login forms, with database-backed validation. And it's pluggable and templated.
What about JSON and REST?
If it involves a database backend, that still has to be done (like above). To help Flask projects along, there are solutions like Flask API (inspired by Django Rest Framework) and Flask-RESTful.
Flask's extension community chugs, while Django's synergy seems unstoppable
That isn't to say Flask has no extension community. It does. But it lacks the cohesion and comprehensiveness of Django's. Even in cases where there are extensions, there will be corner cases where features are just missing.
For instance, without an authentical and permissions system, it's difficult to create an OAuth token system to grant time-block'd permissions to slices of data to make available. Stuff available for free with django-rest-framework's django-guardian integration, which benefit from both Django's ORM and its permission system, in many cases aren't covered by the contrib community at all. This is dicussed in greater detail in open source momentum.
Django is comprehensive, solid, active, customizable, and robust
A deep notion of customizability and using subclassed Field, Forms, Class Based Views, and so on to suit situations.
The components django provided complement each other.
Rather than dragging in hard-requirements, nothing forces you to:
use the Form framework
if using the Form framework, to:
back forms with models (ModelForm)
output the form via as_p(), as_table(), or as_ul()
use class-based views
use a specific class-based view
if using a class-based view, fully implement every method of a specialized-view
use django's builtin User model
Above are just a few examples, but Django doesn't strap projects into using every battery.
That said, the QuerySet object plays a huge role in catalyzing the momentum django provides. It provides easy database-backed form validations, simple object retrieval with views, and code readability. It's even utilized downstream by extensions like django-filters and django-tables2. These two plugins don't even know about each other, but since they both operate using the same database object, you can use django-filter's filter options to facet and search results that are produced by django-tables2.
Open source momentum
Flask, as a microframework, is relatively dormant from a feature standpoint. Its scope is well-defined.
Flask isn't getting bloated. Recent pull requests seem to be on tweaking and refining facilities that are already present.
It's not about stars, or commits, or contributor count. It's about features and support niceties that can be articulated in change logs.
Even then though, it's hard to put things into proportion. Flask includes Werkzeug and Jinja2 as hard dependencies. They run as independent projects (i.e. their own issue trackers), under the pallets organization.
Django wants to handle everything on the web backend. Everything fits together. And it needs to, because it's a framework. Or a framework of frameworks. Since it covers so much ground, let's try once again to put it into proportion, against Flask:
Django |
Flask |
---|---|
Django ORM |
SQLAlchemy, MongoEngine |
Django Migrations |
Alembic |
Django Templates |
Jinja2 |
Django Core / URL's |
Werkzeug |
Django Forms (ModelForm) |
WTForms (WTForms-Alchemy) |
Django Commands |
Flask-Script (flask bundles CLI support as of 0.11) |
There are also feature requests that come in, often driven by need of the web development community, and things that otherwise wouldn't be considered for Flask or Flask extension. Which kind of hurts open source, because there's code that could be reuseable being written, but not worth the effort to make an extension for. So there are snippets for that.
And in a language like Python where packages, modules, and duck typing rule, I feel snippets, while laudable, are doomed to fall short keeping in check perpetual recreation of patterns someone else done. Not to mention, snippets don't have CI, nor versioning, nor issue trackers (maybe a comment thread).
By not having a united front, the oppurtunity for synergetic efforts that bridge across extensions (a la Django ORM, Alchemy, DRF, and django-guardian) fail to materialize, creating extensions that are porous. This leaves devs to fill in the blanks for all-inclusive functionality that'd already be working had they just picked a different tool for the job.
Conclusion
We've covered Flask and Django, their philosophies, their API's, and juxtaposed those against the writer's personal experiences in production and open source. The article included links to specific API's across a few python libraries, documentation sections, and project homepages. Together, they should prove fruitful in this being a resource to come back to.
Flask is great for a quick web app, particularly for a python script to build a web front-end for.
If already using SQLAlchemy models, it's possible to get them working with a Flask application with little work. With Flask, things feel in control.
However, once relational databases come into play, Flask enters a cycle of diminishing returns. Before long, projects will be dealing with forms, REST endpoints and other things that are all best represented via a declarative model with types. The exact stance Django's applications take from the beginning.
There's an informal perception that Batteries included may mean a growing list of ill-maintained API's that get hooked into every request. In the case of Django, everything works across the board. When an internal Django API changes, Django's testsuites to break and the appropriate changes are made. So stuff integrates. This is something that's harder to do when there's a lot of packages from different authors who have to wait for fixes to be released in Flask's ecosystem.
And if things change. I look forward to it. Despite Flask missing out on Django's synergy, it is still a mighty, mighty microframework.
Bonus: Cookiecutter template for Flask projects
Since I still use Flask. I maintain a cookiecutter template project for it.
This cookiecutter project will create a core application object that can load Flask blueprints via a declarative YAML or JSON configuration.
Feel free to use it as a sample project. In terminal:
$ pip install --user cookiecutter
$ cookiecutter https://github.com/tony/cookiecutter-flask-pythonic.git
$ cd ./path-to-project
$ virtualenv .env && . .env/bin/activate
$ pip install -r requirements.txt
$ ./manage.py
Bonus: How do I learn Django or Flask?
Preparation
Understand how python virtual environments and PATH's work:
Check out my book The Tao of tmux available online free for some good coverage of the terminal.
For learning python, here are some free books:
Grab Django's documentation PDF and Flask's documentation PDF. Read it on a smart phone or keep it open in a PDF reader.
Get in the habit of reading python docs on ReadTheDocs.org, a documentation hosting website.
Developing
Make a hobby website in django or flask.
Services like Heroku are free to try, and simple to deploy Django websites to.
For more free hosting options see ripienaar/free-for-dev.
DigitalOcean plans start at $5/mo per instance. Supports FreeBSD with ZFS.
Bookmark and study to this article to get the latest on differences between Django and Flask. While it's a comparison, it'll be helpful in curating the API and extension universe they have.
For free editors, check out good old vim + python-mode, Visual Studio Code, Atom, or PyCharm