Skip to content

Use PEP 696 for model fields#3317

Draft
UnknownPlatypus wants to merge 34 commits into
typeddjango:masterfrom
UnknownPlatypus:pep696-field-typing-skip-null
Draft

Use PEP 696 for model fields#3317
UnknownPlatypus wants to merge 34 commits into
typeddjango:masterfrom
UnknownPlatypus:pep696-field-typing-skip-null

Conversation

@UnknownPlatypus

@UnknownPlatypus UnknownPlatypus commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

I have made things!

Replace _pyi_private_set_type / _pyi_private_get_type attributes on field stubs with PEP 696 TypeVar defaults. This makes field set/get types visible to all type checkers, not just mypy with the plugin.

This is a big PR, sry for that but the plugin code is so entangled I did not manage to make a smaller chunk.
I've rebased to make it a bit more digestible, hopefully that helps.

I'm pretty convinced we can improve the stubs of ForeignKey/ManyToMany and friends using trick similar to what is done to ArrayField in this PR and remove even more plugin code.
Most of the code in transform_into_proper_return_type should go I think.

Related issues

Fixes #766
Fixes #1264
Fixes #1900
Fixes #2043
Fixes #2590
Fixes #2724

Improves #579

Handling null=True

The important part is to make null working (and later primary_key). Two approches have emerged in the other issue discussion, I've investigated further with various type checker and was able to make the _NT typevar approach work with every typechecker I know off!

Using __new__ overloads (django-types is doing that)

Code snippet

from typing import TypeVar, Literal, Generic, overload, reveal_type


_ST = TypeVar("_ST")
_GT = TypeVar("_GT")

class Model:
    pass


class Field(Generic[_ST, _GT]):
    def __init__(self, *, null: bool = False) -> None:
        super().__init__()


_ST_1 = TypeVar("_ST_1", default=int)
_GT_1 = TypeVar("_GT_1", default=int)


class IntegerFieldSimple(Field[_ST_1, _GT_1]): ...

class IntegerFieldWithOverloads(Field[_ST_1, _GT_1]):
    @overload
    def __new__(
        cls, *, null: Literal[True]
    ) -> IntegerFieldWithOverloads[_ST_1 | None, _GT_1 | None]: ...

    @overload
    def __new__(
        cls, *, null: Literal[False] = False
    ) -> IntegerFieldWithOverloads[_ST_1, _GT_1]: ...

    def __new__(   # type: ignore[misc]
        cls, *, null: bool = False
    ) -> (
        IntegerFieldWithOverloads[_ST_1, _GT_1]
        | IntegerFieldWithOverloads[_ST_1 | None, _GT_1 | None]
    ):
        return super().__new__(cls)


class A(Model):
    field1 = IntegerFieldSimple(null=False)
    field2 = IntegerFieldSimple(null=True)
    field3 = IntegerFieldWithOverloads(null=False)
    field4 = IntegerFieldWithOverloads(null=True)

a = A()
reveal_type(a.field1) # Revealed type is "builtins.int"
reveal_type(a.field2) # Revealed type is "Union[builtins.int, None]"
reveal_type(a.field3) # Revealed type is "builtins.int"
reveal_type(a.field4) # Revealed type is "Union[builtins.int, None]"

Works with:

  • ty -> 🟠 Work with explicit __new__ overrides on every custom field (and need to be quoted)
  • pyright -> 🟠 Work with explicit __new__ overrides on every custom field
    Not with:
  • pyrefly -> 🔴 Not working
  • mypy -> 🟠 Work with explicit __new__ overrides on every custom field
  • zuban -> 🟠 Work with explicit __new__ overrides on every custom field, (but a false positive error on the __new__ definition)

Using third type variable _NT to Field encoding nullability as Literal[True] or Literal[False]

Code snippet

from typing import TypeVar, Literal, Generic, overload, Any, reveal_type, Self, Never

_ST = TypeVar("_ST", contravariant=True)
_GT = TypeVar("_GT", covariant=True)
_NT = TypeVar("_NT", Literal[True], Literal[False], default=Literal[False])

class Model:
    pass

class Field(Generic[_ST, _GT, _NT]):
    def __init__(self, null: _NT) -> None: ...
    @overload
    def __get__(self: Field[Any, Any, Literal[False]], instance: Any, owner: Any) -> _GT: ...
    @overload
    def __get__(self: Field[Any, Any, Literal[True]], instance: Any, owner: Any) -> _GT | None: ...
    def __get__(self, instance: Any, owner: Any) -> _GT | None:
        pass

_ST_1 = TypeVar("_ST_1", bound=int, default=int)
_GT_1 = TypeVar("_GT_1", bound=int, default=int)

class IntegerFieldSimple(Field[_ST_1, _GT_1, _NT]): ...

class IntegerFieldWithOverload(Field[_ST_1, _GT_1, _NT]):
    @overload
    def __get__(self: IntegerFieldWithOverload[Any, Any, Literal[False]], instance: Any, owner: Any) -> _GT_1: ...
    @overload
    def __get__(self: IntegerFieldWithOverload[Any, Any, Literal[True]], instance: Any, owner: Any) -> _GT_1 | None: ...
    def __get__(self, instance: Any, owner: Any) -> _GT_1 | None:
        pass

class A(Model):
    field1 = IntegerFieldSimple(null=False)
    field2 = IntegerFieldSimple(null=True)
    field3 = IntegerFieldWithOverload(null=False)
    field4 = IntegerFieldWithOverload(null=True)

a = A()
reveal_type(a.field1) # Revealed type is "int"
reveal_type(a.field2) # Revealed type is "int | None"
reveal_type(a.field3) # Revealed type is "int"
reveal_type(a.field4) # Revealed type is "int | None"

Works with all type-checkers:

  • pyright -> 🟢 Works (but false positive on the explicit override case)
    ---> todo
  • mypy 1.16+ -> 🟢 Works
  • pyrefly -> 🟢 Works
  • ty -> 🟢 Works (but need to be quoted)
  • zuban -> 🟢 Works

Custom field typing

Currently, bare custom field (class HTMLField(models.TextField): ...) are an issue because the TypeVar default machinery makes body_nullable = HTMLField(null=True) resolve to TextField[str, str, Literal[False]], causing this kind of errors

Argument "null" to "HTMLField" has incompatible type "Literal[True]"; expected "Literal[False]"

The mypy plugin currently makes this work by reparametrizing HTMLField to remain generic in _ST_Text, _GT_Text, _NT, so mypy's natural inference picks up _NT = Literal[True] from the call. But other typecheckers cannot do that so it means that for them, they have to explicitely fix their custom fields
following the recommendation in the README ie

from typing_extensions import TypeVar, Literal
from django.db import models
from django.db.models.expressions import Combinable

_ST_Text = TypeVar("_ST_Text", contravariant=True, default=str | int | Combinable)
_GT_Text = TypeVar("_GT_Text", covariant=True, default=str)
_NT = TypeVar("_NT", Literal[True], Literal[False], default=Literal[False])

class HtmlField(models.TextField[_ST_Text, _GT_Text, _NT]):
    pass

That is probably something we can live with because the workaround is documented (and I say workaround, it's more of a regular typing pattern, omitting generics is often an issue and flagged in strict modes)

Alternative I considered

Drop default=Literal[False] from _NT

his Violates PEP 696 ordering (no-default TypeVar can't follow defaulted ones) so we have to either:
- ❌ reorder typevars to Generic[_NT, _ST, _GT] which would completely change the meaning of existing Field[X, Y] annotations, it feels bad
- drop the _ST/_GT defaults we just added and inline the defaults. This would make custom field subclassing anything other than Field very difficult as the _GT/_ST are not reachable anymore. People would have to redeclare the whole __get__ / __set__ machinery which seems bad. It also requires that we do null: _NT = False to not regress Bare instantiation ( models.IntegerField() -> _NT=Literal[False]) but this only works with pyright and pyrefly.
Passing a default value to a TypeVar is not supported by mypy:

  • mypy, we can add a type ignore and the inferrence still works
  • ty, even with a atype ignore, the inference break and gives Unkown

_NT = TypeVar("_NT", bound=bool, default=Any)

But this breaks narrowing to _GT or _GT | None based on null=... because Literal[False] / Literal[True] become bool which is a blocker

Followups

  • Upstream issue for remaining false positives
  • upstream ty issue for # TODO: ty should reject that too
  • Support Fk/OneToOne/m2m with minimal plugin work

@github-actions

This comment has been minimized.

Comment thread django-stubs/contrib/gis/db/models/fields.pyi Outdated
@github-actions

This comment has been minimized.

@UnknownPlatypus UnknownPlatypus force-pushed the pep696-field-typing-skip-null branch from 329fab2 to 5d6b276 Compare April 18, 2026 12:04
@github-actions

This comment has been minimized.

@ahmedasar00

Copy link
Copy Markdown
Contributor

Great work on this! It’s really impressive.!
Are there any specific tasks I can help with to move this forward?

@UnknownPlatypus

This comment was marked as outdated.

@UnknownPlatypus UnknownPlatypus force-pushed the pep696-field-typing-skip-null branch from 5d6b276 to b8c5edb Compare April 18, 2026 22:28
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@UnknownPlatypus

Copy link
Copy Markdown
Contributor Author

@sobolevn A bunch of issues in mypy_primer are because it does not load the plugin, hence the nullable part is bogus and we got false positives we did not have before when it was Any.

I'm gonna be expanding the scope of this pr to do the Using third type variable _NT to Field encoding nullability as Literal[True] or Literal[False] I think

@ahmedasar00

This comment was marked as outdated.

@ngnpope

ngnpope commented Apr 20, 2026

Copy link
Copy Markdown
Member

Still trying to remove unnecessary plugin code and need to test on my work repo (1.5M Loc) for regressions. I think it breaks some parts that are not tested well enough for now.

Not much to do to help I think but thanks for the proposal. When I'll make this ready for review, testing on external codebases would be valuable

We will be able to test out on a 16M LoC codebase at work when you're ready. (FYI @delfick)

@delfick

delfick commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

well, we only run mypy on 5.9 million lines of that (excluding blank lines and comments), but yes, our codebase is often a rich source of edge cases!

@ngnpope

ngnpope commented Apr 20, 2026

Copy link
Copy Markdown
Member

Shh! You make it sound less impressive 😂

@ahmedasar00

This comment was marked as resolved.

@github-actions

This comment has been minimized.

@UnknownPlatypus UnknownPlatypus force-pushed the pep696-field-typing-skip-null branch from 8f638a5 to ebae746 Compare May 1, 2026 21:30
@github-actions

This comment has been minimized.

@UnknownPlatypus UnknownPlatypus force-pushed the pep696-field-typing-skip-null branch 2 times, most recently from 435477f to 91b7025 Compare May 2, 2026 10:36
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@UnknownPlatypus UnknownPlatypus force-pushed the pep696-field-typing-skip-null branch from 91b7025 to c0676f8 Compare May 2, 2026 19:10
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@UnknownPlatypus

Copy link
Copy Markdown
Contributor Author

Just fixed 2 edge cases I found running this on my work code base in the last two commits.

Other than that it is looking good mostly with ~100 unused type ignore and new correct errors surfacing in places that where previously not checked.

However, it's a bit anoying for custom field to have to redeclare the full typevars with defaults like so:

from typing import Literal, reveal_type
from typing_extensions import TypeVar  # for `default=` (PEP 696)
from django.db import models
from django.db.models.expressions import Combinable

_ST = TypeVar("_ST", contravariant=True, default=float | int | str | Combinable)
_GT = TypeVar("_GT", covariant=True, default=int)
_NT = TypeVar("_NT", Literal[True], Literal[False], default=Literal[False])

class MyIntegerField(models.IntegerField[_ST, _GT, _NT]):
    ...

I'm wondering if we should expose our typevars so that people can do something like that instead (if we go down that path, we probably need better names too)

from django.db import models
from django_stubs_ext import ST_Int, GT_Int, NT

class MyIntegerField(models.IntegerField[ST_Int, GT_Int, NT]):
    ...

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an impressive work!

My main concern right now is that existing Field[A, B] annotations can change. For example, we use default=Literal[False] in the third type var, which can be rather limiting? Would it make sence to use Any as the default?

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ngnpope ngnpope left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of comments about test placement below.

A quick note that we've tried this on our large repo and things mostly seem good, but there are some things not quite right. We currently have 235 new errors with the following breakdown:

     10 [arg-type]
     12 [assignment]
    110 [attr-defined]
      5 [call-overload]
     44 [misc]
     14 [operator]
      1 [return-value]
      3 [type-var]
     12 [union-attr]
      2 [unreachable]
     10 [unused-ignore]
     12 [var-annotated]

The first thing I want to look into are the attr-defined. The majority of these look to be an issue with GenericRelation where I think that it's not seeing this as a QuerySet. We're typically seeing something like:

error: "MyModel" has no attribute "aggregate"  [attr-defined]
error: "MyModel" has no attribute "exists"  [attr-defined]
error: "MyModel" has no attribute "filter"  [attr-defined]
error: "MyModel" has no attribute "get"  [attr-defined]
...

I'm not currently sure whether this is something that would have been Any before and is now resolving to something else, and have yet to come up with a reproducer. But I wanted to highlight it now in case anyone else is able to get around to working something out before I can.

Once that's been resolved we can sift the rest. I suspect some of them will be merely due to needing to specify defaults for type variables of field subclasses. But starting with the biggest issue makes sense.

Comment thread tests/assert_type/contrib/postgres/fields/test_array.py
Comment thread tests/assert_type/http/contrib/postgres/fields/test_ranges.py
@UnknownPlatypus

Copy link
Copy Markdown
Contributor Author

My main concern right now is that existing Field[A, B] annotations can change. For example, we use default=Literal[False] in the third type var, which can be rather limiting? Would it make sence to use Any as the default?

This is my concern too, and I'm also concerned the current approach will be hard to scale when we want to add primary_key / default / db_default handling. I'm starting to think the _NT typevar is not the proper solution.

I'm trying another approch with __init__ / __new__ overloads to see if it's better

The first thing I want to look into are the attr-defined. The majority of these look to be an issue with GenericRelation where I think that it's not seeing this as a QuerySet. We're typically seeing something like:

Do you have a sample snippet of one of these so I can add a test case for it ?
I've probably deleted a bit too much mypy plugin in that area

@UnknownPlatypus UnknownPlatypus marked this pull request as draft July 3, 2026 12:56
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on type check results on a corpus of open source code:

zulip (https://github.com/zulip/zulip)
- zilencer/migrations/0026_auditlog_models_extra_data_json.py:17: error: Module has no attribute "core"  [attr-defined]
+ zilencer/migrations/0026_auditlog_models_extra_data_json.py:17: error: Module has no attribute "serializers"  [attr-defined]
- zilencer/migrations/0026_auditlog_models_extra_data_json.py:24: error: Module has no attribute "core"  [attr-defined]
+ zilencer/migrations/0026_auditlog_models_extra_data_json.py:24: error: Module has no attribute "serializers"  [attr-defined]
- zerver/migrations/0452_realmauditlog_extra_data_json.py:17: error: Module has no attribute "core"  [attr-defined]
+ zerver/migrations/0452_realmauditlog_extra_data_json.py:17: error: Module has no attribute "serializers"  [attr-defined]
- zerver/models/users.py:62: error: Need type annotation for "enter_sends"  [var-annotated]
- zerver/models/users.py:68: error: Need type annotation for "left_side_userlist"  [var-annotated]
- zerver/models/users.py:69: error: Need type annotation for "default_language"  [var-annotated]
- zerver/models/users.py:72: error: Need type annotation for "web_home_view"  [var-annotated]
- zerver/models/users.py:73: error: Need type annotation for "web_escape_navigates_to_home_view"  [var-annotated]
- zerver/models/users.py:74: error: Need type annotation for "fluid_layout_width"  [var-annotated]
- zerver/models/users.py:75: error: Need type annotation for "high_contrast_mode"  [var-annotated]
- zerver/models/users.py:76: error: Need type annotation for "translate_emoticons"  [var-annotated]
- zerver/models/users.py:77: error: Need type annotation for "display_emoji_reaction_users"  [var-annotated]
- zerver/models/users.py:78: error: Need type annotation for "twenty_four_hour_time"  [var-annotated]
- zerver/models/users.py:79: error: Need type annotation for "starred_message_counts"  [var-annotated]
- zerver/models/users.py:80: error: Need type annotation for "web_suggest_update_timezone"  [var-annotated]
- zerver/models/users.py:85: error: Need type annotation for "color_scheme"  [var-annotated]
- zerver/models/users.py:93: error: Need type annotation for "web_font_size_px"  [var-annotated]
- zerver/models/users.py:94: error: Need type annotation for "web_line_height_percent"  [var-annotated]
- zerver/models/users.py:99: error: Need type annotation for "web_animate_image_previews"  [var-annotated]
- zerver/models/users.py:113: error: Need type annotation for "demote_inactive_streams"  [var-annotated]
- zerver/models/users.py:117: error: Need type annotation for "web_left_sidebar_show_channel_folders"  [var-annotated]
- zerver/models/users.py:132: error: Need type annotation for "web_mark_read_on_scroll_policy"  [var-annotated]
- zerver/models/users.py:150: error: Need type annotation for "web_channel_default_view"  [var-annotated]
- zerver/models/users.py:164: error: Need type annotation for "emojiset"  [var-annotated]
- zerver/models/users.py:175: error: Need type annotation for "user_list_style"  [var-annotated]
- zerver/models/users.py:186: error: Need type annotation for "web_stream_unreads_count_display_policy"  [var-annotated]
- zerver/models/users.py:190: error: Need type annotation for "web_left_sidebar_unreads_count_summary"  [var-annotated]
- zerver/models/users.py:194: error: Need type annotation for "web_navigate_to_sent_message"  [var-annotated]
- zerver/models/users.py:198: error: Need type annotation for "email_notifications_batching_period_seconds"  [var-annotated]
- zerver/models/users.py:201: error: Need type annotation for "enable_stream_desktop_notifications"  [var-annotated]
- zerver/models/users.py:202: error: Need type annotation for "enable_stream_email_notifications"  [var-annotated]
- zerver/models/users.py:203: error: Need type annotation for "enable_stream_push_notifications"  [var-annotated]
- zerver/models/users.py:204: error: Need type annotation for "enable_stream_audible_notifications"  [var-annotated]
- zerver/models/users.py:205: error: Need type annotation for "notification_sound"  [var-annotated]
- zerver/models/users.py:206: error: Need type annotation for "wildcard_mentions_notify"  [var-annotated]
- zerver/models/users.py:209: error: Need type annotation for "enable_followed_topic_desktop_notifications"  [var-annotated]
- zerver/models/users.py:210: error: Need type annotation for "enable_followed_topic_email_notifications"  [var-annotated]
- zerver/models/users.py:211: error: Need type annotation for "enable_followed_topic_push_notifications"  [var-annotated]
- zerver/models/users.py:212: error: Need type annotation for "enable_followed_topic_audible_notifications"  [var-annotated]
- zerver/models/users.py:213: error: Need type annotation for "enable_followed_topic_wildcard_mentions_notify"  [var-annotated]
- zerver/models/users.py:216: error: Need type annotation for "enable_desktop_notifications"  [var-annotated]
- zerver/models/users.py:217: error: Need type annotation for "pm_content_in_desktop_notifications"  [var-annotated]
- zerver/models/users.py:218: error: Need type annotation for "enable_sounds"  [var-annotated]
- zerver/models/users.py:219: error: Need type annotation for "enable_offline_email_notifications"  [var-annotated]
- zerver/models/users.py:220: error: Need type annotation for "message_content_in_email_notifications"  [var-annotated]
- zerver/models/users.py:221: error: Need type annotation for "enable_offline_push_notifications"  [var-annotated]
- zerver/models/users.py:222: error: Need type annotation for "enable_online_push_notifications"  [var-annotated]
- zerver/models/users.py:234: error: Need type annotation for "desktop_icon_count_display"  [var-annotated]
- zerver/models/users.py:238: error: Need type annotation for "enable_digest_emails"  [var-annotated]
- zerver/models/users.py:239: error: Need type annotation for "enable_login_emails"  [var-annotated]
- zerver/models/users.py:240: error: Need type annotation for "enable_marketing_emails"  [var-annotated]
- zerver/models/users.py:241: error: Need type annotation for "presence_enabled"  [var-annotated]
- zerver/models/users.py:251: error: Need type annotation for "realm_name_in_email_notifications_policy"  [var-annotated]
- zerver/models/users.py:272: error: Need type annotation for "automatically_follow_topics_policy"  [var-annotated]
- zerver/models/users.py:275: error: Need type annotation for "automatically_unmute_topics_in_muted_streams_policy"  [var-annotated]
- zerver/models/users.py:278: error: Need type annotation for "automatically_follow_topics_where_mentioned"  [var-annotated]
- zerver/models/users.py:280: error: Need type annotation for "resolved_topic_notice_auto_read_policy"  [var-annotated]
- zerver/models/users.py:287: error: Need type annotation for "enable_drafts_synchronization"  [var-annotated]
- zerver/models/users.py:290: error: Need type annotation for "send_stream_typing_notifications"  [var-annotated]
- zerver/models/users.py:291: error: Need type annotation for "send_private_typing_notifications"  [var-annotated]
- zerver/models/users.py:292: error: Need type annotation for "send_read_receipts"  [var-annotated]
- zerver/models/users.py:293: error: Need type annotation for "allow_private_data_export"  [var-annotated]
- zerver/models/users.py:296: error: Need type annotation for "receives_typing_notifications"  [var-annotated]
- zerver/models/users.py:300: error: Need type annotation for "web_inbox_show_channel_folders"  [var-annotated]
- zerver/models/users.py:311: error: Need type annotation for "email_address_visibility"  [var-annotated]
- zerver/models/users.py:326: error: Need type annotation for "hide_ai_features"  [var-annotated]
- zerver/models/users.py:443: error: Need type annotation for "realm"  [var-annotated]
- zerver/models/users.py:491: error: Need type annotation for "id"  [var-annotated]
- zerver/models/users.py:509: error: Need type annotation for "delivery_email"  [var-annotated]
- zerver/models/users.py:510: error: Need type annotation for "email"  [var-annotated]
- zerver/models/users.py:512: error: Need type annotation for "realm"  [var-annotated]
- zerver/models/users.py:521: error: Need type annotation for "full_name"  [var-annotated]
- zerver/models/users.py:523: error: Need type annotation for "date_joined"  [var-annotated]
- zerver/models/users.py:531: error: Need type annotation for "tos_version"  [var-annotated]
- zerver/models/users.py:532: error: Need type annotation for "api_key"  [var-annotated]
- zerver/models/users.py:538: error: Need type annotation for "uuid"  [var-annotated]
- zerver/models/users.py:541: error: Need type annotation for "is_staff"  [var-annotated]
- zerver/models/users.py:548: error: Need type annotation for "is_active"  [var-annotated]
- zerver/models/users.py:553: error: Need type annotation for "is_deleted"  [var-annotated]
- zerver/models/users.py:555: error: Need type annotation for "is_bot"  [var-annotated]
- zerver/models/users.py:556: error: Need type annotation for "bot_type"  [var-annotated]
- zerver/models/users.py:557: error: Need type annotation for "bot_owner"  [var-annotated]
- zerver/models/users.py:569: error: Need type annotation for "role"  [var-annotated]
- zerver/models/users.py:611: error: Need type annotation for "long_term_idle"  [var-annotated]
- zerver/models/users.py:614: error: Need type annotation for "last_active_message_id"  [var-annotated]
- zerver/models/users.py:621: error: Need type annotation for "is_mirror_dummy"  [var-annotated]
- zerver/models/users.py:624: error: Need type annotation for "is_imported_stub"  [var-annotated]
- zerver/models/users.py:628: error: Need type annotation for "can_forge_sender"  [var-annotated]
- zerver/models/users.py:630: error: Need type annotation for "can_create_users"  [var-annotated]
- zerver/models/users.py:632: error: Need type annotation for "can_change_user_emails"  [var-annotated]
- zerver/models/users.py:635: error: Need type annotation for "last_reminder"  [var-annotated]
- zerver/models/users.py:642: error: Need type annotation for "rate_limits"  [var-annotated]
- zerver/models/users.py:645: error: Need type annotation for "default_sending_stream"  [var-annotated]
- zerver/models/users.py:651: error: Need type annotation for "default_events_register_stream"  [var-annotated]
- zerver/models/users.py:657: error: Need type annotation for "default_all_public_streams"  [var-annotated]
- zerver/models/users.py:667: error: Need type annotation for "timezone"  [var-annotated]
- zerver/models/users.py:678: error: Need type annotation for "avatar_source"  [var-annotated]
- zerver/models/users.py:681: error: Need type annotation for "avatar_version"  [var-annotated]
- zerver/models/users.py:685: error: Need type annotation for "avatar_hash"  [var-annotated]
- zerver/models/users.py:1262: error: Need type annotation for "user"  [var-annotated]
- zerver/models/users.py:1263: error: Need type annotation for "realm"  [var-annotated]
- zerver/models/users.py:1264: error: Need type annotation for "date_created"  [var-annotated]
- zerver/models/users.py:1267: error: Need type annotation for "external_auth_method_name"  [var-annotated]
- zerver/models/users.py:1268: error: Need type annotation for "external_auth_id"  [var-annotated]
- zerver/models/clients.py:13: error: Need type annotation for "id"  [var-annotated]
- zerver/models/clients.py:14: error: Need type annotation for "name"  [var-annotated]
- zerver/models/user_activity.py:22: error: Need type annotation for "user_profile"  [var-annotated]
- zerver/models/user_activity.py:23: error: Need type annotation for "client"  [var-annotated]
- zerver/models/user_activity.py:24: error: Need type annotation for "query"  [var-annotated]
- zerver/models/user_activity.py:26: error: Need type annotation for "count"  [var-annotated]
- zerver/models/user_activity.py:27: error: Need type annotation for "last_visit"  [var-annotated]
- zerver/models/user_activity.py:36: error: Need type annotation for "user_profile"  [var-annotated]
- zerver/models/user_activity.py:37: error: Need type annotation for "start"  [var-annotated]
- zerver/models/user_activity.py:38: error: Need type annotation for "end"  [var-annotated]
- zerver/models/recipients.py:38: error: Need type annotation for "id"  [var-annotated]
- zerver/models/recipients.py:39: error: Need type annotation for "type_id"  [var-annotated]
- zerver/models/recipients.py:40: error: Need type annotation for "type"  [var-annotated]
- zerver/models/recipients.py:121: error: Need type annotation for "huddle_hash"  [var-annotated]
- zerver/models/recipients.py:123: error: Need type annotation for "recipient"  [var-annotated]
- zerver/models/recipients.py:125: error: Need type annotation for "group_size"  [var-annotated]
- zerver/models/push_notifications.py:19: error: Need type annotation for "kind"  [var-annotated]
- zerver/models/push_notifications.py:25: error: Need type annotation for "token"  [var-annotated]
- zerver/models/push_notifications.py:29: error: Need type annotation for "last_updated"  [var-annotated]
- zerver/models/push_notifications.py:32: error: Need type annotation for "ios_app_id"  [var-annotated]
- zerver/models/push_notifications.py:40: error: Need type annotation for "user"  [var-annotated]
- zerver/models/onboarding_steps.py:9: error: Need type annotation for "user"  [var-annotated]
- zerver/models/onboarding_steps.py:10: error: Need type annotation for "onboarding_step"  [var-annotated]
- zerver/models/onboarding_steps.py:11: error: Need type annotation for "timestamp"  [var-annotated]
- zerver/models/navigation_views.py:14: error: Need type annotation for "user"  [var-annotated]
- zerver/models/navigation_views.py:18: error: Need type annotation for "fragment"  [var-annotated]
- zerver/models/navigation_views.py:21: error: Need type annotation for "is_pinned"  [var-annotated]
- zerver/models/navigation_views.py:24: error: Need type annotation for "name"  [var-annotated]
- zerver/models/muted_users.py:12: error: Need type annotation for "user_profile"  [var-annotated]
- zerver/models/muted_users.py:13: error: Need type annotation for "muted_user"  [var-annotated]
- zerver/models/muted_users.py:14: error: Need type annotation for "date_muted"  [var-annotated]
- zerver/models/groups.py:44: error: Need type annotation for "realm"  [var-annotated]
- zerver/models/groups.py:53: error: Need type annotation for "usergroup_ptr"  [var-annotated]
- zerver/models/groups.py:65: error: Need type annotation for "name"  [var-annotated]
- zerver/models/groups.py:66: error: Need type annotation for "description"  [var-annotated]
- zerver/models/groups.py:67: error: Need type annotation for "date_created"  [var-annotated]
- zerver/models/groups.py:68: error: Need type annotation for "creator"  [var-annotated]
- zerver/models/groups.py:71: error: Need type annotation for "is_system_group"  [var-annotated]
- zerver/models/groups.py:73: error: Need type annotation for "can_add_members_group"  [var-annotated]
- zerver/models/groups.py:76: error: Need type annotation for "can_join_group"  [var-annotated]
- zerver/models/groups.py:77: error: Need type annotation for "can_leave_group"  [var-annotated]
- zerver/models/groups.py:78: error: Need type annotation for "can_manage_group"  [var-annotated]
- zerver/models/groups.py:79: error: Need type annotation for "can_mention_group"  [var-annotated]
- zerver/models/groups.py:82: error: Need type annotation for "can_remove_members_group"  [var-annotated]
- zerver/models/groups.py:86: error: Need type annotation for "realm_for_sharding"  [var-annotated]
- zerver/models/groups.py:87: error: Need type annotation for "deactivated"  [var-annotated]
- zerver/models/groups.py:167: error: Need type annotation for "user_group"  [var-annotated]
- zerver/models/groups.py:168: error: Need type annotation for "user_profile"  [var-annotated]
- zerver/models/groups.py:175: error: Need type annotation for "supergroup"  [var-annotated]
- zerver/models/groups.py:176: error: Need type annotation for "subgroup"  [var-annotated]
- zerver/models/devices.py:19: error: Need type annotation for "user"  [var-annotated]
- zerver/models/devices.py:26: error: Need type annotation for "push_key"  [var-annotated]
- zerver/models/devices.py:28: error: Need type annotation for "push_key_id"  [var-annotated]
- zerver/models/devices.py:31: error: Need type annotation for "push_token_id"  [var-annotated]
- zerver/models/devices.py:33: error: Need type annotation for "pending_push_token_id"  [var-annotated]
- zerver/models/devices.py:36: error: Need type annotation for "push_token_last_updated_timestamp"  [var-annotated]

... (truncated 562 lines) ...

@ngnpope

ngnpope commented Jul 3, 2026

Copy link
Copy Markdown
Member

Do you have a sample snippet of one of these so I can add a test case for it ?
I've probably deleted a bit too much mypy plugin in that area

I'll have to try and run things again and isolate a particular example. Didn't have much success the first time I tried 🙁

Comment on lines +424 to +426
_ST_Email = TypeVar("_ST_Email", contravariant=True, default=str)

class EmailField(CharField[_ST_Email, _GT_Char, _NT]):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to have _GT_Email even though it's the same as _GT_Char to avoid confusion?

Suggested change
_ST_Email = TypeVar("_ST_Email", contravariant=True, default=str)
class EmailField(CharField[_ST_Email, _GT_Char, _NT]):
_ST_Email = TypeVar("_ST_Email", contravariant=True, default=str)
_GT_Email = TypeVar("_GT_Email", covariant=True, default=str)
class EmailField(CharField[_ST_Email, _GT_Email, _NT]):

After all _ST_Text is also the same as _ST_Char and we have that.

@ngnpope

ngnpope commented Jul 3, 2026

Copy link
Copy Markdown
Member

Do you have a sample snippet of one of these so I can add a test case for it ?
I've probably deleted a bit too much mypy plugin in that area

I'll have to try and run things again and isolate a particular example. Didn't have much success the first time I tried 🙁

Ok. Not come up with an example yet, but I did slap a reveal_type() in and it looks like before if we had something like:

class Book(Model):
    ...

class Author(Model):
    ...

    books = GenericRelation(
        "myapp.Book",
        content_type_field="...",
        object_id_field="...",
        related_query_name="...",
    )

Then we'd get:

reveal_type(author.books)  # N: Revealed type is "Any"
reveal_type(author.books.all())  # N: Revealed type is "Any"
reveal_type(author.books.first())  # N: Revealed type is "Any"

With this branch we're getting:

reveal_type(author.books)  # N: Revealed type is "myapp.models.Book"
reveal_type(author.books.all())  # N: Revealed type is "Any"
reveal_type(author.books.first())  # N: Revealed type is "Any"

So the issue is that we just had Any before and now we're getting the Book model.

That might be because we've changed this?

-class GenericRelation(ForeignObject[Any, Any]):
+class GenericRelation(ForeignObject[_ST, _GT, _NT]):

Perhaps we should try to do something better here, but I think we might need to handle GenericForeignKey, GenericRelation, ForeignObject, etc. a little more specially?

Hope that's something to begin with. Will try to follow up with more when I get a chance to dig some more.

@UnknownPlatypus

Copy link
Copy Markdown
Contributor Author

I should probably ho back to Any in the first place or the scope of this mr will explode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

5 participants