Django SQLite: Set Up, Query and Tune the Default Database (Tested)

Django uses SQLite by default: a new project’s settings point at a db.sqlite3 file, which appears after the first migrate. The defaults have a few quirks worth knowing, like how contains filters treat case and how to avoid database is locked. In this article I’ll walk you through a Django project on SQLite from startproject to a working view, then the settings worth changing.

TLDR

  • New Django projects already use SQLite: ENGINE is django.db.backends.sqlite3 and the file is db.sqlite3. Don’t pip install sqlite3. It’s part of Python.
  • python manage.py migrate creates the file and the tables, and python manage.py dbshell opens it in the sqlite3 shell.
  • Set SQLite pragmas with init_command in DATABASES["default"]["OPTIONS"], for example PRAGMA journal_mode=WAL;.
  • "transaction_mode": "IMMEDIATE" plus a larger "timeout" help against database is locked. Both init_command and transaction_mode need Django 5.1 or later.

What is Django with SQLite, and what will we build?

Django supports SQLite through its built-in django.db.backends.sqlite3 backend. Its documentation says “Django supports SQLite 3.37.0 and later” and calls SQLite “an excellent development alternative for applications that are predominantly read-only or require a smaller installation footprint” (Django: SQLite notes).

To show you the whole flow, I will build a project with one blog app and an Article model. The last step serves published articles from a view.

Prerequisites

RequirementVersion used here
Python3.13.14
Django6.1.2
SQLite3.53.4 (the library Python was built with)
sqlite3 shellNeeded only for dbshell
Python 3.13.14 | Django 6.1.2 | SQLite 3.53.4

Building a Django project on SQLite

Run each command from the project folder, inside a virtual environment.

Step 1: Create the project

$ pip install django
Successfully installed asgiref-3.12.1 django-6.1.2 sqlparse-0.6.0
$ django-admin startproject mysite .
$ python manage.py startapp blog
$ ls
blog
manage.py
mysite
venv

Step 2: Check the database settings

startproject already points DATABASES at SQLite:

$ grep -A5 '^DATABASES' mysite/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Step 3: Add the app and a model

Add 'blog' to INSTALLED_APPS:

$ grep -A8 '^INSTALLED_APPS' mysite/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
]

Then define the model in blog/models.py:

from django.db import models


class Article(models.Model):
    title = models.CharField(max_length=100)
    body = models.TextField()
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Step 4: Create and apply the migration

$ python manage.py makemigrations blog
Migrations for 'blog':
  blog/migrations/0001_initial.py
    + Create model Article

sqlmigrate shows the SQL Django will run on SQLite. Notice the AUTOINCREMENT primary key and the bool type:

$ python manage.py sqlmigrate blog 0001
BEGIN;
--
-- Create model Article
--
CREATE TABLE "blog_article" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(100) NOT NULL, "body" text NOT NULL, "published" bool NOT NULL, "created_at" datetime NOT NULL);
COMMIT;

migrate creates db.sqlite3 with Django’s own tables and yours:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
  Applying blog.0001_initial... OK

Step 5: Query the model

Run this through python manage.py shell:

from blog.models import Article

Article.objects.create(title="Hello SQLite", body="Django's default database.", published=True)
Article.objects.create(title="Draft", body="Not yet.")
Article.objects.create(title="Second post", body="Still one file.", published=True)
print(Article.objects.count(), "articles")
print(list(Article.objects.filter(published=True).values_list("title", flat=True)))
Article.objects.filter(title="Draft").update(published=True)
print(Article.objects.filter(published=True).count(), "published after update")
print(Article.objects.filter(title__contains="sqlite").query)
print(list(Article.objects.filter(title__contains="sqlite").values_list("title", flat=True)))
$ python manage.py shell -v 0 < orm.py
3 articles
['Hello SQLite', 'Second post']
3 published after update
SELECT "blog_article"."id", "blog_article"."title", "blog_article"."body", "blog_article"."published", "blog_article"."created_at" FROM "blog_article" WHERE "blog_article"."title" LIKE %sqlite% ESCAPE '\'
['Hello SQLite']

The last line returns Hello SQLite for the filter title__contains="sqlite". Django’s docs warn that on SQLite “all matches are done case-insensitively” for substring matching, so contains behaves like icontains.

Step 6: Look at the database directly

dbshell opens the same file in the sqlite3 shell:

$ echo '.tables' | python manage.py dbshell
auth_group                  blog_article
auth_group_permissions      django_admin_log
auth_permission             django_content_type
auth_user                   django_migrations
auth_user_groups            django_session
auth_user_user_permissions
$ echo 'SELECT id, title, published FROM blog_article;' | python manage.py dbshell
1|Hello SQLite|1
2|Draft|1
3|Second post|1

Running it

Add a view in blog/views.py:

from django.http import JsonResponse

from .models import Article


def published(request):
    rows = Article.objects.filter(published=True).order_by("id").values("id", "title")
    return JsonResponse(list(rows), safe=False)

Point a URL at it in mysite/urls.py:

from django.urls import path

from blog.views import published

urlpatterns = [path("articles/", published)]

Start the server with python manage.py runserver 127.0.0.1:8057 and request the page:

$ curl -s http://127.0.0.1:8057/articles/
[{"id": 1, "title": "Hello SQLite"}, {"id": 2, "title": "Draft"}, {"id": 3, "title": "Second post"}]

Tune SQLite in settings.py

Django passes OPTIONS to the SQLite connection. These three settings matter for SQLite (Django: SQLite notes):

OptionWhat it does
init_commandSQL that runs on every new connection, such as pragmas
transaction_modeHow transactions start: DEFERRED (default), IMMEDIATE or EXCLUSIVE
timeoutSeconds to wait for a lock before raising database is locked

For example:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
        "OPTIONS": {
            "init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
            "transaction_mode": "IMMEDIATE",
            "timeout": 20,
        },
    }
}

After the change, a new connection reports WAL mode and synchronous level 1 (NORMAL):

$ python manage.py shell -v 0 -c "from django.db import connection; c = connection.cursor(); print(c.execute('PRAGMA journal_mode').fetchone()[0], c.execute('PRAGMA synchronous').fetchone()[0])"
wal 1

WAL lets readers keep reading while one connection writes. See SQLite WAL mode.

Common problems

The first two errors came up while testing. The third wasn’t reproduced here; its fixes come from Django’s docs.

ERROR: No matching distribution found for sqlite3

The sqlite3 module ships with Python, so there’s nothing to install:

$ pip install sqlite3
ERROR: No matching distribution found for sqlite3

RuntimeError: Model class blog.models.Article doesn’t declare an explicit app_label

The app isn’t in INSTALLED_APPS. Add it as in Step 3, then run makemigrations again.

OperationalError: database is locked

Another connection held the write lock longer than the timeout. Django’s docs list these fixes:

  • keep transactions short;
  • raise timeout, which only makes SQLite wait longer before it raises the error;
  • set transaction_mode to IMMEDIATE, so transactions wait for the lock up front.

They also say these errors can mean you’ve reached the point where a client/server database fits better. See database is locked.

Conclusion

SQLite is Django’s default for a reason: it needs no server, and migrate sets it up. Turn on WAL and IMMEDIATE transactions with OPTIONS when the site starts taking writes from several users. Move to PostgreSQL when many processes or servers need to write at once (SQLite vs PostgreSQL).

Further reading

Tested with Django 6.1.2 and Python 3.13.14 (SQLite 3.53.4) on 7 October 2026.