Python Django Full Stack Simply Explained (with Diagrams and Real Code)

Python Django Fullstack: The Essentials in One Article — Real Code, Diagrams and Concrete Steps, Excerpts from a 24-Lesson Course.

Python Django Full Stack Simply Explained (with Diagrams and Real Code)

A guide that gets straight to the point: Python Django Fullstack broken down with diagrams, concrete examples and tested commands. Everything comes from a structured 8-chapter course — here are the highlights.

tl;dr
  • Why Django
  • Models and migrations
  • Views and URLs
  • Templates and forms
  • Admin
~$ cat ./parcours.md # Python Django Fullstack — 8 chapters
01
Why Django
→ Django vs Flask vs FastAPI→ Installation et premier projet+ 1 more lessons
02
Models and migrations
→ Models et fields→ Relations+ 1 more lessons
03
Views and URLs
→ Function-based views→ Class-based views (CBV)+ 1 more lessons
04
Templates and forms
→ Template language→ Forms et ModelForms+ 1 more lessons
05
Admin
→ Customizer ModelAdmin→ Actions et filters custom+ 1 more lessons
06
Authentication
→ User model et auth views→ Custom User et Profile+ 1 more lessons
07
Deployment
→ Settings prod/dev→ Gunicorn + Nginx+ 1 more lessons
08
Final project blog ecom
→ Chapter 07 – Final project : Models and admin→ Chapter 07 – Final project : Views and templates+ 1 more lessons
🏁
Final project
→ You leave with a concrete and demonstrable project

Migrations

Basic cycle

output
# 1. Modify a model
class Article(models.Model):
    title = models.CharField(max_length=200)
    summary = models.TextField(blank=True)         # NEW

# 2. Generate the migration
python manage.py makemigrations
# Migrations for 'blog':
#   blog/migrations/0002_article_summary.py

# 3. Apply
python manage.py migrate

# 4. Verify
python manage.py showmigrations

View the generated SQL

output
python manage.py sqlmigrate blog 0002

# Output :
# BEGIN;
# ALTER TABLE blog_article ADD COLUMN summary TEXT NOT NULL DEFAULT '';
# COMMIT;

Revert a migration

output
python manage.py migrate blog 0001
# Revert to 0001 (undoes 0002)

python manage.py migrate blog zero
# Cancel everything for the blog app

Data migration

output
python manage.py makemigrations --empty --name backfill_slugs blog

# Edit the generated file:
from django.db import migrations
from django.utils.text import slugify

def backfill_slugs(apps, schema_editor):
    Article = apps.get_model("blog", "Article")
    for a in Article.objects.all():
        if not a.slug:
            a.slug = slugify(a.title)
            a.save()

def reverse_func(apps, schema_editor):
    pass

class Migration(migrations.Migration):
    dependencies = [("blog", "0001_initial")]
    operations = [migrations.RunPython(backfill_slugs, reverse_func)]

Squashing migrations

output
# After many migrations, compact them:
python manage.py squashmigrations blog 0001 0020

# Creates 0001_squashed_0020 that replaces the 20 files
# New developers avoid running 20 migrations

Migrations without downtime

NOTETo add a NOT NULL column without breaking production:
  1. Deploy 1: Add nullable column
  2. Deploy 2: Backfill via data migration
  3. Deploy 3: Update code to use/set the column
  4. Deploy 4: ALTER NOT NULL migration

Rename a model

output
# If you rename Article -> Post, Django will suggest
# creating a new model + deleting the old one (data loss!)

# Solution: edit the generated migration
operations = [
    migrations.RenameModel("Article", "Post")
]

# Same for fields
operations = [
    migrations.RenameField("Post", "summary", "excerpt")
]

Django vs Flask vs FastAPI

NOTEGoal — Understand Django's philosophy and when to choose it.

Quick comparison

CriterionDjangoFlaskFastAPI
PhilosophyBatteries includedMicro frameworkAPI first, async
ORMBuilt-inExternal SQLAlchemyExternal SQLAlchemy
AdminAuto-generatedMust be codedMust be coded
AuthCompleteExtensionsJWT must be coded
TemplatesDjango templatesJinja2Jinja2 optional
Native asyncPartial (4.x)NoYes
Learning curveLongerVery simpleMedium

When to choose Django

When NOT to choose Django

MTV architecture

output
+----------+   request    +------+   query   +-------+
|  Browser | -----------> | View | --------> | Model |
+----------+              +------+           +-------+
     ^                       |                   |
     |                       v                   v
     |                  +----------+         +-------+
     +----------------- | Template |<--------|  DB   |
                        +----------+         +-------+

# M = Model (ORM, database)
# T = Template (HTML)
# V = View (control logic, equivalent to Controller in MVC)

Django ecosystem

Versions and LTS

NOTEDjango releases:
  • Django 5.0+ : current
  • Django 4.2 LTS : supported until 2026
  • Cycle: new version every 8 months
  • LTS every 2 years, supported for 3 years

Companies using Django

Summary

NOTEKey takeaways
  • Django = batteries included for complex web apps
  • MTV architecture (Model, Template, View)
  • Ideal for: SaaS, CMS, e-commerce, dashboards
  • Pure API: prefer DRF or FastAPI

Chapter 07 – Final project: Models and admin

Requirements

NOTEGoal — Build a minimalist online store with: catalog, cart, orders, business admin.

Project structure

output
myshop/
  manage.py
  myshop/
    settings/
    urls.py
    wsgi.py
  accounts/
    models.py            # Custom User
    views.py
    urls.py
  catalog/
    models.py            # Category, Product
    views.py
    admin.py
  cart/
    cart.py              # Cart class (session)
    views.py
  orders/
    models.py            # Order, OrderItem
    views.py
    admin.py
  templates/
  static/
  media/

accounts/models.py

output
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    phone = models.CharField(max_length=20, blank=True)
    address = models.TextField(blank=True)
    city = models.CharField(max_length=100, blank=True)
    postal_code = models.CharField(max_length=10, blank=True)
    country = models.CharField(max_length=2, default="CA")
    
    class Meta:
        ordering = ["-date_joined"]
    
    def __str__(self):
        return self.username

catalog/models.py

output
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=120, unique=True)
    description = models.TextField(blank=True)
    parent = models.ForeignKey("self", null=True, blank=True,
                                on_delete=models.CASCADE, related_name="children")
    
    class Meta:
        verbose_name_plural = "Categories"
        ordering = ["name"]
    
    def __str__(self): return self.name
    
    def get_absolute_url(self):
        return reverse("catalog:category", kwargs={"slug": self.slug})

class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products")
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    image = models.ImageField(upload_to="products/%Y/%m/")
    stock = models.PositiveIntegerField(default=0)
    available = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["slug"]),
            models.Index(fields=["available", "-created_at"]),
        ]
    
    def __str__(self): return self.name
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)
    
    def get_absolute_url(self):
        return reverse("catalog:product", kwargs={"slug": self.slug})
    
    @property
    def in_stock(self):
        return self.available and self.stock > 0

orders/models.py

output
class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        PAID = "paid", "Paid"
        SHIPPED = "shipped", "Shipped"
        DELIVERED = "delivered", "Delivered"
        CANCELED = "canceled", "Canceled"
    
    user = models.ForeignKey("accounts.User", on_delete=models.PROTECT, related_name="orders")
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    address = models.TextField()
    city = models.CharField(max_length=100)
    postal_code = models.CharField(max_length=10)
    country = models.CharField(max_length=2)
    total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    paid_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ["-created_at"]
    
    def __str__(self): return f"Order #{self.id}"

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
    product = models.ForeignKey("catalog.Product", on_delete=models.PROTECT)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)
    
    @property
    def subtotal(self): return self.price * self.quantity

catalog/admin.py

output
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ["name", "parent", "product_count"]
    prepopulated_fields = {"slug": ("name",)}
    search_fields = ["name"]
    
    def product_count(self, obj):
        return obj.products.count()

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ["name", "category", "price", "stock", "available"]
    list_filter = ["available", "category", "created_at"]
    list_editable = ["price", "stock", "available"]
    search_fields = ["name", "description"]
    prepopulated_fields = {"slug": ("name",)}
    list_per_page = 50

orders/admin.py

output
class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    readonly_fields = ["product", "price", "quantity", "subtotal"]
    can_delete = False

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["id", "user", "status", "total", "created_at", "paid_at"]
    list_filter = ["status", "created_at"]
    search_fields = ["id", "user__username", "user__email"]
    inlines = [OrderItemInline]
    actions = ["mark_shipped"]
    date_hierarchy = "created_at"
    
    @admin.action(description="Mark as shipped")
    def mark_shipped(self, request, queryset):
        queryset.update(status=Order.Status.SHIPPED)
go-further

This article covers the most useful excerpts — the complete Python Django Fullstack course (8 chapters, 24 lessons, corrected exercises and final project) takes you all the way.

./access-the-full-course free course: Vibe Coding

FAQ

How long does it take to learn Python Django Fullstack?
With a structured progression (8 chapters, 24 short practical lessons), you reach an operational level in a few weeks at 30–60 minutes per day. The key is to practice each concept immediately.
Are there any prerequisites?
Basic computer knowledge is enough. If you can use a terminal and read simple code, you're ready.
Where to start concretely?
Reproduce the commands in this article, then follow the complete Python Django Fullstack course: it chains the 24 lessons in order, with exercises and a final project.

📬 Want to receive this type of guide every week? Subscribe for free — real code, zero fluff.