Skip to content

feat(typing): Reject Response dict literals that match no TypedDict arm - #131074

Merged
BYK merged 1 commit into
masterfrom
feat/mypy-response-literal-loose-arm
Nov 13, 2026
Merged

BYK merged 1 commit into
masterfrom
feat/mypy-response-literal-loose-arm

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Requested by @BYK, following up on #131067 (comment).

The new AutofixConflictResponse / AutofixPaymentRequiredResponse TypedDicts in #131067 don't actually constrain the 409/402 bodies. GroupAutofixEndpoint.post also returns Response[ValidationErrorResponse], which is dict[str, Any]. When a dict literal matches no TypedDict arm, the plugin's union-narrowing hook returns mypy's default Response[dict[str, str]], and that type-checks through the dict[str, Any] arm. So {"detail": ..., "code": "bogus"} in the 409 branch passed mypy.

Change. In tools/mypy_helpers/plugin.py, a non-empty dict literal that matches none of the TypedDict arms in the declared union now gets a [response-body] error, but only when a loose (non-TypedDict) arm would have let it through. Without a loose arm, mypy already reports the incompatible return, so the plugin adds nothing. Two kinds of body are still accepted: literals that match an arm, and non-literal bodies like as_validation_errors(serializer) or a variable annotated ValidationErrorResponse.

I also changed how the plugin checks literal values. Each one is now inferred against its declared field type, silently, the way mypy probes overloads. Before, a nested literal like {"detail": {"message": ..., "code": ...}} could never match a nested TypedDict, and with the new check that would have been a false positive.

Why not narrow ValidationErrorResponse? Its docstring explains that dict[str, list[str]] loses DRF's nested error shapes. It's also the arm for real serializer.errors bodies, so the fix belongs in the literal check, not the alias.

Blast radius. With the check on, a full mypy run reported 21 errors in 9 files. The value-inference change cleared one of those, a false positive on the nested _CodedErrorResponse in organization_auth_tokens.py. The other 20 were inline DRF-style field-error bodies such as {"non_field_errors": [...]}, {"refs": [...]} and {"role": "..."}. Each one now has a TypedDict arm, with no runtime change. I added NonFieldErrorsResponse to sentry.apidocs.response_types because two endpoints share that shape; the others are local _...ErrorResponse types. In project_team_details.py, the ValidationErrorResponse arm only covered one {"detail": [...]} literal, so I replaced it. getsentry uses this same plugin through its tools symlink, and its full mypy run is still clean.

With this branch, putting "code": "bogus" in the run_in_flight 409 in group_ai_autofix.py fails with [response-body]. The unmodified file passes. On master, the bogus version passes. The regression tests in tests/tools/mypy_helpers/test_plugin.py cover:

  • a wrong Literal value
  • a typo'd key
  • correct literals, both raw and from a Final constant
  • the annotated-variable opt-out
  • nested TypedDicts
  • no duplicate error when there's no loose arm

via @BYK.

--

View Session [Sentry]

A Response[dict[str, Any]] arm such as ValidationErrorResponse accepts any dict literal,
so a body with a typo'd key or a code outside its Literal type-checked even when the
endpoint declares the intended TypedDict. The Sentry mypy plugin now reports a
[response-body] error when a dict literal matches none of the TypedDict arms and only
passes through a loose arm.

Dict literal values are now inferred against the declared field type, so nested
literals can match nested TypedDicts. The inline field-error bodies this surfaced get
TypedDict arms; runtime behavior is unchanged.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@sentry-junior
sentry-junior Bot requested review from a team as code owners November 13, 2026 15:32
@sentry-junior
sentry-junior Bot requested review from a team as code owners November 13, 2026 15:32
@sentry-junior
sentry-junior Bot requested a review from BYK November 13, 2026 15:32
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 13, 2026
@BYK
BYK enabled auto-merge (squash) November 13, 2026 15:34
@BYK
BYK merged commit b83c51d into master Nov 13, 2026
72 checks passed
@BYK
BYK deleted the feat/mypy-response-literal-loose-arm branch November 13, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant