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
- Create
typemut.tomlin 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
- 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: ignoreor# pragma: no mutate - The annotation is
Any(mutations are meaningless — Any absorbs all types) AddOptionaltargets 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
918b4babab57506114a6175205c111302dbba6cc712b2e74c983a374ba0c3212
|
|
| MD5 |
996ede9506e401aaa6705aab011ced99
|
|
| BLAKE2b-256 |
54a4ca2b0fde5bac5b5c1e00f50df58e0ff64780afc65ec6311760c31bc552fd
|
Provenance
The following attestation bundles were made for typemut-0.1.0.tar.gz:
Publisher:
publish.yml on nkhitrov/typemut
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
typemut-0.1.0.tar.gz -
Subject digest:
918b4babab57506114a6175205c111302dbba6cc712b2e74c983a374ba0c3212 - Sigstore transparency entry: 1166206012
- Sigstore integration time:
-
Permalink:
nkhitrov/typemut@0cb6974c7513c70da6388a2a4e553cb33bc10553 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nkhitrov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0cb6974c7513c70da6388a2a4e553cb33bc10553 -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
572947cd5803b22b4e417e61d83d327e1a23cfc9a0b56c6defd0bc6fadc864ef
|
|
| MD5 |
5800b7dfe3ffc7cc96941235c3c892d5
|
|
| BLAKE2b-256 |
51acff4b424bfd845d4310e183fd58ab3c41fa89db7f6ecc337a5e1e4b334db7
|
Provenance
The following attestation bundles were made for typemut-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on nkhitrov/typemut
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
typemut-0.1.0-py3-none-any.whl -
Subject digest:
572947cd5803b22b4e417e61d83d327e1a23cfc9a0b56c6defd0bc6fadc864ef - Sigstore transparency entry: 1166206078
- Sigstore integration time:
-
Permalink:
nkhitrov/typemut@0cb6974c7513c70da6388a2a4e553cb33bc10553 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nkhitrov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0cb6974c7513c70da6388a2a4e553cb33bc10553 -
Trigger Event:
workflow_dispatch
-
Statement type: