mrkeyoor.com_
Sun 20 Sept 17:50 UTC
PyPIUtilsupdated 20 Sept 2026

libcst review

LibCST 1.9.0 parses Python into an immutable concrete syntax tree that keeps comments, spaces, commas, parentheses, and newline choices. A transformer can therefore replace one call or import without reformatting untouched code. Visitors inspect nodes, matchers describe shapes, and metadata providers attach positions, scopes, or qualified names. Version 1.9.0 adds Python 3.15 grammar support and codemod import helpers, fixes Python 3.14 configuration recognition, and accepts the legal trailing comma after a class-pattern double-star capture.

Verdict

LibCST 1.9.0 installed in 0.4 seconds and occupied 13 MB across two packages in our sandbox, with zero audit findings. Install it for source-preserving Python rewrites; use ast or an existing Ruff fix when exact formatting is not part of the requirement.

We installed it

Lab card: what happened when we installed libcstScreenshot of libcst documentation
Install✓ · 0.4s2 packages on disk · 13 MB
Importimport libcst in 0.62s · compiled extensions · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does libcst install cleanly?

Yes. In a fresh container with an empty cache, pip install libcst finished in 0.4s, leaving 2 packages and 13 MB on disk. pip-audit reported no known vulnerabilities.

What does libcst need to run?

Python >=3.9, and a platform wheel with compiled extensions. In our run import libcst succeeded in 0.62s, and the package ships py.typed for type checkers.

libcst or bowler: which should you use?

bowler: Use it for an older query-oriented codemod interface built around Python source transformations. LibCST 1.9.0 installed in 0.4 seconds and occupied 13 MB across two packages in our sandbox, with zero audit findings.

When should you not use libcst?

The task only reads valid syntax. Python's ast module is smaller and avoids carrying formatting nodes.

API stability4/5LibCST 1.x retains parse_module and parse_expression, immutable nodes, visitor and transformer leave hooks, matchers, MetadataWrapper, and the codemod command structure. Python grammar additions normally extend node support without replacing traversal. Exact output can still shift when validation or parsing bugs are corrected, so transform fixtures should pin both successful edits and no-op cases for each supported release.
Docs4/5The Read the Docs site returns 200 and includes a CST explanation, tutorials, node reference, matchers, metadata providers, codemods, testing utilities, and command usage. The README states the native wheel and Rust fallback clearly. Production transforms that combine scope resolution, qualified names, managed imports, and false-positive controls require reading several sections rather than following one end-to-end recipe.
Maintenance4/5PyPI lists 1.9.0 as current, and GitHub records a push on 2026-08-11, 1,939 stars, and an unarchived repository. Its open count of 174 includes issues and pull requests. Version 1.9.0 tracks Python 3.15 before final release and repairs a Python 3.14 parser-configuration problem, strong evidence that grammar compatibility is maintained even with a sizable queue.
Ecosystem4/5The supplied registry figure is 11,757,459 weekly downloads. One install provides typed syntax nodes, matchers, metadata, import visitors, command execution, and codemod test helpers. It occupies a Python-specific niche: Ruff offers many ready-made fast fixes, Tree-sitter spans languages, and most organization-specific source migrations still need their own LibCST logic and regression corpus.

Use it if

  • A repository-wide Python edit must leave all unrelated formatting and comments byte-for-byte familiar.
  • An autofix needs source positions, scope assignments, or qualified names before changing a syntax node.
  • A tested transform should run over many files through the bundled codemod command.
  • New Python grammar must parse while the output remains ordinary valid Python source.
Skip it if

Setup reality

We installed LibCST 1.9.0 in an unprivileged Python 3.12 Bookworm sandbox in 0.4 seconds. Two packages used 13 MB on disk. Our package inspection found four direct dependencies, compiled .so files, and a py.typed marker. import libcst succeeded in 0.62 seconds. pip-audit reported zero known vulnerabilities. The declared Python floor is 3.9.

Common Linux, macOS, and Windows targets receive wheels. A less common platform falls back to building the native parser and therefore needs current Rust tooling. Licensing is file-specific: most contributions use MIT, derived parser and tokenizer files retain PSF terms, and libcst/_add_slots.py uses Apache 2.0. Keep all notices in a license inventory.

No account or secret is involved. Repository codemods need importable command modules and may use .libcst.codemod.yaml for search paths. Parse the original module before constructing edits so its indentation and newline defaults are available. Nodes never mutate; leave methods must return updated_node or a changed copy. Returning original_node can erase modifications that child visits already produced.

MetadataWrapper copies a module by default and each provider adds whole-tree work. Declare only position, scope, qualified-name, or other providers the transform actually reads. Parallel codemod workers process files separately, so cross-file facts belong in an earlier analysis step. Syntax preservation does not resolve dynamic getattr calls, generated code, strings, or runtime import tricks; conservative matches and exact-output fixtures still decide whether a migration is safe.

Patterns

Prove a no-op parse preserves the file roundtrip-source

source = Path('service.py').read_text(encoding='utf-8')
module = cst.parse_module(source)
assert module.code == source

Keep this assertion beside transform fixtures so input decoding or parse setup cannot silently rewrite an untouched file.

Collect function definitions visit-functions

class Functions(cst.CSTVisitor):
    def __init__(self): self.names = []
    def visit_FunctionDef(self, node): self.names.append(node.name.value)
collector = Functions()
module.visit(collector)

CSTVisitor reads only. Return false from a visit method when traversal should skip that node's children.

Change one name through a transformer rename-name

class Rename(cst.CSTTransformer):
    def leave_Name(self, original_node, updated_node):
        return updated_node.with_changes(value='new_api') if updated_node.value == 'old_api' else updated_node
changed = module.visit(Rename())

Build on updated_node because it already contains changes returned from visited children.

Remove a matched import statement remove-statement

class Drop(cst.CSTTransformer):
    def leave_Import(self, original_node, updated_node):
        if m.matches(updated_node, m.Import(names=[m.ImportAlias(name=m.Name('six'))])):
            return cst.RemoveFromParent()
        return updated_node

Removing the last statement from an indented suite can make invalid Python; replace it with Pass when needed.

Find a call with one named argument match-call-shape

pattern = m.Call(func=m.Attribute(value=m.Name('requests'), attr=m.Name('get')), args=[m.ZeroOrMore(), m.Arg(keyword=m.Name('verify')), m.ZeroOrMore()])
for call in m.findall(module, pattern): print(call)

This is a syntax match. A local object called requests also qualifies until metadata proves its imported name.

Resolve source lines and columns read-positions

class Calls(cst.CSTVisitor):
    METADATA_DEPENDENCIES = (PositionProvider,)
    def visit_Call(self, node):
        pos = self.get_metadata(PositionProvider, node)
        print(pos.start.line, pos.start.column)
MetadataWrapper(module).visit(Calls())

Metadata access requires visiting through MetadataWrapper and declaring every provider dependency.

Queue import edits in a codemod manage-imports

class Modernize(CodemodCommand):
    def transform_module_impl(self, tree):
        self.add_needed_import('pathlib', 'Path')
        self.remove_unused_import('os.path')
        return tree

Version 1.9 command helpers defer placement and duplicate handling to the codemod post-processing pass.

Assert the exact rewritten source test-transform

class ReplaceTests(CodemodTest):
    TRANSFORM = ReplaceOldCall
    def test_comment_survives(self):
        self.assertCodemod('value = old_api(x)  # keep\n', 'value = new_api(x)  # keep\n')

Add no-op and lookalike cases beside the successful edit; exact comparison protects comments and spacing.

Alternatives

PackageRegistryPick it when
bowlerPyPIUse it for an older query-oriented codemod interface built around Python source transformations.
redbaronPyPIUse it when a mutable full-syntax-tree API is preferable to LibCST's immutable node model.
astorPyPIUse it for AST-to-source work where preserving the original formatting is unnecessary.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.