Repository navigation
feat(typing): Reject Response dict literals that match no TypedDict arm - #131074
Merged
Merged
Conversation
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>
BYK
approved these changes
Nov 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by @BYK, following up on #131067 (comment).
The new
AutofixConflictResponse/AutofixPaymentRequiredResponseTypedDicts in #131067 don't actually constrain the 409/402 bodies.GroupAutofixEndpoint.postalso returnsResponse[ValidationErrorResponse], which isdict[str, Any]. When a dict literal matches no TypedDict arm, the plugin's union-narrowing hook returns mypy's defaultResponse[dict[str, str]], and that type-checks through thedict[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 likeas_validation_errors(serializer)or a variable annotatedValidationErrorResponse.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 thatdict[str, list[str]]loses DRF's nested error shapes. It's also the arm for realserializer.errorsbodies, so the fix belongs in the literal check, not the alias.Blast radius. With the check on, a full
mypyrun reported 21 errors in 9 files. The value-inference change cleared one of those, a false positive on the nested_CodedErrorResponseinorganization_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 addedNonFieldErrorsResponsetosentry.apidocs.response_typesbecause two endpoints share that shape; the others are local_...ErrorResponsetypes. Inproject_team_details.py, theValidationErrorResponsearm only covered one{"detail": [...]}literal, so I replaced it. getsentry uses this same plugin through itstoolssymlink, and its fullmypyrun is still clean.With this branch, putting
"code": "bogus"in therun_in_flight409 ingroup_ai_autofix.pyfails with[response-body]. The unmodified file passes. On master, the bogus version passes. The regression tests intests/tools/mypy_helpers/test_plugin.pycover:LiteralvalueFinalconstantvia @BYK.
--
View Session [Sentry]