Selective conversion
Some projects prefer match statements only when a conversion is sufficiently
complex or removes enough repetition. --convert-if filters otherwise eligible
if/elif conversions using metrics from the source chain and the proposed
generated result. Without an explicit option, Matchify uses this filter and
therefore converts every eligible chain:
True
Use --convert-if to restrict conversions to chains that meet a
project-specific threshold. Use --convert-if True if you want the default
behavior spelled out explicitly.
For example, convert chains that contain at least two isinstance checks or at
least four branches:
matchify path/to/project/ --convert-if "isinstance_checks >= 2 or branches >= 4"
Lookup-table conversions are not affected by this option. Matchify plans one
candidate per chain, then checks its required --assume options
and evaluates the filter against that candidate's metrics.
--show and --show-all print the computed metric variables next to each
previewed if/elif conversion so you can turn a shown conversion directly
into a --convert-if expression.
Each preview shows only that candidate's rewrite. Nested conversions have their own previews; writing applies all selected candidates together. Previewing and writing reuse the same plan without parsing or compiling candidates again.
Expression syntax
Expressions support:
- integer constants and the boolean constants
TrueandFalse; - parentheses;
- arithmetic with
+,-,*, and/; and,or, andnot;<,<=,==,!=,>=, and>.
Metric values use normal integer truthiness: zero is false and a nonzero value is true. For example, this converts chains with at least three branches only when no conditions remain in guards:
matchify path/to/project/ --convert-if "branches >= 3 and not guard_conditions"
--convert-if may be specified only once. Combine multiple criteria in one
expression with and and or.
Arithmetic uses normal Python precedence and / performs true division.
Expressions are validated against a restricted syntax tree before being compiled
and evaluated with eval(), with only metric values and no builtins available.
Boolean operators preserve Python's operand values and short-circuit behavior.
Function calls, attribute access, subscripts, other arithmetic
operators, strings, and unknown variables are rejected before any files are
processed.
Useful filters to try
There is no universally best filter: the useful threshold depends on how often
your project wants to use match. These expressions are practical starting
points.
Convert only chains with at least three branches:
matchify path/to/project/ --convert-if "branches >= 3"
Prefer conversions that produce repeated class or sequence structure:
matchify path/to/project/ \
--convert-if "class_patterns >= 2 or sequence_patterns >= 2"
Require the generated patterns to remove more structure than remains in guards:
matchify path/to/project/ \
--convert-if "pattern_nodes - guard_conditions >= 4"
Avoid conversions that retain any guards:
matchify path/to/project/ --convert-if "guarded_cases == 0"
Prefer conversions that combine several alternatives into OR patterns:
matchify path/to/project/ --convert-if "or_alternatives >= 3"
Limit generated nesting while still requiring multiple meaningful cases:
matchify path/to/project/ \
--convert-if "patterns >= 2 and max_pattern_depth <= 3"
Focus on conversions that bind values directly through capture patterns:
matchify path/to/project/ --convert-if "captures > 0"
Rules can be combined to express a project-specific style. For example, this accepts larger flat chains as well as compact class-pattern conversions, but rejects cases with guards:
matchify path/to/project/ \
--convert-if "guarded_cases == 0 and (branches >= 4 or class_patterns >= 2)"
Source metrics
These values describe the original if/elif chain:
branches
The number of if/elif branches, excluding else. This example has
branches == 3:
# Before
if status == 200:
handle_success()
elif status == 404:
handle_missing()
elif status == 500:
handle_error()
else:
handle_other()
# After: matchify --convert-if "branches == 3"
match status:
case 200:
handle_success()
case 404:
handle_missing()
case 500:
handle_error()
case _:
handle_other()
isinstance_checks
The number of recognized isinstance(...) checks. Each call counts once,
even when its class information contains multiple types. This example has
isinstance_checks == 2:
# Before
if isinstance(value, str):
handle_text()
elif isinstance(value, (int, float)):
handle_number()
# After: matchify --convert-if "isinstance_checks == 2"
match value:
case str():
handle_text()
case int() | float():
handle_number()
literal_checks
The number of recognized literal or singleton alternatives. Every value in a
recognized membership test counts separately. This example has
literal_checks == 3:
# Before
if command in ("start", "run"):
launch()
elif command == "stop":
stop()
# After: matchify --convert-if "literal_checks == 3"
match command:
case "start" | "run":
launch()
case "stop":
stop()
attribute_checks
The number of recognized conditions whose subject path inspects at least one
attribute. It counts conditions, not individual dots in a path. This example
has attribute_checks == 2:
# Before
if isinstance(node, Point) and node.x == 1:
handle_one()
elif isinstance(node, Point) and node.x == 2:
handle_two()
# After: matchify --convert-if "attribute_checks == 2"
match node:
case Point(x=1):
handle_one()
case Point(x=2):
handle_two()
sequence_checks
The number of recognized sequence length checks. This example has
sequence_checks == 2 because each branch contains one len(data) == 2
condition:
# Before
if len(data) == 2 and data[0] == "x":
handle_x()
elif len(data) == 2 and data[0] == "y":
handle_y()
# After: matchify --convert-if "sequence_checks == 2"
match data:
case "x", _:
handle_x()
case "y", _:
handle_y()
max_depth
The greatest attribute/subscript depth of any recognized check. A direct name
has depth 0; each attribute or subscript adds one. This example has
max_depth == 2 because node.position.x is two levels below node:
# Before
if (
isinstance(node, Point)
and isinstance(node.position, Position)
and node.position.x == 1
):
handle_one()
elif (
isinstance(node, Point)
and isinstance(node.position, Position)
and node.position.x == 2
):
handle_two()
# After: matchify --convert-if "max_depth == 2"
match node:
case Point(position=Position(x=1)):
handle_one()
case Point(position=Position(x=2)):
handle_two()
Generated-result metrics
These values describe the proposed match statement after Matchify has
compiled it:
patterns
The number of generated cases with a structural pattern. A wildcard case _
does not count. This example has patterns == 2:
# Before
if value == 1:
handle_one()
elif value == 2:
handle_two()
else:
handle_other()
# After: matchify --convert-if "patterns == 2"
match value:
case 1:
handle_one()
case 2:
handle_two()
case _:
handle_other()
pattern_nodes
The total number of generated pattern nodes, including nested patterns,
captures, and wildcards. Starred sequence elements and mapping-rest bindings do
not count because they are not pattern nodes. This example has
pattern_nodes == 3:
# Before
if value == 1:
handle_one()
elif value == 2:
handle_two()
else:
handle_other()
# After: matchify --convert-if "pattern_nodes == 3"
match value:
case 1:
handle_one()
case 2:
handle_two()
case _:
handle_other()
value_patterns
The number of generated qualified value patterns such as Kind.READY,
including those inside OR or nested patterns. Each occurrence
counts once, regardless of how many dots the name contains. Literals,
singletons, captures, class names in class patterns, and guard expressions do
not count. This example has value_patterns == 1:
# Before
if value == Kind.READY:
handle_selected()
elif value == 0:
handle_zero()
# After: matchify --convert-if "value_patterns == 1"
match value:
case Kind.READY:
handle_selected()
case 0:
handle_zero()
Use --convert-if "value_patterns > 0" to select chains that produce these
patterns, or --convert-if "value_patterns == 0" to exclude them.
self_value_patterns
The number of qualified value patterns rooted at the name self. Both
self.val and self.settings.val count; other.val, other.self.val, and
selfish.val do not. This is a syntactic check of the name, without inferring
receiver aliases. Class names and guard expressions do not count.
This metric is a subset of value_patterns, so
value_patterns - self_value_patterns counts qualified value patterns rooted
at other names. This example has self_value_patterns == 1 and
value_patterns == 2:
# Before
if value == self.val:
handle_self()
elif value == other.val:
handle_other()
# After: matchify --convert-if "self_value_patterns == 1"
match value:
case self.val:
handle_self()
case other.val:
handle_other()
class_patterns
The number of generated class pattern nodes, including nested ones. This
example has class_patterns == 2:
# Before
if isinstance(value, str):
handle_text()
elif isinstance(value, int):
handle_number()
# After: matchify --convert-if "class_patterns == 2"
match value:
case str():
handle_text()
case int():
handle_number()
sequence_patterns
The number of generated list, tuple, or open sequence pattern nodes, including
nested sequences. This example has sequence_patterns == 2:
# Before
if len(data) == 2 and data[0] == "x":
handle_x()
elif len(data) == 2 and data[0] == "y":
handle_y()
# After: matchify --convert-if "sequence_patterns == 2"
match data:
case "x", _:
handle_x()
case "y", _:
handle_y()
or_alternatives
The total number of direct alternatives across all generated OR patterns.
Nested OR patterns contribute their alternatives separately. This example has
or_alternatives == 3:
# Before
if value in (1, 2, 3):
handle_small()
elif value == 4:
handle_four()
# After: matchify --convert-if "or_alternatives == 3"
match value:
case 1 | 2 | 3:
handle_small()
case 4:
handle_four()
max_pattern_depth
The greatest structural nesting depth in any generated case pattern. A simple
value, singleton, capture, or class pattern has depth 1. Each enclosing class,
sequence, mapping, OR, or as pattern adds one level. A wildcard case _ has
depth 0. This example has max_pattern_depth == 3:
# Before
if (
isinstance(node, Point)
and isinstance(node.position, Position)
and node.position.x == 1
):
handle_one()
elif (
isinstance(node, Point)
and isinstance(node.position, Position)
and node.position.x == 2
):
handle_two()
# After: matchify --convert-if "max_pattern_depth == 3"
match node:
case Point(position=Position(x=1)):
handle_one()
case Point(position=Position(x=2)):
handle_two()
guarded_cases
The number of generated cases that have a guard, regardless of how many
conditions each guard contains. This example has guarded_cases == 2:
# Before
if value == 1 and enabled and ready:
handle_one()
elif value == 2 and enabled and ready:
handle_two()
# After: matchify --convert-if "guarded_cases == 2"
match value:
case 1 if enabled and ready:
handle_one()
case 2 if enabled and ready:
handle_two()
guard_conditions
The number of individual conditions retained in generated guards. Conditions
joined with and count separately. This example has guard_conditions == 4:
# Before
if value == 1 and enabled and ready:
handle_one()
elif value == 2 and enabled and ready:
handle_two()
# After: matchify --convert-if "guard_conditions == 4"
match value:
case 1 if enabled and ready:
handle_one()
case 2 if enabled and ready:
handle_two()
captures
The number of names bound by generated capture patterns. This example has
captures == 2, one generated capture in each case:
# Before
if len(data) == 2 and data[0] == "x":
result = data[1]
print(result)
elif len(data) == 2 and data[0] == "y":
result = data[1]
print(result)
# After: matchify --convert-if "captures == 2"
match data:
case "x", result:
print(result)
case "y", result:
print(result)
This distinction makes it possible to filter using both the shape of the input and the readability of the result:
matchify path/to/project/ \
--convert-if "branches >= 3 and patterns >= 2 and guard_conditions <= 1"
Rejected conversions
When an otherwise valid conversion does not match the expression, Matchify
leaves that chain unchanged. Rejections are silent by default. Use --verbose
to report their source locations:
matchify path/to/project/ --verbose --convert-if "branches >= 4"
Interaction with assumptions
The candidate and its metrics are independent of --safe, --risky, and
--assume. These options only determine whether its required assumptions are
enabled. Missing assumptions leave the chain unchanged; Matchify does not choose
a different subject or retain extra guards to make it eligible.
The filter is a stylistic preference; it does not enable a conversion whose
required assumptions are missing. --show-all also evaluates the filter for
hypothetical candidates. Arithmetic errors in those gated candidates do not
prevent eligible conversions from being previewed or applied.