# How to migrate from mypy to ty



[ty](https://pydevtools.com/handbook/reference/ty.md) is a fast Python type checker, developed by [Astral](https://astral.sh/) (the creators of [uv](https://pydevtools.com/handbook/reference/uv.md) and [Ruff](https://pydevtools.com/handbook/reference/ruff.md)). It runs faster than [mypy](https://pydevtools.com/handbook/reference/mypy.md), especially on large codebases.

{{< callout type="warning" >}}
ty is currently in beta. While actively developed, it's still missing some features and may not be ready for full production adoption. This guide helps evaluate readiness and plan for migration.
{{< /callout >}}

## Understanding ty


### When to consider migrating

- Projects needing faster type checking on large codebases
- Teams already using other Astral tools (Ruff, uv)
- Projects not relying on mypy plugins

### When to wait

- Projects requiring Pydantic, Django, or SQLAlchemy mypy plugins
- Projects requiring mature, battle-tested tooling

## Installation and basic usage

### Installing ty

ty is distributed as a standalone binary. The easiest way to use it is via [uvx](https://pydevtools.com/handbook/reference/uv.md):

```bash
# Run ty directly without installation
uvx ty check .

# Or install globally with uv
uv tool install ty

# Then run
ty check .
```

For other installation methods, see the [ty installation guide](https://docs.astral.sh/ty/installation/).

### Basic command equivalents

| mypy Command | ty Equivalent |
|--------------|---------------|
| `mypy .` | `ty check .` |
| `mypy src/` | `ty check src/` |
| `mypy -p mypackage` | `ty check path/to/mypackage/` |
| `mypy --python-version 3.11 .` | `ty check . --python-version 3.11` |
| `mypy --strict .` | No direct equivalent (see configuration) |

For all available CLI options, see the [ty CLI reference](https://docs.astral.sh/ty/reference/cli/).

{{< callout type="info" >}}
ty doesn't accept module names like `-p`; pass file or directory paths instead, or use `--project` or `environment.root` to define source roots.
{{< /callout >}}

### Pointing to the Python environment

ty needs to find installed packages to resolve imports:

```bash
# Point to a virtualenv or interpreter
ty check . --python .venv

# Or let ty auto-detect from an active virtualenv
ty check .
```

For more details on how ty discovers and resolves modules, see the [module discovery documentation](https://docs.astral.sh/ty/modules/).

### Output formats

```bash
# Default verbose output
ty check .

# Concise one-line-per-error output
ty check . --output-format concise

# GitHub Actions annotations
ty check . --output-format github

# GitLab Code Quality format
ty check . --output-format gitlab
```

### Automated error suppression with --add-ignore

When migrating from mypy to ty, you may encounter many new type errors that you want to address gradually. The `--add-ignore` flag automates the process of suppressing these errors by adding `ty: ignore[codes]` comments throughout your codebase.

```bash
# Add ignore comments for all current errors
ty check . --add-ignore

# Verify no new errors remain
ty check .
```

ty groups multiple error codes per comment, extends existing `ty: ignore` comments where they already exist, and validates that the file still parses. Treat the suppressed errors as a todo list, not a fix.

## Migrating configuration

### Configuration file locations

mypy supports:
- `mypy.ini`
- `pyproject.toml` → `[tool.mypy]`
- `setup.cfg` → `[mypy]`
- `.mypy.ini`

ty supports:
- `ty.toml` (takes precedence)
- `pyproject.toml` → `[tool.ty]`
- User-level: `~/.config/ty/ty.toml`

For complete configuration details, see the [ty configuration guide](https://docs.astral.sh/ty/configuration/).

### Basic option mapping

| mypy Option | ty Equivalent | Notes |
|-------------|---------------|-------|
| `python_version = "3.11"` | `environment.python-version = "3.11"` | |
| `ignore_missing_imports = true` | `rules.unresolved-import = "ignore"` | Critical for most projects |
| `exclude = ["tests/"]` | `src.exclude = ["**/tests/**"]` | Uses glob patterns |
| `follow_imports = "silent"` | No equivalent | ty doesn't have import following modes |
| `check_untyped_defs = true` | Default behavior | ty checks all code by default |
| `disallow_untyped_defs = true` | See note below | Enforce via Ruff `ANN001`/`ANN201` |
| `strict = true` | No equivalent | Must enable individual options |
| `plugins = ["pydantic.mypy"]` | Not supported | Major migration blocker |

{{< callout type="note" >}}
If you relied on `disallow_untyped_defs = true`, you can approximate it with Ruff's flake8-annotations rules:
```toml
[tool.ruff.lint]
select = [
  "ANN001", # flake8-annotations
  "ANN201",
]
```
See Ruff rule docs for [missing-type-function-argument](https://docs.astral.sh/ruff/rules/missing-type-function-argument/) and [missing-return-type-undocumented-public-function](https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/).
{{< /callout >}}

### Example configuration migration

mypy ([pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md)):
```toml
[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true
exclude = ["tests/", "docs/"]
check_untyped_defs = true
warn_redundant_casts = true
```

ty ([pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md)):
```toml
[tool.ty.environment]
python-version = "3.11"

[tool.ty.terminal]
error-on-warning = true

[tool.ty.src]
exclude = ["**/tests/**", "**/docs/**"]

[tool.ty.rules]
unresolved-import = "ignore"
# Note: check_untyped_defs is ty's default behavior
# warn_redundant_casts equivalent: warnings are on by default
```

> [!TIP]
> [`error-on-warning = true`](https://docs.astral.sh/ty/reference/configuration/#error-on-warning) makes ty exit non-zero on warnings, not just errors. This prevents warnings from accumulating silently and is recommended for any CI or pre-commit setup. Sebastián Ramírez uses this setting across [all his projects](https://pydevtools.com/blog/migrating-from-mypy-to-ty-lessons-from-fastapi.md) including FastAPI, Typer, and SQLModel.

{{< callout type="info" >}}
If using `ty.toml`, drop the `tool.ty` prefix (e.g., `[environment]`, `[src]`, `[rules]`).
{{< /callout >}}

### Per-module overrides

mypy:
```toml
[[tool.mypy.overrides]]
module = ["mypackage.legacy", "mypackage.legacy.*"]
ignore_errors = true
disallow_untyped_defs = false
```

ty:
```toml
[[tool.ty.overrides]]
include = ["mypackage/legacy/**"]
rules = { unresolved-attribute = "ignore", invalid-assignment = "ignore" }
```

{{< callout type="info" >}}
ty's overrides use file glob patterns, not module names. The syntax and available options differ from mypy.
{{< /callout >}}

### Handling mypy plugins

ty has no plugin system and no plan to add one. Some popular libraries may get native ty support over time.

If your project uses [mypy plugins](https://mypy.readthedocs.io/en/stable/extending_mypy.html), this is a significant migration consideration.

| Plugin | ty Support | Workaround |
|--------|------------|------------|
| `pydantic.mypy` | ❌ Not supported | Ignore Pydantic-related rules |
| `django-stubs` | ❌ Not supported | Wait for native support or use mypy |
| `sqlalchemy[mypy]` | ❌ Not supported | Wait for native support or use mypy |
| `numpy.mypy` | ❌ Not supported | May work with stubs |

Workaround for Pydantic projects:
```toml
[tool.ty.rules]
invalid-argument-type = "ignore"  # Pydantic model constructors
invalid-key = "ignore"            # TypedDict/Pydantic interop
invalid-parameter-default = "ignore"  # Field defaults
```

## CI integration

### Current mypy CI patterns

Common patterns found in production projects:

Pattern 1: Dedicated mypy workflow
```yaml
name: Mypy
on: [push, pull_request]
jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -e .[dev]
      - run: mypy src/
```

Pattern 2: Matrix across Python versions
```yaml
strategy:
  matrix:
    python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
  - run: mypy google/genai/
```

### Migrating to ty

Replace mypy with ty:
```yaml
name: Type Check
on: [push, pull_request]
jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -e .[dev]
      - run: pip install uv
      # Use uvx to run ty without installation
      - run: uvx ty check src/ --python .venv
      # Or, if ty is a dev dependency:
      # - run: uv run ty check src/
```

With GitHub Actions annotations:
```yaml
- run: uvx ty check src/ --output-format github
```

### Running both type checkers during transition

During migration, run both checkers:

```yaml
jobs:
  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -e .[dev] mypy
      - run: mypy src/

  ty:
    runs-on: ubuntu-latest
    continue-on-error: true  # Don't fail build yet
    steps:
      - uses: actions/checkout@v4
      - run: pip install -e .[dev]
      - run: uvx ty check src/ --python .venv || true
```

### Handling dual-ignore comments

When running both checkers, you will encounter cases where mypy requires a `# type: ignore` comment that ty considers unnecessary. ty flags these with [`unused-ignore-comment`](https://docs.astral.sh/ty/reference/rules/#unused-ignore-comment), producing lines like:

```python
result = func()  # type: ignore[assignment]  # ty: ignore[unused-ignore-comment]
```

The first comment silences mypy. The second silences ty's complaint about the first. This is an unavoidable cost of the dual-checker approach and a strong signal that the codebase is ready to drop mypy.

## Common migration issues

### Issue 1: ty checks untyped code by default

Symptom: Massive increase in errors for unannotated codebases.

Why it happens: mypy skips function bodies without type annotations by default. ty checks everything.

Example (NetworkX: 580 files, ~192k LOC):
- mypy: 0 errors
- ty: ~1,200 errors

Solution for unannotated code:
```toml
[tool.ty.rules]
# Ignore errors that arise from missing type info
unresolved-attribute = "ignore"
possibly-missing-attribute = "ignore"
invalid-assignment = "ignore"
```

Or exclude unannotated modules (see [file exclusions](https://docs.astral.sh/ty/exclusions/)):
```toml
[tool.ty.src]
exclude = ["legacy/**", "untyped/**"]
```

### Issue 2: Missing import resolution

Symptom: `unresolved-import` errors for installed packages.

Why it happens: ty needs to know where the Python environment is.

Solution:
```toml
[tool.ty.environment]
python = ".venv"
```

Or via CLI:
```bash
uvx ty check . --python .venv
```

For optional dependencies:
```toml
[tool.ty.rules]
unresolved-import = "ignore"
```

### Issue 3: Different type narrowing behavior

Symptom: Errors like "Attribute may be missing" after type guards.

Example (from IPython):
```python
if _is_sizable(completions):
    return len(completions) != 0  # ty: Expected Sized, found Iterator
```

Why it happens: ty and mypy have different type narrowing algorithms.

Solution: Usually requires code changes or ignoring specific rules:
```toml
[tool.ty.rules]
possibly-missing-attribute = "ignore"
```

Or inline comments:
```python
return len(completions) != 0  # ty: ignore[invalid-argument-type]
```

### Issue 4: Pydantic model errors

Symptom: Many errors related to Pydantic BaseModel, Field, TypedDict.

Example (from python-genai):
```python
options: RequestOptions = {}  # ty error: dict not assignable to RequestOptions
```

Why it happens: No Pydantic plugin support in ty.

Solution: Extensive rule ignoring:
```toml
[tool.ty.rules]
invalid-argument-type = "ignore"
invalid-key = "ignore"
invalid-parameter-default = "ignore"
```

### Issue 5: No equivalent for `ignore_errors = true`

Symptom: Modules with `ignore_errors = true` in mypy still report errors.

Solution: Use overrides to ignore all rules for those files:
```toml
[[tool.ty.overrides]]
include = ["src/legacy/**"]
rules = {
  unresolved-attribute = "ignore",
  invalid-assignment = "ignore",
  invalid-argument-type = "ignore"
  # ... add more as needed
}
```

Or exclude them entirely:
```toml
[tool.ty.src]
exclude = ["src/legacy/**"]
```

### Error code mapping reference

This table maps common [mypy error codes](https://mypy.readthedocs.io/en/stable/error_code_list.html) to their [ty rule](https://docs.astral.sh/ty/reference/rules/) equivalents.

| mypy Code | ty Rule | Description |
|-----------|---------|-------------|
| [import-not-found](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-import-target-can-be-found-import-not-found) | [unresolved-import](https://docs.astral.sh/ty/reference/rules/#unresolved-import) | Module not found |
| [attr-defined](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-attribute-exists-attr-defined) | [unresolved-attribute](https://docs.astral.sh/ty/reference/rules/#unresolved-attribute) | Unknown attribute |
| [arg-type](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-argument-types-arg-type) | [invalid-argument-type](https://docs.astral.sh/ty/reference/rules/#invalid-argument-type) | Wrong argument type |
| [assignment](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-assignment-targets-assignment) | [invalid-assignment](https://docs.astral.sh/ty/reference/rules/#invalid-assignment) | Wrong assignment type |
| [return-type](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-return-types-return-value) | [invalid-return-type](https://docs.astral.sh/ty/reference/rules/#invalid-return-type) | Wrong return type |
| [call-arg](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-calls-call-arg) | [invalid-argument-type](https://docs.astral.sh/ty/reference/rules/#invalid-argument-type) | Wrong call arguments |
| [index](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-indexing-operations-index) | [non-subscriptable](https://docs.astral.sh/ty/reference/rules/#non-subscriptable) | Invalid subscript |
| [union-attr](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-attribute-exists-in-each-union-item-union-attr) | [possibly-missing-attribute](https://docs.astral.sh/ty/reference/rules/#possibly-missing-attribute) | Attribute may be missing |
| [override](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-method-override-is-compatible-with-base-class-override) | [invalid-method-override](https://docs.astral.sh/ty/reference/rules/#invalid-method-override) | Invalid method override |
| [redundant-cast](https://mypy.readthedocs.io/en/stable/error_code_list2.html#check-that-cast-changes-type-redundant-cast) | [redundant-cast](https://docs.astral.sh/ty/reference/rules/#redundant-cast) | Unnecessary cast |
| [unused-ignore](https://mypy.readthedocs.io/en/stable/error_code_list2.html#warn-about-unused-type-ignore-comments-unused-ignore) | [unused-ignore-comment](https://docs.astral.sh/ty/reference/rules/#unused-ignore-comment) | Unused `type: ignore` / `ty: ignore` |
| [no-redef](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-each-name-is-defined-once-no-redef) | [conflicting-declarations](https://docs.astral.sh/ty/reference/rules/#conflicting-declarations) | Name redefinition |
| [valid-type](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-type-of-target-is-known-has-type) | [invalid-type-form](https://docs.astral.sh/ty/reference/rules/#invalid-type-form) | Invalid type expression |

## Related resources

- [ty reference](https://pydevtools.com/handbook/reference/ty.md)
- [mypy reference](https://pydevtools.com/handbook/reference/mypy.md)
- [How to try the ty type checker](https://pydevtools.com/handbook/how-to/how-to-try-the-ty-type-checker.md)
- [uv reference](https://pydevtools.com/handbook/reference/uv.md)
- [Ruff reference](https://pydevtools.com/handbook/reference/ruff.md)
