Phoenix vs Django - The battle of brave and wise

Also comparing Elixir vs Python

This is something I thought of on the toilet and decided to write about :D

I'll also compare the two languages with JavaScript, but I won't compare the framework because, in my opinion, there's no Node framework that compares to their frameworks' features. If you already know the two languages, you can skip over to the frameworks.

If you follow me on Twitter or see my GitHub, you'll know that I almost always use Node on my backends, so why did I choose these two languages that I barely use? Well, both of them have batteries-included web frameworks (Phoenix and Django), I used both to build a traditional web-app, and I spent the past week learning Elixir and Phoenix, whereas in Node, I never used Express to build a web-app, only APIs, and I only had the weekend to spend, so I left it out.

The last time I used Django was 2 weeks ago, I wanted to get back into it and use it for GraphQL, but I read an article that convinced me to learn Elixir, so I never started work on the API, but I still made a small project tracker app (traditional Django MVC, not JAMStack).

I learned Elixir in a few days, and it was super easy to pick up (read disclaimer below), the syntax felt close to Python's and using pattern matching is like having a superpower, and I've spent the rest of my week learning Phoenix. It was actually a bit confusing at first because I was looking at tutorials for older versions of the framework with incompatible APIs, but the official docs are fantastic, which is a pattern I found common with all Elixir libraries.

Disclaimer: for new people who just started to learn to code, I learned Python and JavaScript, and 6 years ago I also learned Java. This is not my first language, I'm comfortable with programming concepts enough to be able to pick up a language this easily, and frankly it's a blessing that I can do that. Don't let that discourage you because everyone struggled when they started learning :)

Let's start with the languages first :)

Languages

I'll be comparing the languages in terms of documentation of libraries, how easy it is to start a new project, testing a project, amount of libraries available, and if you care about it, speed. All this information is based on my experience and based on the popular language practices

LanguageDocumentationNew projectsTestingLibrariesSpeed
ElixirFantasticEasyBuilt-inGood enoughSANIC
PythonMostly fantasticPiece of cakeBuilt-in or externalGreatThe slowest one, but still OK
JavaScriptGreatVaryingExternalFantasticOK for most work

Sorry if the table looks awful :(

Starting a new project in JS is varying in difficulty because:

  • Are you using a bundler?
    • Webpack or Rollup?
    • Babel
    • Typescript?
  • Express? Fastify? Hapi? Koa? Nest?
  • Which templating engine?
  • What to use for testing?
  • Setting up the boilerplate takes a long time

Using Django or Flask in Python already makes some choices for you and in the case of Django it even eliminates that boilerplate by making all the choices for you, and just like with Elixir, there's no concept of bundling so that takes off a heavy weight off of your shoulders

The speed really isn't a problem until you're running a large production scale, which is where Elixir shines because it's a compiled language, whereas the others are interpreted. If you want to find out about the difference, Ahmed Nour has an excellent article about the difference

Documentation in Elixir is the best out of them, because there's built in support for doc comments in the language and one library that generates them. I have rarely seen bad Python documentation, and I've seen some bad in JavaScript, but it was great overall.

Testing is another thing built-into Elixir, so you don't feel lazy about writing tests. Python has multiple choices, including built-in, but they mostly feel the same, so you won't have issues using multiple of them in different projects. JavaScript has no built-in testing, so you'll have to set up a testing library.

I also wanted to mention pattern matching, because I said it's like a superpower. To put simply, pattern matching is switch statements on steroids.

To quote from this StackOverflow answer:

the compiler can (i) verify that you covered all cases (ii) detect redundant branches that can never match any value (iii) provide a very efficient implementation (with jumps etc.)

But it's not just that, pattern-matching can also verify the shape of your data, which is tedious to do using switch or if-else.

Let's take a simple example: a generic run function that takes an operation and does something based on it. Here it is implemented without pattern-matching in pseudocode:

function run(data) {
  if data is type tuple and data.length == 2 {
    if data[0] is not type string {
      return {"error", "Invalid input"}
    }
    else {
      switch on data[0] cases:
        "read" {
          return filesystem.read(data[1])
        },
        "add" {
          if data[1] is type list<number> {
            return data[1].reduce(x, y -> x + y)
          }
          else if data[1] is type list<string> {
            return data[1].reduce(x, y -> concatenate(x, y))
          }
          else if data[1] is type list<list<any>> {
            return data[1].reduce(x, y -> List.concatenate(x, y))
          }
          else {
            return {"error", "No such operation with value type: {{ operation }}"}
          }
        }
    }
  }
  else {
    return {"error", "Invalid input"}
  }
}

And this is the same but with pattern-matching:

function run(data) {
  return match on data cases:
    {"read", filename} when filename is type string {
      return filesystem.readfile(filename)
    },
    {"add", numbers} when numbers is type list<number> {
      return numbers.reduce(x, y -> x + y)
    },
    {"add", strings} when strings is type list<string> {
      return strings.reduce(x, y -> concatenate(x, y))
    },
    {"add", lists} when lists is type list<list<any>> {
      return lists.reduce(x, y -> List.concatenate(x, y))
    }
    {operation, _values} when operation is type string {
      return {"error", "No such operation with value type: {{ operation }}"}
    },
    _invalid {
      return {"error", "Invalid input"}
    }
}

Please note that { "values", "between", "curly braces" } mean something is a tuple.

If you use JavaScript, you might be unfamiliar with tuples, you can read up on them here, but the gist of it is a known length array.

Not only is the second one more readable, but it allowed us to achieve the same result in 20 lines, compared to 30. Also, in the first one, the "No such operation with value type" error is handled per operation, rather than inside the pattern-match, and we were also able to give a meaningful name to each variable inside of its operation, rather than being stuck to data[1].

I mentioned this because I know a lot of people (past me included) don't really get the difference and think it's an overhyped switch, so I wanted to clear that up :)

The good thing is that Python should be getting pattern-matching in 3.10 (PEP 636), so you're not missing out on that.

I also thought I should say that I didn't take into account pattern-matching when comparing the languages, but it's something to also take into account when deciding on a language, because it could make things significantly easier depending on what you're doing.

Next up is the frameworks

Phoenix vs Django

I'll compare the two by building a polling app like the one in Django's documentation, and comparing the development speed, complexity, out-of-the-box features, and sprinkling in some personal opinion comparing which one was nicer to work with.

FrameworkDev speedComplexityNicer to work withOut-of-box features
DjangoMediumMediumNoHigh
PhoenixFastMediumYesMedium

I'm saying dev speed is medium for Django mainly because I still can't get used to the directory structure, but if you include how much it provides out-of-the-box it balances the two out, or maybe even puts Django at a slight advantage.

Complexity is the same for both of them, I say it's medium because of the overwhelming amount of files in each project, but because it's so opinionated, working on a new project with others wouldn't get messy because everything is already organized for you and your team.

Now in my opinion, I found Phoenix nicer to work with because:

  • There's auto code reload
  • There's a Webpack project already in there, and it integrates well with it
  • There's CLI commands to create files for you, and it organizes multiple files instead of having you separate them and import them in a "parent" file (Mentioning models, I prefer having each model in a separate file)
  • Uses a repository to access data from models, rather than using the model itself

That being said, no admin area and no authentication built-in is definitely a bad thing. In fact, It would've taken a shorter time to finish its project if an admin area like Django's was built-in.

Side note: I wanted to have the code up for everyone to see, but WSL stopped working and I had to go back to a backup, and like the noob that I am I didn't push my code to GitHub, so just imagine folders inside of folders inside of folders with purple poison icons inside.

There's also another elephant in the room:

Deployment

I don't have a single Django application on the web. Not a single one. Why? Deployment.

It may just be me, unfamiliar with Django, but having a large section in your documentation about deploying your app, ahem, a deployment checklist (seriously, take a look), and having to go through that and change everything to be production ready is a pain. A pain I hadn't encountered even when using Node. In Node, where nothing is opinionated, and you have to do everything yourself, there still are automatic dev and prod environment toggles that automatically do stuff for you.

In Phoenix, you have a main config.exs for common values, dev.exs for your dev environment, test.exs for unit-tests, and prod.exs and prod.secret.exs for your production environment, and I personally really enjoy this. It's clear from the name what each file does, and I don't have to do production checks every time I set a sensitive value (by production checks I mean: IS_PROD == false // or any equivalent).

Overall though, if I built something that wasn't a toy project this isn't a deal-breaker.

Oh, I haven't mentioned the big boy, have I?

Phoenix LiveView

Description from their official website reads:

The most fun you'll ever have building interactive web applications. We guarantee it.

In a nutshell, LiveView allows you to build a real-time web application without JavaScript, just using the regular EEX templates.

I haven't had a use for it, but it's very promising and the people that used it swear it's amazing. But talk is cheap, so here's a video by the man himself, Chris, building a Twitter clone using LiveView. Pretty neat, isn't it?

Wrapping up

So you're now probably wondering which should you use. In my opinion, Elixir seems like it has a bright future ahead of it, being so simple yet so performant, and it's built on a reliable, battle-tested language (Erlang)).

Phoenix is also doing some crazy stuff like LiveView, too, but at the same time, Django is more battle-tested, has a bit more out of the box, and probably has more job offerings than Phoenix.

If you have the time, give them both a try, build the same application using both frameworks, and see which you like more.

If you don't have the time, and you're looking for something that will land you a job quickly, I would advise against Phoenix unless there's a market for it in your area.

If you use another language and framework, and you're looking for a new challenge in your spare time, Elixir and Phoenix will probably be a more interesting one.

Some app ideas to help get you started are Polling, Project management, Library management, and personal finance management. All of them give you a solid exercise managing CRUD, database relations, and help you get comfortable with the framework

But then again, please choose something that fits your project's requirements, because I guarantee you it won't be a pleasant experience working against your framework to achieve something.

Thanks for reading :)

No Comments Yet