I have been writing PHP since I first started programming. It's always been a very flexible and easy-to-use language that grows with you, and coupled with Laravel, you’ve got a gem of beauty, a super-charged modern framework that’s flexible, clean, and powerful!
Python is cool; it's compact, versatile, and has a broad spectrum of use cases, especially in web development and machine learning. Django is well-designed with batteries included and is one of the most mature frameworks around.
Since I have worked extensively with both frameworks and languages, this gives me a unique perspective on things. In this article, I will give you a general overview of both frameworks to help you choose the right one for your needs.
Which is better?
Neither, there is no better stack. Both frameworks are an excellent choice for most use cases.
Disappointing? Sorry, I know you came here to get a final verdict on which is better, but, my goal is to give you a deep technical review of features and functionality so that you can make an informed decision and not base your decision solely on hype or opinions.
Destroy the world!?
Before we continue with more framework-related stuff, it's important to understand one of the most fundamental differences between PHP and Python.
PHP spawns up a worker process that loads Laravel "from scratch" on every request, thus the lifecycle for a Laravel app begins when a request is made and ends when the worker closes the request.
The PHP engine does use a JIT compiler and some fancy caching optimizations to make this process fast, so it's not exactly "from scratch" each time, however, if you declare a global variable, this variable gets destroyed when the request is finished.
Python on the other hand loads Django into memory once at startup, thus any global variables that exist on startup will persist throughout every request until the application restarts.
This is why when you change a PHP file, it immediately updates on the next request without the need for a restart. In Django, you will most likely need to restart the Gunicorn workers for the new code to take effect.
This is great for performance since Django has everything it needs already loaded, Django can thus serve up requests much faster. Not so great if you want to run your application in a shared environment though, such as Cpanel hosting, since each Django instance is bootstrapped once, the entire application will sit in memory all the time waiting for requests.
The PHP application on the other hand only uses up resources (besides a small percentage for caching) when there are requests.
PHP does have alternative runtimes like octane and frankenphp, which work similar to Django's load once in memory approach, however, PHP-FPM is the default and most commonly used runtime in the PHP world.
Package management and setting up
Package management is a vital component in modern development, you almost always will need to pull down at least one external package in your project at some point in time. Both PHP and Python have excellent package managers.
Python’s most popular package manager is PIP. To use PIP you first need a virtual environment:
python3 -m venv venv
source venv/bin/activate
The above commands set up a virtual environment that isolates your project dependencies from the system's Python (a very good idea on Linux! Since the OS heavily relies on Python). Furthermore, you can keep a separate virtual environment for each project.
Now you can run the following to install Django and set up a Django project :
pip instal django
django-admin startproject myproject
PHP has a similar setup process, except PHP uses Composer as its package manager which you can download and install from here. You will need to have PHP installed prior. If you are on Mac or Windows, you basically can skip all of these steps and just download herd. Herd is a Laravel-provided toolset that installs everything you need to build a Laravel application, including the relevant DB servers.
Once composer is installed on your system, you can install Laravel as follows:
composer create-project laravel/laravel myprojectname --prefer-dist
Composer will automatically configure itself within the "myprojectname" project and create a "vendor" folder which is similar to the virtual environment setup in Python.
The only difference is that the "vendor" folder just contains PHP packages, not an entire PHP runtime.
Python copies the whole Python runtime to the virtual environment, thus it's completely isolated (except for C libraries installed via your package manager).
To install a package via composer:
composer require vendor/package
//e.g.
composer require guzzle/guzzle
Eloquent
Laravel has a great ORM called eloquent; it's very close to raw SQL and feels natural for someone like me, who prefers writing SQL. I like using native DB functionality and optimizing queries by hand so I enjoy writing queries using Eloquent.
Here is an example model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
}
Eloquent will automatically associate this model with a " products" table in our database. Furthermore, you do not have to explicitly declare every table field because when you create an instance of this class: the fields automatically become available on instances of the class e.g.: "$product->price".
To start querying the model you can then try the following:
Product::where("price", ">=", 100)
->skip(5)
->take(10)
->get()
This is an elementary example but should give you an idea of how easy it is to use Eloquent.
The great thing about Laravel, in general, is how flexible the framework is, for example, if you do not want to declare a model class, you could actually just use the database facade directly:
use Illuminate\Support\Facades\DB;
DB::table("products")->where("price", ">=", 100)
->skip(5)
->take(10)
->get()
You can also just use regular old RAW SQL as follows:
$products = DB::select("select statement here")
Django ORM
Django has a similar concept of models, however, you have to explicitly declare each field:
import os
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=255)
price = DecimalField(max_digits = 5, decimal_places = 2)
visible = models.BooleanField(default=False)
def __str__(self):
return self.name
Similar to Eloquent, the Django ORM will automatically look for a table named "products".
I constantly forget the fields, especially on larger tables, thus I kinda like this way of doing things since my editor will now have table field definitions to provide IntelliSense suggestions.
To query the Django ORM is a bit weird compared to regular SQL, but because Python is so easy to read, it's not that bad at all.
Product.objects.filter(price__gte=100)[5:15]
To perform raw queries you can use the ".raw" instead of "filter":
query = """SELECT * FROM myapp_product WHERE price >= %s
ORDER BY price LIMIT 10 OFFSET 5"""
products = Product.objects.raw(query, [100])
You can also use the connection cursor directly for more advanced queries:
from django.db import connection
def get_obj_using_raw_sql(sql, binds):
with connection.cursor() as cursor:
cursor.execute("sql here", binds)
row = cursor.fetchone()
return row
Migrations
Both frameworks provide great tooling for migrations, in Laravel you have to explicitly write out your migration files:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('users');
}
}
Next, run the following command to apply these migrations against the database:
php artisan migrate
In Django, since you already declared all fields in the "models.py" file, you do not need to explicitly write out a migration. Simply run:
python manage.py makemigrations
python migrate
Every time you update a model, simply repeat the above and Django will automatically update your DB to be in sync with the model.
The verdict on models & migrations
I like Django's model and migration structure, it is such a fluid experience just to declare fields and run two commands versus having to manually write out a migrations file every time.
With Django's ORM, as queries get more advanced, you will need to start using "Q", "annotate", "aggregate" and so forth. It becomes a little cumbersome to work with compared to Eloquent which keeps that more natural "SQL" like feel regardless of how complicated the query gets. Thus, for querying, I prefer Eloquent.
While this is not exactly advanced, it's sufficient to give you an idea:
Product::where("price", ">=", 200)
->orWhere("discount", ">=", "20")
->sum("price");
In Django:
from myapp.models import Product
from django.db.models import Q
from django.db.models import Sum
products = Product.objects.filter(
Q(price__gte=200) | Q(discount__gte=20)
).aggregate(Sum('price'))
At a glance, it's much easier to understand what's happening in the Eloquent code. Either way, both frameworks handle DB queries quite well and provide enough tooling around DB modeling to suit just about any use case.
Templating
In Laravel the main template engine is called Blade. Blade is fairly easy to understand:
@if(x == y)
// do something
@else
// do someting else
@endif
My {{$variable}} name.
@foreach ($users as $user)
<p>This is user {{ $user->id }}</p>
@endforeach
@include("somefolder.some_blade_file")
Templates are stored in "resources/views" and in your controller, you can reference a template using the path relative to the "views" folder:
function doSomething() {
return view("users.index", [$user => $this->user]);
// Looks for the template in: resources/views/users/index.blade.php
}
Laravel supports both regular PHP files and blade templates, you can use them interchangeably, therefore if you place a file called "index.php" in the user's folder instead, the above code will work just fine as well.
Furthermore, you can also embed PHP code in Blade as well:
@php $x = 1; @endphp
// or
<?php $x = 1;?>
Blade is quite powerful and flexible, in fact, I think it's probably the best template engine I have ever worked with! You can Learn more here, the official Laravel docs are always an excellent resource as well.
Besides Blade, Laravel also supports Intertia, which allows you to write your templates in React and other front-end frameworks.
If you don't want to deal with JS/TypeScript but want interactivity in your views, Blade also can be paired with Livewire. Livewire allows you to build React-like views using just PHP, absolutely powerful and easy to use as well!
Finally, Blade can also be extended using custom "Directives" as follows:
// In class: App\Providers\AppServiceProvider.php
Blade::directive('printFriendlyPostDate', function ($pub_date) {
return "<?php echo date('j F Y', strtotime($pub_date)); ?>";
});
// Use in your template
@printFriendlyPostDate($post->pub_date)
Django templating looks very similar to Blade:
{% if user.is_authenticated %}
Hello, {{ user.username }}.
{% endif %}
Hi, My name is {{user.name}}
<ul>
{% for link in links_list %}
<li><a href="{{link.url}}">{{link.name}}</a></i>
{% endfor %}
</ul>
The major difference between the two is that there is no default convention as to where you should put templates, usually, Django developers place templates in a directory called "templates/" but this is configurable.
To be fair, you can also change Blade's default templates directory, but nobody ever does.
In Django to render a template:
def index(request):
users = Users.objects.order_by("-register_date")[:5]
context = {"users": users}
return render(request, "users/index.html", context)
Notice, Django is much more strict, you have to stick to the template syntax and cannot just use regular Python, however, there is a concept of "template tags" which allow you to write your own custom template functions.
Template tags are similar to the Blade directives mentioned above:
# Place in users/templatetags/custom_tags.py
from django import template
register = template.Library()
@register.filter(name='lower_me')
def lower_me(value):
return value.lower()
# In your template
templates/users/index.html
{% load custom_tags %}
Hi {{name | lower_me}}
Template tags should be placed in a folder called "templatetags" inside your app folder.
While inertia is optimized for Laravel, it's possible to also use this third-party open-source adapter to get inertia support in Django. I haven't used it myself so can't say if it is any good, seems very basic compared to the Laravel integration.
If you looking for an alternative to Livewire, there is Django Unicorn. I briefly played around with it, but it lacks a lot of features and is very basic, so probably better just to use HTMLX or React.
Either way, Django's support for these kinds of frontend templating is not great, however, Django templates are super powerful and you can always manually add your own integration with React or Vue or any other frontend stack.
Django is strict!
Both frameworks are very opinionated, thus there is always the "Laravel way" or "Django way" of doing things.
Django however is a tad bit more strict and rigid. One example is accessing Auth information:
Auth::user()
In Laravel, you can access this Facade just about anywhere in your code, not just in controllers. In Django on the other hand, you have to pass the "request" object down the line manually otherwise other modules in your code will not have access to the user information.
Generally, facades work in this fashion, where you can simply just import the facade anywhere and access any information that the facade has access to.
This flexibility is great! but not always a good thing when it comes to clean code and even security, so Django's rigidity is probably a blessing in disguise. I have seen some weird projects butchering Laravel, whereas the average Django project seems much cleaner and well-structured.
Background jobs
Both frameworks provide a mechanism for handling background jobs, however, Django is a bit more complicated since you have to set up celery and do some configuration in your app to set up queues.
There are too many steps to cover in this article, but you can read further about how to set up Celery here.
In Laravel to create a background job:
php artisan make:job SendEmails
This adds a new class to "app/Jobs/SendEmails.php". Anytime you need to queue a job, simply invoke the class as follows:
SendEmails::dispatch($some_object_or_data);
// OR to push on any other queue besides the default one:
SendEmails::dispatch($some_object_or_data)->onQueue("myqueue");
This will now store jobs in Redis or your SQL DB and process them in the background (via a "default" queue). You will just need to set up one daemon process:
php artisan queue:work
// you can pass --name to specify which queue if you have more than one.
// Or you can use Horizon if you have multiple queues
// Install horizon first before running:
// --> composer require laravel/horizon
// --> php artisan horizon:install
php artisan horizon
Horizon is an optional tool you can install, it basically gives you a metrics dashboard and will automatically run and manage all your queues instead of manually setting up a daemon for each queue.
Routing
Both frameworks provide a similar routing mechanism, in Django routes are maintained in a "urls.py" file inside the project folder:
from django.urls import path, include
from accounts import views
urlpatterns = [
path("user/profile/", views.my_account),
path("user/<int:id>/", views.view_user),
path("users/", views.user_list),
# Group all these URLs with the "admin" prefix
path('admin/', include('some_app_level_urls.admin_urls')),
# Named routes
path('dashboard/', views.dashboard, name="dashboard")
]
Learn more about Django URLs here.
In Laravel, there are "routes/web.php" and "routes/api.php" :
Route::get('/search', 'SearchController@index');
# Named routes
Route::post('/searchTerm', 'SearchController@save')
->name("save_search_term");
# Group routes with a prefix
Route::prefix('stores')->group(function() {
Route::get('/{id}/show', 'StoresController@show');
Route::get('/{id}/products', 'StoresController@products');
});
Learn more about Laravel routes here. Both routers are fairly flexible and easy to use, Laravel does have a little edge since the syntax is declarative using HTTP verbs and you can easily build routes by chaining methods "prefix", "group", "middleware" and more.
Conclusion
Django in terms of design, clean code, and using the HMVT pattern is probably the better-structured framework, plus Python is really easy to work with, easy to learn, and has a vast ecosystem of libraries for just about anything.
Laravel is not just a framework, it’s an entire eco-system with loads of free and paid first-party packages such as Reverb, Pulse, Horizon, Jetstream, Herd, etc…
Thus Laravel’s ecosystem is more complete and will get you up and running fast regardless of the task at hand. Django on the other hand does have batteries included but these are limited and move slowly.
An example is the Django admin, it still looks like an admin panel from the early 2000s and is a bit cumbersome to customize, Laravel on the other hand has many options to choose from and they each look modern with dark theme support and tailwind.
Furthermore, PHP is still one of the most dominant web languages around and constantly evolving to keep up with modern standards.
All in all, both frameworks are a solid choice. If you prefer the “C” style syntax, go with Laravel, if you prefer Python then Django is your best bet.
If you need modern TypeScript and JS framework support, Laravel is probably the better option with Intertia JS and much better tooling, including Livewire.