< Back to frontpage

Django vs Flask


A practitioner's perspective

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

flask.Request and django.http.HttpRequest

URL routing

Routes HTTP requests (GET, POST, PUT, UPDATE), data payload, and URL path

Django's routing and Flask's routing

Views

Invoked when a request matches URL pattern and receives request object

Django's views and Flask's views

Class-based: django and flask

Context information

Passed into HTML template for processing.

django.template.Template.render() (pass dict into django.template.Context object)

flask.render_template() (accepts dict)

HTML template engine

Renders template via context information.

Django's templating and Flask's templating

Response object

Object with HTTP meta information and content to send to the browser.

django.http.HttpResponse and flask.Response

Static file-handling

Handles static files like CSS, JS assets, and downloads.

Static files in django and Static files in Flask

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

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:

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:

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.

  1. HttpRequest is passed into the view as request.

  2. 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.

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 View.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 TemplateResponseMixin.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 importlib.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 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)

  1. User runs ./manage.py (including arguments, e.g. $ ./manage.py collectstatic

  2. 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.

  3. execute_from_command_line() accepts sys.argv and passes them to initialize ManagementUtility

  4. ManagementUtility.execute() (source) pulls a settings attribute for the first time, invokes django:django.setup (populating the app registry)

  5. 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)

  1. Point WSGI server wrapper (e.g. UWSGI) to wsgi.py generated by Django
  2. wsgi.py will run get_wsgi_application()
  3. django.setup()
  4. 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

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:

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):

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 flask.Config.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 python:dict and python: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.

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

Start Flask web server

if __name__ == '__main__':
    app = get_app()
    app.run()

See flask.Flask.run.

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

Batteries included.

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:
  • 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:

DjangoFlask
Django ORMSQLAlchemy, MongoEngine
Django MigrationsAlembic
Django TemplatesJinja2
Django Core / URL'sWerkzeug
Django Forms (ModelForm)WTForms (WTForms-Alchemy)
Django CommandsFlask-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

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

Updates

  • April 28th, 2022:
    • Update django links from 1.11 to 4.0
    • Update python.org links from 2 to 3