Sunday, August 7, 2016

Django vs Flask vs Pyramid: Choosing a Python Web Framework_part2 (end)


5.2 Flask

Flask uses the Django-inspired Jinja2 templating language by default but can be configured to use another language. A programmer in a hurry couldn't be blamed for mixing up Django and Jinja templates. In fact, both the Django examples above work in Jinja2. Instead of going over the same examples, let's look at the places that Jinja2 is more expressive than Django templating.
Both Jinja and Django templates provide a feature called filtering, where a list can be passed through a function before being displayed. A blog that features post categories might make use of filters to display a post's categories in a comma-separated list.
  1. <!-- Django -->
  2. <div class="categories">Categories: {{ post.categories|join:", " }}</div>
  3. <!-- now in Jinja -->
  4. <div class="categories">Categories: {{ post.categories|join(", ") }}</div>
In Jinja's templating language it's possible to pass any number of arguments to a filter because Jinja treats it like a call to a Python function, with parenthesis surrounding the arguments. Django uses a colon as a separator between the filter name and the filter argument, which limits the number of arguments to just one.
Jinja and Django for loops are also similar. Let's see where they differ. In Jinja2, the for-else-endfor construct lets you iterate over a list, but also handle the case where there are no items.
  1. {% for item in inventory %}
  2. <div class="display-item">{{ item.render() }}</div>
  3. {% else %}
  4. <div class="display-warn">
  5. <h3>No items found</h3>
  6. <p>Try another search, maybe?</p>
  7. </div>
  8. {% endfor %}
The Django version of this functionality is identical, but uses for-empty-endfor instead of for-else-endfor.
  1. {% for item in inventory %}
  2. <div class="display-item">{{ item.render }}</div>
  3. {% empty %}
  4. <div class="display-warn">
  5. <h3>No items found</h3>
  6. <p>Try another search, maybe?</p>
  7. </div>
  8. {% endfor %}
Other than the syntactic differences above, Jinja2 provides more control over its execution environment and advanced features. For example, it's possible to disable potentially dangerous features to safely execute untrusted templates, or to compile templates ahead of time to ensure their validity.

5.3 Pyramid

Like Flask, Pyramid supports many templating languages (including Jinja2 and Mako) but ships with one by default. Pyramid uses Chameleon, an implementation of ZPT (the Zope Page Template) templating language. Let's look back at our first example, adding a user's name to the top bar of our site. The Python code looks much the same except that we don't need to explicitly call a render_template function.
  1. @view_config(renderer='templates/home.pt')
  2. def my_view(request):
  3.     # do stuff...
  4.     return {'user': user}
But our template looks pretty different. ZPT is an XML-based templating standard, so we use XSLT-like statements to manipulate data.
  1. <div class="top-bar row">
  2.   <div class="col-md-10">
  3.   <!-- more top bar things go here -->
  4.   </div>
  5.   <div tal:condition="user"
  6.        tal:content="string:You are logged in as ${user.fullname}"
  7.        class="col-md-2 whoami">
  8.   </div>
  9. </div>
Chameleon actually has three different namespaces for template actions. TAL (template attribute language) provides basics like conditionals, basic string formatting, and filling in tag contents. The above example only made use of TAL to complete its work. For more advanced tasks, TALES and METAL are required. TALES (Template Attribute Language Expression Syntax) provides expressions like advanced string formatting, evaluation of Python expressions, and importing expressions and templates.
METAL (Macro Expansion Template Attribute Language) is the most powerful (and complex) part of Chameleon templating. Macros are extensible, and can be defined as having slots that are filled when the macro is invoked.

6 Frameworks in Action

For each framework let's take a look at making an app called wut4lunch, a social network to tell the whole internet what you ate for lunch. Free startup idea right there, totally a gamechanger. The application will be a simple interface that allows users to post what they had for lunch and to see a list of what other users ate. The home page will look like this when we're done.


6.1 Demo App with Flask

The shortest implementation clocks in at 34 lines of Python and a single 22 line Jinja template. First we have some housekeeping tasks to do, like initializing our app and pulling in our ORM.
from flask import Flask
  1. # For this example we'll use SQLAlchemy, a popular ORM that supports a
  2. # variety of backends including SQLite, MySQL, and PostgreSQL
  3. from flask.ext.sqlalchemy import SQLAlchemy
  4. app = Flask(__name__)
  5. # We'll just use SQLite here so we don't need an external database
  6. app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
  7. db = SQLAlchemy(app)
Now let's take a look at our model, which will remain almost the same for our other two examples as well.
  1. class Lunch(db.Model):
  2.     """A single lunch"""
  3.     id = db.Column(db.Integer, primary_key=True)
  4.     submitter = db.Column(db.String(63))
  5.     food = db.Column(db.String(255))
Wow, that's pretty easy. The hardest part was finding the right SQLAlchemy data types and picking a length for our String  fields in the database. Using our models is also extremely simple, thanks to the SQLAlchemy query syntax we'll see later.

Building our submission form is just as easy. After importing Flask-WTForms and the correct field types, you can see the form looks quite a bit like our model. The main difference is the new submit button and prompts for the food and submitter name fields.

The SECRET_KEY field in the app config is used by WTForms to create CSRF tokens. It is also used by itsdangerous (included in Flask) to sign cookies and other data.
from flask.ext.wtf import Form
  1. from wtforms.fields import StringField, SubmitField
  2. app.config['SECRET_KEY'] = 'please, tell nobody'
  3. class LunchForm(Form):
  4.     submitter = StringField(u'Hi, my name is')
  5.     food = StringField(u'and I ate')
  6.     # submit button will read "share my lunch!"
  7.     submit = SubmitField(u'share my lunch!')
Making the form show up in the browser means the template has to have it. We'll pass that in below.
  1. from flask import render_template
  2. @app.route("/")
  3. def root():
  4.     lunches = Lunch.query.all()
  5.     form = LunchForm()
  6.     return render_template('index.html', form=form, lunches=lunches)
Alright, what just happened? We got a list of all the lunches that have already been posted with Lunch.query.all(), and instantiated a form to let the user post their own gastronomic adventure. For simplicity, the variables are passed into the template with the same name, but this isn't required.
  1. <html>
  2. <title>Wut 4 Lunch</title>
  3. <b>What are people eating?</b>
  4. <p>Wut4Lunch is the latest social network where you can tell all your friends
  5. about your noontime repast!</p>
Here's the real meat of the template, where we loop through all the lunches that have been eaten and display them in a <ul>. This almost identical to the looping example we saw earlier.
  1. <ul>
  2. {% for lunch in lunches %}
  3. <li><strong>{{ lunch.submitter|safe }}</strong> just ate <strong>{{ lunch.food|safe }}</strong>
  4. {% else %}
  5. <li><em>Nobody has eaten lunch, you must all be starving!</em></li>
  6. {% endfor %}
  7. </ul>
  8. <b>What are YOU eating?</b>
  9. <form method="POST" action="/new">
  10.     {{ form.hidden_tag() }}
  11.     {{ form.submitter.label }} {{ form.submitter(size=40) }}
  12.     <br/>
  13.     {{ form.food.label }} {{ form.food(size=50) }}
  14.     <br/>
  15.     {{ form.submit }}
  16. </form>
  17. </html>
The <form> section of the template just renders the form labels and inputs from the WTForm object we passed into the template in the root() view. When the form is submitted, it'll send a POST request to the /new endpoint which will be processed by the function below.
from flask import url_for, redirect
  1. @app.route(u'/new', methods=[u'POST'])
  2. def newlunch():
  3.     form = LunchForm()
  4.     if form.validate_on_submit():
  5.         lunch = Lunch()
  6.         form.populate_obj(lunch)
  7.         db.session.add(lunch)
  8.         db.session.commit()
  9.     return redirect(url_for('root'))
After validating the form data, we put the contents into one of our Model objects and commit it to the database. Once we've stored the lunch in the database it'll show up in the list of lunches people have eaten.
  1. if __name__ == "__main__":
  2.     db.create_all()  # make our sqlalchemy tables
  3.     app.run()
Finally, we have to do a (very) little bit of work to actually run our app. Using SQLAlchemy we create the table we use to store lunches, then start running the route handlers we wrote.

6.2 Demo App with Django

The Django version of wut4lunch is similar to the Flask version, but is spread across several files in the Django project. First, let's look at the most similar portion: the database model. The only difference between this and the SQLAlchemy version is the slightly different syntax for declaring a database field that holds text.
  1. # from wut4lunch/models.py
  2. from django.db import models
  3. class Lunch(models.Model):
  4.     submitter = models.CharField(max_length=63)
  5.     food = models.CharField(max_length=255)
On to the form system. Unlike Flask, Django has a built-in form system that we can use. It looks much like the WTForms module we used in Flask with different syntax.
  1. from django import forms
  2. from django.http import HttpResponse
  3. from django.shortcuts import render, redirect
  4. from .models import Lunch
  5. # Create your views here.
  6. class LunchForm(forms.Form):
  7.     """Form object. Looks a lot like the WTForms Flask example"""
  8.     submitter = forms.CharField(label='Your name')
  9.     food = forms.CharField(label='What did you eat?')
Now we just need to make an instance of LunchForm to pass in to our template.
  1. lunch_form = LunchForm(auto_id=False)
  2. def index(request):
  3.     lunches = Lunch.objects.all()
  4.     return render(
  5.         request,
  6.         'wut4lunch/index.html',
  7.         {
  8.             'lunches': lunches,
  9.             'form': lunch_form,
  10.         }
  11.     )
The render function is a Django shortcut that takes the request, the template path, and a context dict. Similar to Flask's render_template, but it also takes the incoming request.
  1. def newlunch(request):
  2.     l = Lunch()
  3.     l.submitter = request.POST['submitter']
  4.     l.food = request.POST['food']
  5.     l.save()
  6.     return redirect('home')
Saving the form response to the database is different, instead of using a global database session Django lets us call the model's ..save() method and handles session management transparently. Neat!

Django provides some nice features for us to manage the lunches that users have submitted, so we can delete lunches that aren't appropriate for our site. Flask and Pyramid don't provide this automatically, and not having to write Yet Another Admin Page when making a Django app is certainly a feature. Developer time isn't free! All we had to do to tell Django-admin about our models is add two lines to wut4lunch/admin.py.
  1. from wut4lunch.models import Lunch
  2. admin.site.register(Lunch)
Bam. And now we can add and delete entries without doing any extra work.

Lastly, let's take a look at the differences in the homepage template.
  1. <ul>
  2. {% for lunch in lunches %}
  3. <li><strong>{{ lunch.submitter }}</strong> just ate <strong>{{ lunch.food }}</strong></li>
  4. {% empty %}
  5. <em>Nobody has eaten lunch, you must all be starving!</em>
  6. {% endfor %}
  7. </ul>
Django has a handy shortcut for referencing other views in your pages. The url tag makes it possible for you to restructure the URLs your application serves without breaking your views. This works because the url tag looks up the URL of the view mentioned on the fly.
  1. <form action="{% url 'newlunch' %}" method="post">
  2.   {% csrf_token %}
  3.   {{ form.as_ul }}
  4.   <input type="submit" value="I ate this!" />
  5. </form>
The form is rendered with different syntax, and we need to include a CSRF token manually in the form body, but these differences are mostly cosmetic.

6.3 Demo App with Pyramid

Finally, let's take a look at the same program in Pyramid. The biggest difference from Django and Flask here is the templating. Changing the Jinja2 template very slightly was enough to solve our problem in Django. Not so this time, Pyramid's Chameleon template syntax is more reminiscent of XSLT than anything else.
  1. <!-- pyramid_wut4lunch/templates/index.pt -->
  2. <div tal:condition="lunches">
  3.   <ul>
  4.     <div tal:repeat="lunch lunches" tal:omit-tag="">
  5.       <li tal:content="string:${lunch.submitter} just ate ${lunch.food}"/>
  6.     </div>
  7.   </ul>
  8. </div>
  9. <div tal:condition="not:lunches">
  10.   <em>Nobody has eaten lunch, you must all be starving!</em>
  11. </div>
Like in Django templates, a lack of the for-else-endfor construct makes the logic slightly more verbose. In this case, we end up with if-for and if-not-for blocks to provide the same functionality. Templates that use XHTML tags may seem foreign after using Django- and AngularJS-style templates that use {{ or {% for control structures and conditionals.

One of the big upsides to the Chameleon templating style is that your editor of choice will highlight the syntax correctly, since the templates are valid XHTML. For Django and Flask templates your editor needs to have support for those templating languages to highlight correctly.
  1. <b>What are YOU eating?</b>
  2. <form method="POST" action="/newlunch">
  3.   Name: ${form.text("submitter", size=40)}
  4.   <br/>
  5.   What did you eat? ${form.text("food", size=40)}
  6.   <br/>
  7.   <input type="submit" value="I ate this!" />
  8. </form>
  9. </html>
The form rendering is slightly more verbose in Pyramid because the pyramid_simpleform doesn't have an equivalent to Django forms' form.as_ul function, which renders all the form fields automatically.

Now let's see what backs the application. First, we'll define the form we need and render our homepage.
  1. # pyramid_wut4lunch/views.py
  2. class LunchSchema(Schema):
  3.     submitter = validators.UnicodeString()
  4.     food = validators.UnicodeString()
  5. @view_config(route_name='home',
  6.              renderer='templates/index.pt')
  7. def home(request):
  8.     lunches = DBSession.query(Lunch).all()
  9.     form = Form(request, schema=LunchSchema())
  10.     return {'lunches': lunches, 'form': FormRenderer(form)}
The query syntax to retrieve all the lunches is familiar from Flask because both demo applications use the popular SQLAlchemy ORM to provide persistent storage. In Pyramid lets you return your template's context dictionary directly instead of needing to call a special render function. The @view_config decorator automatically passes the returned context to the template to be rendered. Being able to skip calling the render method makes functions written for Pyramid views easier to test, since the data they return isn't obscured in a template renderer object.
  1. @view_config(route_name='newlunch',
  2.              renderer='templates/index.pt',
  3.              request_method='POST')
  4. def newlunch(request):
  5.     l = Lunch(
  6.         submitter=request.POST.get('submitter', 'nobody'),
  7.         food=request.POST.get('food', 'nothing'),
  8.     )
  9.     with transaction.manager:
  10.         DBSession.add(l)
  11.     raise exc.HTTPSeeOther('/')
Form data is easy to retrieve from Pyramid's request object, which automatically parsed the form POST data into a dict that we can access. To prevent multiple concurrent requests from all accessing the database at the same time, the ZopeTransactions module provides context managers for grouping database writes into logical transactions and prevent threads of your application from stomping on each others' changes, which can be a problem if your views share a global session and your app receives a lot of traffic.

7 Summary

Pyramid is the most flexible of the three. It can be used for small apps as we've seen here, but it also powers big-name sites like Dropbox. Open Source communities like Fedora choose it for applications like their community badges system, which receives information about events from many of the project's tools to award achievement-style badges to users. One of the most common complaints about Pyramid is that it presents so many options it can be intimidating to start a new project.
By far the most popular framework is Django, and the list of sites that use it is impressive. Bitbucket, Pinterest, Instagram, and The Onion use Django for all or part of their sites. For sites that have common requirements, Django chooses very sane defaults and because of this it has become a popular choice for mid- to large-sized web applications.

Flask is great for developers working on small projects that need a fast way to make a simple, Python-powered web site. It powers loads of small one-off tools, or simple web interfaces built over existing APIs. Backend projects that need a simple web interface that is fast to develop and will require little configuration often benefit from Flask on the frontend, like jitviewer which provides a web interface for inspecting PyPy just-in-time compiler logs.

All three frameworks came up with a solution to our small list of requirements, and we've been able to see where they differ. Those differences aren't just cosmetic, and they will change how you design your product and how fast you ship new features and fixes. Since our example was small, we've seen where Flask shines and how Django can feel clunky on a small scale. Pyramid's flexibility didn't become a factor because our requirements stayed the same, but in the real world new requirements are thrown in constantly.
Written by Ryan Brown

If you found this post interesting, follow and support us.
Suggest for you:

Zero to Hero with Python Professional Python Programmer Bundle

The Python Mega Course: Build 10 Python Applications

Complete Python Bootcamp (Hot)

Learning Python for Data Analysis and Visualization

Learn Python for Beginners!



1 comment:

  1. I suggest using a code syntax highlighter, all blue is very painful to read, particularly when there are several good options of syntax highlighting in blogs.

    ReplyDelete