Introduction
For a long time I wanted to document something I have done many times in production systems but never explained clearly: using Django ORM as a standalone module to connect to an existing database.
In my work I have often dealt with legacy systems where the only reliable source of truth was the database itself. In those situations, Django ORM became my Swiss army knife. With just a few lines of configuration I could connect to an existing database, introspect its schema using inspectdb, and start querying data from the shell through an API I already knew.
Recently I realized I had finally found a simple and reproducible way to demonstrate this approach step by step, so that anyone can try it locally and understand how Django ORM can operate independently from a full Django project.
Databases you already use
Every day we interact with many applications that rely on embedded databases. The most common example is the browser, which usually stores information such as bookmarks and history in SQLite files. SQLite is fully supported by Django out of the box through its built-in backend, which makes it a perfect candidate for a reproducible experiment.
My current browser (Firefox) stores bookmarks in a SQLite file called places.sqlite. If you use a different browser, you simply need to locate the SQLite file inside your browser profile and you can follow this guide in exactly the same way.
The goal of this article is not to explore browser data, but to demonstrate how Django ORM can work in standalone mode against an existing database created and managed by another system.
Minimal Django project
Since our goal is to use Django ORM in standalone mode, we do not need a full Django project structure. For this first step, where we simply want to connect Django to an existing SQLite database, a single file is enough.
Instead of generating an entire Django project, we can create a minimal manage.py file that configures only what is strictly necessary: the database connection. For clarity and flexibility, the path to the SQLite file is read from an environment variable.
The first step is to create a virtual environment with a recent version of Python and install Django in it, using the tools you prefer.
Create a file named manage.py with the following content:
import os
from django.conf import settings
from django.core import management
settings.configure(
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.getenv("SQLITE_PATH"),
}
},
INSTALLED_APPS=[]
)
if __name__ == "__main__":
management.execute_from_command_line()
Before running any command, export the path to your SQLite database:
export SQLITE_PATH=/path/to/your/places.sqlite
This minimal setup does one thing: it tells Django how to connect to an existing SQLite database file. There are no apps, no migrations, no admin, no server. Just a configured database connection and access to Django’s management commands.
With this foundation in place, we can now interact with a real database that Django did not create.
Verifying the database connection
Before using the ORM layer, it is useful to verify that Django is really connected to the intended database.
$ python manage.py dbshell
Because we configured the SQLite backend, Django opens the SQLite shell for the file we specified.
sqlite> .tables
...
moz_places
...
These are real Firefox tables. We are not inside a database created by Django. We are directly connected to a production-grade database managed by another application.
Generating models from an existing database with inspectdb
This is where Django ORM shows its strength. Instead of manually defining models for an unknown schema, we let Django introspect the existing database.
We are interested in the moz_places table and we use the inspectdb command to generate a model based on its structure and store it in a models.py file inside a places directory:
$ mkdir places
$ python manage.py inspectdb moz_places > places/models.py
The generated file includes a comment explaining that it is auto-generated and describing the characteristics of the created model:
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
# * Rearrange models' order
# * Make sure each model has one field with primary_key=True
# * Make sure each ForeignKey and OneToOneField has `on_delete` set to the desired behavior
# * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
After removing the fields we do not need and refining field types and attributes, the essential version of the model looks like this:
from django.db import models
class Place(models.Model):
url = models.URLField()
title = models.CharField(null=True)
description = models.TextField(null=True)
class Meta:
managed = False
db_table = "moz_places"
The key detail is managed = False, which ensures that Django will not attempt to create or modify this table. We did not design the schema and we are not migrating it. We are simply mapping and querying an existing database.
Querying an existing database with Django ORM
To load the model, add the places app to INSTALLED_APPS:
settings.configure(
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.getenv("SQLITE_PATH"),
}
},
INSTALLED_APPS=["places"]
)
Add an empty __init__.py file so that the directory becomes a Python module:
$ touch places/__init__.py
Now open the Django shell:
First, count how many Place instances are in the table:
>>> Place.objects.count()
54184
Count bookmarks that point to the official Django website:
>>> Place.objects.filter(url__startswith="https://www.djangoproject.com").count()
100
Filter by title and retrieve only title and description:
>>> Place.objects.filter(title__contains="Django").values_list("title", "description")
<QuerySet [
("Django", "The web framework for perfectionists with deadlines"),
("Django Chat", "A biweekly podcast on the Django Web Framework"),
("Django Forum", "Discussion of the Django framework"),
("Django Girls", "A one-day workshop about programming ..."),
("Django News", "Weekly Django news, articles, projects, ..."),
("Djangonaut Space", "A contributor mentorship program ..."),
]>
I could continue here by demonstrating more advanced ORM features, but that would go beyond the scope of this article.
The goal was to show how to use Django ORM in standalone mode against an existing database. From here, you can explore Django’s official documentation for more advanced queries, annotations, aggregations, and custom database functions.
Recap
In this article we:
- Created a minimal Django setup in a single file
- Configured
DATABASESto point to an existing database - Used
inspectdbto generate models from an existing schema - Queried real data using Django QuerySets
All of this was done using only one of Django’s “batteries included” components: the ORM.
Practical use cases
This example is deliberately simple and accessible, but the pattern scales far beyond browser data.
It is useful when:
- Exploring legacy databases
- Reverse engineering unknown schemas
- Auditing external datasets
- Prototyping data analysis scripts
- Migrating data between databases
The goal is not to embed Django inside another system, but to demonstrate that Django ORM can already act as a lightweight, standalone data access layer. In the next article of this series, we will extend this minimal setup and explore how it can evolve into a practical data migration workflow.