Python Web Frameworks' Complexity
mindref.blogspot.com
mindref.blogspot.com
Yes, I'm a maintainer of Django, so I'm biased too. But look: there's a good argument to be made that parts of (even of all of) Django is too complex. This, however, is not that argument; this is just a made-up metric.
ETA: I misspoke above: McCabe isn't made up. I meant to write that this was a cherry-picked metric. Leaving the above as is because people have already replied to it.
Yes, McCabe is a real thing. I haven't really found it that useful myself -- I'm not sure that you can adequately measure something fuzzy like "complexity" through static control flow analysis -- but sure, I'm willing to accept that it could be useful in certain circumstances.
What I am saying is that this guy is cherry-picking a specific metric (out of many that he could choose) that makes his tool looks good. I could just as easily come up with a metric that sorts Django to the top of an arbitrary list. That wouldn't be useful, either.
I think that alone makes it a much better metric to judge the experience you'll have with a framework than its cyclomatic complexity (at least for a user of a framework, maybe not as a developer)
The reason it's impossible to find answers for how to fix problem X in Windows (besides it being terribly designed) is that there are a ton of users who are trying to be helpful and providing bad advice.
Googling for solutions to an Ubuntu problem is starting to look grim too. Read some bug reports on Launchpad and then read some on the Debian bug tracker; there's a clear difference in signal/noise.
Circling back to the original point: if a web framework is simple, you can figure out the problem by just reading the code rather than looking online for solutions (many of which are in the form "oh, framework Y automatically does Z for you so you have to...").
EDIT: Reading his post again, maybe he didn't mean that, but the point is still valid.
"Cyclomatic complexity (or conditional complexity) is a software metric (measurement). It was developed by Thomas J. McCabe, Sr. in 1976 ..."
As long as those complex parts are encapsulated well enough that I don't have to peek 99% of the time, I don't think it matters. I am more concerned about how complex the code I write has to be. If by making the framework complex you can simplify the code I have to write, I am all for it.
This is a bit like comparing apples and oranges, usually you'd use 10 as a rough number for your method/function whatever, not for a whole framework. The whole point of modularization/refactoring is to have small parts, I've never heard of any mention regarding to cyclomatic complexity that you cannot use a multitude of small modules.
So to sum it up, it's nice that someone tried to compare python web frameworks, but the method of comparison is misleading at best and wrong at worst.
Django, one of the worst offenders according to your chart, also happens to be a mature project in wide use that works really well and handles most edge cases where, perhaps, some "less complex" frameworks would fall down.
So what does this measurement mean in practical terms? How should I balance this score against the benefit of the framework?
Often, bugs in your code will manifest as the framework throwing exceptions somewhere internally, often with difficult to interpret stack traces until you're familiar with how things work and what parts are safe to ignore.
I've not had much success with interactive debugging in django (recommendations/howtos would be hugely appreciated), but I can imagine a similar situation in which you have to learn what is safe to skip through (and maybe how to automate that to break only at points of interest, especially those points which are in some internal django setup methods)
Eg. Form classes are created dynamically per request and then instantiated. Lots of currying, many places that params can be specified.
# models.py
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
title = db.Column(db.String(200))
content = db.Column(db.Text)
def __init__(self, name, title, content):
self.name = name
self.title = title
self.content = content
# forms.py
import flaskext.wtf as wtf
from flaskext.wtf import Form, validators
from wtforms.ext.sqlalchemy.orm import model_form
import models
PostForm = model_form(models.Post, Form, field_args = {
'name': {'validators': [validators.required()]},
'title': {'validators': [validators.required(), validators.length(min=5)]},
})
# views.py
def post_new():
form = forms.PostForm()
if form.validate_on_submit():
post = models.Post(form.name.data, form.title.data, form.content.data)
db.session.add(post)
db.session.commit()
return redirect(url_for('post.index'))
return render_template('post/new.slim', form=form)
# _post_form.slim
- from 'helpers.slim' import render_field
form method="POST" class="well"
= form.hidden_tag()
= render_field(form.name, class\="span4")
= render_field(form.title, class\="span4")
= render_field(form.content, class\="span4")
.field
input type="submit" class="btn btn-success"
# post/new.slim
- extends 'layout.slim'
- block content
.span6
h2 Creating new post
- include 'post/_post_form.slim'
a href="=url_for('.index')" class="btn btn-primary btn-small" Back
# helpers.slim
- macro render_field(field)
div class="{{ 'field-with-errors' if field.errors else 'field' }}"
- if field.errors
- for error in field.errors
p class="error" = error
= field.label
= (field(**kwargs) | safe)
Don't bother about the slim templates. It's a small jinja2 extension which I use to give me slim syntax.Now this probably looks like a lot. But consider that you write the helper to render the form only once. Also, you can write a helper to generate models and form scaffolding for you. That's what I do.
python manage.py create_model post -f 'name:String(80) title:String(200) content:Text'
It generates me the model, form and form template(models.py, forms.py, _post_form.py). The form of course will have empty validators which I fill.Edit: stupid auto-correct.
Even in spite of all that, it's still the nicest (and by far the most well-documented) framework I've used.
If I had to pick one complaint, I'd say the "define fields as class-variables" for forms and models is a bit too magical, and picking through the metaclass implementation to understand exactly what things happen when can be a pain, and I'm not 100% sold that 'reduced boilerplate' is a good enough reason for it (there may be other good reasons. I'm just whining :)).
While Django is mature, I'm using it right now in fact, I find that I spend a hell of a lot of time trying to figure out why Django is doing a specific thing or how to convince it to do the exact thing I want.
I'm convinced, after a few years of Django work and a year or so of Flask (or just werkzeug libraries), that Django is the wrong level of complexity for a web framework.
For every minute I've saved by having django handle the edge case or having some great feature built in I feel like I've lost a few minutes to fighting against the framework when my use case doesn't fit into the expected usage and I'm fighting against a fairly rigid framework.
This feeling is what I think the OP is trying to get at with the "excessive complexity" measurement, although using a single metric like this is probably not the answer. It does fit decently well with my impressions of the frameworks listed (among the ones I've used anyway) but this is a messy and subjective problem.
I've found myself firmly in the "libraries > frameworks" camp because of this issue so I'm certainly biased towards liking this metric, even with it's faults.
It's a general problem with hierarchies of inheritance. Too many separate places where the actual functionality is, so you have to learn all of them and hold them in your head to understand the few pithy lines of code in front of you.
And that's the issue with the admin and forms. Hard to keep track
And what do you suggest to make it easier for you to read, spaghetti code ? Django makes great use of mixins, mixins you can re-use to make your own views if you dont like the default generic views.
An IDE can help but that's an indicator that the solution has a readability problem.
That's not saying Django should do this, they want to create an easy to use, well integrated framework. That's saying why it might be a good idea to avoid tightly coupled frameworks when designing an application.
Loose coupling, loose coupling, loose coupling!
I suspect -- but don't know -- that these might even turn out to be inversely correlated for some types of frameworks. DSL-based frameworks often require some interesting tricks to get right.
So which is better? A complex framework that lets you write simple, concise code? Or a simple framework that requires a lot of boilerplate and complexity to use?
For everything else, aside from Mastercard, there's room for argument and discussion... but I think the only universal takeaway is that most or all of the frameworks mentioned have solid use cases, and none of them work for everyone in every circumstance.
It has all basic things you would like to have. I mean you can work with templates engine you want, forms, middlewares etc.
Speaking about Django we can find it is not just Django. It is also hundreds of django apps like django-social-auth, South, django-celery, django-mptt etc.
This new framework has no such huge community for now. On the other side the author did a good work. You can use pretty complete framework on Python3 when the most of others can't offer the same. And it is not 2to3. If I correctly remember impressions of the code it was written on Python3 and then ported to python2.
So, I like it. You can use classes or functions for handling requests. You have modules so you can install just wheezy.http and not so many other parts and I believe we should choose the right tool to solve the problem. In one case it can be Django, in others we can take Wheezy.web.
I use web.py, flask and bottlepy depending on the required complexity of a project. They intuitively feel less complex. If I need a step up from web.py, I'd be inclined to go with Pyramid (reddit is built on Pylons, which is a precursor to Pyramid and before that, they used web.py)
To me it's a tradeoff between coding speed and project size/speed/performance. For example, if you use Bottlepy to write a typical webapp, you'd have to write your own session handlers (or use Beaker), and you'd have to write your own utils to transform and sanitize data etc.
However, using Bottle.py for a cookie dropping service? 30 lines of code and you have a very robust and scalable cookie dropping service. Ditto to using bottle for adserving and ad decisioning off redis.
Flask however, in my opinion works well for public API endpoints, while web.py works very very well for web frontends
One would hope that the difference in ordering comes from accounting for the amount of functionality in each project (and even if you prefer bottle to django, you have to admit that django builds in more functionality.)
But, again, it's unclear how that conversion happened. I'm wondering if the author ranked "excessive complexity" however he wanted, and the McCabe complexity is just there to give this the veneer of scientific authenticity.
This I've settled on tornado as my framework and server.
Most Python frameworks, regardless, seem to score quite well-Flask, Turbogears, Bottle-in contrast to Django, which is to be expected as it doesn't try to be simple (it is, however, well documented and easy to learn).
Most of the things aren't needed by most people, but it gets installed anyway.
EDIT: I am an idiot who clearly doesn't know much about Django and spake without prior research.
B) Django doesn't have its own cheeseshop. Django developers use PyPI and pip just like everyone else. (Maybe you're thinking of Crate.io, which is a PyPI mirror that happens to use Django in its implementation.)
As for complexity, I still think it's excessive. It's good to know the internals of a module/package/whatever, and django is really really complex, of which cyclomatic complexity quantifies I believe. I mean, if you have code that branches off everywhere, it'd be hard to follow the logic when inspecting the internals.
I'm curious: where did you read/see that?
One of the first few pages in the tutorial includes:
"Django apps are "pluggable": You can use an app in multiple projects, and you can distribute apps, because they don't have to be tied to a given Django installation."
and I guess it might be possible to mistake 'manage.py startapp ...' as some sort of internal packaging thing if you don't look too deeply.
To answer your question: I am an idiot and I didn't know much about Django. I did once find djangopackages or something like that and simply jumped to the conclusion. Big and Unwieldly is the brand perception I had.
And catering to 'every single edge case' actually seems to contradict the philosophy I've observed on the django-developers list.
And Django's complexity is really good in it's class. "Complex is better than complicated" - and Django tries hard not to be the latter :)
Look, I know that bare-bones Django looks simple, but in reality it isn't. You don't even have "bare-bones" unless you manually disable quite some functionality in settings.py. It's still good if you plan to grow your app. You can always turn the features back on and plug reusable apps with ease.
But for something as simple as getting JSON requests, hitting db and returning response I'd choose something else, some microframework and simplified ORM, for example Bottle and Storm[1]. I had a great success with using CorePost[2] on top of Twisted, but that's only if you can live without an ORM and like Twisted's brand of async.
Long story short: there is no need to employ Django for every little webservice. You can, of course, but you should think about why your using full-stack framework when something simpler, faster and smaller would do just as well.
[1] https://storm.canonical.com/ [2] https://github.com/jacek99/corepost
I'm curious how the analysis in the blog post works. In languages like Python it's common to hide branches inside of data structure lookups like the following:
def f():
...
def g():
...
def h(x):
return {
True: f,
False: g}[not x]()CC is useful for determining number of tests for complete branch/path coverage if you don't pull out the switch on dictionary tricks (among others).
The dict-as-a-switch expression is common to python, but it can be masked as the parent has shown in larger examples.
I have no idea how we could apply static analysis techniques to a construct like this. To be honest I tend to avoid this approach on readability grounds. It could be cleaned up with a dict subclass but I think inlining the dict call mechanics is simpler to read, even if it produces larger code, leads to repetition and couples you to this approach (which would be a strong consideration in the case of writing a library - where this type of code is most prevelant).
This method is designed for individual modules. Here you've applied for the complete system. What sense does this make? Resulting metric is not conveying any meaningful information. And higher numbers mean its not necessarily worse; it just means the program is more complex, which may be due to more functionality.
Right way to compare it - Level the scope of the frameworks (e.g. add SQLAlchemy to pyramids) and compare again. Bring all to equal levels.
...though it would be interesting to see this metric applied to a Ruby framework like Rails