TortoiseORM is a modern, easy-to-use Object-Relational Mapping (ORM) library for Python that leverages the power of asyncio to provide high-performance database operations.
Unlike traditional ORMs that use synchronous operations, TortoiseORM enables you to work with databases asynchronously, making it ideal for modern web applications and APIs where responsiveness is crucial.
This tutorial will guide you through using TortoiseORM with SQLite.
Prerequisites
Before starting this tutorial, make sure you have installed Python 3.13 or a newer version. A basic understanding of Python programming and familiarity with the async/await
concepts in Python are essential. While basic SQL knowledge can be helpful, it is not a strict requirement.
Step 1 — Setting up your TortoiseORM project
Before diving into database operations, you must set up your project environment. In this step, you'll organize your project directory, create a virtual environment to manage dependencies, and install TortoiseORM.
To get started, create a directory for your project and move into it:
mkdir tortoise-tutorial && cd tortoise-tutorial
Next, create a Python virtual environment to keep your project dependencies isolated:
python3 -m venv venv
Activate the virtual environment to start using it:
source venv/bin/activate
Now that your virtual environment is active, install TortoiseORM and pydantic:
pip install tortoise-orm pydantic
This installs the latest version of TortoiseORM and Pydantic, which are necessary for model validation and serialization.
For this tutorial, you'll use SQLite as the database backend because it doesn't require a separate server installation and stores data in a single file, making it a convenient choice. With TortoiseORM set up, you’re now ready to create your first database connection.
Step 2 — Understanding TortoiseORM components and creating your first model
This section introduces the core components of TortoiseORM and guides you through creating your first database model. Understanding these elements is vital for developing efficient and scalable database-driven applications with TortoiseORM.
Before we dive into code, let's explore the key components of TortoiseORM:
- Tortoise: The main controller that manages connections and model registration
- Model: The base class for defining database models
- Field: Various field types for different data types
- QuerySet: Provides methods for database querying
- Connection: Manages database connection pooling and query execution
Let's create a basic script called
database.py
that sets up these components:
from tortoise import Tortoise
# Database connection configuration
DB_URL = "sqlite://db.sqlite3"
# Initialize Tortoise ORM
async def init_db():
await Tortoise.init(
db_url=DB_URL,
modules={'models': ['models']} # Path to your models module
)
# Generate schemas for all models
await Tortoise.generate_schemas()
# Close connections
async def close_db():
await Tortoise.close_connections()
This script sets up the foundation for your TortoiseORM database.
The DB_URL
defines the connection string for SQLite. TortoiseORM supports various databases, but we're using SQLite for simplicity.
The init_db()
function initializes Tortoise with the database URL and registers the model modules. The modules
parameter specifies where to find your model definitions. In this case, it will look for models in the models.py
file.
Finally, the generate_schemas()
method automatically creates tables based on your model definitions. The close_db()
function properly closes database connections when you're done.
For this tutorial, you'll build a simple task management system. Let's define a model for tasks.
Create a file called models.py
:
from tortoise import fields, models
from tortoise.contrib.pydantic import pydantic_model_creator
class Task(models.Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200, nullable=False)
description = fields.TextField(null=True)
is_completed = fields.BooleanField(default=False)
due_date = fields.DateField(null=True)
priority = fields.IntField(default=1)
category = fields.CharField(max_length=100, default="General")
created_at = fields.DatetimeField(auto_now_add=True)
modified_at = fields.DatetimeField(auto_now=True)
def __str__(self):
return self.title
class Meta:
table = "tasks"
# Generate Pydantic model for Task
Task_Pydantic = pydantic_model_creator(Task, name="Task")
TaskIn_Pydantic = pydantic_model_creator(Task, name="TaskIn", exclude_readonly=True)
This code defines a Task
model that maps to a 'tasks' table in the database.
The __tablename__
attribute is defined through the Meta
class with the table
property, specifying the table name as "tasks".
This model includes multiple fields, each with a specific data type to represent different attributes of a task:
id
: A primary key that auto-increments.title
: A string field that cannot be null (nullable=False
).description
: An optional text field for task details.is_completed
: A boolean field with a default value ofFalse
.due_date
: An optional date field.priority
: An integer field with a default value of 1.category
: A string field with a default value of "General".created_at
: A datetime field that automatically records when the task is created.modified_at
: A datetime field that automatically updates when the task is modified.
The __str__
method returns a readable string representation of Task
objects, making them easier to inspect in the console.
A notable feature of TortoiseORM is the pydantic_model_creator
function, which generates Pydantic models for data validation and serialization. This is particularly useful when integrating with APIs like FastAPI. In this example, we create two versions:
Task_Pydantic
: Represents the complete task model including read-only fields.TaskIn_Pydantic
: Used for input validation when creating or updating tasks, excluding read-only fields likeid
and timestamps.
Now, let's create a script to initialize the database. Create a file called init_db.py
:
import asyncio
from database import init_db
async def main():
print("Initializing database...")
await init_db()
print("Database initialized successfully!")
if __name__ == "__main__":
asyncio.run(main())
Since TortoiseORM is asynchronous, you must use asyncio
to run the functions. The script creates the database and generates tables based on your model definitions.
Run this script to initialize the database:
python init_db.py
You should see output similar to:
Initializing database...
Database initialized successfully!
The db.sqlite3
file is created in your project directory, containing an empty 'tasks' table, ready for storing task data.
Step 3 — Adding data to your database
In this section, you'll learn how to insert records into your SQLite database using TortoiseORM's asynchronous API. You'll create task objects, add them to the database, and see how TortoiseORM converts Python objects and database records.
Now that your database structure is set up with the Task
model, let's add some data. Create a file called add_data.py
:
import asyncio
from datetime import date, timedelta
from database import init_db, close_db
from models import Task
async def populate_data():
# Initialize the database
await init_db()
# Create some tasks
today = date.today()
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(days=7)
# Create work tasks
await Task.create(
title="Complete quarterly report",
description="Finalize Q1 financial report for management review",
category="Work",
priority=3,
due_date=tomorrow
)
await Task.create(
title="Team meeting",
description="Weekly team sync meeting",
category="Work",
priority=2,
due_date=today
)
# Create personal tasks
await Task.create(
title="Grocery shopping",
description="Buy ingredients for dinner",
category="Personal",
priority=2,
due_date=today
)
# Create learning tasks
await Task.create(
title="Complete Python course",
description="Finish the advanced Python programming course",
category="Learning",
priority=1,
due_date=next_week
)
await Task.create(
title="Read TortoiseORM documentation",
description="Study the advanced features of TortoiseORM",
category="Learning",
priority=2,
due_date=tomorrow
)
print("Data populated successfully!")
# Close database connections
await close_db()
if __name__ == "__main__":
asyncio.run(populate_data())
In this code, you first call init_db()
to ensure the database is properly initialized. Then, you create task objects using the Task.create()
method, which is an asynchronous method that returns the created object after inserting it into the database.
The script uses the date
and timedelta
classes from the datetime
module to set due dates for tasks. Each task is assigned a category as a string, a priority level, and other attributes.
The Task.create()
method is an awaitable, so you use the await
keyword to wait for the database operation to complete. This is a key aspect of TortoiseORM's asynchronous approach.
Finally, you close the database connections with close_db()
to release resources.
Run this script to add the data:
python add_data.py
You should see output like:
Data populated successfully!
With this data in your database, you can query it in the next step.
Step 4 — Querying data from your database
In this section, you'll retrieve data from your SQLite database using TortoiseORM's asynchronous query API.
Now that you have some tasks in your database, let's write queries to retrieve them. Create a file called query_tasks.py
:
import asyncio
from database import init_db, close_db
from models import Task
async def query_tasks():
# Initialize the database
await init_db()
# Get all tasks
print("==== All Tasks ====")
all_tasks = await Task.all()
for task in all_tasks:
print(f"{task.title} | Category: {task.category} | Due: {task.due_date}")
# Close the session
await close_db()
if __name__ == "__main__":
asyncio.run(query_tasks())
In this code, the Task.all()
method retrieves all tasks from the database as a list of Task
objects. Each object allows access to its attributes, such as title
, category
, and due_date
.
Run this script to see all the tasks in your database:
python query_tasks.py
You should see output like:
==== All Tasks ====
Complete quarterly report | Category: Work | Due: 2025-03-14
Team meeting | Category: Work | Due: 2025-03-13
Grocery shopping | Category: Personal | Due: 2025-03-13
Complete Python course | Category: Learning | Due: 2025-03-20
Read TortoiseORM documentation | Category: Learning | Due: 2025-03-14
The output shows that TortoiseORM retrieves all tasks from the database and presents them in a structured, readable format, displaying key details like title, category, and due date.
In many cases, you may need to retrieve only the records that meet specific criteria. You can achieve this using queries like the following:
# Query tasks by a specific category
work_tasks = await Task.filter(category="Work")
print("\n==== Work Tasks ====")
for task in work_tasks:
print(f"- {task.title}, due on {task.due_date}")
# Query tasks due today
from datetime import date
today = date.today()
today_tasks = await Task.filter(due_date=today)
print("\n==== Tasks Due Today ====")
for task in today_tasks:
print(f"- {task.title} ({task.category})")
The filter()
method allows you to define conditions for your query, similar to SQL's WHERE
clause.
To control the order of the retrieved records, you can use the order_by()
method:
# Order tasks by priority (highest first)
ordered_tasks = await Task.all().order_by('-priority')
print("\n==== Tasks by Priority (Highest First) ====")
for task in ordered_tasks:
print(f"- Priority {task.priority}: {task.title}")
# Order tasks by due date (earliest first)
date_ordered_tasks = await Task.all().order_by('due_date')
print("\n==== Tasks by Due Date ====")
for task in date_ordered_tasks:
print(f"- Due {task.due_date}: {task.title}")
If you need only a single record, use first()
instead of all()
. This retrieves the first matching result and avoids returning a list:
# Get the first task
first_task = await Task.all().first()
# Get a specific task by ID
task_by_id = await Task.get(id=2) # Gets task with ID 2
For more complex queries, TortoiseORM provides the Q
object for combining conditions:
from tortoise.expressions import Q
# Tasks that are high priority OR due today
complex_query = await Task.filter(Q(priority=3) | Q(due_date=today))
These query methods are essential for retrieving data with TortoiseORM.
Now that you can fetch records, the next step is updating existing records in your database.
Step 5 — Updating records in your database
In this section, you'll update existing records in your SQLite database using TortoiseORM. You'll retrieve objects from the database, modify their attributes, and save the changes, allowing you to manage and update your data efficiently.
The most common way to update data with TortoiseORM is to query for a record, modify its attributes, and commit the changes. To get started, create a file called update_tasks.py
:
import asyncio
from database import init_db, close_db
from models import Task
async def update_tasks():
# Initialize the database
await init_db()
# Find the task to update
print("=== Before update ===")
task = await Task.get(title="Read TortoiseORM documentation")
print(f"{task.title} current priority: {task.priority}")
# Update the task's priority
task.priority = 3
await task.save()
# Verify the update
updated_task = await Task.get(id=task.id)
print(f"=== After update ===")
print(f"{updated_task.title} new priority: {updated_task.priority}")
# Close the database connection
await close_db()
if __name__ == "__main__":
asyncio.run(update_tasks())
First, you query the database to find the task you want to update using get()
. Following that, you modify the task's attributes directly, changing its priority. Finally, you save the changes with task.save()
, which sends the UPDATE statement to the database.
Run this script:
python update_tasks.py
You should see output like:
=== Before update ===
Read TortoiseORM documentation current priority: 2
=== After update ===
Read TortoiseORM documentation new priority: 3
This approach, where you modify objects and let TortoiseORM handle the SQL generation, is both intuitive and powerful. It lets you update multiple attributes simultaneously, while TortoiseORM automatically creates the appropriate UPDATE statement.
You can modify multiple attributes of an object before saving:
# Find the task to update
task = await Task.get(title="Complete Python course")
# Update multiple attributes
task.priority = 3
task.is_completed = True
task.title = "Complete Advanced Python course"
# Save changes
await task.save()
TortoiseORM will generate a single UPDATE statement that changes all modified attributes.
If you need to update multiple records that meet specific criteria, use the update()
method on the query object:
# Bulk update example - mark all low priority tasks as completed
tasks_updated = await Task.filter(priority=1).update(is_completed=True)
print(f"Updated {tasks_updated} tasks")
# Increase priority for all work tasks
work_tasks_updated = await Task.filter(category="Work").update(priority=Task.priority + 1)
print(f"Updated {work_tasks_updated} work tasks")
This method is more efficient for bulk updates since it executes a single SQL UPDATE
statement instead of fetching and modifying each record individually.
Now that you can update records in the database, you will delete them next.
Step 6 — Deleting records from your database
In this section, you'll delete records from your SQLite database using TortoiseORM. You'll explore how to remove individual records and perform bulk deletions based on specific conditions.
A typical way to delete records is to retrieve the object and then use the delete()
method.
To implement this, create a file called delete_tasks.py
:
import asyncio
from database import init_db, close_db
from models import Task
async def delete_tasks():
# Initialize the database
await init_db()
# Count tasks before deletion
count_before = await Task.all().count()
print(f"Total tasks before deletion: {count_before}")
# Find and delete a task
task_to_delete = await Task.get(title="Grocery shopping")
if task_to_delete:
print(f"Found task to delete: {task_to_delete.title} in category {task_to_delete.category}")
await task_to_delete.delete()
print(f"Task deleted successfully")
else:
print("Task not found")
# Count tasks after deletion
count_after = await Task.all().count()
print(f"Total tasks after deletion: {count_after}")
# List remaining tasks
remaining_tasks = await Task.all()
print("\nRemaining tasks:")
for task in remaining_tasks:
print(f"- {task.title} ({task.category})")
# Close the database connection
await close_db()
if __name__ == "__main__":
asyncio.run(delete_tasks())
In this code, you first check how many tasks you have by using Task.all().count()
. Then you find the specific task to remove with Task.get()
.
Once found, you delete it with task_to_delete.delete()
and actually perform the deletion by awaiting this coroutine.
To confirm the deletion worked, you count the tasks again and list the remaining ones to see that "Grocery shopping" is no longer in the database.
Run this script:
python delete_tasks.py
You should see output like:
Total tasks before deletion: 5
Found task to delete: Grocery shopping in category Personal
Task deleted successfully
Total tasks after deletion: 4
Remaining tasks:
- Complete quarterly report (Work)
- Team meeting (Work)
- Complete Python course (Learning)
- Read TortoiseORM documentation (Learning)
You can also delete records by primary key if you know it:
# Delete a task by ID
task_id = 2 # ID of the task to delete
await Task.filter(id=task_id).delete()
print(f"Deleted task with ID {task_id}")
For cases where you need to delete multiple records that match certain criteria without retrieving them first, you can use the query's delete()
method:
# Bulk delete example
deleted_count = await Task.filter(is_completed=True).delete()
print(f"Deleted {deleted_count} completed tasks")
This method is more efficient than fetching objects and deleting them individually, especially when dealing with large numbers of records, because it executes a single DELETE statement with a WHERE clause instead of multiple statements.
With this, you now have a solid understanding of the basic CRUD operations with TortoiseORM.
Final thoughts
This tutorial has covered the essential features of TortoiseORM, from setting up an asynchronous database connection to defining models and performing CRUD operations with SQLite.
TortoiseORM’s integration with Pydantic and frameworks like FastAPI makes it a strong choice for API development, allowing for efficient data validation and serialization.
Now that you have a solid foundation, you can explore more advanced topics, such as defining relationships between tables, optimizing queries, and using model signals for event-driven actions.
To learn more, visit the official TortoiseORM documentation.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github