or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdmodel-annotations.mdmonkey-patching.mdprotocols.mdqueryset-types.mdstring-types.md

model-annotations.mddocs/

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

```