mrkeyoor.com_
Thu 06 Aug 02:43 UTC
PyPICLI & Toolingupdated 06 Aug 2026

isort

isort rewrites the import block at the top of a Python file into a consistent order. It splits imports into sections (future, standard library, third party, first party, local), sorts within each section, separates plain import statements from from-imports, merges duplicate from-imports of the same module, and wraps anything that runs past your line length according to one of twelve output styles. It knows which modules are standard library from a per-version list baked into the package, and it works out which are yours from src_paths and the location of your config file. You run it as a command (isort . rewrites in place, isort --check-only . just fails), as a pre-commit hook, from an editor plugin, or as a Python library through isort.code() and isort.check_code(). It does one job and does not touch anything below the imports, which is why it is normally paired with a formatter rather than used instead of one.

Verdict

isort is the tool that defined how Python import blocks are supposed to look, and it is still the most configurable implementation of that idea by a wide margin. On a new project reach for ruff's I rules first, and keep isort for repos that already depend on it or that need grouping behavior ruff does not expose.

API stability3/5Everyday flags and settings names have been steady for years and profiles keep output predictable. The deductions are for real removals rather than deprecations: 8.0.0 dropped the setuptools plugin and the legacy finder path, the 9.0.0 alphas removed deprecated options entirely, and sorting output shifts across releases often enough that an unpinned bump can fail --check-only on unchanged code.
Docs4/5pycqa.github.io/isort documents every setting with its default and CLI equivalent, has a dedicated black compatibility guide, and includes a rendered gallery of all twelve multi-line output modes so you can pick by looking rather than by guessing. It reads as generated reference material though, so working out which combination of five interacting options produces the style you want is still trial and error.
Maintenance4/5Pushed 5 August 2026, under PyCQA, with 8.0.0 and 8.0.1 shipped in February 2026 and a 9.0.0 beta in progress carrying a long list of merged fixes from many different contributors. 80 open issues (89 counting PRs) for a tool with this much configuration surface is a reasonable queue.
Ecosystem5/5Around 31.2 million downloads a week, an official pre-commit hook, editor plugins catalogued on the project wiki, and ruff reimplementing its behavior as the I ruleset, which effectively makes isort the specification everyone else matches.

Use it if

  • Your project already standardizes on it, your CI already runs isort --check-only, and every contributor's editor is wired to it. Switching tools costs a repo-wide diff that nobody asked for
  • You need grouping control beyond alphabetical: custom sections for an internal namespace, forced_separate for a subpackage, known_first_party overrides, or per-module placement decided at runtime through isort.place_module()
  • You want a Python API rather than a CLI: isort.code() returns sorted source as a string and isort.check_code() returns a boolean, which is what you want when import sorting is a step inside your own codemod or linter
  • You need a git hook that only inspects staged files, which isort ships as isort.hooks.git_hook rather than leaving you to script it
  • Your files need partial control: # isort: split to force a section break, # isort: off and # isort: on around a block that must not move, and # isort: skip on a single line
Skip it if

Setup reality

pip install isort needs Python 3.10 or newer and pulls no required dependencies; colored diffs need isort[colors] for colorama. Configuration goes in pyproject.toml under [tool.isort] with underscored keys, and it also reads setup.cfg, .isort.cfg, and tox.ini. The part that catches monorepos is discovery: isort walks up from each file it is given and stops at the first config it finds, so two subdirectories can be sorted under two different rule sets without any warning. Pass --settings-path to force one, or --resolve-all-configs when you actually want per-directory rules. If you run black or ruff format, set profile = "black" before your first commit or the two tools will rewrite each other's wrapping forever. First-party detection is a heuristic over src_paths, which defaults to the config file's directory plus src, so a package laid out any other way gets classified as third party and your imports land in the wrong section until you set known_first_party. isort does not respect .gitignore unless you pass --gitignore or set skip_gitignore, so generated code and vendored trees need explicit skip or extend_skip entries. In CI, --check-only exits 1 and prints nothing useful on its own, so always add --diff. Pin the version in both CI and pre-commit, since sorting output is not frozen across releases. And note that python setup.py isort no longer exists: the setuptools plugin was removed in 8.0.0.

Patterns

Rewrite imports across a projectsort-in-place

isort .
isort src/ tests/
isort mymodule.py another.py

# preview instead of writing
isort --diff .

# only apply if the result still parses
isort --atomic .

isort . rewrites files with no backup, so commit first the very first time. --atomic re-parses the output and refuses to write when it would introduce a syntax error; it is off by default because it cannot parse code written for a different Python version than the interpreter running isort. It checks that the file still parses, not that it still behaves.

Fail the build on unsorted importscheck-in-ci

isort --check-only --diff .

# exit 0  everything already sorted
# exit 1  at least one file would change

# speed it up on a large repo
isort --check-only --diff --jobs 8 .

--check-only alone tells a contributor that something is wrong without saying what, so always pair it with --diff and let CI print the exact patch. Pin the isort version in the same file that installs it: sorting output is not stable across releases, and a floating version turns a green build red without a code change.

Stop isort and black from fightingmatch-your-formatter

# pyproject.toml
[tool.isort]
profile = "black"
line_length = 88

# what the profile actually sets:
#   multi_line_output = 3
#   include_trailing_comma = true
#   force_grid_wrap = 0
#   use_parentheses = true
#   ensure_newline_before_comments = true

Without this, isort wraps a long from-import one way, black rewraps it another, and pre-commit reports both hooks modifying files on every run until it hits its retry limit. Other profiles ship too (django, google, attrs, pycharm, plone), and if you change line_length you must change it in both tools or the disagreement comes straight back.

The settings most projects actually needconfigure-in-pyproject

[tool.isort]
profile = "black"
line_length = 88
src_paths = ["src", "tests"]
known_first_party = ["myapp"]
extend_skip = ["migrations", "vendor"]
extend_skip_glob = ["*/_generated/*"]
skip_gitignore = true
float_to_top = false
lines_after_imports = 2
combine_as_imports = true

Keys use underscores in TOML and dashes on the command line. isort searches upward from each target file and stops at the first config it finds, so in a monorepo different directories can silently get different rules; use --settings-path to force one, or --resolve-all-configs when per-directory rules are the point. skip_gitignore is off by default, which surprises people whose generated code keeps getting reformatted.

Get your own package out of the third-party sectionfix-first-party-detection

[tool.isort]
src_paths = ["src", "tests"]
known_first_party = ["myapp", "myapp_plugins"]
known_local_folder = ["conftest"]
known_third_party = ["custom_vendored_lib"]

# see how isort classifies one module
python -c "import isort; print(isort.place_module('myapp.core'))"

Classification is a heuristic: isort looks for the module under src_paths, which defaults to the config file's directory plus src. A flat layout, a namespace package, or an editable install in a nonstandard location all defeat it, and the symptom is your own imports sorted in with requests and numpy. place_module() tells you the verdict for a single name, which is faster than bisecting a diff.

Exempt files, blocks, and single linesskip-code

import sys

import gevent.monkey; gevent.monkey.patch_all()  # isort: skip

# isort: off
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# isort: on

from . import late_module  # isort: skip

# first line of a file isort must never touch:
# isort: skip_file

This is the escape hatch for imports with side effects, which isort has no way to detect on its own. # isort: off and # isort: on bracket a region; # isort: skip applies to one statement; # isort: skip_file must appear in the file's own comments and takes the whole file out. Use # isort: split when you only want a section boundary rather than an exemption.

Sort staged files before they landpre-commit-hook

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/PyCQA/isort
    rev: 8.0.1
    hooks:
      - id: isort
        args: ["--profile", "black", "--filter-files"]

Pin rev to an exact version and bump it on purpose; pre-commit autoupdate crossing a major will reformat the whole repo in someone's unrelated pull request. --filter-files makes isort apply its own skip settings to the file list pre-commit hands it, which it otherwise ignores because those files were named explicitly. If you also run black, list isort first so black gets the final say on wrapping.

Pick how long from-imports breakchoose-a-wrap-style

# mode 3, vertical hanging indent (what profile="black" uses)
from third_party import (
    lib1,
    lib2,
    lib3,
)

# force_single_line = true: one import per line, zero merge conflicts
from third_party import lib1
from third_party import lib2

# force_sort_within_sections = true: mixes import and from-import
import os
from pathlib import Path
import sys

multi_line_output takes values 0 through 11 and the docs render every one of them, which is the fastest way to choose. force_single_line is worth considering on a repo with many contributors: it produces longer files but a line-level diff, so two people adding imports to the same module stop conflicting. Changing any of these is a whole-repo commit, so record its hash in .git-blame-ignore-revs.

Sort code from inside your own toolinguse-the-python-api

import isort

sorted_src = isort.code("import b\nimport a\n")
isort.file("pythonfile.py")                      # rewrites in place

if not isort.check_code(src, show_diff=True):
    raise SystemExit("imports are not sorted")

cfg = isort.Config(profile="black", known_first_party=["myapp"])
isort.code(src, config=cfg)
isort.place_module("myapp.core")                 # -> 'FIRSTPARTY'

isort.code() takes and returns a string and never touches disk, which makes it the right entry point inside a codemod or a custom linter. Build an isort.Config once and reuse it: constructing one re-reads and re-resolves configuration files, so creating it per file in a loop is a measurable cost on a large tree.

Inject an import into every fileadd-required-imports

isort --add-import 'from __future__ import annotations' src/

# permanently, in config
[tool.isort]
add_imports = ["from __future__ import annotations"]

# and the reverse
# isort --rm-import 'from six import string_types' src/

This is the tidiest way to roll out a from __future__ import across a codebase, since isort also places it in the correct section. It adds the import to every file it processes, including empty __init__.py files and scripts where it is pointless, so scope the run with skip settings or a path argument rather than pointing it at the repo root.

Pull stray mid-file imports into the headerfloat-imports-to-top

isort --float-to-top src/

# in config
[tool.isort]
float_to_top = true

Handy on legacy code where imports drifted into function bodies and conditionals, and it respects # isort: off and # isort: skip while doing it. Be careful: an import placed inside a function is sometimes deliberate, either to break a circular dependency or to defer an expensive module, and hoisting it turns a working file into an ImportError at startup. Run it once, read the diff, do not leave it on.

See the resolved config, or move the job to ruffdebug-or-migrate

isort --show-config          # every setting after all files are merged
isort --show-files .         # exactly which files would be processed
isort --verbose --diff file.py

# equivalent behavior inside ruff
# ruff check --select I --fix .
#
# [tool.ruff.lint.isort]
# known-first-party = ["myapp"]
# combine-as-imports = true

--show-config is the first thing to run when isort behaves differently in CI than on your laptop; nine times out of ten a different config file was discovered. If the resolved settings turn out to be plain (a profile, first-party names, a couple of skips), that config maps almost directly onto ruff's isort section and you can drop a dependency.

Alternatives

PackageRegistryPick it when
ruffPyPIYou want import sorting, linting, and formatting from one fast binary and can accept a config section instead of isort's full option list.
reorder-python-importsPyPIYou prefer one opinionated style with almost no configuration, and you want each from-import split onto its own line so merges never conflict.
usortPyPIYou need sorting that is careful about not reordering imports with side effects, which is the failure mode isort will not protect you from.
autoflakePyPIYour actual problem is unused and duplicate imports rather than their order, which isort deliberately does not address.