A focused linting rewrite

Meta has shipped Fixit 2, a major update to its open-source auto-fixing linter for Python. The release is available on PyPI and is already seeing early use inside Meta’s monorepo, with a broader rollout planned. Fixit 2 is designed to make it easier for developers to write custom lint rules and automatically apply the fixes those rules suggest.

The original Fixit was built for Instagram and open sourced, but it lacked two features Meta needed for its monorepo: support for local, in-repo lint rules and hierarchical configuration. Those gaps limited Fixit’s usefulness across thousands of internal projects, many of which are themselves open source and have distinct linting and CI needs. The Python Language Foundation team—a hybrid group of production engineers and software engineers—decided a partial rewrite was the right move.

What the rewrite changes

Fixit 2 rebuilds the framework and linting engine from the ground up while keeping the core design of lint rules mostly intact. The new architecture delivers:

  • Hierarchical configuration based on the TOML format.
  • Support for local, in-repo lint rules similar to what Flake8 offers.
  • A significantly improved CLI and API for integration with other tools and automation.

The linter builds on LibCST, another Instagram open-source project, which provides a concrete syntax tree for Python. Unlike the standard library’s ast module, LibCST captures every part of the source file—whitespace, comments, and formatting—allowing Fixit to safely modify files without relying on regular expressions or risking broken syntax.

Writing a rule

Creating a new lint rule in Fixit 2 requires fewer than a dozen lines of code, with test cases defined inline. The rule can live right next to the code it governs:

# teambread/rules/hollywood.py
import fixit
import libcst
class HollywoodName(fixit.LintRule):
    VALID = [...] # no lint errors here
    INVALID = [...] # bad code samples here
    def visit_SimpleString(self, node: libcst.SimpleString):
        if node.value in ('"Paul"', "'Paul'"):
            self.report(node, "It's underbaked!")

Suggesting an auto-fix is a matter of including a new CST node when reporting an error:

def visit_SimpleString(self, node: libcst.SimpleString):
    if node.value in ('"Paul"', "'Paul'"):
        new_node = libcst.SimpleString('"Mary"')
        self.report(node, new_node)

Enabling the rule in a project is a simple configuration change:

# teambread/sourdough/fixit.toml
[tool.fixit]
enable = [".rules.hollywood"]

Running the linter against the project shows both errors and suggested changes:

# teambread/sourdough/baker.py
name = "Paul"
print(f"hello {name}!")
$ fixit lint --diff sourdough/baker.py
sourdough/baker.py@7:11 HollywoodName: It's underbaked! (has autofix)
--- a/baker.py
+++ b/baker.py
@@ -6,3 +6,3 @@
def main():
-    name = "Paul"
+    name = "Mary"
    print(f"hello {name}")
🛠️  1 file checked, 1 file with errors, 1 auto-fix available 🛠️
[1]

Using the fix command applies those suggested changes back to the codebase:

$ fixit fix --automatic sourdough/baker.py
sourdough/baker.py@7:11 HollywoodName: It's underbaked! (has autofix)
🛠️  1 file checked, 1 file with errors, 1 auto-fix available, 1 fix applied 🛠️

After the auto-fixes are applied, a final lint run confirms the project is clean:

$ fixit lint sourdough/baker.py
🧼 1 file clean 🧼

From linting to codemods

Any code that triggers an auto-fixing lint rule is an opportunity for automatic replacement, which reduces the effort a developer needs to spend cleaning up their code. At a larger scale, Fixit 2 can also act as a tool for sweeping codemods across a big codebase, with the lint rule left in place to catch any matching code in the future.

The gaps Fixit 2 addresses are ones Meta knows well from years of using Flake8 internally. Flake8 has been a staple since 2016 and supports custom plugins and rules without a central gatekeeper. But it has limitations: writing rules requires building entire plugins with claimed namespace portions, it only points to a line and column for errors without suggesting changes, and it relies on the stdlib ast module, which can’t parse future syntax features until tools catch up. Other Python linters often lack local rule support, hierarchical configuration, auto-fixes, or the performance needed for large codebases.

Fixit 2 is available now via pip install fixit. Meta has published a roadmap, alongside documentation and guides, and feedback is being tracked through the project’s GitHub issues.