Skip to content

Loosen ListFilter.choices() return type to allow custom filter templates#3180

Open
alessio-b2c2 wants to merge 3 commits intotypeddjango:masterfrom
alessio-b2c2:fix/listfilter-choices-return-type
Open

Loosen ListFilter.choices() return type to allow custom filter templates#3180
alessio-b2c2 wants to merge 3 commits intotypeddjango:masterfrom
alessio-b2c2:fix/listfilter-choices-return-type

Conversation

@alessio-b2c2
Copy link
Copy Markdown

@alessio-b2c2 alessio-b2c2 commented Mar 12, 2026

Summary

  • Change ListFilter.choices() return type from Iterator[_ListFilterChoices] to Iterator[Mapping[str, object]] since the base class imposes no structure on the returned dicts
  • Add explicit choices() declarations on SimpleListFilter and FieldListFilter with the specific Iterator[_ListFilterChoices] return type, where Django actually enforces the dict shape
  • Fixes spurious mypy [override] errors for ListFilter subclasses that use custom templates with different dict keys (e.g. multi-select filters yielding {"selected": bool, "value": str, "display": str} without query_string)

Why Mapping[str, object] instead of dict[str, Any]?

Neither mypy nor pyright consider TypedDict a structural subtype of dict (even dict[str, Any]), so using dict[str, ...] as the base return type prevents subclasses from narrowing to a TypedDict without triggering [override] errors.

TypedDict IS recognized as a subtype of Mapping by both type checkers, so Iterator[Mapping[str, object]] allows the base class to accept any dict-like return while letting subclasses narrow to Iterator[_ListFilterChoices].

Motivation

ListFilter is the base class and its choices() method raises NotImplementedError — it's meant to be overridden. The consuming code in Django (admin_list_filter template tag) just passes list(spec.choices(cl)) to the template context without enforcing any specific shape. Subclasses with custom templates can return dicts with entirely different keys, but the current stub forces the _ListFilterChoices TypedDict shape on all subclasses, causing spurious [override] errors.

🤖 Generated with Claude Code

alessio-b2c2 and others added 3 commits March 12, 2026 16:17
ListFilter.choices() is the base class method that imposes no structure
on the returned dicts — subclasses with custom templates can yield dicts
with entirely different keys. Change its return type from
Iterator[_ListFilterChoices] to Iterator[dict[str, object]].

Keep the specific _ListFilterChoices return type on SimpleListFilter and
FieldListFilter where the shape is actually enforced by Django.

This fixes spurious mypy [override] errors for ListFilter subclasses
that use custom templates with different dict shapes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Neither mypy nor pyright consider TypedDict a structural subtype of
dict (even dict[str, Any]), so using dict[str, ...] as the base class
return type prevents subclasses from narrowing it to a TypedDict
without triggering [override] errors.

TypedDict IS recognized as a subtype of Mapping by both type checkers,
so Iterator[Mapping[str, object]] is the correct choice: it allows the
base class to accept any dict-like return while letting subclasses like
SimpleListFilter and FieldListFilter narrow to Iterator[_ListFilterChoices].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ilter

Fixes mypy explicit-override errors in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant