Does Django sometimes feel like Magic to you? In this presentation, we will peek behind the curtain to understand the wizardry that turns an HTTP request into a response.
Despite rumors to the contrary, Django does not actually involve any magic - it's just code! We will dive deep into this code and trace the exact steps that turn an HTTP request into a response.
On this exciting journey, you'll get a high-level overview of the different components that make up Django and dissect elegant code constructs.
Originally presented at DjangoCon Europe 2018 in Heidelberg, Germany.
2. About this tutorial
• not a top-down Django tutorial
• for people who want to get to the bottom of things
3. What we'll cover
• what is Django?
• dissecting the tiniest Django web app
• the same app, but more complicated
4. About me
Daniel Hepper
• degree in computer science
• freelancing for 7 years
• first contact with Django
around 0.95
• NOT a core developer
• varying amounts beard
6. The web framework for
perfectionists with deadlines
Django makes it easier to build better Web apps
more quickly and with less code.
7. Web apps
• web app
• Django project
• django-admin.py startproject
• Django app
• module, part of an web app
• might be re-usable: re-usable app
• python manage.py startapp
9. Web Framework
• you take things from a library,
you put something in a framework
• "Don't call us, we call you"
• But you have to tell us your number
you have to provide an entrypoint
21. WSGI
• Defined in PEP-333 / PEP-3333
• "A [...] standard interface between web servers and
Python web applications or frameworks, to promote web
application portability across a variety of web servers."
• "WSGI is a tool for framework and server developers"
23. WSGI Application
• Callable that accepts two arguments
• a function, method, class, instance with a __call__ method
• result = application(environ, start_response)
• environ: dictionary object, containing CGI-style environment variables
• start_response: callable, accepting two required positional
arguments, third optional
• def start_response(status, response_headers,
exc_info=None):
• When called by the server, the application object must return an iterable
yielding zero or more bytestrings
36. wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
"simple.settings"
)
application = get_wsgi_application()
37. Running a
Django web application
• job of a WSGI server
• two phases
• import the application object
• call the application object for each request
38. A simple WSGI server
from wsgiref.simple_server import make_server
from simple.wsgi import application
httpd = make_server('127.0.0.1', 8000, application)
print("Serving HTTP on port 8000...")
httpd.handle_request()
44. import os
from django.core.wsgi import
get_wsgi_application
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
"simple.settings"
)
application = get_wsgi_application()
wsgi.py
45. import django
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
"""
The public interface to Django's WSGI support.
Return a WSGI callable.
Avoids making django.core.handlers.WSGIHandler a public
API, in case the internal WSGI implementation changes or
moves in the future.
"""
django.setup(set_prefix=False)
return WSGIHandler()
django/core/wsgi.py
46. from django.utils.version import get_version
VERSION = (2, 0, 0, 'final', 0)
__version__ = get_version(VERSION)
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.log import configure_logging
configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/'
if settings.FORCE_SCRIPT_NAME is None
else settings.FORCE_SCRIPT_NAME
)
apps.populate(settings.INSTALLED_APPS)
django/__init__.py
47. [...]
class LazySettings(LazyObject):
"""
A lazy proxy for either global Django settings or a custom settings
object. The user can manually configure settings prior to using
them. Otherwise,Django uses the settings module pointed to by
DJANGO_SETTINGS_MODULE.
"""
def __getattr__(self, name):
"""
Return the value of a setting and cache it in self.__dict__.
"""
if self._wrapped is empty:
self._setup(name)
val = getattr(self._wrapped, name)
self.__dict__[name] = val
return val
[...]
settings = LazySettings()
django/conf/__init__.py
48. [...]
class LazySettings(LazyObject):
"""
A lazy proxy for either global Django settings or a custom settings
object. The user can manually configure settings prior to using
them. Otherwise,Django uses the settings module pointed to by
DJANGO_SETTINGS_MODULE.
"""
def _setup(self, name=None):
"""
Load the settings module pointed to by the environment
variable. This is used the first time settings are needed, if
the user hasn't configured settings manually.
"""
settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
if not settings_module:
desc = ("setting %s" % name) if name else "settings"
raise ImproperlyConfigured(...)
self._wrapped = Settings(settings_module)
[...]
settings = LazySettings()
django/conf/__init__.py
49. [...]
class Settings:
def __init__(self, settings_module):
for setting in dir(global_settings):
if setting.isupper():
setattr(self, setting,
getattr(global_settings, setting))
[...]
mod = importlib.import_module(self.SETTINGS_MODULE)
self._explicit_settings = set()
for setting in dir(mod):
if setting.isupper():
setting_value = getattr(mod, setting)
[...]
setattr(self, setting, setting_value)
[...]
[...]
settings = LazySettings()
django/conf/__init__.py
50. from django.utils.version import get_version
VERSION = (2, 0, 0, 'final', 0)
__version__ = get_version(VERSION)
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.log import configure_logging
configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/'
if settings.FORCE_SCRIPT_NAME is None
else settings.FORCE_SCRIPT_NAME
)
apps.populate(settings.INSTALLED_APPS)
django/__init__.py
51. import django
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
"""
The public interface to Django's WSGI support.
Return a WSGI callable.
Avoids making django.core.handlers.WSGIHandler a public
API, in case the internal WSGI implementation changes or
moves in the future.
"""
django.setup(set_prefix=False)
return WSGIHandler()
django/core/wsgi.py
55. import django
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
"""
The public interface to Django's WSGI support.
Return a WSGI callable.
Avoids making django.core.handlers.WSGIHandler a public
API, in case the internal WSGI implementation changes or
moves in the future.
"""
django.setup(set_prefix=False)
return WSGIHandler()
django/core/wsgi.py
56. import os
from django.core.wsgi import
get_wsgi_application
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
"simple.settings"
)
application = get_wsgi_application()
wsgi.py
58. WSGI Application
• Callable that accepts two arguments
• a function, method, class, instance with a __call__ method
• result = application(environ, start_response)
• environ: dictionary object, containing CGI-style environment variables
• start_response: callable, accepting two required positional
arguments, third optional
• def start_response(status, response_headers,
exc_info=None):
• When called by the server, the application object must return an iterable
yielding zero or more bytestrings
62. class HttpRequest:
"""A basic HTTP request."""
# The encoding used in GET/POST dicts. None means use default
# setting.
_encoding = None
_upload_handlers = []
def __init__(self):
# WARNING: The `WSGIRequest` subclass doesn't call
# `super`.
# Any variable assignment made here should also happen in
# `WSGIRequest.__init__()`.
[...]
django/core/http/request.py
63. class WSGIRequest(HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
path_info = get_path_info(environ)
if not path_info:
# Sometimes PATH_INFO exists, but is empty (e.g.
# accessing the SCRIPT_NAME URL without a
# slash). We really need to operate as if they'd
# requested '/'. Not amazingly nice to force
# the path like this, but should be harmless.
path_info = '/'
[...]
django/core/handlers/wsgi.py
65. class WSGIRequest(HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
path_info = get_path_info(environ)
if not path_info:
# Sometimes PATH_INFO exists, but is empty (e.g.
# accessing the SCRIPT_NAME URL without a
# slash). We really need to operate as if they'd
# requested '/'. Not amazingly nice to force
# the path like this, but should be harmless.
path_info = '/'
[...]
def get_script_name(environ):
"""
Return the equivalent of the HTTP request's SCRIPT_NAME
environment variable. If Apache mod_rewrite is used, return
what would have been the script name prior to any rewriting
(so it's the script name as seen from the client's
perspective), unless the FORCE_SCRIPT_NAME setting is
set (to anything).
"""
[...]
django/core/handlers/wsgi.py
66. class WSGIRequest(HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
path_info = get_path_info(environ)
if not path_info:
# Sometimes PATH_INFO exists, but is empty (e.g.
# accessing the SCRIPT_NAME URL without a
# slash). We really need to operate as if they'd
# requested '/'. Not amazingly nice to force
# the path like this, but should be harmless.
path_info = '/'
[...]
def get_path_info(environ):
"""Return the HTTP request's PATH_INFO as a string."""
path_info = get_bytes_from_wsgi(environ, 'PATH_INFO', '/')
return repercent_broken_unicode(path_info).decode()
django/core/handlers/wsgi.py
67. class WSGIRequest(HttpRequest):
def __init__(self, environ):
[...]
self.environ = environ
self.path_info = path_info
# be careful to only replace the first slash in the path
# because of http://test/something and
# http://test//something being different as
# stated in http://www.ietf.org/rfc/rfc2396.txt
self.path = '%s/%s' % (script_name.rstrip('/'),
path_info.replace('/', '', 1))
[...]
django/core/handlers/wsgi.py
73. class BaseHandler:
[...]
def get_response(self, request):
"""
Return an HttpResponse object for the given
HttpRequest.
"""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
django/core/handlers/base.py
74. # Overridden URLconfs for each thread are stored here.
_urlconfs = local()
[...]
def set_urlconf(urlconf_name):
"""
Set the URLconf for the current thread (overriding the
default one in settings). If urlconf_name is None, revert
back to the default.
"""
if urlconf_name:
_urlconfs.value = urlconf_name
else:
if hasattr(_urlconfs, "value"):
del _urlconfs.value
django/urls/base.py
75. class BaseHandler:
[...]
def get_response(self, request):
"""
Return an HttpResponse object for the given
HttpRequest.
"""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
django/core/handlers/base.py
76. class BaseHandler:
def load_middleware(self):
"""
Populate middleware lists from settings.MIDDLEWARE.
"""
handler = convert_exception_to_response(
self._get_response
)
[...]
self._middleware_chain = handler
def get_response(self, request):
"""
Return an HttpResponse object for the given
HttpRequest.
"""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
django/core/handlers/base.py
77. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view,
then apply view, exception, and template_response
middleware.
This method is everything that happens
inside the request/response middleware.
"""
[...]
django/core/handlers/base.py
78. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view,
then apply view, exception, and template_response
middleware.
This method is everything that happens
inside the request/response middleware.
"""
response = None
if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()
resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
django/core/handlers/base.py
79. class BaseHandler:
[...]
def _get_response(self, request):
[...]
callback, callback_args, callback_kwargs = resolver_match
[...]
# Apply view middleware
for middleware_method in self._view_middleware:
response = middleware_method(request, callback, ...)
if response:
break
[...]
django/core/handlers/base.py
80. class BaseHandler:
[...]
def _get_response(self, request):
[...]
callback, callback_args, callback_kwargs = resolver_match
[...]
# Apply view middleware
[...]
if response is None:
wrapped_callback = self.make_view_atomic(callback)
try:
response = wrapped_callback(
request, *callback_args, **callback_kwargs
)
except Exception as e:
response = self.process_exception_by_middleware(
e, request
)
django/core/handlers/base.py
81. class BaseHandler:
[...]
def _get_response(self, request):
[...]
callback, callback_args, callback_kwargs = resolver_match
[...]
# Apply view middleware
[...]
response = wrapped_callback(
request, *callback_args, **callback_kwargs
)
# Complain if the view returned None (a common error).
[...]
# If the response supports deferred rendering, apply
# template response middleware and then render the response
[...]
return response
django/core/handlers/base.py
82. class BaseHandler:
def load_middleware(self):
"""
Populate middleware lists from settings.MIDDLEWARE.
"""
handler = convert_exception_to_response(
self._get_response
)
[...]
self._middleware_chain = handler
def get_response(self, request):
"""
Return an HttpResponse object for the given
HttpRequest.
"""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
django/core/handlers/base.py
84. class HttpResponseBase:
[...]
# The WSGI server must call this method upon completion of
# the request.
# See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close
def close(self):
for closable in self._closable_objects:
try:
closable.close()
except Exception:
pass
self.closed = True
signals.request_finished.send(sender=self._handler_class)
django/http/response.py
85. class BaseHandler:
[...]
def get_response(self, request):
[...]
response = self._middleware_chain(request)
response._closable_objects.append(request)
# If the exception handler returns a TemplateResponse
# that has not been rendered, force it to be rendered.
if not getattr(response, 'is_rendered', True) and
callable(getattr(response, 'render', None)):
response = response.render()
[...]
return response
django/core/handlers/base.py
86. class BaseHandler:
[...]
def get_response(self, request):
[...]
response = self._middleware_chain(request)
response._closable_objects.append(request)
# If the exception handler returns a TemplateResponse
# that has not been rendered, force it to be rendered.
if not getattr(response, 'is_rendered', True) and
callable(getattr(response, 'render', None)):
response = response.render()
if response.status_code == 404:
logger.warning(
'Not Found: %s', request.path,
extra={'status_code': 404, 'request': request})
return response
django/core/handlers/base.py
92. Function based middleware
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware
93. Class based middleware
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
111. class BaseHandler:
def load_middleware(self):
"""
Populate middleware lists from settings.MIDDLEWARE.
"""
[...]
def get_response(self, request):
"""
Return an HttpResponse object for the given
HttpRequest.
"""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
middleware1(middleware2(middleware3(self._get_response)))))))
django/core/handlers/base.py
113. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
response = None
[...]
return response
django/core/handlers/base.py
114. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
response = None
if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()
[...]
return response
django/core/handlers/base.py
116. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
[...]
return response
django/core/handlers/base.py
117. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
callback, callback_args, callback_kwargs = resolver_match
[...]
# Apply view middleware
for middleware_method in self._view_middleware:
response = middleware_method(request,
callback,
callback_args,
callback_kwargs)
if response:
break
[...]
return response
django/core/handlers/base.py
119. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
if response is None:
wrapped_callback = self.make_view_atomic(callback)
try:
response = wrapped_callback(request,
*callback_args,
**callback_kwargs)
except Exception as e:
response = self.process_exception_by_middleware(
e, request)
[...]
return response
django/core/handlers/base.py
120. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
# Complain if the view returned None (a common error).
if response is None:
if isinstance(callback, types.FunctionType): # FBV
view_name = callback.__name__
else: # CBV
view_name = callback.__class__.__name__ + '.__call__'
raise ValueError(
"The view %s.%s didn't return an HttpResponse object. It
"returned None instead." % (callback.__module__, view_na
)
[...]
return response
django/core/handlers/base.py
121. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
# If the response supports deferred rendering, apply template
# response middleware and then render the response
elif hasattr(response, 'render') and callable(response.render):
for middleware_method in self._template_response_middleware:
response = middleware_method(request, response)
# Complain if the template response middleware
# returned None (a common error).
if response is None:
raise ValueError(...)
[...]
return response
django/core/handlers/base.py
122. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
# If the response supports deferred rendering, apply template
# response middleware and then render the response
elif hasattr(response, 'render') and callable(response.render):
[...]
try:
response = response.render()
except Exception as e:
response = self.process_exception_by_middleware(e, reque
return response
django/core/handlers/base.py
123. class BaseHandler:
[...]
def process_exception_by_middleware(self, exception, request):
"""
Pass the exception to the exception middleware.
If no middleware return a response for this exception,
raise it.
"""
for middleware_method in self._exception_middleware:
response = middleware_method(request, exception)
if response:
return response
raise
django/core/handlers/base.py
125. class BaseHandler:
[...]
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything
that happen inside the request/response middleware.
"""
[...]
# If the response supports deferred rendering, apply template
# response middleware and then render the response
elif hasattr(response, 'render') and callable(response.render):
[...]
try:
response = response.render()
except Exception as e:
response = self.process_exception_by_middleware(e, reque
return response
django/core/handlers/base.py
126. class BaseHandler:
[...]
def get_response(self, request):
"""
Return an HttpResponse object for the given
HttpRequest.
"""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
[...]
return response
django/core/handlers/base.py
128. class BaseHandler:
[...]
def get_response(self, request):
[...]
response = self._middleware_chain(request)
response._closable_objects.append(request)
# If the exception handler returns a TemplateResponse
# that has not been rendered, force it to be rendered.
if not getattr(response, 'is_rendered', True) and
callable(getattr(response, 'render', None)):
response = response.render()
if response.status_code == 404:
logger.warning(
'Not Found: %s', request.path,
extra={'status_code': 404, 'request': request})
return response
django/core/handlers/base.py
129. class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
[...]
def __call__(self, environ, start_response):
[...]
response = self.get_response(request)
response._handler_class = self.__class__
status = '%d %s' % (response.status_code, response.reason_phrase
response_headers = list(response.items())
for c in response.cookies.values():
response_headers.append(('Set-Cookie', c.output(header='')))
start_response(status, response_headers)
if (getattr(response, 'file_to_stream', None) is not None
and environ.get('wsgi.file_wrapper')):
response = environ['wsgi.file_wrapper'](response.file_to_str
return response
django/core/handlers/wsgi.py
130. TIL
• Django is made by humans, not wizards
• Just code, no magic
131. Room for contribution
• Unused attributes _response_middleware and
_request_middleware in BaseHandler
• Wrong or misleading comments
• ExceptionMiddleware does not exist