Skip to main content

Mutation testing for type annotations

Project description

typemut

Mutation testing for Python type annotations.

Standard mutation testing tools (cosmic-ray, mutmut) mutate runtime code and check if tests catch it. typemut mutates only type annotations and checks if type checkers (mypy, pyright) catch the change.

  • Mutant killed = type checker reports an error (types are strict enough)
  • Mutant survived = no type error (types are too loose or type checker coverage is weak)

Installation

pip install typemut
# or
uv add typemut

Quick Start

  1. Create typemut.toml in your project root:
[typemut]
module-path = "src/myproject"
test-command = "make typecheck"  # must exit non-zero on type errors
timeout = 30

[typemut.operators]
remove-union-member = true
swap-literal-value = true
swap-sibling-type = true
strip-annotated = true
remove-optional = true
add-optional = true
swap-container-type = true
  1. Run:
typemut run                          # full pipeline: discover + execute + report
typemut html --open                  # generate HTML report and open in browser

Or from another directory:

typemut -C /path/to/project run

Commands

Command Description
typemut run Full pipeline: discover mutations, run type checker, show report
typemut init Discover mutations and store in SQLite
typemut exec Run type checker against each pending mutation
typemut report Show terminal report
typemut html Generate HTML report with diffs

What It Finds

typemut generates mutations of type annotations and checks whether the type checker catches them. Each mutation operator targets a specific class of type safety issues.

RemoveUnionMember

Removes one member from a union type.

# Original
def handle(value: int | str | float) -> None: ...

# Mutant: remove str
def handle(value: int | float) -> None: ...

Survived = your code doesn't distinguish between union members. If removing str from the union causes no type error, it means no code path relies on value being a str. The union may be overly broad, or the type checker doesn't see the code that handles str specifically.

RemoveOptional

Removes None from X | None.

# Original
def find_user(id: int) -> User | None: ...

# Mutant
def find_user(id: int) -> User: ...

Survived = callers don't check for None. The return type says "might be None" but no consumer's type annotations actually require a None-check. Either the None case is dead code, or callers use # type: ignore.

AddOptional

Adds | None to return types and class fields (parameters are excluded — callers simply won't pass None, making those mutations uninformative).

# Original
class Config:
    name: str

# Mutant
class Config:
    name: str | None

Survived = consumers don't rely on non-None guarantee. The field claims to always have a value, but no typed code would break if it could be None. This often reveals missing type coverage in code that reads the field.

SwapSiblingType

Replaces a class with its sibling (same base class).

class LoanState: ...
class ActiveLoan(LoanState): ...
class ClosedLoan(LoanState): ...

# Original
def process(loan: ActiveLoan) -> None: ...

# Mutant
def process(loan: ClosedLoan) -> None: ...

Survived = the type system doesn't distinguish between sibling states. Common in state machine patterns where the base class is typed as Any or all siblings share the same interface.

SwapLiteralValue

Swaps values inside Literal[...] with other literal values from the same file.

# Original
status: Literal["active"]

# Mutant (if "closed" exists in the same file)
status: Literal["closed"]

Survived = literal values are interchangeable from the type checker's perspective. The code doesn't use literal narrowing or overloads to distinguish between the values.

StripAnnotated

Removes metadata from Annotated[X, ...], leaving just the base type.

# Original
age: Annotated[int, Gt(0)]

# Mutant
age: int

Survived = expected in most cases. Annotated metadata is typically runtime-only (Pydantic validators, etc.). Survived mutants here are normal unless you use mypy plugins that understand the metadata.

SwapContainerType

Swaps between compatible container types.

# Original
items: list[int]

# Mutant
items: tuple[int]

Swap groups: list <-> tuple, set <-> frozenset. Dict has no swap target.

Survived = code doesn't rely on container-specific behavior at the type level. For example, if code only iterates over items, both list and tuple work equally.

Filtering

Annotations are automatically skipped when:

  • The line contains # type: ignore or # pragma: no mutate
  • The annotation is Any (mutations are meaningless — Any absorbs all types)
  • AddOptional targets a function parameter (low signal — callers won't pass None)

Config Reference

[typemut]
module-path = "src/myproject"           # directory to scan for annotations
test-command = "make typecheck"         # command to run type checker
timeout = 30                            # seconds per mutation
excluded-modules = ["src/vendor/*.py"]  # glob patterns to skip
skip-comments = ["type: ignore", "pragma: no mutate"]
db = "typemut.sqlite"                   # database file

[typemut.operators]
# all enabled by default, disable selectively
remove-union-member = true
swap-literal-value = true
swap-sibling-type = true
strip-annotated = true
remove-optional = true
add-optional = true
swap-container-type = true

HTML Report

The HTML report shows:

  • Summary stats and per-module mutation scores
  • Each mutant as a collapsible card with unified diff
  • Color-coded status: killed (green), survived (red), error (orange)
  • Full type checker output per mutant
  • Expand/Collapse All controls
typemut html --open                     # save and open in browser
typemut html -o report.html             # save to specific file

Development

make install    # create venv and install with dev deps
make test       # run tests
make lint       # run mypy

Dependencies

  • parso — CST parsing (preserves formatting and whitespace)
  • rich — terminal reporting
  • click — CLI framework

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typemut-0.1.0.tar.gz (36.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typemut-0.1.0-py3-none-any.whl (26.4 kB view details)

Uploaded Python 3

File details

Details for the file typemut-0.1.0.tar.gz.

File metadata

  • Download URL: typemut-0.1.0.tar.gz
  • Upload date:
  • Size: 36.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for typemut-0.1.0.tar.gz
Algorithm Hash digest
SHA256 918b4babab57506114a6175205c111302dbba6cc712b2e74c983a374ba0c3212
MD5 996ede9506e401aaa6705aab011ced99
BLAKE2b-256 54a4ca2b0fde5bac5b5c1e00f50df58e0ff64780afc65ec6311760c31bc552fd

See more details on using hashes here.

Provenance

The following attestation bundles were made for typemut-0.1.0.tar.gz:

Publisher: publish.yml on nkhitrov/typemut

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file typemut-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: typemut-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 26.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for typemut-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 572947cd5803b22b4e417e61d83d327e1a23cfc9a0b56c6defd0bc6fadc864ef
MD5 5800b7dfe3ffc7cc96941235c3c892d5
BLAKE2b-256 51acff4b424bfd845d4310e183fd58ab3c41fa89db7f6ecc337a5e1e4b334db7

See more details on using hashes here.

Provenance

The following attestation bundles were made for typemut-0.1.0-py3-none-any.whl:

Publisher: publish.yml on nkhitrov/typemut

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page