0
# Model Annotations
1
2
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.
3
4
## Capabilities
5
6
### Generic Annotations Container
7
8
A generic container class for TypedDict-style model annotations that provides type safety for annotated model instances.
9
10
```python { .api }
11
class Annotations(Generic[_Annotations]):
12
"""
13
Generic container for TypedDict-style model annotations.
14
15
Use as `Annotations[MyTypedDict]` where MyTypedDict defines the
16
structure of annotations added to model instances.
17
"""
18
pass
19
```
20
21
### Annotated Model Type
22
23
A convenient type alias that combines Django models with their annotation types using Python's Annotated type.
24
25
```python { .api }
26
WithAnnotations = Annotated[_T, Annotations[_Annotations]]
27
"""
28
Annotated type combining Django models with their annotation types.
29
30
Use as:
31
- `WithAnnotations[MyModel]` - Model with Any annotations
32
- `WithAnnotations[MyModel, MyTypedDict]` - Model with typed annotations
33
34
This enables type-safe access to both model fields and computed annotations.
35
"""
36
```
37
38
### Usage Examples
39
40
Basic model annotations with TypedDict:
41
42
```python
43
from django_stubs_ext import WithAnnotations, Annotations
44
from django.db import models
45
from django.db.models import Count, Avg, Case, When, QuerySet
46
from django.utils import timezone
47
from datetime import timedelta
48
from typing import TypedDict
49
50
class User(models.Model):
51
name = models.CharField(max_length=100)
52
email = models.EmailField()
53
created_at = models.DateTimeField(auto_now_add=True)
54
55
class UserStats(TypedDict):
56
post_count: int
57
avg_rating: float
58
is_active: bool
59
60
# Type-annotated QuerySet with custom annotations
61
def get_user_stats() -> QuerySet[WithAnnotations[User, UserStats]]:
62
return User.objects.annotate(
63
post_count=Count('posts'),
64
avg_rating=Avg('posts__rating'),
65
is_active=Case(
66
When(last_login__gte=timezone.now() - timedelta(days=30), then=True),
67
default=False
68
)
69
)
70
71
# Type-safe usage
72
users_with_stats = get_user_stats()
73
for user in users_with_stats:
74
# Both model fields and annotations are properly typed
75
print(f"{user.name} has {user.post_count} posts") # type: ignore
76
print(f"Average rating: {user.avg_rating}") # type: ignore
77
print(f"Active: {user.is_active}") # type: ignore
78
```
79
80
Generic annotations without specific TypedDict:
81
82
```python
83
from django_stubs_ext import WithAnnotations
84
from django.db import models
85
from django.db.models import F
86
87
class Product(models.Model):
88
name = models.CharField(max_length=100)
89
base_price = models.DecimalField(max_digits=10, decimal_places=2)
90
tax_rate = models.DecimalField(max_digits=5, decimal_places=4)
91
92
# Using WithAnnotations without specific TypedDict (annotations are Any)
93
def get_products_with_total() -> QuerySet[WithAnnotations[Product]]:
94
return Product.objects.annotate(
95
total_price=F('base_price') * (1 + F('tax_rate'))
96
)
97
98
products = get_products_with_total()
99
for product in products:
100
print(f"{product.name}: ${product.total_price}") # type: ignore
101
```
102
103
Complex aggregations with multiple annotation types:
104
105
```python
106
from django_stubs_ext import WithAnnotations, Annotations
107
from django.db import models
108
from django.db.models import Count, Sum, Q, QuerySet
109
from typing import TypedDict
110
111
class Customer(models.Model):
112
name = models.CharField(max_length=100)
113
114
class Order(models.Model):
115
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
116
total = models.DecimalField(max_digits=10, decimal_places=2)
117
status = models.CharField(max_length=20)
118
created_at = models.DateTimeField(auto_now_add=True)
119
120
class CustomerStats(TypedDict):
121
total_orders: int
122
total_spent: float
123
pending_orders: int
124
completed_orders: int
125
126
def get_customer_analytics() -> QuerySet[WithAnnotations[Customer, CustomerStats]]:
127
return Customer.objects.annotate(
128
total_orders=Count('orders'),
129
total_spent=Sum('orders__total'),
130
pending_orders=Count('orders', filter=Q(orders__status='pending')),
131
completed_orders=Count('orders', filter=Q(orders__status='completed'))
132
)
133
134
# Type-safe analytics
135
customers = get_customer_analytics()
136
for customer in customers:
137
stats = {
138
'name': customer.name,
139
'total_orders': customer.total_orders, # type: ignore
140
'total_spent': customer.total_spent, # type: ignore
141
'pending': customer.pending_orders, # type: ignore
142
'completed': customer.completed_orders # type: ignore
143
}
144
print(stats)
145
```
146
147
### Integration with Django QuerySet Methods
148
149
The annotation types work seamlessly with Django QuerySet operations:
150
151
```python
152
from django_stubs_ext import WithAnnotations
153
from django.db.models import Count, Q
154
from typing import TypedDict
155
156
class PostStats(TypedDict):
157
comment_count: int
158
published_comment_count: int
159
160
def get_popular_posts() -> QuerySet[WithAnnotations[Post, PostStats]]:
161
return Post.objects.annotate(
162
comment_count=Count('comments'),
163
published_comment_count=Count('comments', filter=Q(comments__published=True))
164
).filter(
165
comment_count__gte=10
166
).order_by('-published_comment_count')
167
168
# Chainable operations maintain type information
169
popular_posts = get_popular_posts()
170
top_posts = popular_posts[:5] # Still properly typed
171
recent_popular = popular_posts.filter(created_at__gte=last_week)
172
```
173
174
## Types
175
176
```python { .api }
177
from typing import Any, Generic, Mapping, TypeVar
178
from django.db.models.base import Model
179
from typing_extensions import Annotated
180
181
# Type variables for generic annotation support
182
_Annotations = TypeVar("_Annotations", covariant=True, bound=Mapping[str, Any])
183
_T = TypeVar("_T", bound=Model)
184
185
class Annotations(Generic[_Annotations]):
186
"""Use as `Annotations[MyTypedDict]`"""
187
pass
188
189
WithAnnotations = Annotated[_T, Annotations[_Annotations]]
190
"""Alias to make it easy to annotate the model `_T` as having annotations
191
`_Annotations` (a `TypedDict` or `Any` if not provided).
192
193
Use as `WithAnnotations[MyModel]` or `WithAnnotations[MyModel, MyTypedDict]`.
194
"""
195
```