CLI tool
ikamensh/flynt avatar
ikamensh/flynt

flynt: converting old Python string formatting to f-strings

A tool to automatically convert old string literal formatting to f-strings

733 stars39 forksRustMIT

At a glance

What is it?
flynt is a Rust command line tool that rewrites percent-formatted and .format() Python strings into f-strings. Version 2.0 ships as a native binary, dropped the importable Python API, and the pre-commit hook still points at the 1.x line.
Who is it for?
flynt fits teams that want a repeatable, reviewable sweep of old string formatting, either as a one-shot migration on a small codebase or as a pre-commit gate on changed files. It does not fit projects that still support Python versions older than 3.6, that depend on importing flynt as a library, or that need a stable release today.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 96 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

flynt rewrites %-formatting and .format() calls into f-strings

flynt exists for one narrow job: taking Python source that still uses the older string formatting styles and turning it into f-strings. The README describes it as a command line tool that converts a project's Python code from %-formatted and .format(...) strings into Python 3.6 and later f-strings. It targets two concrete patterns. The first is the old percent style, where a value is substituted with a percent sign. The second is the method call style, where placeholders such as named fields are filled by .format(...).

Because the target syntax arrived in Python 3.6, flynt output is only valid on interpreters from 3.6 onwards. That constraint is what pushes the tool out of the naive find-and-replace category. The person who reaches for flynt usually has a legacy codebase they intend to migrate once and then keep clean, or a linter author who wants the same conversion available as a check in continuous integration. The project itself is filed under the topics linter and python, and it is written in Rust with a Python-facing command line interface.

The conversion is a source-to-source rewrite. flynt parses the file, finds the formatting expressions it understands, and writes a new version of that file with f-strings in place of the old calls. Files are modified in place unless you ask for something else, which is the single most important operational detail for a new user and is covered further below.

The 2.0 rewrite made flynt a native binary and removed the import API

The largest change in flynt is architectural. Since version 2.0 the tool is implemented in Rust and ships as a native executable inside the PyPI wheel, with the same command line interface and output as the 1.x series. The README states that it is 10 to 20 times faster with parallel file processing, and the Cargo manifest lists rayon, the thread pool library, among the dependencies, which is consistent with that claim.

The cost of that rewrite is a removal. The Python import API is gone, so flynt is now command line only. If some other part of your stack does `import flynt`, the README tells you to pin `flynt<2`. That is a hard compatibility break for anyone who had wrapped the library, and it is the main migration risk in this project right now.

The packaging still leaves a thin Python surface. The pyproject file uses maturin as the build backend with bindings set to bin, and the project ships a small python package next to the binary so that `python -m flynt` execs the binary itself. Importing flynt under that arrangement explains the removal rather than restoring the old API. The Cargo manifest pins its parser dependencies to a specific ruff tag, 0.14.11, taken from a git source, so the tool parses Python with ruff's parser and codegen crates rather than with a hand-written grammar.

A first conversion run: install, inspect the diff, then rewrite

Installing flynt is a single command from PyPI.

bash
pip install flynt

The README notes that 2.0 was in beta for part of its life and that beta installs require the pre-release flag.

bash
pip install --pre flynt

Now point flynt at a file or a directory. The usage line in the README is short.

bash
flynt {source_file_or_directory}

Given a single file, flynt replaces the applicable formatting in that file. Given a folder, it searches recursively and rewrites every Python file it finds. The README is blunt about the consequence: flynt modifies the files it runs on, and you should add your project to version control before using it.

Before letting it touch a tree, run the dry-run flag, which prints a diff and leaves the files alone. The help output also documents a stdout mode that prints the converted result without writing files, and a verbose mode that lists modified files together with the reason each skipped candidate was not converted.

code
  -d, --dry-run         Do not change the files in-place and print
                        the diff instead. Note that this must be
                        used in conjunction with '--fail-on-change'
                        when used for linting purposes.

For a small sense of scale, the README includes a sample run against a cloned Flask checkout.

code
$ flynt flask
Running flynt v.1.0.3
Modified 21 of 70 files in 0.79s

The numbers there come from the 1.0.3 Python implementation, so treat them as an illustration of the output format rather than a timing you can expect from the Rust build.

Blacklisted folders and the exclude flag decide what a directory run touches

A recursive rewrite is where flynt can cause surprise, because the tool decides on its own which directories are off limits. The README states that it skips some hard-coded folder names, and the list is written out in the source as `blacklist = {'.tox', 'venv', 'site-packages', '.eggs'}`. These are the usual suspects for installed dependencies and isolated environments, and skipping them is what keeps a run from mangling vendored code.

That list is not configurable in the way you might hope. The only filter exposed on the command line is the exclude option, which ignores files whose absolute path contains a given string. The help output describes it in these terms.

code
  -e, --exclude EXCLUDE [EXCLUDE ...]
                        ignore files with given strings in it's
                        absolute path.

Substring matching against the absolute path is a blunt instrument. It works well for a directory name you want to skip entirely, and poorly for anything that needs a precise rule, since a path substring can match files you did not intend to filter. Note also that the excluded check applies to files, while the folder blacklist is hard-coded. If your project keeps generated code or vendored packages outside those four names, you will need the exclude flag for each path fragment, or you should run flynt on selected subdirectories instead of the repository root.

Two further flags shape the scope of a run. The stdout mode implies quiet mode and is incompatible with dry-run and verbose. The verbose mode takes a second level, -vv, which also lists files that were scanned but not changed, which is useful for confirming that a directory was actually covered.

Default conversions stay conservative until you pass -a

flynt is careful about changing behaviour, and the escape hatches are explicit flags rather than configuration. Two switches turn off whole categories of conversion: `--no-tp` stops percent formatting from being transformed and `--no-tf` stops .format() calls from being transformed. Both conversions happen by default.

Two further switches add categories that are off by default. The `-tc` flag replaces string concatenations, defined in the help text as plus operations involving string literals, with f-strings. The `-tj` flag replaces static joins, where the joiner is a string literal and the joined value is a static-length list, with f-strings.

The most consequential flag is `-a`, aggressive mode, which includes conversions with potentially changed behaviour. The help text adds a second level: using `-aa` omits the int() wrapping for percent conversions of integer specifiers. That detail matters because wrapping an integer conversion in int() is what keeps a value that arrives as a string from producing a different result once it sits inside an f-string, and turning it off trades exactness for fewer added calls.

So the default profile rewrites formatting that preserves semantics and leaves everything else alone, and the aggressive profile trades a small amount of semantic certainty for coverage. Nothing in the documentation promises that aggressive conversions are always equivalent, which is why the flag exists as a separate switch rather than as the default.

pyproject.toml settings apply per project, and command line flags win

Since version 0.71 flynt reads per-project settings from pyproject.toml. The configuration section is named `[tool.flynt]`, and it takes the same argument names as the command line. The precedence rule is stated plainly: command line arguments take precedence over the config file. That ordering matters in practice, because a CI job that passes flags can override a developer's local settings without anybody editing a file.

The pyproject file in the repository root shows how the project itself is packaged rather than how it is configured for a target codebase.

toml
[project]
name = "flynt"
# Version derived from Cargo.toml by maturin (2.0.0-beta.1 -> 2.0.0b1).
dynamic = ["version"]
description = "CLI tool to convert a python project's %-formatted strings to f-strings."
readme = "README.md"
license = { text = "MIT" }

Beyond the per-project file, flynt can also be configured globally with a toml file at `~/.config/flynt.toml` on Unix systems. The README excerpt for that path is cut off in the documentation you can see, so the equivalent location on Windows is not documented here.

Two settings are worth planning around in a shared repository. The line-length option caps how long a converted single-line expression may become, with a default of 88 characters, which lines up with the line width the project itself follows. The no-multiline option restricts conversion to single line expressions, which is the setting to reach for if your code style forbids long lines and you would rather skip a candidate than reformat it.

The pre-commit hook still pins rev 1.0.6 while 2.0 remains a beta

flynt can run as a pre-commit hook, which is the recommended way to keep old formatting from coming back. The hook configuration is a short block added to `.pre-commit-config.yaml`.

yaml
-   repo: https://github.com/ikamensh/flynt/
    rev: '1.0.6'
    hooks:
    -   id: flynt

The important detail is in the note attached to it. The README says to keep `rev: '1.0.6'` for now, because pointing rev at a 2.0 checkout makes pre-commit build flynt from source, and that requires a Rust toolchain on the machine running the hook. A wheel-backed hook that installs the prebuilt binary from PyPI will ship with the final 2.0 release. In other words, the hook path currently runs the older implementation even though the package you install with pip is the new native binary. That split is the sharpest rough edge in the project today.

Individual lines can be exempted inside the source. The README documents adding `# noqa [: anything else] flynt [anything else]` or `# flynt: skip` to a line you want the tool to leave alone, which is the practical escape when a conversion would change behaviour on a specific call.

For a linting role rather than a rewrite role, the dry-run flag combines with the fail-on-change flag, which makes flynt exit non-zero when it finds something to change. That is a different adoption path from rewriting an entire codebase at once, and for many teams it is the safer first step.

Notebook support is alpha, 2.0 has no release, and the last push was on 2026-07-19

flynt is not archived, and the last push to the repository was on 2026-07-19. The repository has no GitHub releases, and the version recorded in the Cargo manifest is 2.0.0-beta.2, which matches the version string in the help output. That combination tells you the project is active on the source tree while the packaged releases trail behind it. If you pin a version, you are pinning a beta or a 1.x release rather than a tagged 2.0.

Two features are explicitly flagged as unfinished. The notebook flag, which adds Jupyter notebook search and transformation for .ipynb files, carries a warning that the feature is in alpha and was not thoroughly tested. Enabling it in a shared repository is a calculated risk.

There is also a limit on what flynt will attempt at all. It rewrites formatting it understands and skips the rest, and the verbose mode exists precisely so you can see why a candidate was skipped. Flynt produces code for Python 3.6 and later, so it is the wrong tool for a codebase that still has to run on older interpreters, and it is the wrong tool for a project that has already standardised on f-strings.

One last licensing note, without advice beyond the repository: the project is MIT licensed, the same licence stated in the pyproject metadata and the Cargo manifest, so redistribution of a modified binary follows the MIT terms.

Editorial conclusion

flynt fits teams that want a repeatable, reviewable sweep of old string formatting, either as a one-shot migration on a small codebase or as a pre-commit gate on changed files. It does not fit projects that still support Python versions older than 3.6, that depend on importing flynt as a library, or that need a stable release today. Before running it, check that your code is under version control, review the diff produced by the dry-run flag, and confirm which 1.x or 2.0 revision your CI and pre-commit setup will actually install.

Frequently asked questions

How do I install flynt and convert a directory of Python files to f-strings?

Install it from PyPI with `pip install flynt`, then run `flynt {source_file_or_directory}` with a file or a folder path. Given a folder, flynt searches it recursively and rewrites the Python files it finds, skipping the folder names .tox, venv, site-packages and .eggs.

Can I still import flynt as a Python module after version 2.0?

No. The Python import API was removed in 2.0, which is command line only, and the README tells you to pin `flynt<2` if you need `import flynt`. The 2.0 wheel still ships a tiny python package so that `python -m flynt` execs the native binary.

How do I stop flynt from rewriting files I do not want it to touch?

Use the exclude option, which ignores files whose absolute path contains a given string, and note that CLI arguments take precedence over pyproject.toml settings. The folder blacklist itself is hard-coded, so there is no documented way to add or remove names from it.

Does flynt support Jupyter notebooks?

Yes, through the `-nb, --notebook` flag, which also searches and transforms .ipynb files. The README marks the feature as alpha and warns that it was not thoroughly tested.

How do I configure flynt for a whole project instead of passing flags?

Add a `[tool.flynt]` section to pyproject.toml using the same argument names as the CLI, which has been supported since v0.71. A global toml file at `~/.config/flynt.toml` is also documented for Unix systems.

Official sources

  1. ikamensh/flynt on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ikamensh-flynt.svg)](https://hysenlabs.com/projects/ikamensh-flynt)

Was this useful?