I manage most of my Python projects with Poetry, I really like how it handles dependencies, configures projects and manages virtual environments using simple commands.

Normally I include linting tools as part of my Poetry projects, specifically flake8 and pylint. They served me well, but recently I saw a lot of hype around ruff in online tech circles, so I decided to give it a try.

And I'm glad I did, ruff and poetry are a match made in heaven. I'm really enjoying working with ruff, and how I can have really good static code analysis, and really fast.

In this article, I give a quick guide on how to setup ruff the way I use it on my Python projects, and how to integrate the ruff-lsp to Neovim.

Add ruff to you poetry dependencies

Assuming you already have a poetry project in place, you can add ruff as a development dependency:

poetry add --group dev ruff

This will create the following entry in your pyproject.toml file:

[tool.poetry.group.dev.dependencies]
ruff = "^0.4.2"

Configure ruff

The ruff documentation is pretty good, and the tool is pretty versatible, so you can set ruff to however you like on your projects.

You can use a separate file for ruff configurations: ruff.toml, however, since poetry uses pyproject.toml for project configuration, we can use that file to configure ruff. I normally use the following configuration parameters on my project:

[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = [
    "E",  # pycodestyle errors
    "W",  # pycodestyle warnings
    "F",  # pyflakes
    "I",  # isort
    "B",  # flake8-bugbear
    "C4",  # flake8-comprehensions
    "N",  # PEP8 naming convetions
    "D"  # pydocstyle
]
ignore = [
    "C901",  # too complex
    "W191",  # indentation contains tabs
    "D401"  # imperative mood
]

[tool.ruff.lint.pydocstyle]
convention = "google"

Now you can run poetry run ruff check on your project and enforce the rules you want.

Configure pre-commit checks

I like to add pre-commit hooks to my repositories. Luckily, ruff also provies a pre-commit hook, so you can trigger your selected checks every time you git commit. All you need to do is add the following to your .pre-commit-config.yaml:

- repo: https://github.com/astral-sh/ruff-pre-commit
  # Ruff version.
  rev: v0.4.2
  hooks:
    # Run the linter.
    - id: ruff
    # Run the formatter.
    - id: ruff-format

You can also enable lint fixes:

- repo: https://github.com/astral-sh/ruff-pre-commit
  # Ruff version.
  rev: v0.4.2
  hooks:
    # Run the linter.
    - id: ruff
      args: [ --fix ]
    # Run the formatter.
    - id: ruff-format

This was taken from ruff-pre-commit's documentation.

Configure the ruff Language Server Protocol

This section is for Neovim, you can find instructions for your editor of choice on ruff-lsp docs.

I use mason.nvim to manage my LSPs. I'll probably write an article on how to get all this working on Neovim, but for now, if you use Mason, you can ensure that ruff-lsp is installed:

require('mason-lspconfig').setup({
    ensure_installed = {
        'pyright',
        'ruff_lsp',
        'rust_analyzer',
        'lua_ls'
    },
    handlers = {
        default_setup,
    },
})

I use ruff-lsp for linting and formatting, and pyright for everything else, so my setup looks like this;

-- Ruff for linting and formatting:
require('lspconfig').ruff_lsp.setup {
  init_options = {
    settings = {
      args = {},
    }
  }
}
-- Pyright for everything else:
require('lspconfig').pyright.setup {
    settings = {
        pyright = {
            autoImportCompletion = true,
            -- Using Ruff's import organizer
            disableOrganizeImports = true
        },
        python = {
            analysis = {
                -- Ignore all files for analysis to exclusively use Ruff for linting
                ignore = { '*' }
            }
        }
    }
}

I use venv-lsp.nvim to make sure that my poetry virtual environment is activated before my LSPs are loaded. For this, you need to add the following before any LSP configuration:

-- Activate venv before starting the LSP
require('venv-lsp').init()

And that's it! That's the setup I'm running at the moment, I'll update this guide if I make future improvements.