Skip to content

Latest commit

 

History

History

docs

Get Started

Logo

A formatter for finding and removing unused import statements.

Pycln Docs CI CD FUZZ Codacy Badge Codecov Maintainability

PYPI - Python Version PYPI - Pycln Version Downloads

Forks Stars Issues Pull Requests Contributors Last Commit License

Docstrings: reStructuredText Code style: black Code style: prettier

Installation

Latest Release (PYPI)

Pycln requires Python 3.6+ and can be easily installed using the most common Python packaging tools. We recommend installing the latest stable release from PyPI with pip:

$ pip install pycln

Unreleased (REPOSITORY)

If you want the latest unreleased Pycln version, you can install it from the repository using install.sh (pip):

  • Clone the repository:

    $ git clone https://github.com/hadialqattan/pycln
  • CD into it:

    $ cd pycln
  • Install with install.sh:

    $ ./scripts/install.sh

Uninstall

It doesn't matter which installation method you have used, Uninstall can be done with uninstall.sh (pip):

$ ./scripts/uninstall.sh

Usage

NOTE: Make sure the Python version you run Pycln with is the same or more recent than the Python version your codebase targets.

The Simplest Usage

By default Pycln removes any unused import statement, So the simplest usage is to specify only the path:

$ pycln [PATH]  # using -a/--all flag is recommended.

Pycln Skips

Import Skip

Skip an import statement from Pycln check.

  • Using # nopycln: import comment:

    import x  # nopycln: import
    from xxx import (  # nopycln: import
        x,
        y,
        z
    )
  • Using # noqa comment:

    import x  # noqa
    from xxx import (  # noqa
        x,
        y,
        z
    )

File Wide Skip

Skip a file by typing # nopycln: file anywhere on it.

  • At the beginning:

    # nopycln: file
    import x
  • At the end:

    import x
    # nopycln: file

Global skip

Skip module/package/library imports for all files (globally).

Please see --skip-imports option.

CLI Arguments

Paths

Directories' paths and/or files' paths and/or reading from stdin.

NOTE: Pycln treats .pyi files as regular .py files in the pathfinding functionality, so anything true for .py files is true for .pyi files as well.

Usage

  • Specify a directory to handle all its subdirs/files (recursively):

    $ pycln my/project/directory
  • Specify a file:

    $ pycln my_python_file.py
  • Specify multiple directories and files:

    $ pycln dir1/ dir2/ main.py cli.py
  • Reading from STDIN (- as a file path):

    $ cat file.py | pycln -  # please read the notes below which clarifies the necessity of using `-s/--silence` flag.

    Notes about reading from STDIN:

    • For the time being, both the final report and the formatted code will be sent to STDOUT, therefore, it's necessary to use -s/--silence flag in order to receive only the formatted code via STDOUT.
    • You can read from STDIN and provide normal paths at the same time (the order doesn't matter).

CLI Options

--skip-imports option

Skip module/package/library imports for all files (globally).

Default

[]

Behaviour

  • Takes a list of module/package/library names and skips any import belonging to them.

Usage

  • Via CLI by providing:

    • a list of names in a pythonic list format:
      $ pycln --skip-imports [x, y, z]
    • a list of names in a comma separated str format:
      $ pycln --skip-imports x,y,z
    • --skip-imports multiple times:
      $ pycln --skip-imports x --skip-imports y
  • Via a config file (.toml, .cfg, .yaml, .yml, .json) by providing:

    • a list of names in a pythonic list format (.toml example):
      skip_imports = [x, y, z]
    • a list of names in a comma separated str format (.toml example):
      skip_imports = "x,y,z"
    • unlike CLI, you can't provide multiple skip_imports keys.

--config PATH option

Read configuration from a file.

Default

None

Behaviour

  • All Pycln arguments, options, and flags can be read from a config file.
  • Only these types are accepted .cfg, .toml, .json, .yaml, and .yml.
  • Overrides CLI arguments, options, and flags.

Usage

  • Get configs only from a config file:
    $ pycln --config config_file.cfg  # .toml, .json, .yaml, .yml
  • Get from both the CLI and a config file:
    $ pycln /path/ --diff --config config_file.cfg  # .toml, .json, .yaml, .yml

Example

A NOTE BEFORE THE EXAMPLES:

#: The path argument can be passed either
#: via `paths` keyword as a *list* like:
paths = ["/path/to/src", "./file.py"]

#: OR

#: via `path` keyword as a *string*, for example:
path = "/path/to/src"
.cfg
[pycln]
path = /project/path/
include = .*_util\.py$
exclude = .*_test\.py$
expand_stars = True
verbose = True
diff = True
all = True
no_gitignore = False
.toml
[tool.pycln]
path = "/project/path/"
include=".*_util\.py$"
exclude=".*_test\.py$"
expand_stars=true
verbose=true
diff=true
all=true
no_gitignore=false
.yaml/.yml
pycln:
  path: /project/path/
  include: .*_util\.py$
  exclude: .*_test\.py$
  expand_stars: true
  verbose: true
  diff: true
  all: true
  no_gitignore: false
.json
{
  "pycln": {
    "path": "/project/path/",
    "include": ".*_util.py$",
    "exclude": ".*_test.py$",
    "expand_stars": true,
    "verbose": true,
    "diff": true,
    "all": true,
    "no_gitignore": false
  }
}

-i, --include TEXT option

A regular expression that matches files and directories that should be included on recursive searches.

Default

.*\.pyi?$

Behaviour

  • An empty value means all files are included regardless of the name.
  • Use forward slashes for directories on all platforms (Windows, too).
  • Exclusions are calculated first, inclusions later.

Usage

Assume that we have three files (util_a.py, util_b.py, test_a.py) on a directory called project and we want to reformat only files that start with util_:

$ pycln /path_to/project/ --include util_.*  # or -i util_.*

-e, --exclude TEXT option

A regular expression that matches files and directories that should be exclude on recursive searches.

Default

(\.eggs|\.git|\.hg|\.mypy_cache|__pycache__|\.nox|\.tox|\.venv|\.svn|buck-out|build|dist)/

Behaviour

  • An empty value means no paths are excluded.
  • Use forward slashes for directories on all platforms (Windows, too).
  • Exclusions are calculated first, inclusions later.

Usage

Assume that we have four files (util_a.py, util_b.py, test_a.py, test_b.py) on a directory called project and we want to reformat files that not start with test_:

$ pycln /path_to/project/ --exclude test_.*  # or -e test_.*

-ee, --extend-exclude TEXT option

Like --exclude, but adds additional files and directories on top of the excluded ones. (Useful if you simply want to add to the default).

Default

^$ (empty regex)

Behaviour

  • An empty value means no paths are excluded.
  • Use forward slashes for directories on all platforms (Windows, too).
  • Exclusions are calculated first, inclusions later.

Usage

Assume that we have four files (util_a.py, util_b.py, test_a.py, test_b.py) on a directory called project and we want to reformat files that not start with test_:

$ pycln /path_to/project/ --extend-exclude test_.*  # or -ee test_.*

-a, --all flag

Remove all unused imports (not just those checked from side effects).

Default

False

Behaviour

  • Remove all unused import statements whether they has side effects or not!
  • Faster results, because Pycln will skip side effects analyzing.

Usage

$ pycln /path/ --all  # or -a

Example

original /example.py

import x  # has unnecessary side effects
import y

fixed /example.py (without -a, --all, default)

x module has considered as import with side effects. no change.

import x  # has unnecessary side effects
import y

fixed /example.py (with -a, --all)

x module has considered as unused import. has removed.

import y

-c, --check flag

Do not write the files back, just return the status.

Default

False

Behaviour

  • Return code 0 means nothing would change.
  • Return code 1 means some files would be changed.
  • Return code 250 means there was an internal error.

Usage

$ pycln /path/ --check  # or -c

-d, --diff flag

Do not write the files back, just output a diff for each file on stdout.

Default

False

Behaviour

  • Output useful diffs without modifying the files.

Usage

$ pycln /path/ --diff  # or -d

Example

original /example.py

import x  # unused import
import y, z

y, z

$ pycln example.py --diff --all

--- original/ example.py
+++ fixed/ example.py
@@ -1,4 +1,3 @@
-import x  # unused import
 import y, z

 y, z

All done! 💪 😎
1 import would be removed, 1 file would be changed.

-v, --verbose flag

Also emit messages to stderr about files that were not changed and about files/imports that were ignored.

Default

False

Behaviour

  • Also the report ignored files/imports counters will be enabled.

Usage

$ pycln /path/ --verbose  # or -v

-q, --quiet flag

Do not emit both removed and expanded imports and non-error messages to stderr.

Default

False

Behaviour

  • Errors are still emitted; silence those with -s, --silence.
  • Has no effect when used with --diff.
  • Counters are still enabled.

Usage

$ pycln /path/ --quiet  # or -q

-s, --silence flag

Silence both stdout and stderr.

Default

False

Behaviour

  • Uncaught errors are sill emitted; silence those with 2>/dev/null. (not recommended)
  • No output even when --check has specified.
  • --diff output is still emitted.

Usage

$ pycln /path/ --silence  # or -s

-x, --expand-stars flag

Expand wildcard star imports.

Default

False

Behaviour

  • It works if only if the module is importable.
  • Slower results, because Pycln will do importables analyzing.
  • UnexpandableImportStar message will be emitted to stderr if the module is not importable.

Usage

$ pycln /path/ --expand-stars  # or -x

Example

original /example.py

from time import *
from os import *

sleep(time())
print(path.join("projects", "pycln"))

$ pycln example.py --expand-stars --diff

--- original/ example.py
+++ fixed/ example.py
@@ -1,5 +1,5 @@
-from time import *
-from os import *
+from time import sleep, time
+from os import path

 sleep(time())
 print(path.join("projects", "pycln"))

All done! 💪 😎
2 imports would be expanded, 1 file would be changed.

--no-gitignore flag

Do not ignore .gitignore patterns.

Default

False

Behaviour

  • Also reformat .gitignore excluded files.
  • Do nothing if .gitignore is not present.

Usage

$ pycln /path/ --no-gitignore

--version flag

Show the version and exit.

Default

False

Behaviour

  • Show the current Pycln version.
  • Exit with code 0.

Usage

$ pycln --version

--install-completion flag

Install completion for the current shell.

Default

None

Behaviour

  • Windows Powershell is not supported.

Usage

$ pycln --install-completion

--show-completion flag

Show completion for the current shell, to copy it or customize the installation.

Default

None

Behaviour

  • Only output the completion script.

Usage

$ pycln --show-completion

GUI For Windows

Come on! 😂

Supported Cases

General

All the cases below and more are considered as used.

Single Line

  • Import:

    import x, y
    import a as b
    import foo.bar
    
    foo.bar(b)
    y = 5
    print(x)
  • Import From:

    from xxx import x, y
    from abc import a as b
    from metasyntactic import foo.bar
    from metasyntactic.foo import baz
    
    foo.bar(baz(b))
    y = 5
    print(x)

Multi Line

  • Import:

    import \
        x, y
    
    print(x, y)
  • Import From:

    from xxx import (
        x,
        y
    )
    from metasyntactic import foo, \
        bar
    
    print(foo(bar(x, y)))

Special cases

Side Effects

Pycln takes imports that has side effects into account.

Some behaviours:

  • These behaviours will be changed if -a, --all flag has specified.
  • All Python standrad modules are considered as imports without side effects except (this, antigravity, rlcompleter).
  • Third party and local modules will be statically analyzed, there are three cases:
    • HasSideEffects.YES ~> considered as used.
    • HasSideEffects.MAYBE ~> considered as used.
    • HasSideEffects.NO ~> considered as not used.

Implicit Imports From Sub-Packages

Pycln can deal with implicit imports from sub-packages.

For example:

import os.path  # marked as used.

print(os.getpid())

Import With Importlib

Not supported, also not on the roadmap.

Typing

Pycln takes Python 3.5+ type hints into account.

from typing import List, Tuple  # marked as used.

foo: List[str] = []

def bar() -> Tuple[int, int]:
    return (0, 1)

String

Pycln can understand string type hints.

All the imports below are considered as used:

  • Fully string:

    from ast import Import
    from typing import List
    
    def foo(bar: "List[Import]"):
        pass
  • Nested string (Python 3.7+):

    from ast import Import
    from typing import List
    
    def foo(bar: "List['Import']"):
        pass
  • Semi string:

    from ast import Import
    from typing import List
    
    def foo(bar: List["Import"]):
        pass

Comments

Pycln takes Python 3.8+ variable annotations into account.

All the imports below are considered as used:

  • Assign:

    from typing import List
    
    foo = []  # type: List[str]
  • Argument:

    from typing import List
    
    def foo(
        bar  # type: List[str]
    ):
        pass
  • Function:

    from typing import List, Tuple
    
    def foo(bar):
        # type: (List[str]) -> Tuple[int]
        return (int(bar[0][0]), 1)

Cast

Pycln can understand typing.cast case.

All the imports below are considered as used:

from typing import cast
import foo, bar

baz = cast("foo", bar)  # or typing.cast("foo", bar)

TypeVar

Pycln can understand typing.TypeVar 'str' cases.

All the imports below are considered as used:

from typing import TypeVar
import Foo, Bar, Baz

T1 = TypeVar("T1", "Foo", "Bar")  # unbounded
T2 = TypeVar("T2", bound="Baz")  # bounded

All (__all__)

Pycln looks at the items in the __all__ list, if it match the imports, marks it as used.

import os, time  # These imports are considered as used.

__all__ = ["os", "time"]

List Operations (append and extend)

Pycln considers __all__.append arguments and __all__.extend list items as used names.

  • Append:

    import os, time  # These imports are considered as used.
    
    __all__.append("os", "time")
  • Extend:

    import os, time  # These imports are considered as used.
    
    __all__.extend(["os", "time"])

List Concatenation

Pycln can deal with almost all types of list concatenation.

  • Normal concatenation:

    import os, time  # These imports are considered as used.
    
    __all__ = ["os"] + ["time"]
  • Augmented assignment:

    import os, time  # These imports are considered as used.
    
    __all__ += ["os", "time"]
  • Augmented assignment with concatenation:

    import os, time  # These imports are considered as used.
    
    __all__ += ["os"] + ["time"]

List Comprehension

Not supported, also not on the roadmap.

Init file (__init__.py)

Pycln can not decide whether the unused imported names are useless or imported to be used somewhere else (exported) in case of an __init__.py file with no __all__ dunder.

A detailed description of the problem:

consider the following two cases below:

  • case a1:

    # Assume that we have this project structure:
    .
    ├── __init__.py
    └── file.py

    where __init__.py:

    import x, y  #: These names are unused but imported to be used in another
                #: file in the same package.
                #:
                #: (Pycln should *NOT* remove this import statement).

    and file.py:

    from . import x, y
    
    print(y(x))
  • case a2:

    # Assume that we have this project structure:
    .
    └── __init__.py

    where __init__.py:

    import x, y  #: These names are unused.
                #:
                #: (Pycln should remove this import statement).

Due to the nature of Pycln where it checks every file individually, it can not decide whether x and y are imported to be exported (case a1) or imported but unused (case a2), therefore, I consider using an __all__ dunder is a good solution for this problem.

NOTE: in case you're not sure about what an __all__ dunder does, please consider reading this stackoverflow answer

Now let us review the same two cases but with an __all__ dunder:

  • case b1:

    # Assume that we have this project structure:
    .
    ├── __init__.py
    └── file.py

    where __init__.py:

    import x, y  #: Luckily, Pycln can understand that `x` and `y`
                #: are exported so it would not remove them.
    __all__ = ["x", "y"]

    and file.py:

    from . import x, y
    
    print(y(x))
  • case b2:

    # Assume that we have this project structure:
    .
    └── __init__.py

    where __init__.py:

    import x, y  #: In this case where an `__all__` dunder is used, Pycln
                #: can consider these names as unused confidently
                #: where they are inaccessible from other files
                #: (not exported).
    __all__ = ["something else or even an empty list"]

You may notice that using an __all__ dunder makes the two cases distinguishable for both the developers and QA tools.

Stub files (.pyi) redundant aliases

Pycln skips redundant alias imports in compliance with PEP 484 for the purposes of exporting modules and symbols for static type checking. Additionally, all symbols imported using from foo import * imports are considered publicly exported in a stub file, so these import statements are also ignored for .pyi extensions.

  • case a:

    import X as X  # marked as used.
  • case b:

    from X import Y as Y  # marked as used.
  • case c:

    from socket import *  # marked as used.

Unsupported Cases

Specific

All the cases below are unsupported and not in the roadmap. Only certain import statements are effected (not the entire file). Also, Pycln will not touch these cases at all to avoid any code break.

Semicolon separation

import x; import y

#: Pycln can not handle the above format.
#:
#: Of course you can fix this issue by rewriting the above code as:

import x
import y

Colon inlined

try: import x
finally: import y

#: Pycln can not handle the above format.
#:
#: Of course you can fix this issue by rewriting the above code as:

try:
    import x
finally:
    import y

Global

In case a file contains one of the below cases, the entire file would be skipped.

Form feed character

A form feed is a page-breaking ASCII control character. It forces the printer to eject the current page and to continue printing at the top of another.

# It's a hidden character.
# Forms:
\x0c
\f

Integrations

Version Control Integration

  • Use pre-commit. Once you have it installed, add this to the .pre-commit-config.yaml in your project:

    - repo: https://github.com/hadialqattan/pycln
      rev: v2.1.1 # Possible releases: https://github.com/hadialqattan/pycln/releases
      hooks:
        - id: pycln
          args: [--config=pyproject.toml]
    • Avoid using args in the hook. Instead, store necessary configuration in pyproject.toml so that CLI usage of Pycln behave consistently for your project.

    • Stable branch points to the latest released version.

  • On your pyproject.toml add this section (optional):

    [tool.pycln]
    all = true
  • Then run pre-commit install and you’re ready to go.