From Ruby on Rails to Python Django: Initial Thoughts

8 minute read

I recently made the transition to the Python Django web framework after working with Ruby on Rails for 5 years. Both frameworks are similar but the adjustment to Django was a little more interesting than I expected. What follows is a brief summary of my initial reactions to the Django framework as a Rails developer.

Comparing Project File Structures

Rails is a framework built on the ethos of ‘opinion over configuration’. The project structure of of a typical Rails application is defined by the framework itself. Over the years, new features in Rails have increased the number of directories in a project. What has remained consistent is the separation of the model, view, and controller concerns.

The heart of a Rails Application lives inside of the /App directory with an additional /config directory.

├── assets
├── channels
├── controllers
│   ├── application_controller.rb
│   ├── concerns
│   └── posts_controller.rb
├── helpers
│   ├── application_helper.rb
│   └── posts_helper.rb
├── javascript
├── jobs
├── mailers
├── models
│   ├── application_record.rb
│   ├── concerns
│   ├── post.rb
│   └── user.rb
└── views
    ├── layouts
    └── posts
        ├── _form.html.erb
        ├── _post.html.erb
        ├── edit.html.erb
        ├── index.html.erb
        ├── index.json.jbuilder
        ├── new.html.erb
        ├── show.html.erb
        └── show.json.jbuilder

As of Rails 6, there are additional directories for web socket connections (channels), helper functions typically used in view templates (helpers), javascript files compiled by webpacker (javascript), async jobs (jobs), and email dispatchers (mailers). The asset folder is the legacy location for javascript. This includes project multimedia assets and sass files compiled by the Rails Asset Pipeline.

Python Django projects are organized by ‘Apps’. The root directory includes a manage.py script which is used to run all of the Django CLI commands. The Project folder includes any number of ‘Apps’ along with the project settings (settings.py), project urls (urls.py), and the wsgi server file (wsgi.py). The settings file, among other things, requires you to declare all ‘Apps’ used in the project.

I enjoy organizing different parts of my application into apps. This makes it easier to develop reusable app components that can be shared among projects. Rails supports apps within apps with RailsEngines but the feature is often overlooked and is not as integral to the way most Rails developers architect projects.

Most of the interesting parts of this project are happening in the blog app. Django projects leave you responsible for organizing how your apps are structured. For example, you might define multiple blog model classes within models.py such as ‘Post’ and ‘Comment’. You can also organize your models in a directory similar to the way Rails enforces project directories. The difference is that you are in complete control in Django.

## Typical Django Project Layout
├── db.sqlite3
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── blog
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── forms.py
│   │   ├── migrations
│   │   ├── models.py
│   │   ├── static
│   │   │   └── css
│   │   │       └── blog.css
│   │   ├── templates
│   │   │   └── blog
│   │   │       ├── base.html
│   │   │       ├── post_detail.html
│   │   │       ├── post_edit.html
│   │   │       └── post_list.html
│   │   ├── tests.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── requirements.txt

One of the really cool features that ships with Django is a built an Admin app complete with user authentication! The admin interface can be configured extensively in the admin.py file. Full CRUD functionality for posts can be achieved with just a simple one line registration of Post model.

admin.site.register(Post)

Views (Not Controllers)

Django views take the place of controllers which is a little confusing considering that Rails calls its templates views. Once you get past the confusion of the name the general pattern is straightforward. Url routes are defined in the parent urls.py directory and each App folder typically has its own set of urls that are imported into the project. The routes for a simple blog application look like this:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
    path('post/new/', views.post_new, name='post_new'),
    path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),
]

Each url invokes the corresponding View function imported into the urls.py file. The root path of ‘’ calls the post_list function in views.

def post_list(request):
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')
    return render(request, 'blog/post_list.html', {'posts': posts})

Every view function receives a mandatory request argument and an optional url parameter argument. The function is expected to return a valid HTTP response. In this case it is a rendered post_list.html template. The fetched posts are included in a dictionary which can be accessed by the embedded python code in the template.

An index listing of objects like post_list is simple enough to express but I do miss some of the syntax sugar Rails provides when working with POST and PUT methods. Unlike Django, Rails splits all RESTful actions out to predefined ActionController methods called index, show, new, create, edit, update, and delete. In fact, all we need to do to create a similar list of endpoints is set the posts controller controller as a resource in the config/routes.rb file.

resources :posts, except: [:delete]

The request for the edit post form and the subsequent POST request that is used to save the update is broken down into two separate methods in the posts controller:

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  ...

  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)

    if @post.save
      redirect_to @post, notice: 'Post was successfully created.'
    else
      render :new
    end
  end

  private
    ...

    def post_params
      params.require(:post).permit(:title, :body, :published_at)
    end
end

For the new post page, we just need to create an empty instance of @post with the Post.new method. When Rails receives a POST request for posts, the router will call the PostsController.create method which will create the new post. The post_params variable takes all of the submitted parameters from the request restricted only to the fields allowed by that form - title, body, and published_at.

In the create method, the post_params data is passed to a new post and then saved. A model returns truthy when it’s successfully saved to the DB. We can keep the method pretty short with a simple condition to redirect to the newly created post if it’s saved or simply render the new form if there is a validation error.

Django does all of this stuff in a more obvious way by using conditional statements for the type of request. But Rails embraces this pattern in a more elegant object oriented way that is DRY and does not require writing conditions for different Http Methods in a function. In addition to this, Django requires the manual setting of each post property rather than just giving all of the submitted parameters to a new Post object. What follows is maybe something a little bit more readable for beginners but definitely something that gets harder to read as the complexity of a given view function increases.

def post_new(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.published_date = timezone.now()
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm()
        return render(request, 'blog/post_edit.html', {'form': form})

Templates

The Rails template engine is similar to the Django template system. Like Rails, Django injects any instance variables from the view that renders the template into the template. Django includes a myriad of built in template helpers that make it easy to iterate through objects, format data, and even render complete forms.

Django forms are generic by default and can be injected into Django templates via a Django view.

# Django mysite/blog/form.py
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'text',)

# mysite/blog/views.py
def post_edit(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.published_date = timezone.now()
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
    return render(request, 'blog/post_edit.html', {'form': form}

Injecting class names for styling purposes is a little tricky and involves customization of the form.py file in ways that I think over complicates forms. Rails, in contrast, makes the building blocks of the form and it’s controls available as simple template helper functions. Integrating helpers with standard HTML code is very easy to do in Rails.

Models

The differences between Rails ActiveRecord and the built in Django ORM is probably worthy of a dedicated article. I found myself quickly adjusting to the Django way of writing models. In short you are good as soon as you realize that a model’s ‘objects’ are accessible via the objects property. The rest of the syntax only varies slightly.

Post.find 1
Post.find_by title: 'From Ruby on Rails to Django'
Post.where.not published_at: nil
# Django ORM examples
Post.objects.get(id=1)
Post.objects.get(title='From Ruby on Rails to Django')
Post.objects.exclude(published_at=null)

One impressive feature of Django is the ability to just add properties to a model and then run the python migrate.py makemigrations cli command to generate a migration file for the new fields. Rails requires running a generator command with it’s own dsl with the attributes and types included as arguments: rails g migration AddPublishedDateToPost published_at:date.

Once the migration script runs, Rails models store all of the database fields in the /db/schema.rb file. I like having the ability to see the fields inside of the model itself. While the Rails version is more compact, I prefer the transparency Django models offer.

# Rails app/models/post.rb
class Post < ApplicationRecord

  def publish!
    published_date = Time.now
    save
  end
end
# Django mysite/blog/model.py
class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    published_date = models.DateTimeField(blank=True, null=True)

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    def __str__(self):
        return self.title

Parting Thoughts

While there are may differences between the two frameworks, I do appreciate both frameworks for what they are. I think Rails will always feel more elegant and productive when you choose to work within its strict and well documented best practices. Django is literal representation code and is probably more approachable for developers looking for a little more flexibility. I look forward to breaking down more advanced use cases and continuing to share my thoughts about Django.

Updated: