CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-stubs-ext

Monkey-patching and extensions for django-stubs providing runtime type support for Django generic classes

Pending
Overview
Eval results
Files

model-annotations.mddocs/

Model Annotations

Type-safe containers and annotations for Django model annotations, enabling proper typing of models with computed fields, aggregations, and custom annotations. This system provides compile-time type safety for dynamically computed model attributes.

Capabilities

Generic Annotations Container

A generic container class for TypedDict-style model annotations that provides type safety for annotated model instances.

class Annotations(Generic[_Annotations]):
    """
    Generic container for TypedDict-style model annotations.
    
    Use as `Annotations[MyTypedDict]` where MyTypedDict defines the
    structure of annotations added to model instances.
    """
    pass

Annotated Model Type

A convenient type alias that combines Django models with their annotation types using Python's Annotated type.

WithAnnotations = Annotated[_T, Annotations[_Annotations]]
"""
Annotated type combining Django models with their annotation types.

Use as:
- `WithAnnotations[MyModel]` - Model with Any annotations
- `WithAnnotations[MyModel, MyTypedDict]` - Model with typed annotations

This enables type-safe access to both model fields and computed annotations.
"""

Usage Examples

Basic model annotations with TypedDict:

from django_stubs_ext import WithAnnotations, Annotations
from django.db import models
from django.db.models import Count, Avg, Case, When, QuerySet
from django.utils import timezone
from datetime import timedelta
from typing import TypedDict

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    created_at = models.DateTimeField(auto_now_add=True)

class UserStats(TypedDict):
    post_count: int
    avg_rating: float
    is_active: bool

# Type-annotated QuerySet with custom annotations
def get_user_stats() -> QuerySet[WithAnnotations[User, UserStats]]:
    return User.objects.annotate(
        post_count=Count('posts'),
        avg_rating=Avg('posts__rating'),
        is_active=Case(
            When(last_login__gte=timezone.now() - timedelta(days=30), then=True),
            default=False
        )
    )

# Type-safe usage
users_with_stats = get_user_stats()
for user in users_with_stats:
    # Both model fields and annotations are properly typed
    print(f"{user.name} has {user.post_count} posts")  # type: ignore
    print(f"Average rating: {user.avg_rating}")  # type: ignore
    print(f"Active: {user.is_active}")  # type: ignore

Generic annotations without specific TypedDict:

from django_stubs_ext import WithAnnotations
from django.db import models
from django.db.models import F

class Product(models.Model):
    name = models.CharField(max_length=100)
    base_price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=5, decimal_places=4)

# Using WithAnnotations without specific TypedDict (annotations are Any)
def get_products_with_total() -> QuerySet[WithAnnotations[Product]]:
    return Product.objects.annotate(
        total_price=F('base_price') * (1 + F('tax_rate'))
    )

products = get_products_with_total()
for product in products:
    print(f"{product.name}: ${product.total_price}")  # type: ignore

Complex aggregations with multiple annotation types:

from django_stubs_ext import WithAnnotations, Annotations
from django.db import models
from django.db.models import Count, Sum, Q, QuerySet
from typing import TypedDict

class Customer(models.Model):
    name = models.CharField(max_length=100)

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=20)
    created_at = models.DateTimeField(auto_now_add=True)

class CustomerStats(TypedDict):
    total_orders: int
    total_spent: float
    pending_orders: int
    completed_orders: int

def get_customer_analytics() -> QuerySet[WithAnnotations[Customer, CustomerStats]]:
    return Customer.objects.annotate(
        total_orders=Count('orders'),
        total_spent=Sum('orders__total'),
        pending_orders=Count('orders', filter=Q(orders__status='pending')),
        completed_orders=Count('orders', filter=Q(orders__status='completed'))
    )

# Type-safe analytics
customers = get_customer_analytics()
for customer in customers:
    stats = {
        'name': customer.name,
        'total_orders': customer.total_orders,  # type: ignore
        'total_spent': customer.total_spent,    # type: ignore
        'pending': customer.pending_orders,     # type: ignore
        'completed': customer.completed_orders  # type: ignore
    }
    print(stats)

Integration with Django QuerySet Methods

The annotation types work seamlessly with Django QuerySet operations:

from django_stubs_ext import WithAnnotations
from django.db.models import Count, Q
from typing import TypedDict

class PostStats(TypedDict):
    comment_count: int
    published_comment_count: int

def get_popular_posts() -> QuerySet[WithAnnotations[Post, PostStats]]:
    return Post.objects.annotate(
        comment_count=Count('comments'),
        published_comment_count=Count('comments', filter=Q(comments__published=True))
    ).filter(
        comment_count__gte=10
    ).order_by('-published_comment_count')

# Chainable operations maintain type information
popular_posts = get_popular_posts()
top_posts = popular_posts[:5]  # Still properly typed
recent_popular = popular_posts.filter(created_at__gte=last_week)

Types

from typing import Any, Generic, Mapping, TypeVar
from django.db.models.base import Model
from typing_extensions import Annotated

# Type variables for generic annotation support
_Annotations = TypeVar("_Annotations", covariant=True, bound=Mapping[str, Any])
_T = TypeVar("_T", bound=Model)

class Annotations(Generic[_Annotations]):
    """Use as `Annotations[MyTypedDict]`"""
    pass

WithAnnotations = Annotated[_T, Annotations[_Annotations]]
"""Alias to make it easy to annotate the model `_T` as having annotations
`_Annotations` (a `TypedDict` or `Any` if not provided).

Use as `WithAnnotations[MyModel]` or `WithAnnotations[MyModel, MyTypedDict]`.
"""

Install with Tessl CLI

npx tessl i tessl/pypi-django-stubs-ext

docs

index.md

model-annotations.md

monkey-patching.md

protocols.md

queryset-types.md

string-types.md

tile.json