Linting Python Projects
This page will discuss common topics around maintaining the code quality of Python codebases using the Mill build tool
Formatting and Linting with Ruff
Ruff is a Python linter and code formatter. Mill has built-in support for invoking Ruff on your Python projects, to help you catch common sources of errors and keep your code nice and tidy.
Formatting
First, make your module extend pythonlib.RuffModule
.
package build
import mill._, pythonlib._
object `package` extends RootModule with PythonModule with RuffModule
You can reformat your project’s code by running the ruffFormat
task.
> cat src/main.py # initial poorly formatted source code
from typing import Self
class IntWrapper:
def __init__(self, x:int):
self.x =x
def plus(self, w:Self) -> Self:
return IntWrapper(self.x + w.x)
print(IntWrapper(2).plus(IntWrapper(3)).x)
...
> mill ruffFormat --diff # you can also pass in extra arguments understood by `ruff format`
error: ...
error: @@ -1,7 +1,12 @@
error: from typing import Self
error: +
error: +
error: class IntWrapper:
error: - def __init__(self, x:int):
error: - self.x =x
error: - def plus(self, w:Self) -> Self:
error: - return IntWrapper(self.x + w.x)
error: + def __init__(self, x: int):
error: + self.x = x
error: +
error: + def plus(self, w: Self) -> Self:
error: + return IntWrapper(self.x + w.x)
error: +
error: +
error: print(IntWrapper(2).plus(IntWrapper(3)).x)
error: ...
error: 1 file would be reformatted
> mill ruffFormat
...1 file reformatted
> cat src/main.py # the file is now correctly formatted
from typing import Self
...
class IntWrapper:
def __init__(self, x: int):
self.x = x
...
def plus(self, w: Self) -> Self:
return IntWrapper(self.x + w.x)
...
print(IntWrapper(2).plus(IntWrapper(3)).x)
You can create a ruff.toml
file in your project root to adjust the
formatting options as desired. For example,
> echo indent-width=2 > ruff.toml
> mill ruffFormat
...1 file reformatted
> cat src/main.py # the file is now correctly formatted with 2 spaces indentation
from typing import Self
...
class IntWrapper:
def __init__(self, x: int):
self.x = x
...
def plus(self, w: Self) -> Self:
return IntWrapper(self.x + w.x)
...
print(IntWrapper(2).plus(IntWrapper(3)).x)
Mill also has built-in global tasks, which allow you to run ruff across all projects in your
build, without ever needing to extend RuffModule
.
-
format all Python files globally:
mill mill.pythonlib.RuffModule/formatAll
-
lint all Python files globally:
mill mill.pythonlib.RuffModule/checkAll
You can also pass-in extra arguments to ruff, for example to find unformatted files and show the
diff: mill mill.pythonlib.RuffModule/formatAll --diff
If entering mill.pythonlib.RuffModule/formatAll
is too long, you can add an
External Module Alias to give it a
shorter name that’s easier to type.
> mill mill.pythonlib.RuffModule/formatAll
Linting
package build
import mill._, pythonlib._
object `package` extends RootModule with PythonModule with RuffModule {}
import os
def doit(x: int):
print(f"")
Ruff can be used as a linter, to catch some common code smells. Run ruffCheck
on your module:
> mill ruffCheck
error: ...
error: ...F401 [*] `os` imported but unused
error: |
error: 1 | import os
error: | ^^ F401
error: 2 |
error: 3 | def doit(x: int):
error: |
error: = help: Remove unused import: `os`
error: ...
error: ...F541 [*] f-string without any placeholders
error: |
error: 3 | def doit(x: int):
error: 4 | print(f"")
error: | ^^^ F541
error: |
error: = help: Remove extraneous `f` prefix
error: ...
error: Found 2 errors.
error: [*] 2 fixable with the `--fix` option.
Ruff can fix most errors automatically with ruffCheck --fix
> mill ruffCheck --fix
Found 2 errors (2 fixed, 0 remaining).
> cat src/main.py
...
def doit(x: int):
print("")
Code Coverage
Mill’s support for code coverage analysis is implemented by the coverage.py package.
You can use it by extending CoverageTests
in your test module.
import mill._, pythonlib._
object `package` extends RootModule with PythonModule {
object test extends PythonTests with TestModule.Pytest with CoverageTests
}
def f1():
pass
def f2():
pass
import main
def test_f1():
main.f1()
def test_other():
assert True
You can generate a coverage report with the coverageReport
task.
> mill test.coverageReport
Name ... Stmts Miss Cover
...------------------------------------------------
.../src/main.py 4 1 75%
.../test/src/test_main.py 5 0 100%
...------------------------------------------------
TOTAL ... 9 1 89%
The task also supports any arguments understood by the coverage.py
module.
For example, you can use it to fail if a coverage threshold is not met:
> mill test.coverageReport --fail-under 90
error: ...
error: Coverage failure: total of 89 is less than fail-under=90
Other forms of reports can be generated:
-
coverageHtml
-
coverageJson
-
coverageXml
-
coverageLcov