Claude Code for Tortoise ORM: Async Python ORM — Claude Skills 360 Blog
Blog / AI / Claude Code for Tortoise ORM: Async Python ORM
AI

Claude Code for Tortoise ORM: Async Python ORM

Published: December 24, 2027
Read time: 5 min read
By: Claude Skills 360

Tortoise ORM is async-native ORM for Python. pip install tortoise-orm[asyncpg]. Define model: from tortoise.models import Model; from tortoise import fields. class User(Model): id = fields.IntField(pk=True); name = fields.CharField(max_length=100); email = fields.CharField(max_length=200, unique=True). Init: await Tortoise.init(db_url="postgres://...", modules={"models":["app.models"]}). Schema: await Tortoise.generate_schema(). Create: user = await User.create(name="Alice", email="[email protected]"). Get: user = await User.get(id=1). Get or none: user = await User.get_or_none(email="[email protected]"). Filter: users = await User.filter(is_active=True).all(). User.filter(name__contains="ali"). User.filter(created_at__gte=date). Q objects: User.filter(Q(role="admin") | Q(is_staff=True)). First: await User.filter(...).first(). Count: await User.filter(...).count(). Exists: await User.filter(...).exists(). Update: await User.filter(id=1).update(name="Bob"). Delete: await User.filter(is_active=False).delete(). Prefetch: users = await User.all().prefetch_related("orders"). Values: await User.all().values("id", "email"). Values list: await User.all().values_list("id", flat=True). Annotate: from tortoise.functions import Count; await User.all().annotate(order_count=Count("orders")).values("id","order_count"). Transaction: async with in_transaction(): .... FK: class Order(Model): user = fields.ForeignKeyField("models.User", related_name="orders"). M2M: tags = fields.ManyToManyField("models.Tag"). Migrations: pip install aerich; aerich init -t app.config.TORTOISE_ORM; aerich init-db; aerich migrate; aerich upgrade. FastAPI: register_tortoise(app, config=TORTOISE_ORM, generate_schemas=True). Testing: inherit TortoiseTestCase. Claude Code generates Tortoise ORM models, async CRUD services, and aerich migration workflows.

CLAUDE.md for Tortoise ORM

## Tortoise ORM Stack
- Version: tortoise-orm >= 0.21 | pip install "tortoise-orm[asyncpg]"
- Init: await Tortoise.init(db_url=, modules={"models":["app.models"]})
- Model: class M(Model): id=fields.IntField(pk=True)
- Query: Model.filter(**kwargs) | Model.get(pk) | Model.get_or_none()
- Create: await Model.create(**data) | obj = Model(**data); await obj.save()
- Relations: ForeignKeyField | ManyToManyField + prefetch_related
- Transactions: async with in_transaction(): ...
- Migrations: aerich init + aerich migrate + aerich upgrade

Tortoise ORM Async Database Pipeline

# app/models.py — Tortoise ORM model definitions
from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from enum import Enum

from tortoise import fields
from tortoise.models import Model


class UserRole(str, Enum):
    USER      = "user"
    MODERATOR = "moderator"
    ADMIN     = "admin"


class OrderStatus(str, Enum):
    PENDING   = "pending"
    PAID      = "paid"
    SHIPPED   = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"


# ── Models ────────────────────────────────────────────────────────────────────

class Tag(Model):
    id   = fields.IntField(pk=True)
    name = fields.CharField(max_length=50, unique=True)
    slug = fields.CharField(max_length=50, unique=True)

    class Meta:
        table = "tags"

    def __str__(self) -> str:
        return self.name


class User(Model):
    id         = fields.IntField(pk=True)
    email      = fields.CharField(max_length=255, unique=True)
    first_name = fields.CharField(max_length=100)
    last_name  = fields.CharField(max_length=100)
    role       = fields.CharEnumField(UserRole, default=UserRole.USER)
    is_active  = fields.BooleanField(default=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    updated_at = fields.DatetimeField(auto_now=True)

    # Reverse FK relations are available as:
    # user.orders  (set by Order.user FK)

    class Meta:
        table   = "users"
        indexes = [("email",), ("role", "is_active")]

    def __str__(self) -> str:
        return f"{self.first_name} {self.last_name} <{self.email}>"


class Product(Model):
    id          = fields.IntField(pk=True)
    name        = fields.CharField(max_length=200)
    sku         = fields.CharField(max_length=50, unique=True)
    description = fields.TextField(default="")
    price       = fields.DecimalField(max_digits=10, decimal_places=2)
    stock       = fields.IntField(default=0)
    is_active   = fields.BooleanField(default=True)
    tags        = fields.ManyToManyField("models.Tag", related_name="products")
    created_at  = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "products"

    def __str__(self) -> str:
        return f"{self.sku}{self.name}"


class Order(Model):
    id         = fields.IntField(pk=True)
    user       = fields.ForeignKeyField("models.User", related_name="orders")
    status     = fields.CharEnumField(OrderStatus, default=OrderStatus.PENDING)
    total      = fields.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    notes      = fields.TextField(null=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    updated_at = fields.DatetimeField(auto_now=True)

    class Meta:
        table = "orders"


class OrderLine(Model):
    id         = fields.IntField(pk=True)
    order      = fields.ForeignKeyField("models.Order", related_name="lines")
    product    = fields.ForeignKeyField("models.Product", related_name="order_lines")
    quantity   = fields.IntField()
    unit_price = fields.DecimalField(max_digits=10, decimal_places=2)

    @property
    def subtotal(self) -> Decimal:
        return self.quantity * self.unit_price

    class Meta:
        table = "order_lines"
# app/services.py — async CRUD and query services
from __future__ import annotations

from decimal import Decimal

from tortoise.exceptions import DoesNotExist, IntegrityError
from tortoise.functions import Avg, Count, Sum
from tortoise.queryset import Q
from tortoise.transactions import in_transaction

from app.models import Order, OrderLine, OrderStatus, Product, Tag, User, UserRole


# ── User service ──────────────────────────────────────────────────────────────

class UserService:

    async def create(self, email: str, first_name: str, last_name: str,
                     role: UserRole = UserRole.USER) -> User:
        try:
            return await User.create(
                email=email,
                first_name=first_name,
                last_name=last_name,
                role=role,
            )
        except IntegrityError:
            raise ValueError(f"User with email {email!r} already exists")

    async def get_by_id(self, user_id: int) -> User:
        try:
            return await User.get(id=user_id)
        except DoesNotExist:
            raise ValueError(f"User {user_id} not found")

    async def get_by_email(self, email: str) -> User | None:
        return await User.get_or_none(email=email)

    async def list_active(self, page: int = 1, page_size: int = 20) -> list[User]:
        offset = (page - 1) * page_size
        return await (
            User.filter(is_active=True)
            .order_by("last_name", "first_name")
            .offset(offset)
            .limit(page_size)
            .all()
        )

    async def search(self, query: str) -> list[User]:
        """Search by name or email using Q objects."""
        return await User.filter(
            Q(first_name__icontains=query) |
            Q(last_name__icontains=query)  |
            Q(email__icontains=query)
        ).filter(is_active=True).all()

    async def count_by_role(self) -> list[dict]:
        return await (
            User.all()
            .annotate(count=Count("id"))
            .group_by("role")
            .values("role", "count")
        )

    async def deactivate(self, user_id: int) -> None:
        updated = await User.filter(id=user_id).update(is_active=False)
        if updated == 0:
            raise ValueError(f"User {user_id} not found")


# ── Order service ─────────────────────────────────────────────────────────────

class OrderService:

    async def create_order(
        self,
        user_id: int,
        lines: list[dict],       # [{"product_id": int, "quantity": int}]
    ) -> Order:
        """
        Create an order atomically:
        1. Fetch user and products
        2. Check stock
        3. Create order + lines
        4. Deduct stock
        All inside a single transaction.
        """
        async with in_transaction():
            user = await User.get(id=user_id)

            product_ids = [line["product_id"] for line in lines]
            products    = {p.id: p async for p in Product.filter(id__in=product_ids)}

            for line_data in lines:
                pid = line_data["product_id"]
                if pid not in products:
                    raise ValueError(f"Product {pid} not found")
                if products[pid].stock < line_data["quantity"]:
                    raise ValueError(f"Insufficient stock for product {pid}")

            total = sum(
                products[l["product_id"]].price * l["quantity"]
                for l in lines
            )

            order = await Order.create(user=user, total=total)

            for line_data in lines:
                product = products[line_data["product_id"]]
                await OrderLine.create(
                    order=order,
                    product=product,
                    quantity=line_data["quantity"],
                    unit_price=product.price,
                )
                await Product.filter(id=product.id).update(
                    stock=product.stock - line_data["quantity"]
                )

        return order

    async def get_with_lines(self, order_id: int) -> Order:
        """Fetch order with prefetched lines and their products."""
        return await (
            Order.get(id=order_id)
            .prefetch_related("lines__product", "user")
        )

    async def get_user_orders(self, user_id: int) -> list[Order]:
        return await (
            Order.filter(user_id=user_id)
            .prefetch_related("lines")
            .order_by("-created_at")
            .all()
        )

    async def revenue_by_status(self) -> list[dict]:
        return await (
            Order.all()
            .annotate(total_revenue=Sum("total"), order_count=Count("id"))
            .group_by("status")
            .values("status", "total_revenue", "order_count")
        )

    async def cancel(self, order_id: int) -> None:
        async with in_transaction():
            order = await Order.get(id=order_id).prefetch_related("lines__product")
            if order.status in (OrderStatus.SHIPPED, OrderStatus.DELIVERED):
                raise ValueError("Cannot cancel a shipped or delivered order")

            # Restore stock
            for line in order.lines:
                await Product.filter(id=line.product_id).update(
                    stock=line.product.stock + line.quantity
                )

            order.status = OrderStatus.CANCELLED
            await order.save()
# app/config.py — Tortoise init and FastAPI integration
from __future__ import annotations

import os
from contextlib import asynccontextmanager

from fastapi import FastAPI
from tortoise import Tortoise
from tortoise.contrib.fastapi import register_tortoise

DATABASE_URL = os.environ.get(
    "DATABASE_URL",
    "sqlite://:memory:",
)

TORTOISE_ORM = {
    "connections": {"default": DATABASE_URL},
    "apps": {
        "models": {
            "models": ["app.models", "aerich.models"],
            "default_connection": "default",
        },
    },
}


@asynccontextmanager
async def lifespan(app: FastAPI):
    """FastAPI lifespan: init Tortoise on startup, close on shutdown."""
    await Tortoise.init(config=TORTOISE_ORM)
    await Tortoise.generate_schema(safe=True)   # safe=True: no-op if tables exist
    yield
    await Tortoise.close_connections()


def create_app() -> FastAPI:
    app = FastAPI(lifespan=lifespan)
    return app


# Alternatively use register_tortoise helper (handles lifespan automatically):
def create_app_v2() -> FastAPI:
    app = FastAPI()
    register_tortoise(
        app,
        config=TORTOISE_ORM,
        generate_schemas=True,
        add_exception_handlers=True,
    )
    return app

For the SQLAlchemy async alternative — SQLAlchemy async requires AsyncSession, async_scoped_session, separate AsyncEngine creation, and explicit await session.flush() before accessing IDs, while Tortoise ORM uses await Model.create(**data) that returns the saved instance with its id populated, async for obj in Model.filter(...): streams results without loading all in memory, and prefetch_related("lines__product") resolves two levels of FK in two queries instead of N+1. For the GINO alternative — GINO extends SQLAlchemy core with asyncio but is no longer actively maintained, while Tortoise ORM’s aerich migration tool generates Django-style numbered migration files from model diffs, class Meta: indexes and unique_together map to database-level constraints, and TortoiseTestCase wraps each test in a transaction that rolls back after the test — no test database teardown required. The Claude Skills 360 bundle includes Tortoise ORM skill sets covering Model field types, ForeignKeyField and ManyToManyField, filter/get/get_or_none/create CRUD, Q objects for complex queries, prefetch_related for eager loading, annotate with Count Sum Avg, in_transaction for atomic writes, FastAPI register_tortoise integration, aerich migration workflow, and TortoiseTestCase for isolated async tests. Start with the free tier to try async ORM code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free