Back to Scaling Python Applications guides

Getting Started with TortoiseORM for Python

Stanley Ulili
Updated on March 13, 2025

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:
database.py
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:

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 of False.
  • 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:

  1. Task_Pydantic: Represents the complete task model including read-only fields.
  2. TaskIn_Pydantic: Used for input validation when creating or updating tasks, excluding read-only fields like id and timestamps.

Now, let's create a script to initialize the database. Create a file called init_db.py:

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:

Output
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:

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:

Output
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:

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:

Output
==== 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:

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:

Output

=== 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:

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:

Output
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.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github