In short
Django and Flask are the two dominant Python web frameworks: Django is a "batteries-included" full-stack framework with its own ORM, admin, forms, auth and template engine, while Flask is a microframework that supplies only routing, request handling and Jinja2 templates and leaves the rest to extensions. Both sit behind the WSGI (synchronous) or ASGI (asynchronous) interface, which is how a Python application talks to a web server such as gunicorn or uvicorn behind Nginx. The topic is examined in MCA/BCA "Web Technologies" and "Enterprise Application Development" papers through MVT architecture, request-response flow, ORM and template questions, and is tested in almost every Python backend interview in India. Employers hiring on this stack include Zoho, Freshworks, Zomato, Swiggy, Dream11, Razorpay, Instamojo, government e-governance vendors (NIC, C-DAC projects) and most Bengaluru/Pune/Hyderabad product startups. Django 5.2 is the current long-term-support release (April 2025) and Flask 3.x the current Flask series; releases advance every few months, so always check the installed version with pip show django.
Explained
Where a Python framework sits: WSGI and ASGI
- A browser request reaches Nginx/Apache (static files, TLS, reverse proxy), which forwards dynamic requests to an application server — gunicorn, uWSGI (WSGI) or uvicorn/daphne (ASGI).
- WSGI (PEP 3333) is a synchronous calling convention: the server calls
application(environ, start_response);environis a dict of CGI-style request data, and the app returns an iterable of bytes. - ASGI is the async successor: a coroutine
app(scope, receive, send)that also supports WebSockets, long-polling and server-sent events. Django exposes both (wsgi.pyandasgi.py); Flask 3 supportsasync defviews but is WSGI at heart. - Neither Django nor Flask should serve production traffic through its own
runserver/app.run()— those are single-process development servers with no process management and no security hardening.
Django architecture: MVT
- Django calls its pattern MVT — Model, View, Template. The *Model* is the data layer, the *View* is a Python function or class that receives an
HttpRequestand returns anHttpResponse, the *Template* renders HTML. The framework itself plays the "controller" role (URL resolver), which is why it is not called MVC. - Project vs app:
django-admin startproject sitecreates the project (settings, root URLconf, wsgi/asgi);python manage.py startapp jobscreates a reusable app. One project, many apps, listed inINSTALLED_APPS. - Request flow: URL received → middleware (request phase) → URL resolver matches a
path()pattern inurls.py→ view executes, uses models/forms → template rendered → middleware (response phase) → response returned. - Settings:
settings.pyholdsDEBUG,ALLOWED_HOSTS,DATABASES,INSTALLED_APPS,MIDDLEWARE,TEMPLATES,STATIC_ROOT,TIME_ZONE(useAsia/KolkataandUSE_TZ = True). Secrets belong in environment variables, never in the repository. - Middleware is a chain of hooks around every request —
SecurityMiddleware,SessionMiddleware,CsrfViewMiddleware,AuthenticationMiddleware. Order matters; a custom middleware is a callable that wrapsget_response. - Class-based views (CBVs) give reusable generic behaviour:
ListView,DetailView,CreateView,UpdateView,DeleteView, mixins such asLoginRequiredMixin. Function-based views (FBVs) are simpler and easier to read for one-off logic.
Django ORM, migrations and admin
- A model is a Python class subclassing
models.Model; each class attribute is a field (CharField,TextField,IntegerField,DecimalField,DateTimeField,BooleanField,EmailField,FileField,JSONField). Django creates an auto-incrementing primary key unless one is declared. - Relationships:
ForeignKey(many-to-one, needson_delete),OneToOneField,ManyToManyField(Django creates the join table, or you supply one withthrough=).related_namecontrols the reverse accessor. - Migrations:
makemigrationsturns model changes into numbered Python files inapp/migrations/;migrateapplies them and records each in thedjango_migrationstable;sqlmigrateshows the SQL;showmigrationslists state. Migrations are version-controlled schema history — never edit the database by hand in a Django project. - QuerySets are lazy: no SQL runs until the QuerySet is iterated, sliced *with a step*, or passed to
list(),len(),bool(); a plain slice such asqs[:20]just returns another unevaluated QuerySet (it adds LIMIT). Chainingfilter().exclude().order_by()builds one query. - Field lookups:
__exact,__iexact,__contains,__icontains,__in,__gt/gte/lt/lte,__startswith,__range,__isnull,__date,__year.Q()objects give OR and NOT;F()objects reference columns for in-database arithmetic (F('views') + 1) and avoid race conditions. - Aggregation:
aggregate(Avg('salary'))returns a dict;annotate(n=Count('applications'))adds a per-row value; combined withvalues()for GROUP BY. - The N+1 problem: looping over 50 rows and touching a related object issues 51 queries. Fix with
select_related()(SQL JOIN, for ForeignKey/OneToOne) andprefetch_related()(second query plus Python-side joining, for ManyToMany and reverse FK). Also useonly(),defer(),values_list(),iterator(),bulk_create(),update(). - Transactions:
transaction.atomic()as a decorator or context manager;select_for_update()to lock rows. - Django admin: register a model with
admin.site.registeror@admin.register, and customise withModelAdmin(list_display,list_filter,search_fields,readonly_fields,inlines). This free CRUD back office is Django's biggest practical advantage and the reason it is chosen for internal tools and government dashboards. - Forms:
forms.Formfor arbitrary input,forms.ModelFormto bind a model;is_valid()runs field validation plusclean_<field>()andclean(); validated data appears incleaned_data. Forms render widgets and errors automatically.
Templates: Django Template Language and Jinja2
- Django's own language is deliberately restricted: variable interpolation uses a doubled pair of curly braces around a name, logic uses percent-sign tags
{% if %},{% for %},{% url %},{% csrf_token %},{% block %}/{% extends %}for inheritance, and{% include %}for partials. - Filters transform values with a pipe:
date:"d M Y",length,default,floatformat,truncatewords,safe. Autoescaping of HTML is on by default in the Django Template Language; plain Jinja2 defaults to autoescaping *off*, but Flask turns it on for templates with HTML/XML extensions — either way it is the standard XSS defence. - Template inheritance is the normal layout technique: a
base.htmlwith named blocks that child templates override. Context processors inject globals (request,user) into every template. - Jinja2 (used by Flask, and optionally by Django) is faster and more permissive — it allows calling functions with arguments, macros, and full Python-like expressions. Flask resolves templates from the
templates/folder and static assets fromstatic/.
Flask: microframework design
- Minimum application: create
app = Flask(__name__), decorate a function with@app.route("/"), return a string orrender_template(...). - Routing:
@app.route("/job/<int:job_id>", methods=["GET", "POST"]); convertersstring,int,float,path,uuid.url_for("view_name", id=5)builds URLs in reverse. - Contexts: the application context (
current_app,g) and the request context (request,session) are thread/task-local proxies pushed per request. Accessingrequestoutside a request raises "Working outside of request context" — the single most common Flask error. - Request object:
request.args(query string),request.form,request.files,request.json,request.headers,request.cookies,request.method. - Responses: return a string, a tuple
(body, status, headers),jsonify(...),redirect(url_for(...)),abort(404).@app.errorhandler(404)customises error pages. - Blueprints split a large Flask app into modules with their own routes, templates and URL prefix — Flask's answer to Django apps. Registered with
app.register_blueprint(bp, url_prefix="/api"). - Application factory pattern: a
create_app(config)function that builds and configures the app, so tests and CLI can create isolated instances; this is the recommended production layout. - Extensions supply what the core omits: Flask-SQLAlchemy (ORM), Alembic/Flask-Migrate (migrations), Flask-WTF (forms + CSRF), Flask-Login (sessions), Flask-RESTful or Flask-Smorest (APIs), Flask-Caching, Flask-Mail, Marshmallow (serialisation).
- Signals (via blinker) and
before_request/after_request/teardown_appcontexthooks give middleware-like behaviour.
Security, sessions and Indian compliance
- CSRF: Django enables it by default — every unsafe POST form needs the CSRF token tag and the cookie; Flask needs Flask-WTF to get the same protection.
- SQL injection is prevented by parameterised queries in both ORMs; it returns the moment you use
raw(),extra()or string-formatted SQL. - XSS: autoescaping protects by default; marking content
safeor|safedisables it, so never mark user input safe. - Clickjacking/headers:
XFrameOptionsMiddleware,SECURE_HSTS_SECONDS,SECURE_SSL_REDIRECT,SESSION_COOKIE_SECURE,CSRF_COOKIE_SECURE, Content-Security-Policy. - Sessions: Django stores a session id cookie with server-side data (DB, cache or signed cookie); Flask's default session is a cryptographically signed (not encrypted) cookie, so never put secrets in it. Passwords are hashed — Django ships PBKDF2 by default with Argon2 and bcrypt available.
- **
DEBUG = Falsein production**, withALLOWED_HOSTSset;DEBUG = Trueleaks settings and tracebacks to the internet. - Indian rules that affect a deployed app: the Digital Personal Data Protection Act, 2023 with the DPDP Rules, 2025 — the core Data Fiduciary duties (notice, consent, breach intimation, erasure) and the omission of IT Act s.43A take effect only from 13 May 2027; until then IT Act s.43A and the SPDI Rules, 2011 govern sensitive personal data. CERT-In's 2022 directions require incident reporting within 6 hours, logs retained 180 days within India, and synchronised NTP. Payment flows (Razorpay, PayU, UPI) must not store card data — RBI tokenisation applies — and GST invoicing in an app must handle the 5%/18%/40% slabs in force since 22 September 2025.
Testing, background work and deployment
- Testing: Django's
TestCasewraps each test in a transaction and gives aClientfor fake requests;pytest-djangois the common choice. Flask exposesapp.test_client()andapp.test_cli_runner(). - Background jobs: Celery with Redis/RabbitMQ for both frameworks (emails, report generation, scraping); Django has
django-celery-beatfor schedules. - Caching: Django's cache framework (Redis/Memcached backends), per-view
@cache_page, template fragment caching, low-levelcache.get/set; Flask-Caching for Flask. - Static and media:
collectstaticgathers static files intoSTATIC_ROOTfor Nginx or a CDN; user uploads go toMEDIA_ROOTor object storage (S3 compatible). - Deployment stack: Nginx → gunicorn (
gunicorn config.wsgi:application -w 9 -b 127.0.0.1:8000) → PostgreSQL + Redis, supervised by systemd, usually containerised with Docker. Environment variables viadjango-environorpython-dotenv. - Scaling: stateless app processes behind a load balancer, sessions and cache in Redis, read replicas for the database, connection pooling (
CONN_MAX_AGE, PgBouncer).
Key points
- WSGI - synchronous Python-to-server interface (PEP 3333); ASGI - async successor supporting WebSockets.
- MVT - Django's Model-View-Template; the framework's URL resolver acts as the controller.
- Project vs app - one Django project contains many apps; apps are reusable and listed in
INSTALLED_APPS. - Migrations -
makemigrationswrites the change,migrateapplies it,django_migrationsrecords it. - Lazy QuerySet - no SQL until evaluated; chaining filters produces a single query.
- select_related vs prefetch_related - JOIN in one query for FK/OneToOne vs a second query for M2M/reverse FK; both cure N+1.
- F() and Q() - column-level arithmetic in SQL, and OR/NOT in filters.
- ModelForm - form fields generated from a model, with validation and
save(). - Django admin - automatic CRUD back office, customised through
ModelAdmin. - Middleware - ordered request/response hooks;
CsrfViewMiddlewaregives default CSRF protection. - Flask microframework - routing + Jinja2 only; everything else is an extension.
- Blueprint - modular grouping of Flask routes with its own URL prefix, equivalent to a Django app.
- Application factory -
create_app()builds configured instances for test and production isolation. - Request context -
request,sessionandgare per-request proxies; using them outside a request raises an error. - Flask session cookie - signed, not encrypted; readable by the client.
- Autoescaping - on by default in Django templates, and switched on by Flask for HTML templates (bare Jinja2 defaults to off);
|safeon user input reopens XSS. - Never ship with DEBUG = True and always set
ALLOWED_HOSTS. - Choose Django for CRUD-heavy products needing admin, auth and ORM; choose Flask for small services, ML model serving and anything where you want to pick each component.
Formulas and examples
Throughput capacity: C = W / T_avg (C in requests/second, W workers, T_avg mean response time in seconds)
Little's law: L = λ × W_t (L = mean number of requests in the system, λ = arrival rate in req/s, W_t = mean time in system in s)
Queries issued by an unoptimised loop: Q = 1 + N (the N+1 problem; N = rows in the outer QuerySet)
Effective response time with a cache: T_eff = h × T_cache + (1 − h) × T_db (h = cache hit ratio, 0 to 1)
Pagination: OFFSET = (page − 1) × page_size; total pages = ceil(total_rows / page_size)
Cost per million requests = monthly server cost (₹) / monthly requests in millions
Example 1 — sizing gunicorn. A Django app runs on a 4-vCPU cloud VM; mean response time is 120 ms; peak traffic is 50 req/s.
- W = (2 × 4) + 1 = 9 workers.
- Capacity per worker = 1 / 0.120 = 8.33 req/s → C = 9 × 8.33 = 75 req/s.
- 50 req/s < 75 req/s, so one VM suffices with about 33% headroom. If T_avg rose to 200 ms, C = 9 / 0.2 = 45 req/s and the peak would start queueing — the fix is faster queries, not more workers.
Example 2 — Little's law and blocking I/O. λ = 40 req/s and each request spends 250 ms in the system (180 ms of it waiting on a slow external API).
- L = 40 × 0.250 = 10 requests in flight simultaneously.
- With 9 sync workers only 9 can be served; the tenth queues. Options: raise to 13 workers, switch to
gthread/geventworkers (concurrency per worker), or move the external call to a Celery task — the last is the correct design because the wait is I/O, not CPU.
Example 3 — curing N+1. A job list page shows 50 listings, each printing job.employer.name; each query takes 3 ms.
- Unoptimised: Q = 1 + 50 = 51 queries, time ≈ 51 × 3 = 153 ms.
- With
Job.objects.select_related('employer')[:50]: 1 JOIN query ≈ 8 ms. - Saving = 153 − 8 = 145 ms, a speed-up of 153 / 8 ≈ 19×.
Example 4 — cache hit ratio. A dashboard view costs 150 ms from PostgreSQL and 2 ms from Redis; @cache_page achieves a 90% hit ratio.
- T_eff = 0.90 × 2 + 0.10 × 150 = 1.8 + 15.0 = 16.8 ms — about 8.9× faster than 150 ms.
- At h = 0.99: T_eff = 0.99 × 2 + 0.01 × 150 = 1.98 + 1.5 = 3.48 ms.
Example 5 — pagination and hosting cost. A Flask job board shows 20 results per page over 4,350 rows, and runs on a ₹1,800/month 2-vCPU VM at an average of 10 req/s.
- Page 7 query: OFFSET = (7 − 1) × 20 = 120, LIMIT 20. Total pages = ceil(4350 / 20) = 218.
- Monthly requests = 10 × 86,400 × 30 = 25,920,000 = 25.92 million.
- Cost per million = 1800 / 25.92 ≈ ₹69.4 per million requests (VM prices vary by provider and region).
| Aspect | Django | Flask |
|---|---|---|
| Type | Full-stack, batteries included | Microframework |
| ORM | Built-in Django ORM + migrations | None; usually SQLAlchemy + Alembic |
| Admin interface | Automatic, customisable | None (Flask-Admin extension) |
| Forms and validation | Built-in Forms/ModelForms | Flask-WTF extension |
| Auth and sessions | Built-in user model, permissions | Flask-Login + manual work |
| CSRF protection | On by default | Via Flask-WTF |
| Templates | Django Template Language (Jinja2 optional) | Jinja2 |
| Modularity unit | App | Blueprint |
| Routing style | Central urls.py with path() | @app.route decorators |
| Learning curve | Steeper, many conventions | Gentle, few conventions |
| Best fit | CRUD products, portals, dashboards, e-governance | Microservices, ML model serving, small APIs, prototypes |
| Async support | ASGI, async views, async ORM in progress | Async views on WSGI; use FastAPI/Quart for heavy async |
# Django: model, view, URL
class Job(models.Model):
title = models.CharField(max_length=200)
employer = models.ForeignKey('Employer', on_delete=models.CASCADE,
related_name='jobs')
city = models.CharField(max_length=80, db_index=True)
ctc_lpa = models.DecimalField(max_digits=5, decimal_places=2, null=True)
posted_on = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-posted_on']
indexes = [models.Index(fields=['city', 'posted_on'])]
def job_list(request):
qs = (Job.objects.select_related('employer')
.filter(city__iexact=request.GET.get('city', 'Kochi')))
return render(request, 'jobs/list.html', {'jobs': qs[:20]})
# urls.py
urlpatterns = [path('jobs/', job_list, name='job-list')]
# Flask: factory, blueprint, route
from flask import Flask, Blueprint, request, jsonify, render_template
bp = Blueprint('jobs', __name__)
@bp.route('/jobs', methods=['GET'])
def jobs():
city = request.args.get('city', 'Kochi')
rows = Job.query.filter(Job.city.ilike(city)).limit(20).all()
if request.accept_mimetypes.best == 'application/json':
return jsonify([r.as_dict() for r in rows])
return render_template('jobs.html', jobs=rows)
def create_app(config_object='config.Production'):
app = Flask(__name__)
app.config.from_object(config_object)
db.init_app(app)
app.register_blueprint(bp, url_prefix='/api')
return app
Where it is used
- Indian product companies and startups: Zomato, Swiggy, Dream11, Razorpay, Instamojo and Freshworks teams use Python services (Django/Flask/FastAPI) for internal tools, dashboards and APIs; Django admin is very common for ops back offices.
- E-governance and education portals: NIC/C-DAC and state-government vendors build citizen portals, admission and scholarship systems on Django for its auth, admin and audit-friendly migrations.
- Service companies and GCCs: TCS, Infosys, Wipro, Cognizant, LTIMindtree, Capgemini staff Python full-stack projects; "Python Developer (Django/REST)" is a standard job title at 3-12 LPA depending on experience.
- Data and ML serving: Flask is the default wrapper for exposing a scikit-learn or PyTorch model as an HTTP endpoint; analytics teams pair it with pandas and Celery.
- Job roles: Python/Django backend developer, full-stack developer, API developer, DevOps engineer deploying gunicorn+Nginx+Docker, SDET writing pytest suites.
- Campus projects and placements: college-level job portals, library systems, hospital management and attendance apps are overwhelmingly built in Django — and interviewers probe the ORM and migration details of exactly those projects.
Interview questions
- Q: What is the difference between MVC and Django's MVT? A: They are the same separation of concerns; in MVT the "view" is the request-handling Python code and the "template" is the presentation layer, while Django's URL resolver performs the controller's dispatch role.
- Q: Why is a Django QuerySet called lazy? A: Building and chaining it issues no SQL; the query runs only when the QuerySet is iterated, sliced with a step, or converted with
list(),len()orbool(). - Q: What is the N+1 query problem and how do you fix it? A: One query for the parent rows plus one per row for a related object;
select_related()solves it with a JOIN for ForeignKey/OneToOne andprefetch_related()with a second query for many-to-many or reverse relations. - Q: Difference between
select_relatedandprefetch_related? A:select_relateddoes a single SQL JOIN and works only on forward FK/OneToOne;prefetch_relatedruns a separate query and joins in Python, so it also handles M2M and reverse FK. - Q: What do
makemigrationsandmigratedo? A:makemigrationsrecords model changes as migration files;migrateexecutes the pending ones against the database and logs them indjango_migrations. - Q: When would you choose Flask over Django? A: For a small service, a model-serving API, or when you want to pick your own ORM, auth and structure; Django wins when you need its admin, ORM, forms and auth out of the box.
- Q: What is a Flask blueprint? A: A reusable collection of routes, templates and static files registered onto an app with an optional URL prefix, used to modularise a large Flask application.
- Q: What are application and request contexts in Flask? A: Context objects pushed per application and per request that make
current_app/gandrequest/sessionavailable as thread-local proxies; touchingrequestoutside a request context raises a runtime error. - Q: How does Django protect against CSRF? A:
CsrfViewMiddlewarerequires a token in every unsafe request, matched against a cookie; templates insert it with the csrf token tag and AJAX must send theX-CSRFTokenheader. - Q: Is the Flask session secure? A: It is cryptographically signed with
SECRET_KEY, so the client cannot tamper with it, but it is not encrypted — the contents are readable, so never store secrets there. - Q: What are
F()andQ()objects used for? A:F()refers to a database column so arithmetic happens in SQL atomically (avoiding read-modify-write races);Q()builds complex filters with OR and NOT. - Q: How do you deploy a Django app in production? A: Nginx for TLS and static files, gunicorn/uvicorn running the WSGI/ASGI app under systemd, PostgreSQL and Redis,
DEBUG = FalsewithALLOWED_HOSTSset, secrets in environment variables andcollectstaticrun at build time. - Q: How would you handle a slow third-party call inside a view? A: Push it to a Celery task and return immediately, because blocking a synchronous worker on I/O consumes a whole worker slot and collapses throughput.
Common mistakes
- Running
manage.py runserverorapp.run()in production, or leavingDEBUG = True— it exposes settings, SQL and tracebacks. - Writing queries inside template loops or view loops, creating N+1 query storms, then blaming the database.
- Hardcoding
SECRET_KEY, database passwords or API keys insettings.pyand committing them to Git. - Editing or deleting migration files on a database that has already applied them, which breaks the migration history for everyone else.
- Marking user-supplied content
|safeor usingmark_safe, re-opening the XSS hole that autoescaping had closed. - Forgetting the CSRF token in a POST form (Django) or not installing Flask-WTF and assuming Flask protects POSTs by default.
- Confusing project with app in Django, or defining Flask routes in one giant file instead of blueprints with an application factory.
- Storing user data or secrets in the Flask session cookie, and assuming "signed" means "encrypted".
- Using string formatting to build SQL in
raw()orcursor.execute(), which reintroduces SQL injection.
Related topics
REST APIs · Back-end development with Node.js · Authentication and authorisation: sessions, OAuth and JWT · API design: GraphQL, gRPC and OpenAPI · Spring Boot and microservices development · Front-end frameworks: React basics