Skip to main content

Matchify

ci

pypi version Python Versions PyPI - Downloads GitHub Sponsors

Matchify automatically converts eligible if/elif/else chains into Python 3.10+ match statements while preserving runtime behavior and source formatting.

Examples

Simple equality chain:

# Before
if x == 1:
    print("one")
elif x == 2:
    print("two")
else:
    print("other")

# After
match x:
    case 1:
        print("one")
    case 2:
        print("two")
    case _:
        print("other")

isinstance with attributes:

# Before
if isinstance(node, Point) and node.x == 5:
    print("x is 5")
elif isinstance(node, Point):
    print("other point")

# After
match node:
    case Point(x=5):
        print("x is 5")
    case Point():
        print("other point")

Sequence patterns:

# Before
if len(point) == 2 and point[0] == 0 and point[1] == 1:
    print("origin offset")
elif len(point) == 2 and point[0] == 1:
    print("other pair")

# After
match point:
    case 0, 1:
        print("origin offset")
    case 1, _:
        print("other pair")

Nested patterns (isinstance inside sequences):

# Before
if len(x) == 2 and isinstance(x[0], Point) and x[1] == 2:
    print("point and 2")
elif len(x) == 2 and x[0] == 1 and x[1] == 1:
    print("ones")

# After
match x:
    case Point(), 2:
        print("point and 2")
    case 1, 1:
        print("ones")

Nested sequences:

# Before
if (
    len(data) == 2
    and len(data[0]) == 2
    and data[0][0] == 1
    and data[0][1] == 2
    and data[1] == 3
):
    print("nested list")
elif (
    len(data) == 2
    and isinstance(data[0], Point)
    and len(data[1]) == 2
    and data[1][0] == 0
    and data[1][1] == 0
):
    print("point with coordinates")

# After
match data:
    case [1, 2], 3:
        print("nested list")
    case Point(), [0, 0]:
        print("point with coordinates")

Class patterns with sequence attributes:

# Before
class Data:
    def __init__(self, value):
        self.value = value


obj = Data([1, 2, 3])
if (
    isinstance(obj, Data)
    and len(obj.value) == 3
    and obj.value[0] == 1
    and obj.value[1] == 2
    and obj.value[2] == 3
):
    print("data with list")
elif isinstance(obj, Data):
    print("other data")


# After
class Data:
    def __init__(self, value):
        self.value = value


obj = Data([1, 2, 3])
match obj:
    case Data(value=[1, 2, 3]):
        print("data with list")
    case Data():
        print("other data")

Installation

Install "matchify" as a command-line tool with uv:

uv tool install matchify

Or run it without installing:

uvx matchify path/to/project/

Key Features

  • Automatic conversion of if/elif/else chains to Python 3.10+ match statements
  • Preserves formatting and code structure using LibCST
  • Supports multiple pattern types:
    • Literal comparisons (x == 1, x == "value")
    • Identity checks (x is None, x is True)
    • isinstance checks (isinstance(x, MyClass))
    • Class patterns with attributes (isinstance(p, Point) and p.x == 5)
    • Sequence patterns (len(x) == 2 and x[0] == 0 and x[1] == 1)
    • Nested sequences ([[1, 2], 3])
    • Sequence attributes in class patterns (Data(value=[1, 2, 3]))
    • Or patterns for isinstance tuples (isinstance(x, (int, float)))
  • Parallel processing for fast conversion of large codebases
  • Safe transformations - only converts when semantics are preserved

Usage

# Convert a single file
matchify path/to/file.py

# Convert all Python files in a directory
matchify path/to/project/

# Convert with verbose output
matchify path/to/project/ -v

# Check whether files would be converted without writing changes
matchify path/to/project/ --check

# Use parallel processing (default: number of CPUs)
matchify path/to/project/ -j 8

# Enable one risky assumption explicitly
matchify path/to/project/ --assume pure-subjects

# Disable all risky assumptions
matchify path/to/project/ --safe

# Enable all risky assumptions
matchify path/to/project/ --risky

pre-commit

Matchify provides two pre-commit hooks.

Use matchify to automatically rewrite files, similar to the default Black hook:

repos:
- repo: https://github.com/15r10nk/matchify
  rev: v0.1.0
  hooks:
  - id: matchify

Use matchify-check to only report files that would be converted without modifying them:

repos:
- repo: https://github.com/15r10nk/matchify
  rev: v0.1.0
  hooks:
  - id: matchify-check

Risky assumptions

By default, Matchify enables no risky assumptions. --safe makes that explicit. --risky enables all available risky assumptions. When a skipped if/elif chain would require a risky assumption, the CLI prints the file location and the required --assume value instead of converting that chain.

--assume=pure-subjects

Permits transformations such as a.x == 1 and b.y == 2 into a match on (a.x, b.y). This evaluates every subject eagerly, so enable it only when those name, attribute, and subscript reads cannot raise exceptions or produce observable side effects. Without the option, later and operands remain guards and preserve short-circuiting.

# Before
if a.x == 1 and b.y == 2:
    handle_first()
elif a.x == 3 and b.y == 4:
    handle_second()

# After
match (a.x, b.y):
    case 1, 2:
        handle_first()
    case 3, 4:
        handle_second()

--assume=use-object

Permits generic attribute patterns such as object(x=1) when different branches inspect attributes of a common object without an explicit isinstance check. This performs pattern-time attribute lookups, so enable it only when those lookups cannot raise exceptions or produce observable side effects.

# Before
if value.x == 1:
    handle_x()
elif value.y == 2:
    handle_y()

# After
match value:
    case object(x=1):
        handle_x()
    case object(y=2):
        handle_y()

--assume=identity-equality

Permits conversions from qualified identity comparisons such as op is Op.ADD to value patterns such as case Op.ADD. Match value patterns compare with equality, not identity, so enable it only when identity and equality are equivalent for those values.

# Before
if op is Op.ADD:
    add()
elif op is Op.SUB:
    subtract()

# After
match op:
    case Op.ADD:
        add()
    case Op.SUB:
        subtract()

--assume=hashable-subjects

Permits membership tests against literal sets to become OR patterns. Set membership hashes the subject and can raise TypeError for an unhashable value, while a pattern only performs equality comparisons. Enable it only when match subjects are hashable. Custom __hash__ and __eq__ implementations may still make lookup behavior or side effects differ from pattern matching.

# Before
if value in {1, 2}:
    handle_small()
elif value == 3:
    handle_three()

# After
match value:
    case 1 | 2:
        handle_small()
    case 3:
        handle_three()

--assume=list-sequence-pattern

Permits a sequence pattern to imply an explicit isinstance(value, list) check. Python sequence patterns can also match other sequence types, so enable it only when that broader match is acceptable.

# Before
if isinstance(value, list) and len(value) == 1 and value[0] == 1:
    handle_one()
elif value is None:
    handle_none()

# After
match value:
    case 1,:
        handle_one()
    case None:
        handle_none()

--assume=tuple-sequence-pattern

Permits a sequence pattern to imply an explicit isinstance(value, tuple) check. Python sequence patterns can also match other sequence types, so enable it only when that broader match is acceptable. Checks against (list, tuple) require both sequence assumptions.

# Before
if isinstance(value, tuple) and len(value) == 1 and value[0] == 1:
    handle_one()
elif value is None:
    handle_none()

# After
match value:
    case 1,:
        handle_one()
    case None:
        handle_none()

--assume=lookup-equality

Permits dictionary lookup tables embedded in statements to become match statements. Dictionary lookup uses hashing while patterns use equality, and dictionary values are evaluated only in the selected case instead of eagerly when constructing the dictionary. Enable it only when those equality and evaluation-order differences are acceptable. Tuple keys, including nested tuples, become sequence patterns and can therefore also match equivalent non-tuple sequences.

# Before
result = {"create": "POST", "read": "GET"}[operation]

# After
match operation:
    case "create":
        result = "POST"
    case "read":
        result = "GET"
    case _matchify_key:
        raise KeyError(_matchify_key)

Development

Development and repository-testing notes are in CONTRIBUTING.md.

Issues

If you encounter any problems, please report an issue along with a detailed description.

License

Distributed under the terms of the MIT license, "matchify" is free and open source software.

Download files

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

Source Distribution

matchify-0.2.0.tar.gz (158.6 kB view details)

Uploaded Source

Built Distribution

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

matchify-0.2.0-py3-none-any.whl (31.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: matchify-0.2.0.tar.gz
  • Upload date:
  • Size: 158.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for matchify-0.2.0.tar.gz
Algorithm Hash digest
SHA256 dd7a72f1d1232ea82b2a8972f27ed923c0a5df15b36844641c9b80ab989d28f6
MD5 8ef267ce6de45fe43b562917c986d344
BLAKE2b-256 0bf8d9e7f5390111d630114cf69a6e20574123d1937fba4e556e4a17566cc93c

See more details on using hashes here.

Provenance

The following attestation bundles were made for matchify-0.2.0.tar.gz:

Publisher: ci.yml on 15r10nk/matchify

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

File details

Details for the file matchify-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: matchify-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 31.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for matchify-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eb90171f9d1a878927a4a019bfe517a1b8617d74221bb017eafbe1b85ddb9369
MD5 80e8ad69cc50e89e09aae20c1a53f70d
BLAKE2b-256 244cabbf369de8779a80c8f60b83769e0e3f39e7d39265fd4ef73ea05ef909f3

See more details on using hashes here.

Provenance

The following attestation bundles were made for matchify-0.2.0-py3-none-any.whl:

Publisher: ci.yml on 15r10nk/matchify

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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page