How do I keep Python code consistently formatted and free of obvious errors?
Short answer
Use a formatter to standardise layout automatically and a linter to catch likely bugs before running. Running both in a pre-commit hook stops style debates and catches mistakes early.
The short version
Use a formatter to impose a consistent layout automatically, and a linter to flag likely bugs and dead code. ruff does both and is fast; black plus flake8 is the older combination. Wire them into a pre-commit hook so they run before code is committed rather than being remembered by hand.
Formatting
A formatter rewrites your code to a single canonical style, so nobody argues about line breaks or quote characters.
ruff format .
The value is not that the chosen style is optimal; it is that it is decided. Every file looks the same, diffs show only real changes rather than reformatting noise, and code review stops being about whitespace.
Linting
A linter reads the code without running it and reports problems: an unused import, a variable assigned but never used, a bare except, a mutable default argument.
ruff check .
Many of these are exactly the bugs discussed elsewhere in this library. A linter catches the mutable default argument trap automatically, before it ever bites you, which is a strong argument for running one.
Fixing automatically
Many findings can be corrected without human input.
ruff check --fix .
Always review the changes rather than trusting them blindly, but for imports and simple issues this saves a great deal of tedium.
Configuration lives in pyproject.toml
Put the settings with the project so everyone gets the same behaviour.
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I"] # errors, pyflakes, import sorting
Keeping this in the project file, not in someone's editor settings, is what makes the standard shared rather than personal.
Run it automatically
A pre-commit hook runs the tools on changed files before each commit, so nothing unformatted or obviously broken gets in.
pip install pre-commit
pre-commit install
with a small .pre-commit-config.yaml naming the tools. The point is that the check is not optional and not something anyone has to remember.
What a linter cannot do
It cannot tell you the code is correct, only that it does not contain certain recognisable mistakes. It will not catch a wrong join, a misread column, or a statistical error. Linting is cheap insurance against a class of small bugs, not a substitute for tests or for thinking.
A type checker on top
For a further layer, mypy checks your type hints and reports mismatches, catching a category of bug that a linter does not. It is worth adding to a codebase that others depend on, once formatting and linting are in place.
The pay-off
Consistent formatting makes code readable and diffs meaningful. Linting removes a family of silly bugs. Together they cost almost nothing to set up and remove a surprising amount of friction from working with other people, which is exactly the sort of thing worth automating once and then forgetting about.