Learn Django the Right Way β From Basics to Advanced
Build a strong foundation by understanding how Django processes every request. Explore URLs, views, templates, models, middleware, authentication, ORM, REST APIs, deployment, and everything you need to become a confident Django developer.
π
Last Updated
May 2026
β±οΈ
Read Time
17 min
π―
Level
Beginner β Intermediate
What Is Django, Beyond "a Python Web Framework"
Django is a high-level Python web framework built by a team at the Lawrence Journal-World newspaper in Lawrence, Kansas, and released as open source in July 2005. It was named after jazz guitarist Django Reinhardt. The framework was born under newsroom deadline pressure β journalists needed new features shipped in hours, not weeks β and that origin explains almost everything about Django's design philosophy: it is deliberately opinionated and comes with far more built in than a minimal framework like Flask.
Django calls itself "the web framework for perfectionists with deadlines," and the phrase that matters more for understanding its architecture is "batteries included." Out of the box you get an ORM, an admin interface, a user authentication system, a templating engine, form handling with built-in CSRF protection, and a migration system β all pre-integrated, not bolted on separately. Flask and FastAPI give you a thin core and expect you to pick your own ORM, auth library, and admin tooling; Django hands you an opinionated, working default for all of it.
That trade-off β less initial flexibility in exchange for far less setup decision-making β is the single thing to understand before writing a line of Django code. Everything else in this guide is really just consequences of that one choice.
MVT, Not MVC β And the Difference Actually Matters
Django's own documentation calls its architecture MVT (Model-View-Template) rather than the more familiar MVC. The naming isn't just marketing β the roles genuinely map differently. In Django, the Model defines your data (same idea as MVC). The Template is what MVC calls the View β the HTML rendering layer. And Django's View is actually what MVC calls the Controller β the Python function or class that receives a request and decides what data to fetch and which template to render.
Django Term
Traditional MVC Equivalent
What It Actually Does
Model
Model
Defines database schema as Python classes; Django's ORM turns them into SQL
Template
View
HTML files with Django Template Language tags β the presentation layer
View
Controller
Python function/class that processes a request and returns a response
URL dispatcher
Router
urls.py maps incoming URL patterns to specific view functions
If you've read Rails or Laravel tutorials before touching Django, this naming mismatch is exactly why a Django "View" felt backwards at first β it isn't the HTML-rendering piece at all, it's the logic layer that decides what to render.
The Full Journey: URL Typed β Page Rendered
This is the part that actually explains most "why isn't my view running" and "why is this data stale" confusion. A single browser request passes through a specific, ordered pipeline before HTML ever comes back.
π Browser RequestGET /products/42/
TCP connection
π¦ WSGI/ASGI ServerGunicorn/Uvicorn hands off to Django
request object built
π§± Middleware Stack (inbound)Security, Session, CSRF, Auth β in order
passed inbound
πΊοΈ URL Resolverurls.py matches pattern β view function
match found
βοΈ View FunctionQueries the ORM, builds context dict
query needed
ποΈ Model / ORM LayerProduct.objects.get(id=42) β SQL
queryset returned
π Template EngineRenders context into HTML via product.html
HttpResponse built
π§± Middleware Stack (outbound)Response headers, session cookie set
outbound processing
π¨οΈ HTTP ResponseRendered HTML sent to browser
Code Execution Flow β from source to output
Middleware order in settings.py's MIDDLEWARE list genuinely matters and is a common source of confusing bugs: it runs top-to-bottom on the way in, then bottom-to-top on the way out. Put AuthenticationMiddleware before something that depends on request.user existing, and it works fine. Get the order wrong, and you'll hit an AttributeError: 'WSGIRequest' object has no attribute 'user' that has nothing to do with your view code at all β the actual bug is three lines away in a settings file most beginners never think to check.
The ORM's Best Feature Is Also Its Most Common Trap
Django's ORM lets you write Book.objects.filter(author__country="India") instead of raw SQL, and it's genuinely one of the better ORMs in any language β readable, chainable, and it generates reasonably efficient SQL by default. The trap almost every intermediate Django developer hits at least once is the N+1 query problem, and it's sneaky specifically because the code that causes it looks completely innocent.
π Python (views.py)orders_list_n_plus_one.py
# Looks fine. Runs 1 query for the orders...
orders = Order.objects.filter(status='pending')
for order in orders:
# ...but THIS line runs a fresh query for EVERY single order,
# because order.customer wasn't fetched upfront.
# 50 pending orders = 1 + 50 = 51 queries for one page load.
print(order.customer.email)
π Python (views.py)orders_list_fixed.py
# select_related() does a SQL JOIN upfront for foreign-key relations,
# fetching customer data in the SAME query as the orders.
orders = Order.objects.filter(status='pending').select_related('customer')
for order in orders:
print(order.customer.email) # No extra query β already loaded
# For many-to-many or reverse foreign-key relations, use prefetch_related()
# instead β select_related() only works for forward FK/OneToOne fields.
This isn't a rare edge case β it's specifically the kind of bug that passes code review, works fine on your laptop with 8 test rows in the database, and then quietly turns a dashboard page from a 40ms response into an 1,800ms response once a client has a few thousand real orders. Django's debug toolbar (django-debug-toolbar) shows exact query counts per page and is worth installing before you need it, not after a production slowdown ticket.
Migrations β Why Django Won't Let You Just Edit the Database
When you change a model β add a field, rename a column, add an index β Django doesn't touch the database directly. It generates a migration file, a Python script describing exactly how to transform the schema, via python manage.py makemigrations. You then apply it with python manage.py migrate. This two-step separation exists so schema changes are version-controlled, reviewable in a pull request, and reversible.
The error every Django beginner eventually hits, usually in their first week deploying to a shared staging database, is:
Output
django.db.utils.ProgrammingError: relation "orders_order" does not exist
LINE 1: SELECT ... FROM "orders_order" WHERE ...
^
This nearly always means one thing: you wrote a model and ran the server, but never ran makemigrations and migrate β Django's models exist in Python, but the actual PostgreSQL/MySQL table they describe was never created. The fix is almost always exactly those two commands, in that order, and it's worth checking python manage.py showmigrations first to see which migrations are recorded as applied versus pending before assuming anything more exotic is wrong.
Django vs FastAPI vs Flask β An Honest Comparison
This gets asked in nearly every Indian product-company interview for a backend role, and the honest answer isn't "it depends" dressed up politely β the three frameworks genuinely optimize for different things.
Aspect
Django
FastAPI
Flask
Philosophy
Batteries included, opinionated
Minimal core + async-first, typed
Minimal core, unopinionated
Built-in ORM
Yes (Django ORM)
No β pair with SQLAlchemy/Tortoise
No β pair with SQLAlchemy
Admin panel
Yes, auto-generated from models
No
No
Async support
Since 4.1 (2022), improving each release
Native, async-first from day one
Limited, bolted on
Auto API docs
No (needs Django REST Framework + drf-spectacular)
My honest take: for an internal admin tool or a content-driven product where you'll spend real time in the admin panel managing data, I'd pick Django every time β the auto-generated admin alone saves days of throwaway CRUD-screen work. For a pure JSON API backing a mobile app or a microservice under genuine throughput pressure, FastAPI's native async handling and automatic OpenAPI docs usually win, and Django REST Framework on top of Django starts to feel like reassembling what FastAPI gives you natively. Flask's niche has narrowed the most of the three β it's still excellent for small, focused services, but FastAPI has taken a lot of the "lightweight and modern" ground Flask used to own alone.
Where Django's "Batteries Included" Actually Pays Off
π οΈ
Auto-Generated Admin Panel
Register a model with admin.site.register(Product) and you get a full working CRUD interface β search, filters, inline editing β with zero HTML written. For internal tools and content management, this alone can save a genuine week or two of throwaway front-end work.
ποΈ
The ORM
Model changes become Python objects, not raw SQL strings. Combined with the migration system, schema evolution is trackable in git and reviewable like any other code change.
π
Security Defaults Baked In
CSRF protection, XSS-escaping in templates by default, clickjacking protection via X-Frame-Options, and SQL injection protection through parameterized queries are all on by default β not opt-in extras a busy team might skip.
π€
Built-In Authentication
User model, login/logout views, password hashing (PBKDF2 by default, configurable to Argon2), permission and group systems β ready to use without picking and wiring together a third-party auth library.
π
Forms & Validation
Django Forms handle rendering, server-side validation, and CSRF tokens together, keeping validation logic in one place instead of scattered between client-side JS and ad hoc server checks.
Django encourages splitting a project into self-contained 'apps' (accounts, orders, catalog) that can, in principle, be lifted into another project β a structural discipline many minimal frameworks leave entirely up to the team.
π
Internationalization Out of the Box
Built-in i18n/l10n support with translation files (.po/.mo) and locale-aware formatting is genuinely useful for products targeting multiple Indian language markets without bolting on a separate library.
π¦
Massive, Mature Package Ecosystem
Django REST Framework for APIs, django-allauth for social login, celery integration for background jobs, django-storages for S3/cloud file storage β the ecosystem around Django is unusually mature for a framework its age.
Your First Django View
Unlike a single-file Flask app, a minimal Django project always has at least three moving pieces wired together: a URL pattern, a view function, and the project's urls.py routing table.
π Python (views.py)views.py
from django.http import HttpResponse
def greet_visitor(request):
return HttpResponse("Hello, World!")
π Python (urls.py)urls.py
from django.urls import path
from . import views
urlpatterns = [
path('hello/', views.greet_visitor, name='greet-visitor'),
]
Extremely Fast to Build Standard CRUD AppsModel, admin registration, and a form together get a working create-read-update-delete interface running in under an hour for straightforward data β hard to match without Django's specific combination of pieces.
Security Defaults Reduce Common MistakesCSRF, XSS escaping, and SQL injection protection being on by default means a rushed junior developer under deadline pressure is far less likely to accidentally ship a critical vulnerability than in a bare-metal framework.
The ORM Scales Well With Disciplineselect_related/prefetch_related, database indexes via Meta.indexes, and query annotation cover the vast majority of real-world performance needs without dropping to raw SQL.
Enormous, Stable EcosystemDjango REST Framework alone has effectively become the default way to build REST APIs in the Python world, with a plugin ecosystem built around it spanning a decade-plus.
Excellent DocumentationDjango's official docs are frequently held up as a model for other open-source projects to follow β thorough, example-driven, and kept genuinely current with each release.
β Disadvantages
Monolithic by DefaultDjango nudges you toward a single deployable application. Splitting a Django project into microservices later is more friction than starting with a framework designed API-first from day one.
ORM Abstraction Can Hide Expensive QueriesThe N+1 problem covered earlier is exactly this risk in practice β the ORM's readability can mask what's actually happening at the database level until it becomes a production performance issue.
Async Support Is Still MaturingDjango gained async views in 4.1, but large parts of the ORM remain effectively synchronous under the hood, so a fully async Django stack still has rough edges compared to a framework built async-first.
Steeper Initial Learning Curve Than FlaskUnderstanding the full request-response pipeline, app structure conventions, and the ORM's query API takes longer to absorb than Flask's much smaller surface area.
Can Feel Heavy for Tiny ProjectsSpinning up Django's full project structure for a 50-line internal script or a single-endpoint microservice is often more scaffolding than the task actually needs.
Companies Actually Running Django in Production
βΆ
πΈ Instagram β historically ran (and largely still runs) on Django at extraordinary scale, and has been a major upstream contributor of async-related performance work back to the framework itself.
βΆ
π§ Spotify β uses Django for parts of its internal tooling and backend services, valuing the speed of building reliable internal admin and data-management tools.
βΆ
π§³ Disqus & Bitbucket β both built core parts of their platforms on Django during periods of very rapid user growth, leaning on its batteries-included stability under scale pressure.
βΆ
π¦ Indian Fintech & Edtech Backends β a large share of Series AβC Indian startups (lending platforms, D2C-adjacent SaaS, edtech content platforms) default to Django + DRF for their core backend specifically because a small team can ship a secure, admin-manageable product fast without hiring a dedicated DevOps-heavy platform team early on.
βΆ
ποΈ Government & Public Sector Portalsβ Django's built-in security defaults and mature admin tooling make it a common choice for public-facing government portals and internal case-management systems where auditability and a reviewable admin trail matter as much as raw performance.
Django Interview Questions Companies Actually Ask
The roles map differently than the names suggest: Django's Model matches MVC's Model, but Django's Template corresponds to MVC's View (the rendering layer), and Django's View actually plays the role of MVC's Controller (the logic that decides what data to fetch and how to respond). Django itself handles the URL-routing piece that a traditional MVC framework might leave to the Controller.
It occurs when code fetches a list of objects with one query, then triggers a separate query for each object's related data inside a loop β turning what should be 1-2 queries into N+1. Django's select_related() (for foreign-key/one-to-one relations, using SQL JOINs) and prefetch_related() (for many-to-many and reverse foreign-key relations, using a second batched query) solve this by fetching related data upfront.
makemigrations inspects your models and generates Python files describing the schema changes needed β it doesn't touch the database. migrate actually applies those changes to the configured database. Separating them lets migration files be reviewed in pull requests like any other code change, and lets the same migration be applied consistently across development, staging, and production databases.
Django's CsrfViewMiddleware requires a valid, per-session CSRF token to be submitted with any state-changing POST/PUT/DELETE request. Django Template Language's {% csrf_token %} tag injects this token into forms automatically, and Django rejects requests missing a valid token with a 403 Forbidden response β this protection is on by default for all views unless explicitly exempted.
A project is the overall Django installation β settings, root URL configuration, WSGI/ASGI entry point β for one deployable site. An app is a self-contained module handling one specific piece of functionality (e.g., 'orders', 'accounts', 'catalog'), designed in principle to be reusable across different projects. One project typically contains several apps.
The ORM (Object-Relational Mapper) lets you interact with the database using Python classes and methods instead of raw SQL β a model class maps to a database table, and an instance maps to a row. A QuerySet is a lazy, chainable representation of a database query β Model.objects.filter(...) doesn't hit the database until the QuerySet is actually evaluated (iterated, sliced, or cast to a list), which lets you chain multiple filters efficiently before the final SQL is generated.
Middleware is a chain of components that process every request on the way in and every response on the way out, in the order listed in settings.py's MIDDLEWARE list. A concrete example: AuthenticationMiddleware must run before any middleware or view that reads request.user, since it's the one that actually attaches the user object to the request. Placing it after such a dependency causes an AttributeError at runtime.
A function-based view is a plain Python function taking a request and returning a response β explicit and easy to read for simple logic. A class-based view (like ListView, DetailView, or a custom View subclass) uses inheritance to reuse common patterns (pagination, permission checks, HTTP-method dispatch) across many views, trading some directness for less repeated boilerplate on standard CRUD-style views.
Practice Questions
1. A view runs Order.objects.filter(status='shipped') and then loops over the result printing order.customer.name for each order. Django Debug Toolbar shows 41 queries for 40 orders. What's happening, and how do you fix it?
Medium
β AnswerThis is the N+1 query problem: 1 query fetches the orders, then 1 additional query runs per order to fetch its related customer. Fix it by adding .select_related('customer') to the original queryset, which fetches customer data via a SQL JOIN in the same initial query, collapsing 41 queries down to 1.
2. You add a new field to a model and immediately run the server. It throws django.db.utils.ProgrammingError: relation "blog_post" does not exist. What did you skip?
Easy
β AnswerYou never ran makemigrations and migrate after changing the model. The Python model class was updated, but the actual database table structure it maps to was never created or altered to match β Django's ORM describes the schema, it doesn't apply it automatically on server start.
3. Why does Django hash passwords with PBKDF2 (or Argon2) by default instead of storing them with a simple SHA-256 hash?
Medium
β AnswerSimple fast hashes like SHA-256 are exactly what makes brute-force and rainbow-table attacks feasible β a modern GPU can compute billions of SHA-256 hashes per second. PBKDF2 and Argon2 are deliberately slow, computationally expensive hashing algorithms (with configurable iteration counts/memory cost) specifically designed to make large-scale password-cracking attempts impractically slow, even if a database of hashes leaks.
4. What's the practical difference between get_object_or_404() and Model.objects.get() in a view?
Easy
β AnswerModel.objects.get() raises a DoesNotExist exception if no matching row is found, which β if uncaught β becomes an ugly, unhandled 500 server error for the end user. get_object_or_404() wraps the same lookup and automatically converts that missing-object case into a clean 404 Not Found response, which is almost always the correct user-facing behavior for a 'fetch this specific record' view.
5. A junior developer disables CSRF protection on a POST endpoint using @csrf_exempt because 'it kept failing in Postman.' What's actually going wrong, and is disabling it the right fix?
Hard
β AnswerPostman doesn't automatically carry the CSRF cookie and token the way a real browser session does, so the request fails validation β that's the middleware doing exactly its job. Disabling CSRF protection removes a real defense against cross-site request forgery for actual end users. The correct fix is including the CSRF token in the Postman request (fetch it from the session first) for testing, or, for a genuine external API endpoint, using Django REST Framework's token/session authentication instead of exempting a regular form-based view.
6. Why might grid... β actually, why does Django recommend prefetch_related() instead of select_related() for a ManyToManyField?
Hard
β Answerselect_related() works by generating a SQL JOIN, which only makes sense for relationships that resolve to a single related row per object β ForeignKey and OneToOneField. A ManyToManyField (or reverse ForeignKey) can match multiple rows per object, which a single JOIN can't cleanly represent without duplicating the parent row. prefetch_related() instead runs a second, separate query for the related objects and joins them together in Python, which correctly handles the one-to-many/many-to-many shape.
Is Django the Right Call for Your Next Project?
The question worth asking isn't "is Django good" β at this point, with two decades of production hardening behind it, that's settled. The real question is whether your project's shape matches what Django optimizes for: a data-heavy application where an admin interface, a relational database, and server-rendered or lightly API-driven pages are the core of the product. If that describes what you're building, Django will very likely get you to a secure, working product faster than assembling the equivalent from smaller pieces yourself.
Your Situation
Should You Reach for Django?
Content-heavy site or internal admin tool
β Yes β the admin panel alone is a major head start
Startup MVP needing to ship fast with a small team
β Yes β batteries-included means fewer early decisions
High-throughput async API / real-time service
β οΈ Consider FastAPI β async support is improving but not yet as native
Tiny single-purpose microservice
β οΈ Flask or FastAPI will likely feel lighter
Team already fluent in Python wanting a proven, stable stack
β Yes β Django's stability track record is genuinely strong
Whatever you decide, the request-response pipeline covered earlier β middleware, URL resolution, views, ORM, templates β is the mental model that makes every future Django bug report readable instead of mysterious. Learn that pipeline properly before memorizing shortcuts, and most of what looks like 'Django being unpredictable' later turns out to be one specific, findable step in that chain.
Frequently Asked Questions (FAQ)
Django has more upfront concepts to learn (project structure, the ORM, migrations, the admin), but it also removes a lot of decisions a beginner isn't equipped to make well yet, like which ORM or auth library to pick. Flask teaches you more of the underlying HTTP/routing mechanics directly since there's less abstraction. Neither order is wrong β Django-first suits people who want to ship a real working app quickly; Flask-first suits people who want to understand the plumbing before the framework hides it.
Yes, extensively β Django REST Framework (DRF), built on top of Django, is one of the most widely used ways to build production REST APIs in Python. Many real-world apps use Django + DRF as a pure JSON backend with a completely separate React or Vue frontend, using Django's templating only for the admin panel.
Async views have been supported since Django 4.1 (2022), and each release has continued extending async support further into the ORM and middleware layers. It's genuinely usable for mixed workloads now, but a fully async-first application still tends to feel more natural in a framework like FastAPI that was designed async-native from its first release.
Django ships configured for SQLite by default in a fresh project, purely because it requires zero setup for local development. For any real production deployment, PostgreSQL is the most common and generally recommended choice β Django's ORM supports PostgreSQL-specific features (like ArrayField and JSONField with indexing) that other databases don't offer equivalents for.