Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

OpenRewrite Python

OpenRewrite automated refactoring for Python source code. This package provides the recipe framework, the Python Lossless Semantic Tree (LST), and the testing helpers you use to author and test Python recipes.

Installation

pip install openrewrite

How it works

OpenRewrite for Python uses a split JVM/Python architecture. You author and unit-test recipes in pure Python, but running a recipe against a real codebase is orchestrated by the JVM runtime via the Moderne CLI (with Python support configured) over an RPC bridge. There is no standalone, in-process Python parser entry point.

Quick start

The fastest way to author and exercise a recipe is the test harness, which parses a before snippet, runs your recipe, and asserts the result matches after:

from rewrite.test import RecipeSpec, python

def test_renames_a_call():
    spec = RecipeSpec(recipe=RenameFunctionCall(
        old_name="assertEquals",
        new_name="assertEqual",
    ))
    spec.rewrite_run(
        python("assertEquals(a, b)", "assertEqual(a, b)"),
    )

python(before, after) asserts a change; python(before) asserts no change.

Writing a recipe

A recipe is a @dataclass subclassing Recipe that returns a visitor from editor(). Each option must have a default value, or the recipe cannot be discovered or run.

from dataclasses import dataclass, field

from rewrite import ExecutionContext, Recipe, TreeVisitor, option
from rewrite.java import J
from rewrite.java.tree import MethodInvocation
from rewrite.python.visitor import PythonVisitor


@dataclass
class RenameFunctionCall(Recipe):
    """Rename calls to a function from one name to another."""

    old_name: str = field(default="", metadata=option(
        display_name="Old function name",
        description="The name of the function whose calls should be renamed.",
        example="assertEquals",
    ))

    new_name: str = field(default="", metadata=option(
        display_name="New function name",
        description="The name to rename matching calls to.",
        example="assertEqual",
    ))

    @property
    def name(self) -> str:
        return "com.yourorg.RenameFunctionCall"

    @property
    def display_name(self) -> str:
        return "Rename a function call"

    @property
    def description(self) -> str:
        return "Rename calls to a function from one name to another."

    def editor(self) -> TreeVisitor[J, ExecutionContext]:
        old_name = self.old_name
        new_name = self.new_name

        class Visitor(PythonVisitor[ExecutionContext]):
            def visit_method_invocation(self, method: MethodInvocation, p: ExecutionContext) -> J:
                method = super().visit_method_invocation(method, p)
                if method.name.simple_name == old_name:
                    renamed = method.name.replace(_simple_name=new_name)
                    return method.replace(_name=renamed)
                return method

        return Visitor()

Returning None from a visit method removes the node entirely — which is how recipes delete code.

Inspecting type attribution

Most recipe debugging is one question: what type did this expression get, and if none, where was it lost? Set REWRITE_PYTHON_DUMP_TYPES and any test prints the attribution its own parse produced — which is the attribution a MethodMatcher pattern written for that test has to match:

$ REWRITE_PYTHON_DUMP_TYPES=1 pytest tests/recipes/test_my_recipe.py -s

--- type attribution: my_recipe.py ---
line:col  kind                   source            type
4:1       MethodDeclaration      def f(arr)        my_recipe f(..) -> <none>
4:7       NamedVariable          arr               ⚠ <unknown>
5:5       MethodInvocation       socket.getfqdn()  socket getfqdn(..) -> str
6:12      MethodInvocation       arr.tostring()    ⚠ <unknown> tostring(..) -> <unknown>
6:12        └ select:Identifier  arr               <unknown>

The text before -> is a pattern you can paste into MethodMatcher.create(...) or uses_method(...). socket.getfqdn() resolves from the file's own imports, so it carries a declaring type; arr.tostring() does not, and the indented select line names the receiver that lost it. <none> is a slot the parser left empty and <unknown> is a JavaType.Unknown — worth keeping apart, since a MethodInvocation always carries some method type.

The variable accepts comma-separated flags: missing lists only unattributed nodes, all widens beyond calls and declarations, and supertypes shows each declaring type's ancestry — which bounds how general a pattern can be, since a type recording no supertype can only be matched by its own name or a wildcard.

A recipe gated on a type that never resolved is the usual reason a test sees no change, so that failure names the unattributed nodes without being asked:

Expected recipe to produce a change for:
def f(arr):
    return arr.tostring()

Nodes with no type attribution (a recipe gated on one of these cannot fire):
  1:7  NamedVariable  arr  -> <unknown>
  2:12  MethodInvocation  arr.tostring()  -> <unknown> tostring(..) -> <unknown>

Against a file on disk

rewrite-python-types <file.py> runs the same report outside a test, for reading an existing project or sweeping a corpus. Note that it resolves types against the file's own directory, so a project laid out differently from your test workspace can attribute differently — prefer the test-harness output when writing a pattern for a test.

flag
--ty attribute types with a ty client (off by default, so the zero-config invocation still runs)
--only-missing list only the nodes whose type is missing
--all every type-bearing node, not just calls and declarations
--supertypes show each declaring type's ancestry
--tree the nested structure with prefixes, for structural rather than type questions
--json the listing as JSON, for a test or CI check that asserts a fixture gained attribution
--diff-ty parse twice, with and without ty, and report the nodes that differ

--diff-ty answers "does my recipe need type attribution to work?" — a recipe gated only on rows that read the same in both columns runs without a type check:

$ rewrite-python-types --diff-ty probe.py
line:col  kind               source            without ty                          with ty
4:1       MethodDeclaration  def probe(arr)    ⚠ <none> probe(..) -> <none>        probe probe(..) -> <none>
4:11      NamedVariable      arr               ⚠ <none>                            ⚠ <unknown>
5:5       MethodInvocation   socket.getfqdn()  socket getfqdn(..) -> <none>        socket getfqdn(..) -> str
6:12      MethodInvocation   arr.tostring()    ⚠ <unknown> tostring(..) -> <none>  ⚠ <unknown> tostring(..) -> <unknown>

4 of 4 nodes differ

An unannotated parameter leaves arr.tostring() unresolved even under ty.

From a test or a REPL, print_types(source_file) writes the same listing and build_type_report(source_file) returns it as data. Both are read-only, and the in-process parse behind the command is for diagnostics only.

Running recipes with the Moderne CLI

Expose an activate() function so the CLI can discover your recipe:

from rewrite.marketplace import RecipeMarketplace, Python

def activate(marketplace: RecipeMarketplace) -> None:
    marketplace.install(RenameFunctionCall, Python)

Then install and run it against a repository whose Python LSTs you've built, passing each option as a -P parameter:

# From your recipe project directory, install it into the CLI's marketplace:
mod config recipes pip install .

# Build the LSTs for the repository you want to refactor, then run the recipe:
mod build /path/to/your/repo
mod run /path/to/your/repo --recipe=com.yourorg.RenameFunctionCall \
    -P old_name=assertEquals -P new_name=assertEqual

Learn more

License

Moderne Source Available License - see LICENSE.md

Release files for openrewrite 8.93.0.dev20260923020012

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openrewrite 8.93.0.dev20260923020012
File Size Uploaded
openrewrite-8.93.0.dev20260923020012.tar.gz 411.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for openrewrite 8.93.0.dev20260923020012
File Interpreter ABI Platform
openrewrite-8.93.0.dev20260923020012-py3-none-any.whl Python 3 none any Details

Total release size: 861.1 kB

Release files / openrewrite-8.93.0.dev20260923020012.tar.gz

Download URL openrewrite-8.93.0.dev20260923020012.tar.gz
Size 411.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6a6aaf3e69121222caea2f33712a9f3929344c6f28970168c7f1f5e83d356f3b
BLAKE2b-256 checksum
How to use checksums
dbac2c8350c538f3ef410ee91213fb3215bd40f28b870d9ee8d96f9d3694eb10
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / openrewrite-8.93.0.dev20260923020012-py3-none-any.whl

Download URL openrewrite-8.93.0.dev20260923020012-py3-none-any.whl
Size 450.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cc50be50d8107d862aa1c6c96141e70c183b1afe90ddd4e3de3d85f3d9dc74ce
BLAKE2b-256 checksum
How to use checksums
573a185bbbcb3dc9d893eb12e61f2fdf8e3ddf37778ffb9e9adb1ca219cd90fc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

8.92.8

2 release files

8.92.7

2 release files

8.92.6

2 release files

8.92.5

2 release files

8.92.4

2 release files

8.92.3

2 release files

8.92.2

2 release files

8.91.3

2 release files

8.91.2

2 release files

8.91.1

2 release files

8.91.0

2 release files

8.90.4

2 release files

8.90.3

2 release files

8.90.2

2 release files

8.90.1

2 release files

8.90.0

2 release files

8.89.2

2 release files

8.89.1

2 release files

8.89.0

2 release files

8.88.5

2 release files

8.88.4

2 release files

8.88.0

2 release files

8.87.7

2 release files

8.87.6

2 release files

8.87.5

2 release files

8.87.4

2 release files

8.87.3

2 release files

8.87.2

2 release files

8.87.1

2 release files

8.87.0

2 release files

8.86.5

2 release files

8.86.4

2 release files

8.85.7

2 release files

8.85.6

2 release files

8.85.5

2 release files

8.85.4

2 release files

8.85.3

2 release files

8.85.2

2 release files

8.85.1

2 release files

8.85.0

2 release files

8.84.9

2 release files

8.84.8

2 release files

8.84.7

2 release files

8.83.4

2 release files

8.83.3

2 release files

8.83.2

2 release files

8.83.1

2 release files

8.83.0

2 release files

8.82.1

2 release files

8.82.0

2 release files

8.81.1

2 release files

8.81.0

2 release files

8.80.1

2 release files

8.80.0

2 release files

8.79.6

2 release files

8.79.5

2 release files

8.79.4

2 release files

8.79.3

2 release files

8.79.2

2 release files

8.78.0

2 release files

8.77.2

2 release files

8.77.1

2 release files

8.77.0

2 release files

8.76.4

2 release files

8.76.3

2 release files

8.76.2

2 release files

8.76.1

2 release files

8.76.0

2 release files

8.75.9

2 release files

8.75.8

2 release files

8.75.7

2 release files

8.75.6

2 release files

8.75.5

2 release files

8.75.4

2 release files

8.75.3

2 release files

8.74.3

2 release files

8.74.2

2 release files

8.74.1

2 release files

8.74.0

2 release files

8.73.2

2 release files

8.73.1

2 release files

8.73.0

2 release files

8.72.3

2 release files

8.72.2

2 release files

8.72.1

2 release files

8.72.0

2 release files

1.44.3

2 release files

1.44.1

2 release files

1.43.1

2 release files

1.43.0

2 release files

1.42.1

2 release files

1.42.0

2 release files

1.41.0

2 release files

1.40.0

2 release files

1.39.3

2 release files

1.39.2

2 release files

1.39.0

2 release files

1.38.0

2 release files

1.36.0

2 release files

1.35.1

2 release files

1.34.0

2 release files

1.33.1

2 release files

1.33.0

2 release files

1.32.1

2 release files

1.32.0

2 release files

1.30.0

2 release files

1.29.0

2 release files

1.28.0

2 release files

1.27.5

2 release files

1.27.4

2 release files

1.27.3

2 release files

1.27.2

2 release files

1.26.2

2 release files

1.25.2

2 release files

1.25.1

2 release files

1.25.0

2 release files

1.24.7

2 release files

1.24.6

2 release files

1.24.5

2 release files

1.24.4

2 release files

1.24.3

2 release files

1.24.2

2 release files

1.24.1

2 release files

1.24.0

2 release files

1.22.6

2 release files

1.22.5

2 release files

1.22.4

2 release files

1.22.3

2 release files

1.22.2

2 release files

1.22.1

2 release files

1.22.0

2 release files

1.21.2

2 release files

1.21.1

2 release files

1.21.0

2 release files

1.20.4

2 release files

1.20.3

2 release files

1.20.2

2 release files

1.20.1

2 release files

1.20.0

2 release files

1.19.3

2 release files

1.19.0

2 release files

1.18.2

2 release files

1.18.0

2 release files

1.17.2

2 release files

1.15.4

2 release files

1.15.3

2 release files

1.15.2

2 release files

1.15.1

2 release files

0.0.0

2 release 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