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.

build.mill (download, browse)
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

build.mill (download, browse)
package build
import mill._, pythonlib._

object `package` extends RootModule with PythonModule with RuffModule {}
src/main.py (browse)
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.

build.mill (download, browse)
import mill._, pythonlib._

object `package` extends RootModule with PythonModule {

  object test extends PythonTests with TestModule.Pytest with CoverageTests

}
src/main.py (browse)
def f1():
    pass

def f2():
    pass
test/src/test_main.py (browse)
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