Django from first principles, part 2

MP 91: A single file, serving a basic version of the project's home page.

Note: This is the second post in a series about building a full Django project, starting with a single file. This series will be free to everyone, as soon as each post comes out.

In this post, we'll start building the BlogMaker Lite home page. Instead of doing this by running startproject and startapp, as is typically done in Django tutorials, we'll just write a single file that serves the home page.

The home page at this stage will only show the title BlogMaker Lite. This is similar to the Hello world! message that's typically displayed in a single-file introduction to other web frameworks. In the next post we'll expand the home page so it provides an appropriate description of what users can expect from the site.

Clients, servers, requests, and responses

If you want to understand how to use a web framework like Django, you really need to develop a clear understanding of a few terms: clients, servers, requests, and responses. If you're reading this you're probably online. You've almost certainly been using these ideas for a long time, even if you haven't been aware of them by name.

When you visit a web page your browser acts as a client, which sends a request to a server. The server receives the request, and sends a response back to your browser. For example if you're reading the web version of this post, we could represent the process like this:

diagram of request-response cycle involving a remote server
When you read a typical web page, your browser sends a request to a remote server. The server responds with an HTML file, which your browser then displays.

In the diagram above, "your system" might be a phone, a tablet, or a laptop. You enter the URL for this webpage in a browser (or click a link to this page), and your browser (the client) sends a request to the server that hosts Mostly Python. The server processes the request, and returns an HTML file representing this page. That HTML file is the response. 1

The steps shown in the above diagram happen billions of times every day. The actual number might be even higher than that; I don't know the exact scale of internet traffic these days. Every time someone visits a web site or gets a resource over an internet connection, some variation of this cycle happens.

All that on one machine?

As a web developer, you'll be making all this happen on your own system. For example, when you're working on a Django project on your own computer, this is what the process looks like:

diagram of request-response cycle happening entirely on the developer's system
You can develop web apps on your own system. You still use a browser to issue requests, by entering a URL in your browser's address bar. A server process running on your computer responds by sending the appropriate HTML file back to the browser.

When you're learning to use a framework like Django and you're building a project on your own system, the client is still your browser. But this time, the server is also on your system. The server in this case is a Django process that's "listening" for certain requests. That Django process receives your request and issues a response, just as it would on a remote server.

A bit simplified

The diagrams and explanations above are a bit simplified. For example the request is made up of more than just a URL; it can contain other information such as what kind of browser you're using and session data that helps with authentication. However, the simplified process described here is the foundation of almost all web development work.

If you understood what's been covered so far, great! You'll recognize some of these parts in what we're about to build. If you didn't really understand this process, don't worry much about it now. We'll explain these parts in a number of different ways, and you'll see the process in action throughout this series. You'll have a few different ways to think about all this, both in your development work and in your ongoing use of the sites you visit every day. At some point, it will almost certainly start to make more sense.

Building BlogMaker Lite

Let's get started!

Make a virtual environment

We'll need to start with a virtual environment. If you're unfamiliar with them, a virtual environment is an isolated place on your system where you can build a Python project. Once it's set up, you can update it separately from all your other projects. You can also recreate the same environment any time you need, and you can build an identical environment on any other system as well. This will become important at the end of this series, when we deploy the project to a remote server.

Make a new folder for your project. Then use Python's venv module to create a virtual environment, if your IDE doesn't make one for you:

$ mkdir bml_project
$ cd bml_project
bml_project$ python -m venv .venv
bml_project$ source .venv/bin/activate
(.venv)bml_project$ pip install --upgrade pip

On Windows, the command to activate the virtual environment is a little different:

bml_project> .venv\Scripts\activate

With an active virtual environment, we can install Django:

(.venv)bml_project$ pip install Django
Collecting Django
  ...
Successfully installed Django-5.0.4 ...

Okay! If you see the message that Django was successfully installed, you can move on. If you have trouble with these steps, please post a comment and I'll try to walk you through these steps on your system. There are a lot of different systems out there, so don't be shy about asking a question.

Build the home page

We're starting the project as a single file, so let's jump right in. We'll write the file, run the project, visit the home page, and then try to understand what's happening.

Here's the file that serves the home page:

from django.conf import settings
from django.http import HttpResponse
from django.urls import path
from django.core.handlers.wsgi import WSGIHandler
from django.core.management import \
    execute_from_command_line

settings.configure(
    ROOT_URLCONF=__name__,
    DEBUG=True,
    SECRET_KEY="my-secret-key",
)

def index(request):
    return HttpResponse("BlogMaker Lite")

urlpatterns = [
    path("", index)
]

application = WSGIHandler()

if __name__ == "__main__":
    execute_from_command_line()
blogmaker_lite.py

We'll look at all the parts of this file in a moment. For now, either type it all in, or copy and paste it into your editor. Save the file as blogmaker_lite.py, in the folder bml_project.2

Run the project

In a terminal window, run the project with the following command:

(.venv)blm_project$ python blogmaker_lite.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 30, 2024 - 06:21:45
Django version 5.0.3, using settings None
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

The runserver command starts a server on your system that runs until you tell it to stop. This is what lets Django respond any time you issue a request. (If you're on Windows and have trouble running this command, try opening a simple console instead of a PowerShell terminal.)

The important thing to notice here is where the server is listening for requests. That's shown in the highlighted line above: http://127.0.0.1:8000. If you type or paste that address into your browser, you should see the project's simple home page:

The BlogMaker Lite home page. At this stage of development, this is equivalent to a typical Hello world example that many other frameworks showcase in their documentation.

Everything we talked about at the start of this post is happening here. The browser is acting as the client. The request is being passed through the URL; The part of the URL that reads 127.0.0.1 is a reference to the local computer. Most requests get routed to the internet, but this routes the request to a port on your local system. Django's development server, which we started with the runserver command, listens for requests on port 8000.

When Django received the request, it prepared a response consisting of the string "BlogMaker Lite". This string was packaged up into an HTML file, which was returned to the browser. The browser then displayed that HTML file as a standard web page.

Using localhost

The location 127.0.0.1 is mapped to the name localhost. So you can also reach the home page by using the address http://localhost:8000. When you're entering an address in your browser, you can often shorten this to localhost:8000.

Understanding the parts

Now that we have a working project, let's go back and understand the parts we've written so far.

Imports

We made a number of imports at the start of the file:

from django.conf import settings
from django.http import HttpResponse
from django.urls import path
from django.core.handlers.wsgi import WSGIHandler
from django.core.management import \
  execute_from_command_line

Django is a web framework "with batteries included". That's a nice way to say that Django comes with most of what you'll need to build a full, modern web project. Minimalist frameworks require you to choose a larger number of additional packages and install them as your project grows. That's part of why comparisons between Django and other "simpler" frameworks aren't always fair. When people actually build out real-world projects using those frameworks, they end up installing third-party packages that are roughly equivalent to much of what Django includes by default. 3

The import statements shown here support the minimum functionality needed to serve a simple web page.

Settings

Django is a framework for building web apps. The phrase "web apps" encompasses a huge range of use cases these days. You can have a mostly static website with some content that's just for members. You can build an iPhone app that's served by a Django backend. You can even build out a full "desktop" application that runs in the browser. Because of this wide range of use cases, there are a lot of settings available in Django.

To serve the simplest version of a home page, there are three settings we need to define right away:

settings.configure(
    ROOT_URLCONF=__name__,
    DEBUG=True,
    SECRET_KEY="my-secret-key",
)

The first setting, ROOT_URLCONF, is short for root URL configuration. This tells Django where to find the URLs it should listen for. Here, __name__ refers to the current file, blogmaker_lite.py. We're going to define the URL that Django should listen for in this file.

The second setting, DEBUG, tells Django whether to show debugging information in the response if anything goes wrong. We want this set to True for local development; otherwise it's much harder to troubleshoot when you don't get the response you're expecting. We'll set this to False when we deploy the project to a remote server.

The final setting, SECRET_KEY, is used to help keep the project secure when it's deployed. We'll set this to an appropriately unique value when we deploy the project. For local work, it just needs to be set to something, or you'll get a bunch of warnings in your terminal every time you load a page.

The view function

Django, and many other frameworks, follow an "MVC" architecture. That's short for model-view-controller. Later, we'll build a model of the data we want to work with. We'll define ways the user can view the data in the project. There are a bunch of different ways we can control how users interact with the project, and how Django responds to requests that come in.

For now, we define one way for users to view the project:

def index(request):
    return HttpResponse("BlogMaker Lite")

This is a view function. It's called index() because that's the name typically used for a project's home page. It takes in the request object, which Django packages up based on the information it received in the request. We're not using any of that information right now, but later we'll be able to find out things like "Is the user authenticated? If they are, what's their username?"

The index() function returns an HTTP response, consisting of the single string "BlogMaker Lite". The function HttpResponse() wraps this string in a basic HTML file. You can see the HTML file that Django generates if you look at the project's home page in your browser's inspector tool:

BlogMaker Lite home page with Safari inspector pane open
It's a great idea to look at your project using your browser's inspector tool. Here, you can see the HTML that Django's HttpResponse() function generated.

In this project, we'll write one view function for each page in the project.

URL patterns

We need to define the URLs that our project listens for. Django listens for URLs that come in through port 8000 on localhost, but it's not going to respond to just any request that comes in. We'll define which URLs it should respond to.

Here's the first URL we define:

urlpatterns = [
    path("", index)
]

We write code that represents URL patterns, because we don't actually want to write out every possible URL that might come in. For example once we have a number of blogs, we'll want to be able to support URLS such as:

http://localhost:8000/blogs/1/
http://localhost:8000/blogs/2/
http://localhost:8000/blogs/3/
...
http://localhost:8000/blogs/1000000/

If we become the next great blog host, we don't want to have to write a new URL for every blog that someone starts! Instead we'll define a pattern that all these URLs fall into.

Each URL pattern can be thought of as a path to a view function. The function call path("", index) defines a URL pattern that maps an empty string to the index() function we defined earlier.

What does an empty string mean for a URL? Consider the following snippet:

$ python
>>> base_url = "http://localhost:8000"
>>> url_parts = ""
>>> url = base_url + "/" + url_parts
>>> url
'http://localhost:8000/'

The base URL for all requests is http://localhost:8000. That exact URL will only work for the home page; most requests will have additional parts. Here we assign that base URL to a variable, base_url. Then we assign the additional parts to url_parts. The actual URL that needs to be handled is built by combining the base URL with the URL parts, separated by a forward slash.

If url_parts is an empty string, we get the URL you entered into your browser earlier. Django works in a slightly different way than this, because it's actually deconstructing the URLs it receives, rather than building URLs. But the call path("", index) effectively maps the base URL http://localhost:8000 to the view function named index.

WSGI

The acronym WSGI stands for web server gateway interface. We have this line in our file:

application = WSGIHandler()

This line causes Django's WSGIHandler class to handle requests that come in, and build an appropriate response that can be returned.

It's often relatively easy to find the source code of the parts of Django you're working with. The import path for WSGIHandler appears at the top of blogmaker_lite.py:

from django.core.handlers.wsgi import WSGIHandler

If you go to the main Django repository on GitHub, you'll see a folder called django. Click on that, and then click on each of the names you see in the import statement in turn: core, handlers, and wsgi.py. In the file wsgi.py, search for WSGIHandler. You'll end up here:

class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        ...

    def __call__(self, environ, start_response):
        ...
        return response
django/core/handlers/wsgi.py

If you're brand new to Django, don't spend much time trying to understand this code right now. You should keep in mind as you're learning Django, however, that all the source code is just a few clicks away. It's not magic; it's a lot of work over many years figuring out how to efficiently turn requests into responses. It's kind of nice to see the simple line return response. That's the core of what Django does.

In our example, this tells Django what part of its codebase should be used to process requests and generate responses.

execute_from_command_line()

Here's the last couple lines of blogmaker_lite.py:

if __name__ == "__main__":
    execute_from_command_line()

The if block lets us run this file directly. When we do, the function execute_from_command_line() takes any arguments we pass in, and executes them.

Here's the command we used to run this file:

(.venv)blm_project$ python blogmaker_lite.py runserver

Here we're asking Django to execute the runserver command. This starts a process that listens for requests coming in through the address localhost:8000. When you want to stop processing requests, you can press Control-C in the terminal where you issued this command.

Conclusions

Whew! That's a lot! But if you followed all this, even very roughly, you're doing great. This was a long post, but it covered the most fundamental aspects of how Django projects work.

We did all this with a single file, blogmaker_lite.py. In a typical Django project, the responsibilities handled in this one file are spread across a number of different files such as settings.py, views.py, urls.py, and manage.py. We'll be creating those files as our project expands, but we'll only do so when it's necessary for this project.

In the next post, we'll build out a more meaningful version of the project's home page.

Resources

You can find the code file from this post in this commit of the django-first-principles GitHub repository.


1

Servers often send other resources as well, such as CSS and JavaScript files. Servers can also respond by sending any number of file types: images, text files, PDFs, and much more.

2

For a moderate-length file like this, it can be really good to type it in by hand. Doing so makes you pay attention to each part of the program in a way that's much different than just copying and pasting. You'll likely make some typos, and fixing them gives you practice with debugging and troubleshooting.

That said, this shouldn't become a frustrating experience. If you start to spend too much time troubleshooting typos, and it's more frustrating than beneficial, it's probably a better idea to paste the file into your editor and move on.

3

Most mature Django projects include some third-party packages as well, but Django includes most of what you need for your project in its own libraries. The third-party packages you do need in a Django project are typically packages that need to be maintained on a different timeframe than Django itself. For example, there are some authentication packages that benefit greatly from being updated more frequently than Django can push out new releases.