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:
ENGINEisdjango.db.backends.sqlite3and the file isdb.sqlite3. Don’tpip install sqlite3. It’s part of Python. python manage.py migratecreates the file and the tables, andpython manage.py dbshellopens it in thesqlite3shell.- Set SQLite pragmas with
init_commandinDATABASES["default"]["OPTIONS"], for examplePRAGMA journal_mode=WAL;. "transaction_mode": "IMMEDIATE"plus a larger"timeout"help againstdatabase is locked. Bothinit_commandandtransaction_modeneed 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
| Requirement | Version used here |
|---|---|
| Python | 3.13.14 |
| Django | 6.1.2 |
| SQLite | 3.53.4 (the library Python was built with) |
sqlite3 shell | Needed 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):
| Option | What it does |
|---|---|
init_command | SQL that runs on every new connection, such as pragmas |
transaction_mode | How transactions start: DEFERRED (default), IMMEDIATE or EXCLUSIVE |
timeout | Seconds 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_modetoIMMEDIATE, 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
- SQLite WAL mode
- SQLite advantages
- Python SQLite3 tutorial
- database is locked
- SQLite vs PostgreSQL
- Django docs: Databases: SQLite notes
Tested with Django 6.1.2 and Python 3.13.14 (SQLite 3.53.4) on 7 October 2026.
