Skip to content

Support dict as parameter for choices #399

Description

@Tenzer

The Django documentation shows that you can provide a dict as input to the choices parameter on the CharField type: https://docs.djangoproject.com/en/6.0/ref/models/fields/#choices. It is, however, not accepted by the type annotations here.

Example file:

from django.db import models


class Foo(models.Model):
    STATUSES = {
        "new": "New",
        "open": "Open",
        "closed": "Closed",
        "archived": "Archived",
    }

    status = models.CharField(choices=STATUSES)

Output from mypy:

models.py:12: error: Argument "choices" to "CharField" has incompatible type "dict[str, str]"; expected "Iterable[tuple[Never, str] | tuple[str, Iterable[tuple[Never, str]]]] | type[TextChoices]"  [arg-type]

Output from Pyright:

models.py
  models.py:12:39 - error: Argument of type "dict[str, str]" cannot be assigned to parameter "choices" of type "Iterable[tuple[_C@CharField, str] | tuple[str, Iterable[tuple[_C@CharField, str]]]] | type[TextChoices]" in function "__new__"
    Type "dict[str, str]" is not assignable to type "Iterable[tuple[_C@CharField, str] | tuple[str, Iterable[tuple[_C@CharField, str]]]] | type[TextChoices]"
      "dict[str, str]" is not assignable to "Iterable[tuple[_C@CharField, str] | tuple[str, Iterable[tuple[_C@CharField, str]]]]"
        Type parameter "_T_co@Iterable" is covariant, but "str" is not a subtype of "tuple[_C@CharField, str] | tuple[str, Iterable[tuple[_C@CharField, str]]]"
          Type "str" is not assignable to type "tuple[_C@CharField, str] | tuple[str, Iterable[tuple[_C@CharField, str]]]"
            "str" is not assignable to "tuple[_C@CharField, str]"
            "str" is not assignable to "tuple[str, Iterable[tuple[_C@CharField, str]]]"
      Type "dict[str, str]" is not assignable to type "type[TextChoices]" (reportArgumentType)
1 error, 0 warnings, 0 informations

Ouput from ty:

error[[invalid-argument-type](https://ty.dev/rules#invalid-argument-type)]: Argument to constructor `CharField.__new__` is incorrect
  --> models.py:12:31
   |
12 |     status = models.CharField(choices=STATUSES)
   |                               ^^^^^^^^^^^^^^^^ Expected `Iterable[tuple[Unknown, str] | tuple[str, Iterable[tuple[Unknown, str]]]]`, found `dict[str, str]`
   |
info: type `dict[str, str]` is not assignable to protocol `Iterable[tuple[Unknown, str] | tuple[str, Iterable[tuple[Unknown, str]]]]`
info: └── protocol member `__iter__` is incompatible
info:     └── incompatible return types: `Iterator[str]` is not assignable to `Iterator[tuple[Unknown, str] | tuple[str, Iterable[tuple[Unknown, str]]]]`
info:         └── protocol `Iterator[str]` is not assignable to protocol `Iterator[tuple[Unknown, str] | tuple[str, Iterable[tuple[Unknown, str]]]]`
info:             └── protocol member `__next__` is incompatible
info:                 └── incompatible return types: `str` is not assignable to `tuple[Unknown, str] | tuple[str, Iterable[tuple[Unknown, str]]]`
info: Matching overload defined here
   --> venv/lib/python3.14/site-packages/django-stubs/db/models/fields/__init__.pyi:702:9
    |
702 |       def __new__(
    |           ^^^^^^^
    |
   ::: venv/lib/python3.14/site-packages/django-stubs/db/models/fields/__init__.pyi:722:9
    |
722 | /         choices: Iterable[tuple[_C, str] | tuple[str, Iterable[tuple[_C, str]]]]
723 | |         | type[TextChoices] = ...,
    | |_________________________________- Parameter declared here
    |
info: Non-matching overloads for function `__new__`:
info:   (cls, verbose_name: str | None = ..., *, name: str | None = ..., primary_key: bool = ..., max_length: int = ..., db_collation: str | None = ..., unique: bool = ..., blank: bool = ..., null: Literal[True], db_index: bool = ..., default: _C@CharField | (() -> _C@CharField) = ..., db_default: _C@CharField | Func = ..., editable: bool = ..., auto_created: bool = ..., serialize: bool = ..., unique_for_date: str | None = ..., unique_for_month: str | None = ..., unique_for_year: str | None = ..., choices: Iterable[tuple[_C@CharField, str] | tuple[str, Iterable[tuple[_C@CharField, str]]]] = ..., help_text: str = ..., db_column: str | None = ..., db_comment: str | None = ..., db_tablespace: str | None = ..., validators: Iterable[(...) -> None] = ..., error_messages: dict[str, Any] | None = ...) -> CharField[str | None]

Found 1 diagnostic

Tested with these versions:

Package           Version
----------------- ---------------
Django            6.0.7
django-types      0.24.0
mypy              2.2.0
pyright           1.1.411
ty                0.0.57

I can see there's already a type defined that allows for a dict to be passed to the choices parameter:

_FieldChoices: TypeAlias = _LiteralFieldChoices | Callable[[], _LiteralFieldChoices]

It is, however, not used where the type checkers need it:

choices: Iterable[tuple[_C, _StrOrPromise] | tuple[str, Iterable[tuple[_C, _StrOrPromise]]]]
| type[TextChoices] = ...,

At first glance, it looks like it would be enough to change the latter lines to choices: _FieldChoices = ..., but I wonder if there's something deeper at play here which doesn't make it as straight forward?

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions