Django is a high-level Python web framework designed to encourage rapid development and clean, pragmatic design. It follows the Model-View-Template (MVT) architectural pattern and comes with batteries included--ORM, admin interface, authentication, and much more.
So Django is the Python world's equivalent to what PHP developers enjoy with Laravel or WordPress--but faster, more secure, and highly scalable.
Installation is so similar on both Windows and Linux environments that this article will just show you how on windows.
Install Python
Create a virtual environment
python -m venv myenv
myenv\Scripts\activate
Note: A virtual environment (often created via python -m venv myenv or tools like venv / virtualenv) is an isolated Python environment with its own installation directories for Python packages (i.e. site-packages), independent from the "global" Python installation on your system.
When you "activate" the virtual environment, commands like pip install ... install packages into that environment, and python resolves to the environment's interpreter (or wrapper) rather than the system-wide one.
While you do not have to create a virtual environment it is recommended so that dependancies do not conflict.
Install Django
pip install django
OR
python3 -m pip install django
Create your first project
#change directory to where you want to start
d:
mkdir django
cd d:\django
#run django-admin to create a project
django-admin startproject mysite
#that created the following structure
mysite/
├─ manage.py
└─ mysite/
├─ __init__.py
├─ asgi.py
├─ settings.py
├─ urls.py
└─ wsgi.py
#switch to the mysite directory
cd mysite
#start the server - this will create a db.sqlite3 database and start the server on port 8000
python manage.py runserver
#OR to run on a different port just add the port at the end like this
python manage.py runserver 8080
#OR if you want the site to be available to others on your network do the following but note this is just a development server - not intended for production use.
python manage.py runserver 0.0.0.0:8000
#Note: to see all options available with runserver do this:
python manage.py help runserver
Visit http://127.0.0.1:8000/ or http://localhost:8000/ to see your new Django site running!
Django is now installed and running so let's create an blog app inside your project.
Stop the server with Ctrl+C.
python manage.py startapp blog
Add blog to INSTALLED_APPS in mysite/settings.py:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog",
]
Open blog/models.py and add a simple Post model with a slug for clean URLs and a status to allow drafts.
from django.db import models
from django.utils import timezone
from django.urls import reverse
class Post(models.Model):
DRAFT = "draft"
PUBLISHED = "published"
STATUS_CHOICES = [
(DRAFT, "Draft"),
(PUBLISHED, "Published"),
]
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True, help_text="URL-friendly unique identifier")
author = models.CharField(max_length=100)
body = models.TextField()
created = models.DateTimeField(default=timezone.now, editable=False)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=PUBLISHED)
class Meta:
ordering = ["-created"] # newest first
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("blog:post_detail", kwargs={"slug": self.slug})
Create and apply migrations:
python manage.py makemigrations
python manage.py migrate
Create a superuser so you can log in:
python manage.py createsuperuser
# Follow prompts for username, email (optional), and password
Register the model in blog/admin.py:
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "status", "author", "created", "updated")
list_filter = ("status", "created", "updated")
search_fields = ("title", "body", "author")
prepopulated_fields = {"slug": ("title",)}
ordering = ("-created",)
Run the server and visit http://127.0.0.1:8000/admin/, then add some posts.
python manage.py runserver
Inside the blog app, create a urls.py and define routes:
blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.PostListView.as_view(), name="post_list"),
path("<slug:slug>/", views.PostDetailView.as_view(), name="post_detail"),
]
Include the app URLs in the project-level urls (mysite/urls.py):
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls", namespace="blog")),
]
Use Django's generic class-based views in blog/views.py:
from django.views.generic import ListView, DetailView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 5 # optional pagination
def get_queryset(self):
# Show only published posts, newest first (ordering defined in Meta)
return Post.objects.filter(status=Post.PUBLISHED)
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
context_object_name = "post"
slug_field = "slug"
slug_url_kwarg = "slug"
Create a templates directory and tell Django where to find it. Make a templates folder at the project root (mysite/templates) and update mysite/settings.py:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"], # add this
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
Base template templates/base.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}My Django Blog{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
</head>
<body>
<header>
<h1><a href="{% url 'blog:post_list' %}">My Django Blog</a></h1>
<nav>
<a href="{% url 'blog:post_list' %}">Home</a>
<a href="/admin/">Admin</a>
</nav>
<hr>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<hr>
<p>© {{ now|date:"Y" }} My Django Blog</p>
</footer>
</body>
</html>
List template templates/blog/post_list.html:
{% extends "base.html" %}
{% load static %}
{% block title %}Blog -- Posts{% endblock %}
{% block content %}
{% for post in posts %}
<article>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p><small>By {{ post.author }} • {{ post.created|date:"M j, Y" }}</small></p>
<p>{{ post.body|truncatechars:200 }}</p>
<p><a href="{{ post.get_absolute_url }}">Read more →</a></p>
<hr>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{# Pagination controls #}
{% if is_paginated %}
<nav aria-label="Pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">← Newer</a>
{% endif %}
<span>Page {{ page_obj.number }} of {{ paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Older →</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}
Detail template templates/blog/post_detail.html:
{% extends "base.html" %}
{% block title %}{{ post.title }} -- Blog{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<p><small>By {{ post.author }} • {{ post.created|date:"M j, Y" }}</small></p>
<div>{{ post.body|linebreaks }}</div>
</article>
<p><a href="{% url 'blog:post_list' %}">← Back to all posts</a></p>
{% endblock %}
Enable static files in mysite/settings.py (Django already includes django.contrib.staticfiles in INSTALLED_APPS).
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
Create static/css/styles.css:
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; max-width: 780px; margin: 2rem auto; padding: 0 1rem; }
header h1 a { text-decoration: none; }
nav a { margin-right: .75rem; }
article h2 { margin-bottom: .25rem; }
hr { border: none; border-top: 1px solid #ddd; margin: 1.5rem 0; }
Collectstatic is not needed for local dev, but for production you'll run python manage.py collectstatic.
Post entries.status is Published (not Draft).If you want a simple comment section, add this model to blog/models.py:
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
name = models.CharField(max_length=80)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created"]
def __str__(self):
return f"Comment by {self.name}"
Migrate and register in admin:
python manage.py makemigrations
python manage.py migrate
blog/admin.py:
from .models import Post, Comment
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "status", "author", "created", "updated")
list_filter = ("status", "created", "updated")
search_fields = ("title", "body", "author")
prepopulated_fields = {"slug": ("title",)}
inlines = [CommentInline]
To show comments in the detail page, update templates/blog/post_detail.html:
{% extends "base.html" %}
{% block title %}{{ post.title }} -- Blog{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<p><small>By {{ post.author }} • {{ post.created|date:"M j, Y" }}</small></p>
<div>{{ post.body|linebreaks }}</div>
</article>
<section>
<h2>Comments</h2>
{% if post.comments.all %}
<ul>
{% for c in post.comments.all %}
<li>
<p><strong>{{ c.name }}</strong> -- <small>{{ c.created|date:"M j, Y, g:i a" }}</small></p>
<p>{{ c.body|linebreaks }}</p>
</li>
{% empty %}
<li>No comments yet.</li>
{% endfor %}
</ul>
{% endif %}
</section>
<p><a href="{% url 'blog:post_list' %}">← Back to all posts</a></p>
{% endblock %}
For a full comment form (create via POST), you would add a
ModelForm, aCreateView/view function, and CSRF-protected form template. The inline admin shown here is the fastest route to a working demo.
Create templates/404.html and templates/500.html to customize error pages (shown when DEBUG = False).
<!-- templates/404.html -->
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block content %}
<h1>Sorry, we couldn't find that.</h1>
<p><a href="{% url 'blog:post_list' %}">Back home</a></p>
{% endblock %}
python manage.py runserver
Open:
Smoke test checklist:
Published).Tag model or django-taggit)DEBUG = False, configure ALLOWED_HOSTS, and serve static files with WhiteNoise or via nginx.mysite/
├─ manage.py
├─ mysite/
│ ├─ __init__.py
│ ├─ asgi.py
│ ├─ settings.py
│ ├─ urls.py
│ └─ wsgi.py
├─ blog/
│ ├─ __init__.py
│ ├─ admin.py
│ ├─ apps.py
│ ├─ migrations/
│ │ └─ 0001_initial.py
│ ├─ models.py
│ ├─ tests.py
│ ├─ urls.py
│ └─ views.py
├─ templates/
│ ├─ base.html
│ └─ blog/
│ ├─ post_detail.html
│ └─ post_list.html
└─ static/
└─ css/
└─ styles.css
You now have a working Django blog you can extend as you like. Add posts in the admin and enjoy your new site!