Skip to content
Sign UpLog In
This post is read-only. Explore Repls and connect with other creators on Community.View Community
The info in this post might be out of date, check out our docs instead. View docs

Learning Web Development w/ Python Part 4


Learning Web Development with Python and Django

Part 4

Part 1


In Part 3, we learnt about apps, and set up our external database on ElephantSQL. In this tutorial, we will learn how to use databases with Django to create our Web Forum app.

In the last tutorial, we started our web app. You can use the same repl for this tutorial.

Setting up Django Admin

The first thing we need to do is to set up a Django admin account. This is an account that we can use to access the database on our website.
Start the repl, and instead of just hitting enter, type:

> python createsuperuser

You will be asked to enter a username and password - make sure that they are ones that you can remember! You do not have to enter your email if you don't want to.
Note: when you enter your password, no text will appear. This is for privacy reasons - typing works normally, it is just not displayed.

Cannot infer image mime type

Now that you have created your admin account, you can run the server and open it in a new tab.
To access your admin page, you need to add /admin to the end of your URL - so your URL should be
Enter your login details that you just created, and you should be presented with the Django admin page:

Cannot infer image mime type

Now we are ready to start creating our database.

What is a database?

Before we create our database, let's try to understand what a database actually is. You can visualise a database like a Excel spreadsheet Google sheet.

Cannot infer image mime type

A database is similar, but many features are re-named:

  • Sheet (e.g. People, Repls) becomes Entity (or table)
  • Column (e.g. First Name, Has A Pet?) becomes Attribute
  • Row (e.g. row 4) becomes Record

In reality, the data is not stored like this, but this is a good way of thinking of it.

Another key difference with a database is that each attribute in a database (column) has to be the same data type. Examples of data types are: text, datetime, number or boolean. Notice that these are not quite the same as data types in programming - e.g. a "string" is "text" in a database.

In nearly all databases, a primary key is used. This is a unique identifier, i.e. an attribute that has a different value for every single record in the database. It is most often a number. Django adds a primary key by default, so we don't need to worry about including one when designing our entity.

Creating our first Entity

Let's create an entity to store information about each Post that is made to the web forum.
Before creating your entity, it is good to make a plan about what attributes the entity should have. For this entity, some good attributes could be:

  • title - Title of post. Limited to 50 characters.
  • text - Content of the post. Not limited.
  • author - Creator of the post. Limited to 30 characters.
  • date - Date that post was created.

Django creates and modifies entities using models. To see this in action, navigate to Posts/ and add the following code:

from django.db import models class Post(models.Model): title = models.CharField(max_length=50) text = models.TextField() author = models.CharField(max_length=30) date = models.DateTimeField(auto_now_add=True)

What does this code do?

  • On Line 1, we import models. This is a very handy module in Django that allows us to interface with databases very easily.
  • On Line 3 we define our Post class. This is our database entity. It inherits from models.Model, which means that all the code from models.Model, which interfaces with the database, applies to our entity.
  • On Lines 4-7 we declare the attributes that our entity is going to have
    • title is a CharField, or character field - this just means that it has letters in it. We have set the maximum size to be 50 characters.
    • text is a TextField. This is similar to a CharField, but has no limit to the amount of text it can hold.
    • author is another CharField, this time with a maximum length of 30 characters.
    • date is a DateTimeField - not surprisingly, for storing date and time. The auto_now_add=True is a useful Django function which means that the date and time will be set automatically to whenever the Post was created.

Now we have created our model, we need to apply it to the database. This is called migrating.
Run the repl, but instead of just hitting enter to start the server, type the following line:

> python makemigrations

This tells Django to look for changes to the database model. Now you need to apply those changes, by typing:

> python migrate

This creates our entity in the database! The last thing we need to do, in order to see the database on our admin page, is change the file Posts/ Add the following:

from django.contrib import admin from .models import Post

This code imports the admin module from Django, and tells Django to add our entity (Post) to the admin page.
If you go to your admin page now, you can see that our entity has been added!

Cannot infer image mime type

Click on Add next to Post and create a record for the database.

Cannot infer image mime type

If you save the record, then you can see that the record has been added to the database (The post "Post object (1)" was added successfully.).

Database Web Pages

It's all very well being able to change the database when you're the administrator, but how do we get the database to interact with a web page? The answer is that we use some of Django's pre-defined views. Our current home page is not very interesting - let's change it so that it lists all of the posts in the database.


Go to Posts/ and delete all the code from it. Now we can start coding our view.

from django.views.generic import ListView from .models import Post class HomePageView(ListView): model = Post template_name='home.html'
  • Line 1 of our code imports ListView, which is a Django view specifically for listing records in an entity.
  • On Line 2 we import our Post entity from This is the entity that our page will be based off of.
  • On Line 4, we create our view, HomePageView. Our view inherits from ListView, which means that we can use our view to list all of the records.
  • On Line 5 we define which entity we are using - the Post model. This is just the entity that we created in and imported on Line 2
  • On Line 6, we state which template we will be using - home.html


We will also need to update our template in order to display all of our records.
Go to templates/home.html and delete all the html from it. Now we can start writing our template.

{% extends 'base.html' %} {% block title %}Home{% endblock title %} {% block content %} {% for post in object_list %} <div class="post"> <h3>{{ post.title }}</h3> <p>{{ }}</p> </div> <p>----------------------</p> {% endfor %} {% endblock content %}

The first 4 lines are standard html along with Django's template language, which we have covered before.
On Line 5, we have our first new piece of code.

{% for post in object_list %}

This is a new part of Django's template language. When we use ListView, it passes in to the html document a list of all the records in the entity. This list can be referenced with object_list. So here, we are saying "for each post in object_list, do the following".
On Line 6, we use a div element, with a post class. This is not necessary, but will be useful when styling the page with CSS later.
On Lines 7-8 we put the title of the post, and the author, on to the screen.
On Line 9, we close the div, then on Line 10 we add a divider to separate the items on the page.
On Line 11 we end the for loop, so only the html between Lines 5-11 are repeated for each record


We already created the URL for the home page, so we can skip this step.

If you run the server now, and navigate to your home page (, then you should see something like this:

Cannot infer image mime type

It worked! Go to the admin page, and add another post. Now if you go back to the homepage, you should see something like this:

Cannot infer image mime type

Congratulations! You've made your first database-driven web page!

Individual Pages

Now we are going to make individual pages for each post - so each post has its own page that people can visit. To do this, we will use another Django class, DetailView.


Add the following to Posts/

from django.views.generic import ListView, DetailView # new from .models import Post # stuff class PostPageView(DetailView): model = Post template_name = 'post_page.html'

We create a view that inherits from DetailView, and set the model to our Post entity and the template to post_page.html (that we haven't created yet).


Create a new file at templates/post_page.html, and add the following html:

{% extends 'base.html' %} {% block title %}{{ post.title }}{% endblock title %} {% block content %} <p>Posted by: {{ }}</p> <p>On: {{ }}</p> <h1>{{ post.title }}</h1> <p>{{ post.text }}</p> {% endblock content %}

This sets up a basic html template for the post. It can be confusing as to why post is used - the reason is that it is the lowercase name of our entity. You may see it also written as object, e.g.

<h1>{{ object.title }}</h1>

The rest of the html is fairly straightforward - we are just putting in all the data that we defined in our entity model in Posts/


To configure the URL, we are going to use the primary key of the database (remember, the primary key is a unique number given to every record of the database). Django automatically adds a primary key to each entity, and its keys are always incremented - the first record added to the database has a primary key of 1, the second has a primary key of 2, and so on.

Go to Posts/, and add the following code:

from django.urls import path from .views import HomePageView, PostPageView # new urlpatterns = [ path('post/<int:pk>/',PostPageView.as_view(), name='post'), # new path('', HomePageView.as_view(), name='home'), ]

This should all be familiar - the only new thing here is on Line 5. The <int:pk> tells Django to use the primary key of the number after /post/ in the URL. int just tells Django that the key will be an integer.
If you run the server now, and go to, you should get a page displaying the information about the first post you created!

Cannot infer image mime type

Linking to the Post

However, typing in the URL all the time is not very user-friendly, so let's go back to the home page and add a link to each post.

We just need to edit the template for this, so navigate to templates/home.html and change the html to:

{% extends 'base.html' %} {% block title %}Home{% endblock title %} {% block content %} {% for post in object_list %} <div class="post"> <h3><a href="{% url 'post' %}">{{ post.title }}</a></h3> <p>{{ }}</p> </div> <p>----------------------</p> {% endfor %} {% endblock content %}

We've met the url function before - it takes a name and returns the URL. Here, the name takes an argument - the primary key of the post - so we just put that afterwards, and Django takes care of everything.

If you go to your home page now, you can click on the titles to view the post!

Creating and Editing Posts

Now, what about creating and editing posts? Django makes this too very quick and easy.

First, let's create a page where you can make a new post.


Go to Posts/, and add the following code:

# stuff from django.views.generic.edit import CreateView # stuff class PostCreateView(CreateView): model = Post template_name = 'new_post.html' fields = ['title','author','text']

First, we import CreateView, which is yet another pre-built class that Django supplies us with. This one is made especially for creating records for entities.

Then we declare our PostCreateView, inheriting from CreateView. We set the entity model to Post and the template to new_post.html (which we haven't created yet). Then we set our fields. This is the data that we want the user to create. For this, the date will be created automatically, so we just need 'title', 'author' and of course 'text'.


Create a new file at templates/new_post.html, and add the following html:

{% extends 'base.html' %} {% block title %}Create a new post{% endblock title %} {% block content %} <h2>Post a Question to the Forum!</h2> <form action="" method="POST">{% csrf_token %} {{ form.as_p }} <input type="submit" value="Save"> </form> {% endblock content %}

Here, the most interesting bit is from Lines 8-11.

  • On Line 8 we create a html form element. This is an element that lets users input information. The action attribute is where to send the data when it is submitted - if we set it to "", Django will take care of it.
  • The method is how the data is submitted. There are two main methods, POST and GET. GET puts the information in the URL - it is often used for searching. For example, a google search gives you a URL such as This is a GET because the data is in the URL. POST doesn't put the data in the URL, so it is more secure.
  • Then we have {% csrf_token %}. This is a Django function to stop people using Cross-Site Request Forgery to attack users on your website. Always have this on your forms!
  • On Line 9 we tell Django to put the form in as <p> elements.
  • On Line 10, we have a "submit" button. When the user clicks this, the data will be sent to Django
  • On Line 11, we end the form.


Go to and add the following:

from .views import HomePageView, PostPageView, PostCreateView urlpatterns = [ path('create',PostCreateView.as_view(), name='create_post'), path('post/<int:pk>/',PostPageView.as_view(), name='post'), path('', HomePageView.as_view(), name='home'), ]

This is all standard.

The last thing we need to do is specify what URL the form needs to go to once the form has been filled in. To do this, go to Posts/ and add the following:

from django.db import models from django.urls import reverse # new class Post(models.Model): # stuff def get_absolute_url(self): return reverse('post',args=[str(])

First, we import the reverse function - this is similar to the url function that we use with the template language. When the form is submitted, Django automatically checks the get_absolute_url method, so we just need to define it in order for the redirect to happen. Here we are redirecting to the post page, giving the id (pk) of the record as an argument. This means that when the form is completed, it will redirect to the page with the post on it.

Now we can go to our, fill in the form, and create a new post!


Next, we will make a view that allows website visitors to edit existing posts to the forum. To do this, we will use the UpdateView that Django gives us.

The process for creating new pages should be pretty familiar by now.


Add the following to Posts/

from django.views.generic.edit import CreateView, UpdateView class PostUpdateView(UpdateView): model = Post template_name = 'edit_post.html' fields = ['title','text']

This is pretty similar to the CreateView - the only difference is that we don't let the user change the author.


Create a file at templates/edit_post.html and add the following html:

{% extends 'base.html' %} {% block title %}Edit Post{% endblock title %} {% block content %} <h1>Post title</h1> <form action="" method="POST">{% csrf_token %} {{ form.as_p }} <input type="submit" value="Update"> </form> {% endblock content %}

This is almost identical to the CreateView.


Finally, add the following to Posts/

from .views import HomePageView, PostPageView, PostCreateView, PostUpdateView urlpatterns = [ path('post/<int:pk>/edit/',PostUpdateView.as_view(), name='update_post'), # stuff ]

This will create an edit page at
To test this out, try just entering 1 instead of <pk> and you should be able to edit your first post!

Deleting Records

Finally, we will add a page that allows users to delete posts, using Django's DeleteView.


Add this to Posts/

from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.urls import reverse_lazy class PostDeleteView(DeleteView): model = Post template_name = 'delete_post.html' success_url = reverse_lazy('home')

This is fairly standard. The success_url tells Django what page to redirect to after the post has been deleted. We are using reverse_lazy instead of reverse here - what is the difference? reverse runs straight away, which means it can run before the rest of the program is ready - before 'home' has been initialised. To fix this, reverse_lazy is used, which waits until everything has loaded before getting the URL.


Create a file at templates/delete_post.html and add the following html:

{% extends 'base.html' %} {% block title %}Delete Post{% endblock title %} {% block content %} <h1>Delete Post</h1> <form action="" method="POST">{% csrf_token %} <p>Are you sure you want to delete <strong>{{ post.title }}</strong></p> <input type="submit" value="Confirm"> </form> {% endblock content %}

This will just create a page with a Confirm button and a confirmation message. <strong> creates bold text.


Finally, add this to Posts/

from .views import HomePageView, PostPageView, PostCreateView, PostUpdateView, PostDeleteView urlpatterns = [ path('post/<int:pk>/delete/',PostDeleteView.as_view(),name='delete_post'), # stuff ]

If you go to , you should be able to delete your post.

Cannot infer image mime type


That was quite a long tutorial, but we learnt how to use databases with Django! We went through it quite quickly, so you may want to read it more than once to gain a proper understanding of what is happening. You can post any questions below. In the next tutorial, we will look at user authentication, as well as making the site a bit more user-friendly.

As always, please upvote if you found this tutorial helpful, it supports me and lets me know that you want more! If you have any questions, post in the comments and I (or someone else) will answer them.

4 years ago




what's up

waiting for Tutorial 5!

3 years ago

Hi. Thank you for this tutorial. It was very helpful. When will you post the next lesson?

3 years ago

Thank you :) I posted this tutorial ages ago, and I never got round to the next part. I'll see if I can find the tutorial file and complete it.

So hopefully I will post it soon :)

3 years ago

createsuperuser fails

Traceback (most recent call last): File "", line 22, in <module> main() File "", line 18, in main execute_from_command_line(sys.argv) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/core/management/", line 419, in execute_from_command_line utility.execute() File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/core/management/", line 413, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/core/management/", line 354, in run_from_argv self.execute(*args, **cmd_options) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/contrib/auth/management/commands/", line 79, in execute return super().execute(*args, **options) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/core/management/", line 398, in execute output = self.handle(*args, **options) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/contrib/auth/management/commands/", line 157, in handle validate_password(password2, self.UserModel(**fake_user_data)) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/contrib/auth/", line 44, in validate_password password_validators = get_default_password_validators() File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/contrib/auth/", line 19, in get_default_password_validators return get_password_validators(settings.AUTH_PASSWORD_VALIDATORS) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/contrib/auth/", line 30, in get_password_validators validators.append(klass(**validator.get('OPTIONS', {}))) File "/home/runner/Django-Try-Again/venv/lib/python3.8/site-packages/django/contrib/auth/", line 213, in __init__ with open(password_list_path) as f: FileNotFoundError: [Errno 2] No such file or directory: '/home/runner/.cache/pip/pool/d4/1e/2a/common-passwords.txt.gz'
7 months ago

This used to work for me. But it no longer does. I haven't changed anything. Can somebody please take a look and advise? I was so proud of this little application when it worked :(

3 years ago

Hmm, that's strange. Could you try forking the repl?'s web repls, especially Django, seem to be a bit odd in that they can sometimes stop working, but forking or refreshing often fixes them. Sometimes just leaving them for a while fixes them as after they 'go to sleep' allocates them a different container.

3 years ago

Thank you! I think you're right. I have had to wait an hour or two in the past and when I go back to it, it miraculously works :D
Waiting for your next Python web application tutorial !!!

3 years ago

Thanks, Archie. Well explained for beginners.

3 years ago

Can we use this code in our own projects? Also when is the next tutorial?

4 years ago

You can! Also, a fair amount of Django is repetitive, so for a lot of things you can just copy/paste and change a few names of files/classes to get it up and running.

The next part should hopefully be out in 2 days or so.

4 years ago
Load more