Skip to content

refactor(typing): enforce strong mypy checks across Sentry's entire Python codebase - #129431

Closed
joshuarli wants to merge 91 commits into
getsentry:masterfrom
joshuarli:typing-hillclimb-campaign
Closed

joshuarli wants to merge 91 commits into
getsentry:masterfrom
joshuarli:typing-hillclimb-campaign

Conversation

@joshuarli

@joshuarli joshuarli commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

All modules covered by the mypy disallow_untyped_defs weaklist now pass strict mypy, so this branch removes the weaklist and the remaining disable_error_code allowlist.

Result

  • Weaklist: 0 entries (master: 1,776 — 1,480 exact, 296 wildcard)
  • Module allowlist: 0 entries (master: 2)
  • Full mypy with no overrides: Success: no issues found in 8460 source files
  • 1,186 changed src/ files; 1,186 strict-checked, 0 still weaklisted
  • 2,297 files changed across 91 commits
  • No net-new # type: ignore (4 moved, 5 removed)
  • check_typing_campaign --goal exits 0; --status reports 0 weaklist entries
  • check_stronglist passes

Verification

  • SENTRY_LOG_LEVEL=WARNING .venv/bin/python -m mypy --warn-unused-configs → no issues
  • .venv/bin/python -m tools.mypy_helpers.check_stronglist pyproject.toml → pass
  • .venv/bin/python -m tools.mypy_helpers.check_typing_campaign --base origin/master → pass
  • make test-api-docs → 143 passed, including example validation
  • Focused rule-action tests for performance issues → pass
  • Every backend test node id that failed on the previous head now passes locally (256 tests across the affected files); relay and symbolicator suites collect cleanly
  • prek run -q → pass
  • CI on head e7cba434a78: backend typing, all 30 backend test shards, api docs, and pre-commit lint pass
  • getsentry / backend dispatch → success; the downstream getsentry backend typing run → failure: 68 errors in 31 files against this branch (master baseline: 4)

getsentry follow-up

The generic Bounded*Field contracts require nullable declarations to name an optional get type, e.g. BoundedBigIntegerField[int | None, int | None](null=True). Sentry is updated, but getsentry still has 39 unparameterized nullable bounded fields; downstream narrowing accounts for most of the remaining errors. A companion getsentry change is required before that downstream job can be green; the sentry-side dispatch check itself is green.

Contracts worth reviewing

  • Event.group remains Group | None; callers and tests narrow with assertions.
  • BoundedIntegerField, CharField, and UUIDField are generic over set/get types; non-null defaults stay non-optional.
  • snuba.query overloads distinguish the totals=True tuple from the scalar result.
  • WrappingU32IntegerField joins the other generic fields; nullable file and blob sizes now declare optional get types, and download responses keep their existing header behavior.
  • snuba.query result keys are str | int because grouped columns keep their runtime type; tagstore narrows tag keys to strings at its boundary.
  • RuleCallback is a named-parameter protocol, so rule futures accept keyword futures.
  • Check-in helpers take partial TypedDict overrides; monitor audit writes use explicit calls.
  • Group-snooze rate paths assert their configured thresholds before use.
  • IntegrationIssueConfigField keeps Any only for arbitrary JSON form defaults and choices so the OpenAPI schema stays resolvable; client key choices are emitted as arrays.
  • The apidocs failure fixture builds an intentionally untyped serializer dynamically instead of a weaklist entry or ignore.

Tooling

  • check_stronglist accepts a pyproject with no weaklist or allowlist.
  • check_typing_campaign checks a whole-branch diff against the base revision's weaklist, while --status/--goal use the working tree.

Integrated behavior fixes

OpenAPI

make test-api-docs passes locally, including example validation. Schema generation needed fixes for arbitrary-JSON integration form fields, client key choices (now rendered as arrays), nullable environment facet names, and nullable release file sizes. The optional prod-vs-dev schema diff still reports annotation-driven changes; review that separately.

Note: the green Type Coverage Diff check measures TypeScript, not Python, and does not substantiate this PR.

Companion

The downstream getsentry fixes are in getsentry/getsentry#22868: it parameterizes the remaining nullable bounded fields and aligns the affected billing, audit, export, and Vercel contracts. It reports only the four pre-existing getsentry baseline errors against this branch.

Phase two

The strict-typing ratchet stacked on this branch is #129444. It records the remaining mypy --strict errors, enforces that changed files cannot add more, and removes the first batch of inferred-Any call sites.

Remaining red checks

  • CodeQL reports 39 alerts in touched files (5 high) from pre-existing patterns; the huge diff makes CodeQL re-report them. No new alert class comes from this branch.
  • migration drift compares the squashed migration state against the database. It reports the raw-SQL indexes from 1234_grouprelease_new_release_id_indexes missing from state, which is pre-existing on master; this branch contains no migration changes.
  • (Optional) Shows the difference between the prod and dev schema is expected to differ due to annotation-driven schema changes; review it separately.
  • Vercel requires deployment authorization.

Shipping

This PR merges first. #129444 and getsentry/getsentry#22868 are stacked on it.

  1. Undraft and get review approval. The diff has no migrations, so there is no migration-lock ordering to coordinate.
  2. Merge to master as usual.
  3. Post the merge SHA. ref(typing)!: enable strict mypy repo-wide #129444 rebases onto it, and getsentry#22868 needs it to typecheck.

Do not merge #129444 before this one: that PR turns on strict = true repo-wide, which only holds once the modules cleaned up here have landed.

Rebuild the accepted typing campaign on current master as an
annotation-only change. Excludes the campaign evaluator, its policy
modules, the campaign log, and owner-owned baseline edits. Behavior
changes are carved into separate PRs; this commit preserves runtime
behavior, including ReleaseFile.save positional compatibility and the
release serializer's existing stats and newGroups output.

Fix two refactor regressions: starred group search views keep their
createdBy/starred/stars fields, and Seer commit lookups stay keyed by
primary key instead of sha.

Allow the weaklist guard to accept narrowing a wildcard into named
modules, which is how the flags endpoints wildcard graduates.
OpenAPI schema generation raised UnableToProceedError for object-valued
mappings and abstract Mapping/Sequence annotations introduced by the
typing pass. Resolve object as a generic object and treat Mapping and
Sequence like dict and list.
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Oct 31, 2026
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

@joshuarli

Copy link
Copy Markdown
Member Author

getsentry verification found one cross-repo typing contract: getsentry/api/serializers/targeted_broadcast.py adds fields to the now-precise AdminBroadcastSerializerResponse. Companion fix: getsentry/getsentry#22867. The getsentry backend-typing check will stay red until that lands; retrigger with the Trigger: getsentry tests label after it merges.

BasePaginator.build_queryset assigned the reversed ordering to queryset.query.order_b, a typo for order_by, when the queryset was already ordered by the descending key and a previous-page cursor needed ascending order. The assignment was silently dropped, so the previous page was filtered as if ascending but still sorted descending, returning the wrong rows. The ordering is now written to order_by, matching the sibling branch.
Exercise the branch that reverses an explicitly descending queryset for
a previous-page cursor. Without the order_by fix, the previous page
returns the current row instead of the preceding page.
Postponed annotations hide NotRequired from TypedDict runtime
introspection, so ActorSerializerResponse.email was emitted as required
and rejected the team-owner examples. Remove the postponed annotations
from that module.

Resolve object-valued mappings as unconstrained values instead of
objects; the examples contain arrays and strings in those maps.
The release archive manifest and artifact index are user-uploaded JSON.
Validate that the decoded value is an object before treating it as one,
so malformed input fails at the boundary instead of raising
AttributeError deeper in the archive reader.

Review split: getsentry#129432.
Bring the reviewed behavior changes into the canonical campaign branch so
the typing diff reflects the final contracts: release serializer defaults,
Teams missing-id failure, blank origin preservation, Jira query
preconditions, and the paginator previous-page fix.
The manifest root is validated, but `files` still needs an object shape
before it is counted and indexed. Reject non-object values at the archive
boundary and cover both malformed shapes.

Review split: getsentry#129432.
Reject non-object thresholds, unknown or non-integer maximum keys, and
non-string units at write time so the serializer casts are backed by a
validated invariant.

Review split: getsentry#129434.
Manifest headers come from uploaded archives. Treat any non-string
debug-id value as absent instead of passing it into symbolic, and drop
the casts that assumed the header value was always a string.

Review split: getsentry#129435.
Gate new Any usage in graduated modules and require a
`# justification:` comment for casts that read JSON, external
responses, metadata, or config. The hook checks staged changes locally;
`--base` and `--status` support branch-wide checks and campaign metrics.

Run the hook against the stage and add tests for graduated/weaklisted
modules, trust-boundary casts, and local casts.
Compare Any tokens against the file's previous revision instead of
flagging every modified line that still contains an existing Any. Limit
the whole-branch mode to Python files under src/.
Use a validating sourcemap-header helper instead of casting arbitrary
header values, and document the remaining provider-response casts with
explicit justifications so the campaign gate enforces the boundary.
Annotate the remaining functions in these modules and remove their
weaklist entries. Full mypy passes with the stricter config, so these
modules are now enforced rather than merely annotated.
getsentry deployments extend the admin broadcast response with plans,
roles, trial status, region, platform, product, early-adopter, and
organization filters. Model them as optional fields on the shared
response so getsentry code stays type-compatible without a companion
change.
@joshuarli
joshuarli requested review from a team as code owners November 1, 2026 06:50
@joshuarli
joshuarli requested review from msonnb and stephanie-anderson and removed request for a team November 1, 2026 06:50
@joshuarli
joshuarli marked this pull request as draft November 1, 2026 09:06
@joshuarli

Copy link
Copy Markdown
Member Author

Superseded by #129447, which combines this PR and its stacked counterpart into a single branch.

@joshuarli joshuarli closed this Nov 1, 2026
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 Scope: Frontend Automatically applied to PRs that change frontend components Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants