بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Python
How refac moves Python files with Rope and renames Python symbols with basedpyright.
Python has two jobs with two toolchains. File moves run two engines in order, Rope as the primary and Pyrefly as the fallback, and the first one to succeed wins. Symbol rename uses basedpyright. A server is started for the command and stopped when it ends.
Required tooling
For moves, the driver looks for Python in this order:
.venv/bin/pythonrelative to the working directory, which is preferred.- The system
python3.
rope must be importable from whichever Python is found. Pyrefly is only needed as a fallback when Rope is absent or fails at runtime. The backend is available if either engine is reachable.
pip install rope
# or, for the fallback only:
pip install pyreflyFor rename, install basedpyright with pip install basedpyright, pipx install basedpyright, or uv tool install basedpyright. Plain pyright is not enough, because it cannot list the overrides of a method and refac renames a method together with its overrides. refac looks in PATH, ~/.local/bin, and the project's .venv/bin and venv/bin. For any other place, set REFAC_PYTHON_SERVER to the full path of basedpyright-langserver. refac doctor python checks the setup.
Moves
refac move \
--project-path /path/to/project \
--source-path myapp/utils/formatters.py \
--target-path myapp/core/formatters.pyPython moves files one at a time. Folders are not supported.
| Order | Engine | Why |
|---|---|---|
| 1st | Rope | More reliable import rewriting for file moves |
| 2nd | Pyrefly | Used when Rope is absent or raises an error at runtime |
If Rope fails at runtime, and not just when it is missing, a warning is logged and Pyrefly is tried automatically. Run your tests after a move, and they should inshallah pass.
Preview
Rope applies one move after the other and reads the files it has moved, so it cannot plan on the unchanged project. --dry-run therefore runs the real Rope move on a throw-away copy of the project and reports the difference to the original: the moves, and per file the number of changed passages. The copy holds only the .py and .pyi files, at most 500 MiB (REFAC_DRY_RUN_COPY_MAX_MB), and the project itself is never touched. A path outside the project cannot be copied, so Rope refuses it in a dry run, and the dispatcher then falls back to Pyrefly, as a real move does when Rope fails. Pyrefly is planned from its own answer without a copy.
Limitations
__init__.py re-exports. Rope does not trace imports that go through __init__.py re-exports. If a package re-exports a symbol and callers import it through the re-export, for example from myapp.utils import format_date where utils/__init__.py does from .formatters import format_date, those indirect callers are not updated. They keep working at runtime because utils/__init__.py itself is updated, but the import path in the caller file is unchanged. Search for the old module name afterwards.
Namespace packages. Both engines expect __init__.py files to resolve package boundaries. Projects without them may see incomplete import updates.
Pyrefly. It is invoked through the LSP willRenameFiles request and does not guarantee that all importers are updated in every project layout.
Rename
refac rename \
--project-path /path/to/project \
--file shop/shapes.py \
--symbol area \
--new-name surface \
--line 12refac renames a function, class, method, property, attribute, constant, parameter, or local across the files of the project, including __init__.py re-exports and __all__ lists, keyword arguments, and the tests. --project-path is the folder that pyright treats as the root. It reads pyrightconfig.json or [tool.pyright] there and resolves imports from it. Any folder works, because refac does not guess a root from marker files.
Several renames of one project run in one server session with --batch. See Usage.
What refac adds to the server
- Overrides are renamed with the method. pyright renames the method it is asked about and its calls through that class, but not the methods that override it. refac asks the server for the overrides, renames each one with the same new name, and proves every group separately. A base method rename inshallah won't leave an override behind.
- Starting from an override renames only that override and its subclasses. The server cannot find the base method from there, so the base and the siblings stay and are listed in the note as lines that still spell the old name. Rename from the base method to change the whole family.
- Special methods are refused.
__init__,__enter__, and every other__name__are refused as the old or the new name, because the interpreter calls them by spelling and nothing in the code mentions the calls. - Module names point to
refac move. Infrom shop.report import describe,reportis a file. A module or package name in an import path is renamed withrefac move, which updates the imports. - Clashes are refused. A new name that equals an existing local, parameter, member, or global changes what the code means. The usages that would be lost or gained are listed and nothing is written.
- Leftovers are reported. A call on a parameter without an annotation (
thing.area()), a name in a string, a docstring, or a comment is not linked to the symbol. The note lists the lines (shop/report.py:15) and ends with therg -wcommand that finds them. Annotate the parameter and rename again, and the call will inshallah be linked.
Limitations
- Dynamic access (
getattr,setattr,__dict__, serialization keys) and reflection such astype(x).__name__are text and not symbols. A class rename changes the printed name. - Code in
site-packagesand virtual environments is not edited, and it is not scanned for leftovers. - A project with several independent roots needs one call per root.