Python for Humans · Draft v0.1 · Oct 29, 2026 · Draft position: chapter 17 · Build a local Django page that calls the existing word counter and renders a saved text's top words.
The other side of the web: Django (optional)
- You should already know
- Functions, dictionaries, imports, local files, assertions and project dependencies with uv.
- You'll build
- A local web page showing the most common words in a saved text.
- You'll be able to
- Start and stop a Django development server, route a request to a view, render a template and check a response.
- You need
- Your local setup, any plain-text editor, uv, an internet connection for installation and text_tools.py from Modules.
So far, your programs have been web clients. Requests asks another server for weather or a dataset and receives a response.
Let's turn that around. Your browser will make the request, and your Python program will supply the response. First a greeting, then a page displaying the word counts your existing helper already knows how to calculate.
This chapter is optional. It introduces one small Django app running on your own computer. You can continue to your own projects without completing it. Our views will be functions; you can follow this chapter even if you haven't built the card game's class.
Give Python a Web Address
A browser needs an address to visit and a server listening there. Django is a Python web framework: it supplies machinery for receiving requests, choosing code to run and returning responses.
Our part will connect a URL to a function, call the word counter and describe how its results should appear as HTML.
The browser will display the HTML. Our Python code runs in a separate server process in the terminal. Closing or refreshing the browser page doesn't start or stop that process.
Make a Separate Project
Open a terminal in your existing python-for-humans course folder. Create a new folder and enter it:
mkdir wordsite cd wordsite
We'll keep this web project in its own environment. This draft uses Python 3.14 and Django 6.0, a combination listed in Django's supported Python versions. uv can download the Python version if needed.
Run these commands inside wordsite:
uv init --bare --vcs none --no-workspace --python 3.14 uv python pin 3.14 uv add "django>=6.0,<6.1" uv run python --version uv run django-admin --version
The version checks should show Python 3.14 and Django 6.0, with patch numbers that may differ. The quoted dependency asks uv for a Django release from the 6.0 series. uv.lock records the exact installed versions.
--no-workspace keeps this project separate from the parent course project's dependency setup. uv python pin 3.14 writes .python-version in this folder. It tells uv which Python version to use here, independently of the parent course folder's environment. This project's installation doesn't need Requests.
Generate the Django project:
uv run django-admin startproject config .
The final dot means use this current folder. config is the name of the Python package holding the site's configuration. The important new files are:
wordsite/
.python-version
pyproject.toml
uv.lock
manage.py
config/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py
manage.py runs commands for this Django project. config/settings.py holds its settings, and config/urls.py chooses how URLs reach our code. The generated __init__.py makes config a Python package. We can leave the deployment-related asgi.py and wsgi.py alone for this exercise.
These generation commands are setup steps. If you're returning to a project you've already created, resume editing it rather than generating another copy over its files.
Start and Stop the Server
Django's generated settings include some built-in apps that use a database. Prepare their tables once:
uv run python manage.py migrate
This creates a local SQLite database, db.sqlite3, and applies Django's initial migrations, instructions for setting up database tables. We won't write database code in this chapter; our report will read a text file. If you started the server first, its unapplied-migrations message is asking for this setup step.
Now start the development server:
uv run python manage.py runserver
Leave this command running. Open http://127.0.0.1:8000/ in your browser. You should see Django's welcome page.
127.0.0.1 refers to your own computer. 8000 is the port where this server listens. This address doesn't publish a website for other people to visit.
Checkpoint: Return to the terminal and press Ctrl-C to stop the server. Refresh the browser; it should fail to connect. Start the same command again and refresh once more. The welcome page should return.
While the server is running, that terminal belongs to it. To run another command, stop it first or open a second terminal in wordsite. If port 8000 is already in use, run uv run python manage.py runserver 8001 and visit port 8001 instead.
Keep using this local development server while learning. Django's production deployment instructions are a later step.
Add an App for the Report
Stop the server. In wordsite, run:
uv run python manage.py startapp counts
A Django project supplies the whole site's configuration. An app groups a feature within it. Our counts app will contain the report's view and template.
The generated counts folder includes views.py, where we'll write the request-handling function. Files such as models.py and admin.py are for features we won't need yet.
In config/settings.py, find INSTALLED_APPS and add 'counts' to the existing list. The list should now be:
# In config/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'counts', ]
Keep the rest of the generated settings. Registering the app also lets Django find its templates later.
Answer a Request With a Function
Replace the contents of counts/views.py with:
# counts/views.py (first version) from django.http import HttpResponse def index(request): return HttpResponse('Hello from your Python program.')
A view is code that receives a request and returns a response. Here, index is an ordinary function. Django supplies its request argument when someone visits the matching URL.
HttpResponse creates a response object containing our text. Django uses that object to send an HTTP answer to the browser. Returning this object matters: print() would write to the server's terminal instead of answering the browser.
Create a new file, counts/urls.py:
# counts/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), ]
from . import views imports the views module from this same Python package. path() connects a URL route to a view. The empty route '' matches this app's starting address. name='index' gives the route a name Django can refer to elsewhere.
We pass views.index, the function itself. Writing views.index() here would try to call it during setup without a request.
Now replace config/urls.py with:
# config/urls.py from django.urls import include, path urlpatterns = [ path('', include('counts.urls')), ]
This sends the site's starting route to counts.urls. We've replaced the generated admin route because this page doesn't use it.
Start the server again. Refresh the local page. It should now say:
Hello from your Python program.
Change one word in the greeting, save and refresh. The development server usually reloads Python changes automatically. Watch the terminal for errors; restart the server if a newly added file hasn't been picked up.
Bring Back the Word Counter
Stop the server. Use your editor or file manager to copy text_tools.py and test_text_tools.py from the course folder into wordsite, beside manage.py. These are copies for this separate project; changing one later won't update the other automatically. Within this project, the view will import the helper from that one module.
Run its existing checks:
uv run test_text_tools.py
Create sample.txt beside manage.py, with exactly:
Red red blue. Blue red green!
Using the chapter's counting rules, we expect red three times, blue twice and green once. Start with this predictable input before switching to Shakespeare or your own material.
Replace counts/views.py with this complete version:
# counts/views.py from collections import Counter from django.conf import settings from django.shortcuts import render from text_tools import count_words def index(request): source = settings.BASE_DIR / 'sample.txt' with open(source, encoding='utf-8') as saved: text = saved.read() counts = count_words(text) top_words = Counter(counts).most_common(10) context = { 'title': 'Word counts', 'source_name': source.name, 'top_words': top_words, } return render(request, 'counts/index.html', context)
The generated BASE_DIR setting is a Path for the project folder containing manage.py. Joining it to 'sample.txt' finds our input independently of the terminal's current folder. source.name supplies just its filename for the page.
The view calls the same count_words() function our terminal programs use. Then Counter, from Python's standard library, wraps the resulting word-to-count dictionary. .most_common(10) returns up to ten (word, count) pairs in descending count order. These pairs are tuples, fixed sequences whose positions can be unpacked much like lists. The optional ranking exercise in Modules used this tool too; you don't need that exercise to follow this version.
For our tiny text, the pairs are:
[('red', 3), ('blue', 2), ('green', 1)]
context is a dictionary of values for the page. render() fills the named template with that context and returns an HTTP response. The path 'counts/index.html' names a template, not a browser URL.
Describe the Page With HTML
Create the nested folders and file shown here:
counts/
templates/
counts/
index.html
The inner counts folder keeps this template's name distinct from another app's index.html. Use those exact folder names and the .html extension.
Put this in index.html:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ title }}</title> </head> <body> <h1>{{ title }}</h1> <p>Source: {{ source_name }}</p> <table> <thead> <tr><th>Word</th><th>Count</th></tr> </thead> <tbody> {% for word, count in top_words %} <tr><td>{{ word }}</td><td>{{ count }}</td></tr> {% empty %} <tr><td colspan="2">No words to count.</td></tr> {% endfor %} </tbody> </table> </body> </html>
HTML describes the page's structure. <h1> marks its main heading, <p> a paragraph and <table> a table. A <tr> is a table row; <th> marks a heading cell and <td> a data cell. Most tags have a closing partner, such as </h1>.
The braces belong to Django's template language:
{{ title }}inserts a value from the context dictionary.{% for word, count in top_words %}visits the pairs, giving their two values the nameswordandcountfor that row.{% empty %}supplies a row when there are no pairs.{% endfor %}ends the repeated section. Templates use these markers rather than Python indentation to delimit a loop.
Django escapes inserted text for HTML by default. A word containing < will display as text rather than becoming a new HTML tag. Keep that behavior when displaying words from a file.
Start the server and refresh. The page should show:
| Word | Count |
|---|---|
| red | 3 |
| blue | 2 |
| green | 1 |
Change Word counts in the context dictionary to a title of your own. Save and refresh. Then add another green to sample.txt and refresh again. File changes are read on the next request; no new download is involved. Restore the original tiny text after this experiment.
Check One Request Without Opening a Browser
Django supplies a test client that calls the application without starting a listening server. We can use it with ordinary assertions.
Stop the server and save this as check_page.py beside manage.py:
# check_page.py import os from pathlib import Path from tempfile import TemporaryDirectory import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') django.setup() from django.test import Client, override_settings def test_word_count_page(): with TemporaryDirectory() as directory: base = Path(directory) (base / 'sample.txt').write_text( 'Red red blue.\nBlue red green!\n', encoding='utf-8' ) with override_settings(BASE_DIR=base, ALLOWED_HOSTS=['testserver']): response = Client().get('/') assert response.status_code == 200 html = response.content.decode('utf-8') assert '<td>red</td><td>3</td>' in html assert '<td>blue</td><td>2</td>' in html assert '<td>green</td><td>1</td>' in html if __name__ == '__main__': test_word_count_page() print('Page check passed.')
Run:
uv run check_page.py
Unlike manage.py, this standalone script needs to select the settings module and initialize Django. os.environ.setdefault() supplies that setting through the process's environment, and django.setup() loads Django's configuration and apps.
TemporaryDirectory() gives the check its own disposable folder, removed when the block ends. write_text() saves the known input there. The override_settings() block temporarily points our view's BASE_DIR to that folder and permits the test client's testserver hostname. The real sample.txt stays in place.
The assertions check a successful status and the rendered word/count cells. If you rearrange the HTML inside those rows, update the matching assertions too. This request check complements the existing tests of count_words(); it doesn't replace them or confirm the page's visual layout.
If the Page Doesn't Work
A browser saying it can't connect usually means the server stopped or you're visiting the wrong port. Check the terminal and address first.
A Django 404 means the server answered but no route matched. Visit / and check both urls.py files. The root route is written '', without a leading slash.
A Python traceback in the terminal or Django's development error page means our code failed while handling the request. FileNotFoundError points toward sample.txt and its location. TemplateDoesNotExist points toward the nested template folders or the app registration. Keep the original error visible while checking one explanation at a time.
For reference, our additions sit here:
wordsite/
manage.py
sample.txt
text_tools.py
test_text_tools.py
check_page.py
config/
settings.py
urls.py
counts/
views.py
urls.py
templates/
counts/
index.html
...other generated files and the uv environment...
Your Turn
Predict the Refresh
You change only sample.txt. Which part of the next request reads that change? Then suppose you stop the server and refresh the page. Why can't the saved HTML template answer the browser on its own at this address?
Follow a Broken Route
Temporarily change views.index to views.missing in counts/urls.py. Save and inspect the terminal error. Explain why the problem occurs before the counter can read any text. Restore the route and rerun the page check.
Display a Text of Your Own
Copy a permitted UTF-8 text into wordsite under a new filename. Change the view's source path and choose a word limit. Check a few counts with your existing terminal tools, then compare them with the page.
An empty input should show No words to count.. Verify that separately with an empty text file. Keep downloading or refreshing public data in a separate script, as in The Web, rather than making every page visit download it again.
For a further change, include the number of distinct words in the context with len(counts) and display it in a paragraph. Use the tiny input to check that the result is three.
What I've Learned
Explain where the Python runs, what the browser receives and how a URL reaches the word counter. Include how you start and stop the local server.
Optional: The Greeting as a Class
If you've completed Classes, here's how those ideas connect to Django. Our first greeting returned an HttpResponse from a function. A class-based view can return the same response from a method.
Keep the word-counting index() function. Add these imports at the top of counts/views.py and this class below the function:
# Add to counts/views.py from django.http import HttpResponse from django.views import View class GreetingView(View): def get(self, request): return HttpResponse('Hello from your Python program.')
class GreetingView(View): introduces inheritance. GreetingView is a new class based on Django's View, inheriting its request-handling machinery. We supply a get() method for HTTP GET requests, the kind the browser makes when visiting this page.
Like the deck's methods, get() receives an instance as self. Django supplies the request as its next argument. We don't need our own __init__ here; the inherited initialization is sufficient.
Replace counts/urls.py with this version, keeping the report at / and adding a greeting at /hello/:
# counts/urls.py (optional greeting route) from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('hello/', views.GreetingView.as_view(), name='hello'), ]
as_view() is provided by Django's View. Calling it returns a function that Django can call when a request arrives. That function creates a fresh GreetingView instance for each request, and the inherited dispatch() method selects get() for a GET request.
Earlier we passed views.index without calling it because it was already the function we wanted Django to call. Here, calling .as_view() produces that function from our class. Don't replace it with GreetingView() or call get() yourself in the URL configuration.
Start the server and visit the greeting. It should match the earlier function's message. Visit / again to confirm the word-count page still works, and rerun uv run check_page.py.
The deck's instance lasts as long as our game keeps it. Django's view instance lasts for one request, so an instance attribute won't preserve a score across page visits. Keeping progress across requests involves tools such as sessions or a database, beyond this comparison.
Both view styles return HTTP responses. Our word-count page can keep using its function; you now have a starting point for reading Django examples that use classes. The class-based view introduction develops that approach further.
Where Django Can Go Next
Django's models describe data stored through its database tools, its ORM lets Python code query that data, and its admin provides an interface for managing registered models. Forms, accounts and deployment build on further ideas. The official Django tutorial takes you through a database-backed application.
Our one-page app already connects familiar functions to the web. You can keep improving it or choose a different project using the skills you've learned.