Skip to content

Commit de8b7bb

Browse files
Document custom field typing (#2578)
Co-authored-by: sobolevn <[email protected]>
1 parent 82f5f68 commit de8b7bb

File tree

1 file changed

+54
-0
lines changed

1 file changed

+54
-0
lines changed

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,60 @@ def assert_zero_count(model_type: type[models.Model]) -> None:
340340
```
341341

342342

343+
### How to type a custom `models.Field`?
344+
345+
> [!NOTE]
346+
> This require type generic support, see <a href="#i-cannot-use-queryset-or-manager-with-type-annotations">this section</a> to enable it.
347+
348+
349+
Django `models.Field` (and subclasses) are generic types with two parameters:
350+
- `_ST`: type that can be used when setting a value
351+
- `_GT`: type that will be returned when getting a value
352+
353+
When you create a subclass, you have two options depending on how strict you want
354+
the type to be for consumers of your custom field.
355+
356+
1. Generic subclass:
357+
358+
```python
359+
from typing import TypeVar, reveal_type
360+
from django.db import models
361+
362+
_ST = TypeVar("_ST", contravariant=True)
363+
_GT = TypeVar("_GT", covariant=True)
364+
365+
class MyIntegerField(models.IntegerField[_ST, _GT]):
366+
...
367+
368+
class User(models.Model):
369+
my_field = MyIntegerField()
370+
371+
372+
reveal_type(User().my_field) # N: Revealed type is "int"
373+
User().my_field = "12" # OK (because Django IntegerField allows str and will try to coerce it)
374+
```
375+
376+
2. Non-generic subclass (more strict):
377+
378+
```python
379+
from typing import reveal_type
380+
from django.db import models
381+
382+
# This is a non-generic subclass being very explicit
383+
# that it expects only int when setting values.
384+
class MyStrictIntegerField(models.IntegerField[int, int]):
385+
...
386+
387+
class User(models.Model):
388+
my_field = MyStrictIntegerField()
389+
390+
391+
reveal_type(User().my_field) # N: Revealed type is "int"
392+
User().my_field = "12" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
393+
```
394+
395+
See mypy section on [generic classes subclasses](https://mypy.readthedocs.io/en/stable/generics.html#defining-subclasses-of-generic-classes).
396+
343397
## Related projects
344398

345399
- [`awesome-python-typing`](https://github.com/typeddjango/awesome-python-typing) - Awesome list of all typing-related things in Python.

0 commit comments

Comments
 (0)