Neovim Python LSP setup
My take on a sane Neovim LSP and static type checker configuration.
WARNING: this post might be a water under the bridge!
Time passes, love dyes, configuration files change. My Neovim setup has evolved a lot since this post has been initially written, and keeping the text up to date is becoming too exhausting (though I try to). Therefore, check the last update time before following any of below instructions.
Last modified: November 5, 2026
As a dedicated Python Developer I want to be productive and write reliable code. To some point this could be assured by various programming tools, such as linters, formatters, type checkers, and LSP servers. Most modern IDEs offer out-of-the-box support of these clever utilities, so some developers don’t even think about them.
I’ve been using Vim and Neovim text editiors as my primary coding tools for years now, and I had to figure out all this LSP stuff myself to make it work. I don’t mind to do some hands-on tinkering, especially if the topic is of interest for me. Besides, thankfully, Nevim provides some first-class support for LSP servers.
So, this is my take on a sane Neovim LSP configuration for a Python developer.
My language server choice #
There are plenty of Python LSP implementations, all of them have some cool and sometimes unique features. Probably, the most popular among them is Pyright, which combines type checking with LSP functionality. It is an open source core of the Microsoft’s Pylance. Pyright might be a good place to start.
There is also a bunch of language servers based on an amazing Jedi Python refactoring package. It powers some essential LSP functions like symbols search, renaming, showing references and definitions, and detecting virtual environments. The languages servers that use Jedi are listed on its wiki:
- jedi-language-server
- python-language-server (currently unmaintained)
- python-lsp-server (fork from python-language-server)
- anakin-language-server
I personally prefer python-lsp-server (or pylsp) for the following reasons:
- It is written in Python, so I can easily understand its source and patch it.
- It has a plugin mechanism which allows me to choose from a bunch of external tools and easily write my own plugins.
Additionally, I would really like to use Ruff, which comes with a built-in LSP server. Unfortunately, it doesn’t support some LSP tricks like definitions, references and autocompletion.
The good news is that we can use both pylsp and ruff at the same time, combining the flexibility of the first with
the crazy performance of the later.
Neovim setup basics #
A detailed overview of my Neovim config is beyond the scope of this post, but I still need to point out some fundamentals.
So you might already know that Neovim is configured by a bunch of files. Those are not just configuration files, but proper scripts in Lua programming language (or Vimscript, if you are an old school person). My configuration scripts are organised in a following way:
nvim/
├── ftplugin
│ └── python.lua
├── init.lua
└── lua
├── config
│ └── lazy.lua
└── plugins
├── lspconfig.lua
├── lualine.lua
└── ... and all the other plugins I useThe init.lua script contains some basic config like encoding, indentation rules, highlighting, etc.
The lua/plugins/ directory contains a separate configuration script for each plugin.
The ftplugin/ contains language/extension specific configurations (e.g. if you want to have 2-spaced indentation for
Perl and 4-spaced for Python).
I use lazy.nvim to install and update Neovim plugins, it is very handy and simple to use.
That’s mostly simple as that.
Installing Python LSP with pipx
#
I am aware of Mason, a Neovim plugin to manage LSPs, linters, etc. Might give it a try some day.
For now my preferred approach is pipx, a tool to install Python packages in dedicated virtual environments.
IMHO this is the most convenient way to install Python utilities: linters, formatters, dependency managers, and any
Python LSP implementation you prefer.
pipx install python-lsp-server[all]If you plan to use any plugins, don’t forget to inject them in the same virtual environment where pylsp has been
installed to. Otherwise it won’t manage to discover them.
pipx inject install python-lsp-server <plugin-name>We’ll also need ruff as a separate tool:
pipx install ruffUsing mypy with pylsp
#
WARNING: Currently I prefer to run type checks via pre-commit hooks rather than while writing code, so I dropped
mypyfrom my Neovim config. I am leaving this section just in case anyone needs it.
First of all, inject pylsp-mypy plugin in the pylsp environment:
pipx inject install python-lsp-server pylsp-mypyWhen using LSP with integrated mypy make sure that it knows where to look for third-party packages. If you are using
a virtual environment in your project, you need to specify its path for mypy. It could be achieved via mypy.ini
configuration file in the root of your project with the following lines (assuming your virtual environment is located
in .venv/ in the project root):
[mypy]
python_executable=./.venv/bin/pythonIf you want to disable all type messages for a specific project, just add this setting in the same file:
[mypy]
ignore_errors=truelspconfig setup
#
Now we need to configure Neovim to use the preferred LSP. This can be achieved pretty easily with the help of lspconfig, a total must-have for your Neovim LSP configuration. This plugin provides basic configurations for various LSP servers.
I have it set up just as any other plugin in it’s own dedicated script:
{
"neovim/nvim-lspconfig",
config = function()
-- Enable Ruff server
vim.lsp.enable("ruff")
-- Enable pylsp
vim.lsp.config("pylsp", {
cmd = {"pylsp", "-vv", "--log-file", "/tmp/pylsp.log"},
settings = {
pylsp = {
plugins = {
-- Disable built-in linters and formatters in favor of Ruff
pyflakes = { enabled = false },
pycodestyle = { enabled = false },
pydocstyle = { enabled = false },
mccabe = { enabled = false },
yapf = { enabled = false },
autopep8 = { enabled = false },
isort = { enabled = false },
-- Enable mypy plugin for type checking (not my case!)
pylsp_mypy = {enabled = true},
}
}
}
})
vim.lsp.enable("pylsp")
end
}With this you should have diagnostic messages from ruff and pylsp in your Neovim buffer with Python code:

You may find more configuration options
here, and here is an
example config from the lspconfig repo.
Adding handy key bindings #
The Neovim’s LSP functionality is much more than just showing diagnostic messages. You probably would like to have some LSP actions binded to hotkeys, like showing a method signature or unwrapping a long error message.
The following Lua code will execute each time any LSP attaches the current buffer. Keep in mind while all these actions
are not pylsp-specific, some LSP implementations might not have them.
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('UserLspConfig', {}),
callback = function(ev)
-- Buffer local mappings
local opts = { buffer = ev.buf }
-- Go to the definition of the symbol (return with CTRL-I)
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
-- Show a hover window with symbol's docs
vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
-- Rename the symbol inside current buffer
vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, opts)
-- Show symbol usage in the project
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
-- Show signature help
vim.keymap.set('n', 'gs', vim.lsp.buf.signature_help, opts)
-- Show diagnostics message
vim.keymap.set('n', '<space>e', vim.diagnostic.open_float, opts)
-- Go to next diagnostics message
vim.keymap.set('n', '<space>n', vim.diagnostic.goto_next, opts)
-- Format the whole buffer
vim.keymap.set('n', '<space>f', vim.lsp.buf.format, opts)
})More useful LSP methods and their binding examples could be found in the lspconfig docs:
:help lspconfig-keybindings.
Advanced completion #
By default Neovim provides only literals from open buffers for autocompletion. This is not very handy and will definitely make our lives harder compared to our IDE wielding colleagues. It can be extended though with the help of LSP.
First of all, we need nvim-cmp Neovim plugin enhancing built-in completion
capabilities. Here is my nvim-cmp configuration:
{
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
},
config = function()
local cmp = require("cmp")
cmp.setup({
-- I don't like when completion options appear without a direct command
completion = {
autocomplete = false
},
-- Use built-in Neovim snippet engine
snippet = {
expand = function(args)
vim.snippet.expand(args.body)
end,
},
-- Bindings
mapping = cmp.mapping.preset.insert({
['<C-n>'] = cmp.mapping.complete(),
['<Tab>'] = cmp.mapping.select_next_item(),
['<S-Tab>'] = cmp.mapping.select_prev_item(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
}),
-- Use completion options from language servers, buffers and path
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'buffer' },
{ name = 'path' },
})
})
end,
}Now we need pylsp to provide Neovim with new completion options. To do that we enable some built-in plugins and inform
pylsp about Neovim’s completion capabilities. The following code is an extended version of
previously shown lspconfig setup (I’ve highlighted changes for you):
{
"neovim/nvim-lspconfig",
config = function()
local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- Enable Ruff server
vim.lsp.enable("ruff")
-- Enable pylsp
vim.lsp.config("pylsp", {
capabilities = capabilities,
settings = {
pylsp = {
plugins = {
jedi_completion = { enabled = true },
jedi_hover = { enabled = true },
jedi_references = { enabled = true },
jedi_signature_help = { enabled = true },
jedi_symbols = { enabled = true, all_scopes = true },
-- Disable linters and formatters
pyflakes = { enabled = false },
pycodestyle = { enabled = false },
pydocstyle = { enabled = false },
mccabe = { enabled = false },
yapf = { enabled = false },
autopep8 = { enabled = false },
isort = { enabled = false },
-- For type checking
pylsp_mypy = {enabled = true},
}
}
}
})
vim.lsp.enable("pylsp")
end
}And now we can get not only literals, but also objects attributes in our completion options:

To be continued? #
I still occasionally update this article weeks after it was originally published, and many months after I started to dig into language servers and Neovim LSP support, and I still find some new and exciting ways to adjust my setup. Well, I like doing that! I will definitely continue this project, and for sure there will be more interesting updates to this post.