Skip to content

ref(typing)!: enforce strict mypy across Sentry's Python codebase (types only) - #129614

Open
sentry-junior[bot] wants to merge 1 commit into
masterfrom
josh/typing-strict-types-only
Open

sentry-junior[bot] wants to merge 1 commit into
masterfrom
josh/typing-strict-types-only

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

This is the types-only version of #129447. It turns on strict = true for the whole codebase and empties the mypy weaklist. It keeps today's master runtime behavior. The behavior fixes that were folded into #129447 come back as separate PRs stacked on this branch.

Changes from #129447

  • strict = true with warn_return_any = false. The no-any-return errors were most of the work, and fixing them added return cast(...) calls, isinstance/raise TypeError guards, and # justification: comments. Those are gone.
  • Where strict types and master behavior conflict, master behavior stays and the types give way: a cast, an Any, or a scoped # type: ignore[...]. Examples:
  • get_mapping, post_mapping and get_sequence in shared_integrations/client/base.py are pure casts. They no longer raise ApiError when a response has an unexpected shape.
  • Bounded*Field are bare subclasses again, with # type: ignore[type-arg] on 7 class lines. The [int | None, int | None] subscripts are gone from call sites. These fields type-check as Any, as they do on master.
  • These are removed and can come back as separate PRs if we still want them: check_typing_campaign.py with its hook and tests, the weaklist sort tooling, the .github/workflows/pre-commit.yml rewrite, and the inviteStatus nullability change in static/app/types/organization.tsx.
  • I tried allow_redefinition = true. Under mypy 2.1 it adds about 150 errors in existing code, so it's off and the variable renames stay.

Rebased on master (e39c07b6). Conflicts were resolved in master's favor, with types added on top. Master code added since the old base needed a few more stub entries in fixtures/stubs-for-mypy (PGAccessExclusive/PGLock, two selenium exceptions, WebDriverWait.until accepting Literal[False]) and annotations on a handful of new tests.

Deliberately left in

  • About 95 assert x is not None lines for type narrowing. I haven't checked each one against master behavior.
  • UNBOUNDED_DATETIME = cast("datetime", None) in tests/snuba/search/test_backend.py. Widening snuba_search to datetime | None spreads into run_eap_group_search and _prepare_params_for_categories.

Stack (all based on this branch, land after it):

#129433 isn't restacked. Master's owned_by_acting_user() already returns Q(pk__in=[]) for anonymous users, so those queries are skipped there.

No getsentry change needed. getsentry's backend typing flagged 7 errors against an earlier push of this branch. Each one was a Sentry signature this PR had made narrower than master's contract, so they're fixed here, not in getsentry:

  • BaseApiClient.request/get/get_cached/post/put/patch/delete/head_cached return Any again, as on master. The get_mapping/get_sequence helpers keep their typed returns.
  • OAuth2Provider.get_oauth_client_id() returns Any. getsentry overrides it with an option typed str | int.
  • AbstractFile.getfile() returns Any. getsentry's uploader treats it as a BufferedIOBase.
  • BidirectionalMapping is generic in its values (BidirectionalMapping[KT, VT]), so lookups infer str instead of object.
  • BitField subclasses BigIntegerField[Any, Any], so subscription.flags.<name> resolves the way it does on master.

The BitField fix also restores two master behaviors the earlier push had changed: plain string flags are added to labels again, and _arg_flags holds the normalized flag list again, which is what deconstruct() writes into migrations.

getsentry/getsentry#22868 is no longer needed for this PR.

Verification

  • mypy (full repo, strict): Success: no issues found in 8527 source files, on this branch and on this branch with all 10 stacked PRs applied.
  • ruff check / ruff format --check on the changed Python files, plus ruff --select F821,F401 across src and tests.
  • getsentry master's mypy run against this branch reports the same 9 errors it reports against Sentry master: 6 are local-checkout gaps (scripts.* and devlocal modules missing in my sandbox) and 3 are already in getsentry/jobs/migrate_legacy_seer.py. None come from this PR.
  • I did not run pytest locally because there's no Postgres or Redis in my sandbox. CI is the test signal.

Supersedes #129447 (fork branch, so I couldn't push to it).

via @joshuarli.

--

View Session [Sentry]

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 3, 2026
@sentry-junior
sentry-junior Bot force-pushed the josh/typing-strict-types-only branch from 82096e2 to 9fda5d0 Compare November 3, 2026 00:12
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

decision-model-linter

api

src/sentry/core/endpoints/project_details.py

  • warn readsRequestBodyOutsideSerializer = TRUE (0.84): Request body values should be read from a serializer's validated_data. This file reads a value directly from request.data or request.POST.

src/sentry/integrations/gitlab/webhooks.py

  • warn readsWebhookPayloadOutsideSerializer = TRUE (0.72): Third-party webhook payloads should be validated with a serializer or typed schema before their fields are read. This file reads fields directly from the incoming payload.

src/sentry/integrations/msteams/webhook.py

  • warn readsWebhookPayloadOutsideSerializer = TRUE (0.80): Third-party webhook payloads should be validated with a serializer or typed schema before their fields are read. This file reads fields directly from the incoming payload.

src/sentry/integrations/slack/requests/event.py

  • warn readsWebhookPayloadOutsideSerializer = TRUE (0.84): Third-party webhook payloads should be validated with a serializer or typed schema before their fields are read. This file reads fields directly from the incoming payload.

src/sentry/releases/endpoints/project_release_files.py

  • warn readsRequestBodyOutsideSerializer = TRUE (0.85): Request body values should be read from a serializer's validated_data. This file reads a value directly from request.data or request.POST.

audit

src/sentry/web/frontend/setup_wizard.py

  • warn issuesOrRevokesTokenWithoutRecord = TRUE (0.78): Token changes should be logged with create_audit_entry for org tokens or capture_security_activity for personal tokens. This file creates, revokes, or rescopes a token, but neither call was found in it. Ignore this if a function the file calls already logs the change.

decision-model-linter: 3423 calls on 1214 files, 7,322,057 input tokens, $0.31, model typesafe/jev-1.13-20260917


try:
# grab the filter out of the brackets of the string that looks
# like so: members[value eq "123124"]
regex_search = re.search(r"\[(.*?)\]", operation["path"])
regex_search = re.search(r"\[(.*?)\]", cast(str, operation["path"]))
Turn on `strict = true` for the whole codebase and empty the mypy
weaklist. This change is types only and keeps master's runtime
behavior. Behavior fixes land as separate PRs on top.

- `strict = true` with `warn_return_any = false`. There are no
  `return cast(...)` calls or runtime guards that exist only to satisfy
  `no-any-return`.
- Where strict typing and master behavior conflict, master wins: use a
  cast, `Any`, or a scoped `# type: ignore[...]`, for example the
  paginator `order_b` typo that #127799 fixes.
- The `get_mapping`/`post_mapping`/`get_sequence` client helpers are
  pure casts and never raise for an unexpected shape.
- `Bounded*Field` stay bare subclasses (`# type: ignore[type-arg]`), so
  call sites don't need `[int | None, int | None]` subscripts.

Co-Authored-By: Joshua Li <14209781+joshuarli@users.noreply.github.com>
@sentry-junior
sentry-junior Bot force-pushed the josh/typing-strict-types-only branch from 9fda5d0 to 73fa505 Compare November 3, 2026 00:31
@sentry-junior
sentry-junior Bot marked this pull request as ready for review November 3, 2026 00:39
@sentry-junior
sentry-junior Bot requested review from a team as code owners November 3, 2026 00:39
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

This PR is too large for Bugbot to review. It changes 24,218 lines and 3,674,192 characters. Split the change into smaller pull requests to get a review.

@sentry-junior
sentry-junior Bot requested review from a team as code owners November 3, 2026 00:39
@sentry-junior
sentry-junior Bot requested review from a team and gi0baro as code owners November 3, 2026 00:40
@sentry-junior
sentry-junior Bot requested review from msonnb and stephanie-anderson and removed request for a team November 3, 2026 00:40
Comment on lines +174 to 178
csv_content = cast(str, self._generate_csv(projects))
response = HttpResponse(content_type="text/csv", status=200)
response["Content-Disposition"] = 'attachment; filename="stats_summary.csv"'
response.write(csv_content)
return response

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty stats download writes None into HttpResponse

_generate_csv returns None for empty projects; cast(str, ...) is a no-op at runtime, so response.write(None) raises AttributeError. Return an empty string instead of None when there are no projects.

Evidence
  • massage_sessions_result_summary returns projects={} when Snuba totals are empty (e.g. future/empty range; covered by test_future_request).
  • On download, line 174 does cast(str, self._generate_csv(projects)); cast does not convert None.
  • _generate_csv returns None when not len(projects) (lines 212-213).
  • response.write(csv_content) then calls .encode on None and raises AttributeError.

Identified by Warden · sentry-backend-bugs · BKS-KKR

@joshuarli
joshuarli marked this pull request as draft November 3, 2026 02:16
@joshuarli
joshuarli requested a review from armenzg November 3, 2026 02:17
@joshuarli
joshuarli marked this pull request as ready for review November 3, 2026 02:17

This branch has not been deployed

No deployments
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.

2 participants