Skip to content

Operators Module

The operators module provides the mutation operator system for pytest-gremlins. Operators identify AST patterns and generate mutated variants (gremlins).

Overview

Mutation operators are the core of mutation testing. Each operator:

  1. Identifies specific AST node patterns it can mutate
  2. Generates one or more mutated variants of matching nodes
  3. Provides human-readable descriptions for reports

Module Exports

Python
from pytest_gremlins.operators import (
    GremlinOperator,      # Protocol for all operators
    OperatorRegistry,     # Central operator registration
    ArithmeticOperator,   # +, -, *, /, //, %, **
    BooleanOperator,      # and/or, True/False, not
    BoundaryOperator,     # Off-by-one (value +/- 1)
    ComparisonOperator,   # <, <=, >, >=, ==, !=
    ReturnOperator,       # return value mutations
)

Protocol

GremlinOperator

All mutation operators must implement this protocol.

GremlinOperator

Bases: Protocol

Protocol for all mutation operators.

A GremlinOperator identifies specific AST patterns and generates mutated variants (gremlins) of those patterns.

Attributes:

Name Type Description
name str

Unique identifier for this operator (e.g., 'comparison', 'arithmetic').

description str

Human-readable description for reports.

name property

Python
name

Return unique identifier for this operator.

description property

Python
description

Return human-readable description for reports.

can_mutate

Python
can_mutate(node)

Return True if this operator can mutate the given AST node.

Parameters:

Name Type Description Default
node AST

The AST node to check.

required

Returns:

Type Description
bool

True if this operator can generate mutations for this node.

Source code in src/pytest_gremlins/operators/protocol.py
Python
def can_mutate(self, node: ast.AST) -> bool:
    """Return True if this operator can mutate the given AST node.

    Args:
        node: The AST node to check.

    Returns:
        True if this operator can generate mutations for this node.
    """
    ...

mutate

Python
mutate(node)

Return all mutated variants of this node.

Each returned AST node represents one gremlin (mutation).

Parameters:

Name Type Description Default
node AST

The AST node to mutate.

required

Returns:

Type Description
list[AST]

List of mutated AST nodes, one for each possible mutation.

Source code in src/pytest_gremlins/operators/protocol.py
Python
def mutate(self, node: ast.AST) -> list[ast.AST]:
    """Return all mutated variants of this node.

    Each returned AST node represents one gremlin (mutation).

    Args:
        node: The AST node to mutate.

    Returns:
        List of mutated AST nodes, one for each possible mutation.
    """
    ...

Protocol Methods

Method Returns Description
name str Unique identifier (e.g., 'comparison')
description str Human-readable description
can_mutate(node) bool Whether operator can mutate this node
mutate(node) list[AST] List of mutated AST variants

Implementing a Custom Operator

Python
import ast
import copy

class CustomOperator:
    """Example custom mutation operator."""

    @property
    def name(self) -> str:
        return 'custom'

    @property
    def description(self) -> str:
        return 'Custom mutations for demonstration'

    def can_mutate(self, node: ast.AST) -> bool:
        # Return True for nodes this operator handles
        return isinstance(node, ast.Constant) and node.value == 42

    def mutate(self, node: ast.AST) -> list[ast.AST]:
        # Return list of mutated variants
        if not isinstance(node, ast.Constant):
            return []
        mutated = copy.deepcopy(node)
        mutated.value = 0
        return [mutated]

Registry

OperatorRegistry

Central registry for managing mutation operators.

OperatorRegistry

Python
OperatorRegistry()

Central registry for gremlin operators.

This class manages the registration and retrieval of mutation operators. Operators are registered by their name and can be retrieved individually or as a group.

Example

from pytest_gremlins.operators import ComparisonOperator registry = OperatorRegistry() registry.register(ComparisonOperator) 'comparison' in registry.available() True

Source code in src/pytest_gremlins/operators/registry.py
Python
def __init__(self) -> None:
    """Initialize an empty registry."""
    self._operators: dict[str, type[GremlinOperator]] = {}

register

Python
register(operator_class, name=None)

Register an operator class.

Parameters:

Name Type Description Default
operator_class type[GremlinOperator]

The operator class to register.

required
name str | None

Optional name to register under. If not provided, uses the operator's name property.

None
Source code in src/pytest_gremlins/operators/registry.py
Python
def register(
    self,
    operator_class: type[GremlinOperator],
    name: str | None = None,
) -> None:
    """Register an operator class.

    Args:
        operator_class: The operator class to register.
        name: Optional name to register under. If not provided,
              uses the operator's name property.
    """
    key = name if name is not None else operator_class().name
    self._operators[key] = operator_class

register_decorator

Python
register_decorator(name=None)

Decorator to register an operator class.

Parameters:

Name Type Description Default
name str | None

Optional name to register under.

None

Returns:

Type Description
Callable[[type[GremlinOperator]], type[GremlinOperator]]

Decorator function that registers the class.

Example

registry = OperatorRegistry() @registry.register_decorator('comparison') ... class ComparisonOperator: ... ...

Source code in src/pytest_gremlins/operators/registry.py
Python
def register_decorator(
    self,
    name: str | None = None,
) -> Callable[[type[GremlinOperator]], type[GremlinOperator]]:
    """Decorator to register an operator class.

    Args:
        name: Optional name to register under.

    Returns:
        Decorator function that registers the class.

    Example:
        >>> registry = OperatorRegistry()
        >>> @registry.register_decorator('comparison')
        ... class ComparisonOperator:
        ...     ...
    """

    def decorator(operator_class: type[GremlinOperator]) -> type[GremlinOperator]:
        self.register(operator_class, name=name)
        return operator_class

    return decorator

get

Python
get(name)

Get a single operator by name.

Parameters:

Name Type Description Default
name str

The registered name of the operator.

required

Returns:

Type Description
GremlinOperator

An instance of the requested operator.

Raises:

Type Description
KeyError

If no operator is registered with the given name.

Source code in src/pytest_gremlins/operators/registry.py
Python
def get(self, name: str) -> GremlinOperator:
    """Get a single operator by name.

    Args:
        name: The registered name of the operator.

    Returns:
        An instance of the requested operator.

    Raises:
        KeyError: If no operator is registered with the given name.
    """
    if name not in self._operators:
        raise KeyError(f"Unknown operator: '{name}'")
    return self._operators[name]()

get_all

Python
get_all(enabled=None)

Get operator instances.

Parameters:

Name Type Description Default
enabled list[str] | None

If provided, only return these operators (in order). If None, return all registered operators.

None

Returns:

Type Description
list[GremlinOperator]

List of operator instances.

Source code in src/pytest_gremlins/operators/registry.py
Python
def get_all(self, enabled: list[str] | None = None) -> list[GremlinOperator]:
    """Get operator instances.

    Args:
        enabled: If provided, only return these operators (in order).
                 If None, return all registered operators.

    Returns:
        List of operator instances.
    """
    if enabled is None:
        return [op() for op in self._operators.values()]

    operators: list[GremlinOperator] = []
    for name in enabled:
        if name in self._operators:
            operators.append(self._operators[name]())
        else:
            warnings.warn(f"Unknown operator '{name}' requested, ignoring", UserWarning, stacklevel=2)
    return operators

available

Python
available()

List all registered operator names.

Returns:

Type Description
list[str]

List of registered operator names.

Source code in src/pytest_gremlins/operators/registry.py
Python
def available(self) -> list[str]:
    """List all registered operator names.

    Returns:
        List of registered operator names.
    """
    return list(self._operators.keys())

Registry Methods

Method Description
register(cls, name=None) Register an operator class
register_decorator(name=None) Decorator for registration
get(name) Get single operator by name
get_all(enabled=None) Get list of operator instances
available() List registered operator names

Usage Examples

Python
from pytest_gremlins.operators import OperatorRegistry, ComparisonOperator

# Create a registry
registry = OperatorRegistry()

# Register operators
registry.register(ComparisonOperator)
registry.register(CustomOperator, name='custom')

# List available operators
print(registry.available())  # ['comparison', 'custom']

# Get specific operator
op = registry.get('comparison')

# Get all operators
all_ops = registry.get_all()

# Get subset of operators
subset = registry.get_all(enabled=['comparison', 'arithmetic'])

Using the Decorator

Python
registry = OperatorRegistry()

@registry.register_decorator('my_operator')
class MyOperator:
    @property
    def name(self) -> str:
        return 'my_operator'
    # ...

Built-in Operators

pytest-gremlins includes five built-in operators covering common mutation patterns.

ComparisonOperator

Mutates comparison operators to catch boundary and off-by-one bugs.

ComparisonOperator

Mutate comparison operators.

Generates mutations for comparison operators, swapping them with related operators to catch off-by-one and boundary condition bugs.

Mutations

  • < -> <=, >
  • <= -> <, >
  • -> >=, <

  • = -> >, <

  • == -> !=
  • != -> ==

name property

Python
name

Return unique identifier for this operator.

description property

Python
description

Return human-readable description for reports.

can_mutate

Python
can_mutate(node)

Return True if this operator can mutate the given AST node.

Parameters:

Name Type Description Default
node AST

The AST node to check.

required

Returns:

Type Description
bool

True if this is a Compare node with supported operators.

Source code in src/pytest_gremlins/operators/comparison.py
Python
def can_mutate(self, node: ast.AST) -> bool:
    """Return True if this operator can mutate the given AST node.

    Args:
        node: The AST node to check.

    Returns:
        True if this is a Compare node with supported operators.
    """
    if not isinstance(node, ast.Compare):
        return False

    return any(type(op) in self.MUTATIONS for op in node.ops)

mutate

Python
mutate(node)

Return all mutated variants of this node.

Parameters:

Name Type Description Default
node AST

The AST node to mutate.

required

Returns:

Type Description
list[AST]

List of mutated AST nodes, one for each possible mutation.

Source code in src/pytest_gremlins/operators/comparison.py
Python
def mutate(self, node: ast.AST) -> list[ast.AST]:
    """Return all mutated variants of this node.

    Args:
        node: The AST node to mutate.

    Returns:
        List of mutated AST nodes, one for each possible mutation.
    """
    if not isinstance(node, ast.Compare):
        return []

    mutations: list[ast.AST] = []

    for i, op in enumerate(node.ops):
        op_type = type(op)
        if op_type in self.MUTATIONS:
            for replacement_op_type in self.MUTATIONS[op_type]:
                mutated = copy.deepcopy(node)
                mutated.ops[i] = replacement_op_type()
                mutations.append(mutated)

    return mutations

get_symbol

Python
get_symbol(op)

Get the symbol for a comparison operator.

Parameters:

Name Type Description Default
op cmpop

A comparison operator AST node.

required

Returns:

Type Description
str

The symbol string (e.g., '<', '<=', '=='), or '?' if unknown.

Source code in src/pytest_gremlins/operators/comparison.py
Python
def get_symbol(self, op: ast.cmpop) -> str:
    """Get the symbol for a comparison operator.

    Args:
        op: A comparison operator AST node.

    Returns:
        The symbol string (e.g., '<', '<=', '=='), or '?' if unknown.
    """
    return self.OP_TO_SYMBOL.get(type(op), '?')

Mutations

Original Mutations
< <=, >
<= <, >
> >=, <
>= >, <
== !=
!= ==

Example

Python
# Original code
if age >= 18:
    return "adult"

# Mutations generated:
# 1. if age > 18:   (>= to >)
# 2. if age < 18:   (>= to <)

ArithmeticOperator

Mutates arithmetic operators to catch calculation errors.

ArithmeticOperator

Mutate arithmetic operators.

Generates mutations for arithmetic operators, swapping them with related operators to catch calculation errors.

Mutations

    • -> -
    • -> +
    • -> /
  • / -> *
  • // -> /
  • % -> //
  • ** -> *

name property

Python
name

Return unique identifier for this operator.

description property

Python
description

Return human-readable description for reports.

can_mutate

Python
can_mutate(node)

Return True if this operator can mutate the given AST node.

Parameters:

Name Type Description Default
node AST

The AST node to check.

required

Returns:

Type Description
bool

True if this is a BinOp node with a supported arithmetic operator.

Source code in src/pytest_gremlins/operators/arithmetic.py
Python
def can_mutate(self, node: ast.AST) -> bool:
    """Return True if this operator can mutate the given AST node.

    Args:
        node: The AST node to check.

    Returns:
        True if this is a BinOp node with a supported arithmetic operator.
    """
    if not isinstance(node, ast.BinOp):
        return False

    return type(node.op) in self.MUTATIONS

mutate

Python
mutate(node)

Return all mutated variants of this node.

Parameters:

Name Type Description Default
node AST

The AST node to mutate.

required

Returns:

Type Description
list[AST]

List of mutated AST nodes, one for each possible mutation.

Source code in src/pytest_gremlins/operators/arithmetic.py
Python
def mutate(self, node: ast.AST) -> list[ast.AST]:
    """Return all mutated variants of this node.

    Args:
        node: The AST node to mutate.

    Returns:
        List of mutated AST nodes, one for each possible mutation.
    """
    if not isinstance(node, ast.BinOp):
        return []

    op_type = type(node.op)
    if op_type not in self.MUTATIONS:
        return []

    mutations: list[ast.AST] = []
    for replacement_op_type in self.MUTATIONS[op_type]:
        mutated = copy.deepcopy(node)
        mutated.op = replacement_op_type()
        mutations.append(mutated)

    return mutations

get_symbol

Python
get_symbol(op)

Get the symbol for an arithmetic operator.

Parameters:

Name Type Description Default
op operator

An arithmetic operator AST node.

required

Returns:

Type Description
str

The symbol string (e.g., '+', '-', '*'), or '?' if unknown.

Source code in src/pytest_gremlins/operators/arithmetic.py
Python
def get_symbol(self, op: ast.operator) -> str:
    """Get the symbol for an arithmetic operator.

    Args:
        op: An arithmetic operator AST node.

    Returns:
        The symbol string (e.g., '+', '-', '*'), or '?' if unknown.
    """
    return self.OP_TO_SYMBOL.get(type(op), '?')

Mutations

Original Mutations
+ -
- +
* /
/ *
// /
% //
** *

Example

Python
# Original code
total = price * quantity

# Mutation generated:
# total = price / quantity  (* to /)

BooleanOperator

Mutates boolean logic to catch logic errors.

BooleanOperator

Mutate boolean operators and values.

Generates mutations for boolean logic to catch logic errors.

Mutations

  • and -> or
  • or -> and
  • not x -> x
  • True -> False
  • False -> True

name property

Python
name

Return unique identifier for this operator.

description property

Python
description

Return human-readable description for reports.

can_mutate

Python
can_mutate(node)

Return True if this operator can mutate the given AST node.

Parameters:

Name Type Description Default
node AST

The AST node to check.

required

Returns:

Type Description
bool

True if this is a boolean operation or value we can mutate.

Source code in src/pytest_gremlins/operators/boolean.py
Python
def can_mutate(self, node: ast.AST) -> bool:
    """Return True if this operator can mutate the given AST node.

    Args:
        node: The AST node to check.

    Returns:
        True if this is a boolean operation or value we can mutate.
    """
    if isinstance(node, ast.BoolOp):
        return True

    if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
        return True

    return isinstance(node, ast.Constant) and isinstance(node.value, bool)

mutate

Python
mutate(node)

Return all mutated variants of this node.

Parameters:

Name Type Description Default
node AST

The AST node to mutate.

required

Returns:

Type Description
list[AST]

List of mutated AST nodes, one for each possible mutation.

Source code in src/pytest_gremlins/operators/boolean.py
Python
def mutate(self, node: ast.AST) -> list[ast.AST]:
    """Return all mutated variants of this node.

    Args:
        node: The AST node to mutate.

    Returns:
        List of mutated AST nodes, one for each possible mutation.
    """
    if isinstance(node, ast.BoolOp):
        return self._mutate_boolop(node)

    if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
        return self._mutate_not(node)

    if isinstance(node, ast.Constant) and isinstance(node.value, bool):
        return self._mutate_bool_constant(node)

    return []

Mutations

Original Mutations
and or
or and
not x x
True False
False True

Example

Python
# Original code
if is_admin and is_active:
    grant_access()

# Mutation generated:
# if is_admin or is_active:  (and to or)

BoundaryOperator

Mutates integer constants in comparisons by ± 1 to catch off-by-one errors.

BoundaryOperator

Mutate boundary conditions in comparisons.

Generates mutations for integer constants in comparisons by shifting them by ± 1 to catch off-by-one errors.

Mutations

  • x >= 18 -> x >= 17, x >= 19
  • x > 0 -> x > -1, x > 1

name property

Python
name

Return unique identifier for this operator.

description property

Python
description

Return human-readable description for reports.

can_mutate

Python
can_mutate(node)

Return True if this operator can mutate the given AST node.

Parameters:

Name Type Description Default
node AST

The AST node to check.

required

Returns:

Type Description
bool

True if this is a comparison with an integer constant.

Source code in src/pytest_gremlins/operators/boundary.py
Python
def can_mutate(self, node: ast.AST) -> bool:
    """Return True if this operator can mutate the given AST node.

    Args:
        node: The AST node to check.

    Returns:
        True if this is a comparison with an integer constant.
    """
    if not isinstance(node, ast.Compare):
        return False

    if self._has_integer_constant_on_left(node):
        return True

    return self._has_integer_constant_in_comparators(node)

mutate

Python
mutate(node)

Return all mutated variants of this node.

Parameters:

Name Type Description Default
node AST

The AST node to mutate.

required

Returns:

Type Description
list[AST]

List of mutated AST nodes, one for each boundary shift.

Source code in src/pytest_gremlins/operators/boundary.py
Python
def mutate(self, node: ast.AST) -> list[ast.AST]:
    """Return all mutated variants of this node.

    Args:
        node: The AST node to mutate.

    Returns:
        List of mutated AST nodes, one for each boundary shift.
    """
    if not isinstance(node, ast.Compare):
        return []

    mutations: list[ast.AST] = []

    if self._has_integer_constant_on_left(node):
        mutations.extend(self._mutate_left_constant(node))

    if self._has_integer_constant_in_comparators(node):
        mutations.extend(self._mutate_comparator_constants(node))

    return mutations

Mutations

For each integer constant in a comparison:

Original Mutations
n n - 1, n + 1

Example

Python
# Original code
if age >= 18:
    return "adult"

# Mutations generated:
# 1. if age >= 17:   (18 to 17)
# 2. if age >= 19:   (18 to 19)

Note

BoundaryOperator only targets integer constants within comparison expressions. Boolean values (True/False) are excluded.


ReturnOperator

Mutates return statements to verify tests check return values.

ReturnOperator

Mutate return statements.

Generates mutations for return statements to verify that tests actually check return values.

Mutations

  • return x -> return None
  • return True -> return False
  • return False -> return True

name property

Python
name

Return unique identifier for this operator.

description property

Python
description

Return human-readable description for reports.

can_mutate

Python
can_mutate(node)

Return True if this operator can mutate the given AST node.

Parameters:

Name Type Description Default
node AST

The AST node to check.

required

Returns:

Type Description
bool

True if this is a return statement with a non-None value.

Source code in src/pytest_gremlins/operators/return_value.py
Python
def can_mutate(self, node: ast.AST) -> bool:
    """Return True if this operator can mutate the given AST node.

    Args:
        node: The AST node to check.

    Returns:
        True if this is a return statement with a non-None value.
    """
    if not isinstance(node, ast.Return):
        return False

    if node.value is None:
        return False

    return not (isinstance(node.value, ast.Constant) and node.value.value is None)

mutate

Python
mutate(node)

Return all mutated variants of this node.

Parameters:

Name Type Description Default
node AST

The AST node to mutate.

required

Returns:

Type Description
list[AST]

List of mutated AST nodes.

Source code in src/pytest_gremlins/operators/return_value.py
Python
def mutate(self, node: ast.AST) -> list[ast.AST]:
    """Return all mutated variants of this node.

    Args:
        node: The AST node to mutate.

    Returns:
        List of mutated AST nodes.
    """
    if not isinstance(node, ast.Return):
        return []

    if node.value is None:
        return []

    if isinstance(node.value, ast.Constant) and node.value.value is None:
        return []

    mutations: list[ast.AST] = []

    mutations.append(self._mutate_to_none(node))

    if isinstance(node.value, ast.Constant) and isinstance(node.value.value, bool):
        mutations.append(self._mutate_bool(node))

    return mutations

Mutations

Original Mutations
return x return None
return True return False
return False return True

Example

Python
# Original code
def is_valid(data):
    return True

# Mutations generated:
# 1. return None    (value to None)
# 2. return False   (True to False)

Operator Selection

Via CLI

Bash
# Use all operators (default)
pytest --gremlins

# Use specific operators
pytest --gremlins --gremlin-operators=comparison,boundary

# Use single operator
pytest --gremlins --gremlin-operators=arithmetic

Via pyproject.toml

TOML
[tool.pytest-gremlins]
operators = ["comparison", "arithmetic", "boolean"]

Programmatically

Python
from pytest_gremlins.instrumentation.transformer import get_default_registry

# Get the default registry with all 5 operators
registry = get_default_registry()

# Get specific operators
ops = registry.get_all(enabled=['comparison', 'boundary'])

Operator Statistics

The default operators generate mutations as follows:

Operator AST Nodes Mutations per Node
comparison Compare 1-2 per operator
arithmetic BinOp 1
boolean BoolOp, UnaryOp, Constant 1
boundary Compare with int constants 2 per constant
return Return 1-2

A typical function with 10 lines might generate 5-15 gremlins depending on the code patterns present.