Chapter 5: Django 404 (page not found)
What Actually Happens When Django Returns 404?
- User requests a URL (e.g. /polls/this-question-does-not-exist/)
- Django checks all patterns in urls.py (project + included apps)
- No match → Django raises Http404 exception internally
- Django catches it and looks for a custom 404 handler
- If nothing custom → shows default “Not Found” debug page (DEBUG=True) or plain white “404 Not Found” (DEBUG=False)
Goal today: Replace both the ugly debug page and the boring server default with your own beautiful, branded 404 page.
Step 1: Create the 404 Template (Simplest & Most Common Way)
Django automatically looks for a file called 404.html in these locations (in order):
- Folders in TEMPLATES[‘DIRS’] (your project-level templates/ folder)
- Each app’s templates/ subfolder
Recommended place: project-level templates/404.html
Create it:
|
0 1 2 3 4 5 6 7 |
# if not already there mkdir templates |
Now mysite/templates/404.html:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
{% extends "base.html" %} {% block title %}Page Not Found – 404{% endblock %} {% block content %} <div style="text-align: center; padding: 6rem 1rem 8rem; max-width: 700px; margin: 0 auto;"> <h1 style="font-size: 6rem; margin: 0; color: var(--primary); opacity: 0.9;"> 404 </h1> <h2 style="font-size: 2.2rem; margin: 1rem 0 1.5rem; color: #444;"> Oops! Page Not Found </h2> <p style="font-size: 1.2rem; color: var(--gray); margin-bottom: 2.5rem; line-height: 1.7;"> The page you're looking for might have been removed, had its name changed, or is temporarily unavailable.<br> Maybe you mistyped the URL? Or maybe this poll got too controversial and we had to hide it 😅 </p> <div style="margin: 2rem 0;"> <a href="{% url 'pages:home' %}" class="btn" style="font-size: 1.2rem; padding: 1rem 2.5rem;"> Go Back to Home </a> <a href="{% url 'polls:index' %}" class="btn" style="margin-left: 1.5rem; background: #444; padding: 1rem 2.5rem;"> Browse All Polls </a> </div> <p style="font-size: 0.95rem; color: var(--gray); margin-top: 4rem;"> If you think this is a mistake, feel free to <a href="mailto:support@yourdomain.com">contact us</a>. </p> </div> {% endblock %} |
- Extends your base.html → keeps header, nav, footer consistent
- Friendly tone (Hyderabad style 😄)
- Clear call-to-actions (home + polls list)
Step 2: Make Sure Django Finds It (settings.py)
In mysite/settings.py → make sure:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # ← must include this! 'APP_DIRS': True, ... }, ] |
If ‘DIRS’ is empty → Django won’t look in project-level templates/
Step 3: Test It (Two Ways)
Way A – DEBUG = True (development)
Just visit a wrong URL:
http://127.0.0.1:8000/polls/non-existing-slug/
→ You should see your custom 404.html instead of the yellow Django debug page
Way B – DEBUG = False (production simulation)
Temporarily set in settings.py:
|
0 1 2 3 4 5 6 7 |
DEBUG = False ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] |
Visit same wrong URL → should show your 404 page (not the plain “Not Found” text)
Important: After testing → set DEBUG = True back!
Step 4: Bonus – Custom 404 View (When You Want Logic)
Most projects don’t need this — the template is enough.
But if you want (e.g. log 404s, show suggested polls, etc.):
In mysite/views.py (create file if missing):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
from django.shortcuts import render def custom_404(request, exception): return render(request, '404.html', { 'page_title': '404 – Page Not Found', 'suggested_polls': [...] # optional – query some popular polls }, status=404) |
In mysite/urls.py (at the very bottom):
|
0 1 2 3 4 5 6 |
handler404 = 'mysite.views.custom_404' |
Now Django calls your view instead of auto-template lookup.
When to use custom view:
- Want to pass extra context (suggested articles, search form…)
- Log 404s to analytics/sentry
- Show different 404 for logged-in vs anonymous
Most people (including me in 2026): just use 404.html template — simpler.
Step 5: Also Prepare 500.html (Server Error)
Create templates/500.html — same style:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{% extends "base.html" %} {% block title %}Server Error – 500{% endblock %} {% block content %} <div style="text-align: center; padding: 6rem 1rem;"> <h1 style="font-size: 5rem; color: #dc3545;">500</h1> <h2>Oops… Something Broke on Our Side</h2> <p style="font-size: 1.2rem; color: #555; margin: 2rem 0;"> Our team has been notified. Please try again later.<br> Sorry for the inconvenience! </p> <a href="{% url 'pages:home' %}" class="btn">Back to Home</a> </div> {% endblock %} |
Django auto-uses 500.html when DEBUG=False and a server error occurs.
Quick Checklist – Did You Do It Right?
- templates/404.html exists
- TEMPLATES[‘DIRS’] includes BASE_DIR / ‘templates’
- Extends base.html (keeps navigation & design)
- Tested with wrong URL → custom page shows
- Tested with DEBUG=False → still looks good
Common “Why Is It Not Working?” Moments
| Symptom | Likely Cause | Fix |
|---|---|---|
| Still see yellow debug page | DEBUG = True (normal in dev) | Nothing — debug page only shows when DEBUG=True |
| Blank or default “Not Found” | DEBUG = False + no 404.html | Create templates/404.html |
| TemplateDoesNotExist: 404.html | Wrong folder or missing in DIRS | Check path & settings |
| 404 page shows but no style / header | Forgot {% extends “base.html” %} | Add extends at the very top |
| Custom view not called | handler404 not set in urls.py | Add handler404 = ‘mysite.views.custom_404’ |
Your Quick Task Right Now
- Create templates/404.html as shown
- Visit a nonsense URL (e.g. /polls/iamnotreal/ or /random-page/)
- Confirm your nice branded 404 appears
- (Optional) Temporarily set DEBUG=False, test again, revert back
Tell me what’s next:
- “It works! Now show me custom 500 page + error logging”
- “How to make 404 suggest similar polls or search?”
- “I want funny 404 messages that change randomly”
- “Got wrong page / still debug screen – here’s what I see”
- Or finally ready for: “Let’s finish voting – POST form + F() increment”
You’ve just made your app much more user-friendly — small detail, big difference. You’re doing really well — let’s keep going! 🚀🇮🇳
