Skip to content

chore: replace mypy with ty for type checking - #784

Open
joshmarkovic wants to merge 2 commits into
dbt-msft:masterfrom
joshmarkovic:jm/ty-type-checker
Open

chore: replace mypy with ty for type checking#784
joshmarkovic wants to merge 2 commits into
dbt-msft:masterfrom
joshmarkovic:jm/ty-type-checker

Conversation

@joshmarkovic

Copy link
Copy Markdown
Contributor

Closes #712.

What

Replaces mypy with ty, completing the Astral toolchain consolidation from #707 (lint) and #711 (format). Scope and rules live in [tool.ty] in pyproject.toml; ty==0.0.64 in the dev group is the single source of the version, read by the pre-commit hook, make ty and CI alike.

  • .pre-commit-config.yaml: two mirrors-mypy hooks become one local hook running uv run --frozen ty check.
  • .github/workflows/type-check.yml: new job running the same command.
  • Makefile: make mypy becomes make ty; lint and test updated.
  • .gitignore: .mypy_cache/ dropped, ty writes no cache.

Coverage is unchanged

The adapter package only, with unresolvable dbt.* imports ignored, which is what mypy's --ignore-missing-imports did. dbt is split across distributions: dbt-core and dbt-adapters own dbt/ and dbt/adapters/ in site-packages, this repo contributes dbt/adapters/sqlserver, and the editable install joins them at runtime via an import hook that no static checker can follow.

Why ty runs in Actions and not on pre-commit.ci

pre-commit.ci blocks network access while hooks run, and ty needs it to resolve this project's dependencies. Astral's own recommendation is ci: skip plus a separate CI invocation. pre-commit.ci is this repo's only lint runner today, so without the new job the swap would drop type checking from CI entirely. If a general lint workflow lands later, this job folds into it.

A local hook is used rather than astral-sh/ty-pre-commit so the version is declared once: the upstream hook hardcodes --ty-version in its entry, which would mean a rev pin and a dev-group pin drifting apart, the same dual-pin mypy had. Version bumps now arrive through the existing dependabot pip group as a pyproject.toml + uv.lock change. The hook needs uv on PATH, already a requirement via make dev.

Source changes

Fourteen touches to reach a clean baseline:

  • Annotate Column: Type[SQLServerColumn], which __init__ overwrites with the SQLServerColumnNative subclass. The only real fix.
  • Six suppressions on the behavior flags: dbt-common declares BehaviorFlag's optional keys with a NotRequired that falls back to Optional under a try/except ImportError shim, so source and docs_url read as required. Reproduced in isolation; the suppressions go stale (and ty says so) once that shim is dropped.
  • Two unknown-argument suppressions on SQLServerIndexConfigChange, whose action field comes from the unresolvable RelationConfigChange base.
  • Drop the # type: ignore on SQLServerRelation.type, which suppressed nothing and ty reports as unused.

Inline suppressions rather than disabling invalid-return-type, unknown-argument and missing-typed-dict-key project-wide, so those rules keep working everywhere else.

Two corrections to the issue

  • Motivation 1 is already resolved: the pathspec override was removed in the dbt-core 1.12 upgrade (dd61d69), and pathspec now resolves to 1.0.4 on its own.
  • Motivation 2 is overstated for this repo. Measured on dbt/adapters: mypy 2.1.0 at 2.7s cold and 0.19s warm cache, ty at 0.12s. Real but not the 28x in the issue. The durable wins are one toolchain and config in pyproject.toml.

Worth flagging: ty is still 0.0.x with no stability guarantees, hence the exact pin.

Testing

  • pre-commit run -a passes, including ty.
  • uv run --frozen ty check, the CI command, is clean.
  • 312 unit tests pass.
  • Injected a deliberate -> int: return "not an int" and confirmed the hook fails on it, so the config is not a no-op.
  • Verified --frozen leaves uv.lock byte-identical, so a commit cannot silently relock.

Follow-up

With an extra search path pointing at site-packages, ty does resolve dbt.* and reports 9 findings mypy never saw: an AdapterPlugin protocol mismatch, from_dict incompatible across RelationConfigBase and DataClassDictMixin, mp_context=None against SpawnContext, a can_expand_to override incompatibility, and an unresolved attribute on self.connections. That search path is not committable, it hardcodes a Python version and breaks in CI, but the findings are real and deserve their own issue.

Completes the Astral toolchain consolidation started in dbt-msft#707 and dbt-msft#711:
Ruff for lint and format, ty for types. Scope and rules move into
[tool.ty] in pyproject.toml, and ty is pinned there as the single source
of the version, read by the pre-commit hook, `make ty` and CI alike.

Coverage is unchanged: the adapter package only, with unresolvable
`dbt.*` imports ignored, which is what mypy's --ignore-missing-imports
did. `dbt` is split across distributions, so no static checker can join
dbt-adapters' `dbt/adapters/` in site-packages with this repo's
`dbt/adapters/sqlserver`.

ty cannot run on pre-commit.ci, which blocks the network access ty needs
to resolve dependencies, so the hook is skipped there and a Type check
workflow runs it in GitHub Actions instead. Without that job the swap
would drop type checking from CI entirely.

Source changes needed to reach a clean baseline:

- Annotate `Column: Type[SQLServerColumn]`, which __init__ overwrites
  with the SQLServerColumnNative subclass.
- Suppress `missing-typed-dict-key` and `invalid-return-type` on the
  behavior flags. dbt-common declares BehaviorFlag's optional keys with
  a NotRequired that falls back to Optional under a try/except
  ImportError shim, so every key reads as required.
- Suppress `unknown-argument` on the two SQLServerIndexConfigChange
  constructions, whose `action` field comes from the unresolvable
  RelationConfigChange base.
- Drop the `# type: ignore` on SQLServerRelation.type, which suppressed
  nothing and ty reports as unused.

Closes dbt-msft#712
@joshmarkovic
joshmarkovic marked this pull request as ready for review July 30, 2026 13:38
@axellpadilla

axellpadilla commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

@joshmarkovic taking into account the failure requieres changes, .gitignore: .mypy_cache/ dropped, ty writes no cache. could be kept, some could use the extension still.
Seems you got a good reason for the closed too #776 , ty not yet compatible with the app

@axellpadilla axellpadilla added this to the v1.12.0 milestone Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Evaluate migrating type checking from mypy to ty

2 participants