Rails vs Django: an in-depth technical comparison

I’d like to start with a disclaimer. I have been developing websites using Django for 3 years now and it’s no secret that I like Django. I wrote an open-source app for it and I have started sending patches to Django. I have however written this article to be as unbiased as possible and there’s plenty of compliments (and criticism) to both frameworks.

Six months ago I joined a project at my University using Ruby on Rails and have been working with it since then. The first thing I did was to look for for reviews and comparisons between the two frameworks and I remember being frustrated. Most of them were a little shallow and compared them on a higher level while I was looking for answers to questions like ‘how are database migrations handled by both?’, ‘what are the differences on the template syntax?’, ‘how is user authentication done?’. The following review will answer these questions and compare how the model, controller, view and testing is handled by each web framework.

A short introduction

Both frameworks were born out of the need of developing web applications faster and organizing the code better. They follow the MVC principle, which means the modelling of the domain (model), the presentation of the application data (view) and the user’s interaction (controller) are all separated from each other. As a side note, Django actually considers the framework to be the controller, so Django addresses itself as a model-template-view framework. Django’s template can be understood as the view and the view as the controller of the typical MVC scheme. I’ll be using the standard MVC nomenclature on this post.

Ruby on Rails

rails Ruby on Rails (RoR) is a web framework written in Ruby and is frequently credited with making Ruby “famous”. Rails puts strong emphasis on convention-over-configuration and testing. Rails CoC means almost no config files, a predefined directory structure and following naming conventions. There’s plenty of magic everywhere: automatic imports, automatically passing controller instance variables to the view, a bunch of things such as template names are inferred automatically and much more. This means a developer only needs to specify unconventional aspects of the application, resulting in cleaner and shorter code.


django Django is a web framework written in Python and was named after the guitarrist Django Reinhardt. Django’s motivation lies in the “intensive deadlines of a newsroom and the stringent requirements of the experienced Web developers who wrote it”. Django follows explicit is better than implicit (a core Python principle), resulting in code that is very readable even for people that are not familiar with the framework. A project in Django is organized around apps.  Each app has its own models, controllers, views and tests and feels like a small project. Django projects are basically a collection of apps, with each app being responsible for a particular subsystem.


Let’s start by looking how each framework handles the MVC principle. The model describes how the data looks like and contains the business logic.

Creating models

Rails creates models by running a command in terminal.

rails generate model Product name:string quantity_in_stock:integer 

The command above will automatically generate a migration and an empty model file, which looks like this:

class Product < ActiveRecord::Base


Coming from a Django background, one thing that annoyed me was the fact that I couldn’t know which fields a model has just by looking into its model file. What I learned was that Rails uses the model files basically only for business logic and stores how all models looks like in a file called schemas.rb. This file is automatically updated every time a migration is ran. If we take a look at this file we can see how our Product model looks like.

create_table "products", :force => true do |t|
  t.string   "name",
  t.integer  "quantity_in_stock",
  t.integer  "category_id",
  t.datetime "created_at", :null => false
  t.datetime "updated_at", :null => false

Notice how there are two extra fields in the model. created_at and updated_at are fields that are added to every model in Rails and will be set automatically.

In Django models are defined in a file called models.py. The same Product model would look like this

class Product(models.Model):
    name = models.CharField()
    quantity_in_stock = models.IntegerField()
    category = models.ForeignKey('Category')
    created_at = models.DateTimeField(auto_now_add=True) # set when it's created
    updated_at = models.DateTimeField(auto_now=True) # set every time it's updated

Notice that we had to explicitly add created_at and updated_at in Django. We also had to tell Django how these fields behave through the parameters auto_now_add and auto_now.

Model field defaults and foreign keys

Rails will per default allow fields to be null. You can see on the example above that the three fields we created allowed null. The reference field to a Category will also neither create an index nor a foreign key constraint. This means referential integrity is not guaranteed. Django’s default is the exact opposite. No field is allowed to be null unless explicitly set so. Django’s ForeignKey will also create a foreign key constraint and an index automatically. Although Rails decision here may be motivated by performance concerns, I’d side with Django here as I believe this decision avoids (accidental) bad design and unexpected situations. For example, a previous student in our project wasn’t aware that all fields he had created allowed null per default. After a while we noticed that some of our tables contained data that made no sense, such as a poll with null as the title. Since Rails doesn’t add FKs, following our example we could have for example deleted a category that is still referenced by other products and these products would then have invalid references. An option is to use a third-party app that adds support for automatic creation of foreign keys.


Migrations allow the database schema to be changed after it has already been created (actually, in Rails everything is a migration — even creating). I have to give props to Rails for supporting this out-of-the-box for a long time. This is done using Rails’ generator

$ rails generate migration AddPartNumberToProducts part_number:string

This would add a new field called part_number to the Product model.

Django only supports migrations by using an third-party library called South, however, I find South’s approach to be both somewhat cleaner and more practical. The equivalent migration above could be done by directly editing the Product model definition and adding a new field

class Product(models.Model):
    ... # old fields
    part_number = models.CharField()

and then calling

$ python manage.py schemamigration products --auto

South will automatically recognize that a new field was added to the Product model and create a migration file. It can then be synced by calling

$ python manage.py migrate products

Django is finally going to support migrations in its newest version (1.7) by integrating South into it.

Making queries

Thanks to object-relation mapping, you will not have to write a single SQL line in either framework. Thanks to Ruby’s expressiveness you can actually write range queries quite nicely.

Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)

This would find all Clients created yesterday. Python doesn’t support syntax like 1.day, which is extremely readable and succinct, neither the .. range operator. However, sometimes in Rails I feel like I’m writing prepared statements again. To select all rows where a particular field is greater than a value, you have to write

Model.where('field >= ?', value)

Django’s way of doing this is not much better, but in my opinion, more elegant. The equivalent line in Django is looks like this:



Controllers have the task of making sense of a request and returning an appropriate response. Web applications typically support adding, editing, deleting and showing details of a resource and RoR’s conventions really shine here by making the development of controllers short and sweet. Controllers are divided into methods, each representing one action (show for fetching the details of a resource, new for showing the form to create a resource, create for receiving the POST data from new and really creating the resource, etc.) The controllers’ instance variables (prefixed with @) are automatically passed to the view and Rails knows from the method name which template file to use as a view.

class ProductsController < ApplicationController
  # automatically renders views/products/show.html.erb
  def show
    # params is a ruby hash that contains the request parameters
    # instance variables are automatically passed to views
    @product = Product.find(params[:id])

  # returns an empty product, renders views/products/new.html.erb
  def new
    @product = Product.new

  # Receives POST data the user submitted. Most likely coming from
  # a form in the 'new' view.
  def create
    @product = Product.new(params[:product])
    if @product.save
      redirect_to @product
      # overrides default behavior of rendering create.html.erb
      render "new"

Django has two different ways of implementing controllers. You can use a method to represent each action, very similar to how Rails’ does it, or you can create a class for each controller action. Django however doesn’t have separate new and create methods, the creation of a resource happens in the same controller where an empty resource is created. There is also no convention on how to name your views. View variables need to be passed explicitly from the controller and the template file to be used needs to be set as well.

# django usually calls the 'show' method 'detail'
# the product_id parameter comes from the routing
def detail(request, product_id):
    p = Product.objects.get(pk=product_id) # pk = primary key

    # renders detail.html with the third parameter passed as context
    return render(request, 'products/detail.html', {'product': p})

def create(request):
    # check if form was submitted
    if request.method == 'POST':
        # similar to RoR' 'create' action
        form = ProductForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            new_product = form.save()
            return HttpResponseRedirect(new_product.get_absolute_url())
        # similar to RoR' 'new' action
        form = ProductForm() # An empty form

    return render(request, 'products/create.html', { 'form': form })

The amount of boilerplate in this example in Django is obvious when compared to RoR. Django seems to have noticed this and developed a second way of implementing controllers by harnessing inheritance and mixins. This second method is called class-based views (remember, Django calls the controller, view) and was introduced in Django 1.5 for promoting code reuse. Many commons actions such as displaying, listing, creating and updating a resource already have a class from which you can inherit from, greatly simplifying the code. Repetitive tasks such as specifying the view filename to be used, fetching the object and passing the object to the view are done automatically. The same example above would only be 4 lines using this pattern.

# Supposes the routing passed in a parameter called 'pk'
# containing the object id and uses it for fetching the object.
# Automatically renders the view /products/product_detail.html
# and passes product as a context variable to the view.
class ProductDetail(DetailView):
    model = Product

# Generates a form for the given model. If data is POSTed,
# automatically validates the form and creates the resource.
# Automatically renders the view /products/product_create.html
# and passes the form as a context variable to the view.
class ProductCreate(CreateView):
    model = Product

When the controllers are simple, using class-based views is usually the best choice, as the code generally ends up very compact and readable. However, depending on how non-standard your controllers are, many functions may need to be overridden to achieve the desired functionality. A common case is when the programmer wants to pass in additional variables to the view, this is done by overriding the function get_context_data. Do you want to render a different template depending on a particular field of the current object (model instance)? You’ll have to override render_to_response. Do you want to change how the object will be fetched (default is using the primary key field pk)? You’ll have to override get_object. For example, if we wanted to select a particular product by its name instead of id and to also pass to our view which products are similar to it, the code would look like this:

class ProductDetail(DetailView):
    model = Product

    def get_object(self, queryset=None):
        return get_object_or_404(Product, key=self.kwargs.get('name'))

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super(ProductDetail, self).get_context_data(**kwargs)

        # Add in the related products
        context['related_products'] = self.get_object().related_products
        return context


Rails views uses the Embedded Ruby template system which allows you to write arbitrary Ruby code inside your templates. This means it is extremely powerful and fast, but with great power comes great responsibility. You have to be very careful to not mix your presentation layer with any other kind of logic. I have another example involving fellow students again. A new student had joined our RoR Project and was working on a new feature. It was time for code review. We started at the controller and the first thing that struck me was how empty his controller looked like. I immediately looked at his views and saw massive ruby blocks intermixed with HTML. Yes, Rails it not to blame for a programmers lack of experience, but my point is that frameworks can protect the developer from some bad practices. Django for instance has a very frugal template language. You can do ifs and iterate through data using for loops, but there’s no way to select objects that were not passed in from the controller as it does not execute arbitrary Python expressions. This is a design decision that I strongly believe that pushes developers into the right direction. This would have forced the new student in our project to find the correct way of organizing his code.

Assets: CSS, Javascript and images

Rails comes with an excellent built-in asset pipeline. Rails’ asset pipeline is capable of concatenating, minifying and compressing Javascript and CSS files. Not only that, it also supports other languages such as Coffeescript, Sass and ERB. Django’s support of assets it pretty much shameful compared to Rails and leaves everything for the developer to handle. The only thing Django offers is something called static files, which basically collects all static files from each app to a single location. A third-party app called django_compressor offers a solution similar to Rails’ asset pipeline.


Forms in web applications are the interface through which users give input. Forms in Rails consist of helper methods that are used directly in the views.

<%= form_tag("/contact", method: "post") do %>
  <%= label_tag(:subject, "Subject:") %>
  <%= text_field_tag(:subject) %>
  <%= label_tag(:message, "Message:") %>
  <%= text_field_tag(:message) %>
  <%= label_tag(:subject, "Sender:") %>
  <%= text_field_tag(:sender) %>
  <%= label_tag(:subject, "CC myself:") %>
  <%= check_box_tag(:sender) %>
  <%= submit_tag("Search") %> 
<% end %>

The input fields subject, message, etc. can than be read at a controller through the ruby hash (a dictionary-like structure) params, for example params[:subject] and params[:message]. Django on the other hand abstracted the concept of forms. Forms are normal classes that encapsulate fields and can contain validation rules. They look a lot like models.

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField()
    sender = forms.EmailField()
    cc_myself = forms.BooleanField(required=False)

Django knows that the CharField counterpart in html are text input boxes and that BooleanFields are checkboxes. If you wish, you can change which input element will be used through the widget field. Forms in Django are instantiated in the controller.

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            return HttpResponseRedirect('/thanks/') # Redirect after POST
        form = ContactForm() # An unbound form

    return render(request, 'contact.html', { 'form': form })

Django will throw in validation for free. By default all fields are required, except when defined otherwise (such as cc_myself). Using the snippet above, if the form fails the validation, the same form will be automatically displayed with an error message and the input given is shown again. The code below displays a form in a view.

<form action="/contact/" method="post"> 
{{ form.as_p }} <!-- generates a form very similar to rails' -->
<input type="submit" value="Submit" />

URLs and routing

Routing is the task of matching a particular URL to a controller. Rails makes building REST web services a breeze and routes are expressed in terms of HTTP verbs.

get '/products/:id', to: 'products#show'

In the example above a GET request to /products/any_id will be automatically routed to the controller products and the action show. Since it’s such a common task to have all actions (create, show, index, etc) in a controller and thanks to convention-over-configuration, RoR created a way of quickly declaring all common routes, called resources. If you followed Rails’ conventions when naming your controller methods this is very handy.

# automatically maps GET /products/:id to products#show
#                    GET /products to products#index
#                    POST /products to products#create
#                    DELETE /products/:id to products#destroy
#                    etc.
resources :products

Django does not use the HTTP verb to route. Instead it is more verbose and uses regular expressions to match URLs to controllers.

urlpatterns = patterns('',
    # matches the detail method in the products controller
    url(r'^products/(?P\d+)/$', products.views.DetailView.as_view(), name='detail'),
    # matches the index method, you get the gist
    url(r'^products/$', products.views.IndexView.as_view(), name='index'),
    url(r'^products/create/$', products.views.CreateView.as_view(), name='create'),
    url(r'^products/(?P\d+)/delete/$', products.views.DeleteView.as_view(), name='delete'),

Since regular expressions are used, simple validation is automatically built in. Requesting /products/test/ will not match any routing rule and a 404 will be raised, as test is not a valid integer. The difference in philosophy can be seen here again. Django doesn’t have any convention when naming controller actions, so Django does not have any cool helpers like Rails’ resource and every route has to be explicitly defined. This results in each controller requiring several routing rules.


Testing is just a breeze in Rails and there’s a much stronger emphasis on it in Rails than in Django.


Both frameworks support fixtures (a fancy word for sample data) in a very similar way. I’d however give Rails a bonus point for making it more practical writing them as it infers from the file name for which model you’re writing them. Rails uses YAML-formatted fixtures, which is a human-readable data serialization format.

# users.yml (Rails now knows we're creating user fixtures)
  name: John Smith
  birthday: 1989-04-17
  profession: Blacksmith

  name: Bob Costa
  birthday: 1973-08-10
  profession: Surfer

All fixtures are automatically loaded and can be accessed directly as local variables on test cases

users(:john).name # John Smith

Django also supports YAML fixtures but developers tend to use JSON-formatted ones.

    "model": "auth.user",
    "fields": {
      "name": "John Smith",
      "birthday": "1989-04-17",
      "profession": "Blacksmith",
    "model": "auth.user",
    "fields": {
      "name": "Bob Costa",
      "birthday": "1973-08-10",
      "profession": "Surfer",

There’s no magic, so notice how they are more verbose as you have to explicitly define which model it is and then list the fields under fields

Testing models

Both frameworks are pretty identical in unit testing the models. Checks are made using a bunch of different types of assertions such as assert_equal, assert_not_equal, assert_nil, assert_raises, etc.

class AnimalTest < ActiveSupport::TestCase
  test "Animals that can speak are correctly identified" do
     assert_equal animals(:lion).speak(), 'The lion says "roar"'
     assert_equal animals(:cat).speak(), 'The cat says "meow"'

The same code in Django is very similar.

class AnimalTestCase(TestCase):
    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        # no way of directly accessing the fixtures, so we have to
        # manually select the objects
        lion = Animal.objects.get(name="lion") 
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

Testing controllers

Rails really shines thanks to its magic here again. Rails uses the class name to infer which controller is being tested and testing a particular action is as simple as calling http_verb :action_name. Let’s take a look at an example.

class UsersControllerTest < ActionController::TestCase
  test "should get index" do
    get :index # GET request to the index action
    assert_response :success # request returned 200
    # assigns is a hash containing all instance variables
    assert_not_nil assigns(:users)

The code is fairly straightforward to understand what’s happening. The first test line simulates a request to the index action on the Users controller. The second line then checks if the request was successful (response code 200-299). assigns is a hash that contains the instance variables that are passed to the views. So the third line is testing if an instance variable called users was set and is not nil.

There’s also a couple of additional assert helpers such as assert_difference that are very convenient.

  # assert_difference checks if the number being tested
  # changed between the start and the end of the block
  assert_difference('Post.count') do
    # creates a post
    post :create, post: {title: 'Some title'}

Testing the controller in Django is done by using a class called Client which acts as a dummy web browser. Here’s how the equivalent code looks like in Django.

class UsersTest(unittest.TestCase):
    def setUp(self):
        self.client = Client()

    def test_index(self):
        """ should get index """
        response = self.client.get(reverse('users:index'))
        self.assertEqual(response.status_code, 200)

First we have to initialize the Client at the test setup. The first line of test_index simulates a GET request to the controller action index of Users. reverse looks up the URL for the index action. Notice how the code is more verbose and there are no helpers such as assert_response :success. response.context contains the variables that were passed to the view.

It’s clear that the Rails magic here is really helpful. Rails/Ruby also has just so many great third-party apps such as factory_girlRSpecMocha and Cucumber that the task of writing tests is a pleasure.

Tools and other features

Dependency management

Both frameworks have fantastic dependency management tools available. Rails uses Bundler, which tracks the required gems for running a particular ruby application through a file called Gemfile.

gem 'nokogiri' 
gem 'rails', '3.0.0.beta3' 
gem 'rack', '>=1.0' 
gem 'thin', '~>1.1'

Dependencies can be added by simpling a new line to the Gemfile. All the required gems can be installed by simply calling:

bundle install

Django strongly recommends the use of virtualenv for having isolated Python environments. pip is then used for managing python packages. A package can be installed by calling:

pip install django-debug-toolbar

The project requirements can then be gathered with:

pip freeze > requirements.txt

Management commands

Almost every project ends up having common administration tasks such as precompiling assets, cleaning logs, etc. Rails uses Rake for handling these tasks. Rake is very flexible and makes developing tasks easy, especially sophisticated ones that depend on other tasks.

desc "Eats some food. Cooks it and sets the table before eating."
task eat: [:cook, :set_the_table] do
  # Before eating delicious food, :cook and :set_the_table will be done
  # The code for eating food can be directly written here

Rake tasks can have prerequisites. The task above, named eat, runs the tasks cook and set_the_table before executing. Rake also support namespaces for grouping common tasks. Tasks can be executed by simply callig its name:

rake eat

Django management commands are less flexible and do not support prerequisites neither namespaces. Nonetheless they get the job done, albeit less elegantly.

class Command(BaseCommand):
    help = 'Eats some food'

    def handle(self, *args, **options):
        call_command('cook') # this is how a management command is called in code
        set_the_table() # but subtasks should just be regular functions in python
        # code for eating stuff

If we save the contents above in a filed named eat.py we can call it with:

python manage.py eat

Internationalization and localization

Internationalization in Rails is somewhat rudimentar. Translation strings are defined as ruby hashes in files under the folder config/locales.

# config/locales/en.yml
en: # the language identifier
  greet_username: "Hello, %{user}!" # translation_key: "value"

# config/locales/pt.yml
  greet_username: "Olá, %{user}!"

Translation is made through a function t. The first parameter is the key that identifies which string is to be used (for example greet_username). Rails will automatically select the correct language.

t('greet_username', user: "Bill") # Hi, Bill or Olá, Bill

I found the process of having to decide the key names to use and registering them manually on locale files very cumbersome. Django packs the very convenient gettext. Translation is also done through a helper function, ugettext, but this time the key is the untranslated string itself.

ugettext('Hi, %(user)s.') % {'user': 'Bill'} # Hi, Bill or Olá, Bill

Django will then examine all source code and automatically collect strings to be translated by calling:

django-admin.py makemessages -a

The command above will generate a file for each language that you want to translate to. The files look like this:

# locale/pt_BR/LC_MESSAGES/django.po
msgid "Hi, %(user)s." # key
msgstr "Olá, %(user)s" # value (translation)

Notice that I have already filled in the translation at msgstr (they’re originally empty). Once the translations have been made, they have to be compiled.

django-admin.py compilemessages

This way of localizing projects is a lot more practical as there’s no need to think about key names and looking them up when needed.

User authentication

I have to say I was somewhat shocked when I learned that RoR does not come bundled with any sort of user authentication. I can’t think of any project that wouldn’t need authentication and user management. The most popular gem for this is devise, which unsurprisingly, is the most popular gem for Rails, having about half the stars Rails has on Github.

Even though Django has packed an authentication framework since forever, the solution provided wasn’t the most flexible until about a year ago, when version 1.5 came along and brought the configurable user model. Until then, you were forced to take Django’s definition of a “user”, you couldn’t change fields neither add new ones. Finally, that’s not the case anymore, and you can swap out the user model for one that you define yourself.

Third party libraries

There’s not much to say here. This post has already mentioned plenty of third party libraries for both frameworks and it’s clear that both enjoy a plethora of apps. Django Packages is an excellent website for searching Django Apps. I haven’t found such a website for Rails.


I don’t have concrete data to prove it, but I’m pretty sure Rails’ community is bigger. RoR has more than the double amount of stars on Github than Django. It also has the double the amount of questions tagged Rails on Stackoverflow. There also appears to be widely more jobs for RoR developers than Django (241 vs 58 on stackoverflow careers). Rails is huge and has so many fantastic resources for learning such as Rails Casts and Rails for Zombies. Django has Getting Started with Django but it just doesn’t compare. I learned Django using a great resource called The Django Book, but it has been outdated for several years now. Don’t get me wrong, there’s plenty of activity around Django too and if you have any questions, you will most likely find the solution very easily by googling it, but it’s just not as big as Rails.


Both Ruby on Rails and Django are outstanding frameworks for web development. They are a big help for developing modularized, clean code and do a great job in reducing time spent on common activities. I can not live anymore without an ORM framework, a templating engine and a session management system.  The question is then, how do I pick one?

You can not really go wrong by picking either one of them. My suggestion is always to try both and figure out which one you’re more confortable with. The decision may come down to which language you prefer or which principle you want to follow: convention-over-configuration or explicit is better than implicit. With CoC you get automatic imports, controller instance variables are automatically passed to the view and writing tests is convenient. With explicit is better than implicit, it’s always obvious what the code is doing, even for those not familiar with it.

From my personal experience I prefer Django. I like Python’s explicitness and I love Django’s forms and the fact that the framework is more defensive (limited template language, null not enabled by default on model fields). I know however plenty of folks that can’t live without Rails’ magic and its superior testing environment.