A formatter for finding and removing unused import statements.
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
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
It doesn't matter which installation method you have used, Uninstall can be done with
uninstall.sh
(pip):
$ ./scripts/uninstall.sh
NOTE: Make sure the Python version you run Pycln with is the same or more recent than the Python version your codebase targets.
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.
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 )
Skip a file by typing
# nopycln: file
anywhere on it.
-
At the beginning:
# nopycln: file import x
-
At the end:
import x # nopycln: file
Skip module/package/library imports for all files (globally).
Please see --skip-imports option.
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.
-
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 viaSTDOUT
. - You can read from
STDIN
and provide normal paths at the same time (the order doesn't matter).
- For the time being, both the final report and the formatted code will be sent to
Skip module/package/library imports for all files (globally).
[]
- Takes a list of module/package/library names and skips any import belonging to them.
-
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
- a list of names in a pythonic list format:
-
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.
- a list of names in a pythonic list format (
Read configuration from a file.
None
- 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.
- 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
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
}
}
A regular expression that matches files and directories that should be included on recursive searches.
.*\.pyi?$
- 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.
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_.*
A regular expression that matches files and directories that should be exclude on recursive searches.
(\.eggs|\.git|\.hg|\.mypy_cache|__pycache__|\.nox|\.tox|\.venv|\.svn|buck-out|build|dist)/
- An empty value means no paths are excluded.
- Use forward slashes for directories on all platforms (Windows, too).
- Exclusions are calculated first, inclusions later.
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_.*
Like --exclude, but adds additional files and directories on top of the excluded ones. (Useful if you simply want to add to the default).
^$
(empty regex)
- An empty value means no paths are excluded.
- Use forward slashes for directories on all platforms (Windows, too).
- Exclusions are calculated first, inclusions later.
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_.*
Remove all unused imports (not just those checked from side effects).
False
- Remove all unused import statements whether they has side effects or not!
- Faster results, because Pycln will skip side effects analyzing.
$ pycln /path/ --all # or -a
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
Do not write the files back, just return the status.
False
- 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.
$ pycln /path/ --check # or -c
Do not write the files back, just output a diff for each file on stdout.
False
- Output useful diffs without modifying the files.
$ pycln /path/ --diff # or -d
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.
Also emit messages to stderr about files that were not changed and about files/imports that were ignored.
False
- Also the report ignored files/imports counters will be enabled.
$ pycln /path/ --verbose # or -v
Do not emit both removed and expanded imports and non-error messages to stderr.
False
- Errors are still emitted; silence those with
-s, --silence
. - Has no effect when used with
--diff
. - Counters are still enabled.
$ pycln /path/ --quiet # or -q
Silence both stdout and stderr.
False
- 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.
$ pycln /path/ --silence # or -s
Expand wildcard star imports.
False
- 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.
$ pycln /path/ --expand-stars # or -x
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.
Do not ignore
.gitignore
patterns.
False
- Also reformat
.gitignore
excluded files. - Do nothing if
.gitignore
is not present.
$ pycln /path/ --no-gitignore
Show the version and exit.
False
- Show the current Pycln version.
- Exit with code 0.
$ pycln --version
Install completion for the current shell.
None
- Windows Powershell is not supported.
$ pycln --install-completion
Show completion for the current shell, to copy it or customize the installation.
None
- Only output the completion script.
$ pycln --show-completion
Come on! 😂
All the cases below and more are considered as used.
-
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)
-
Import:
import \ x, y print(x, y)
-
Import From:
from xxx import ( x, y ) from metasyntactic import foo, \ bar print(foo(bar(x, y)))
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.
Pycln can deal with implicit imports from sub-packages.
For example:
import os.path # marked as used.
print(os.getpid())
Not supported, also not on the roadmap.
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)
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
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)
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)
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
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"]
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"])
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"]
Not supported, also not on the roadmap.
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.
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.
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.
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
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
In case a file contains one of the below cases, the entire file would be skipped.
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
-
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.