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
widen-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.

WidenType

Replaces a concrete class with its parent (base) class to find places where a more abstract type could be used.

class Animal: ...
class Cat(Animal): ...
class Dog(Animal): ...

# Original
def feed(pet: Cat) -> None: ...

# Mutant
def feed(pet: Animal) -> None: ...

Survived = the code doesn't rely on the concrete subclass. The function could accept the broader base type, suggesting the annotation is more specific than necessary.

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
widen-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.2.0.tar.gz (45.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.2.0-py3-none-any.whl (36.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: typemut-0.2.0.tar.gz
  • Upload date:
  • Size: 45.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.2.0.tar.gz
Algorithm Hash digest
SHA256 7727b972965d8a3ad7a567c5230a2bb9bb14aa6b7fe50aa1f1d25e5316f5b348
MD5 ca14cb7f6a029f5956b2f4b577fc5aff
BLAKE2b-256 7016c4f450d9cf49e051088c1f3fa101275de264b31cfaf8181c5ce78e188141

See more details on using hashes here.

Provenance

The following attestation bundles were made for typemut-0.2.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: typemut-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 36.5 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 49114f3bc51c5ea7fc824bed33f546b08fa4fdee2f027193db682843224888c3
MD5 7f8e27d68e0d883b73cd909e60ec7857
BLAKE2b-256 a3653190b6bc1476d688dbe1fe6add148b77cfea5a1dd7872b58da39bd3cbf48

See more details on using hashes here.

Provenance

The following attestation bundles were made for typemut-0.2.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