Open-source project
openlawlibrary/pygls avatar
openlawlibrary/pygls

pygls: writing your own Language Server Protocol server in Python

A pythonic generic language server

812 stars142 forksPythonApache-2.0

At a glance

What is it?
pygls is a Python framework for building Language Server Protocol servers, and the whole point is that a working server with completion support fits in about a dozen lines. The costs are a hard pin on lsprotocol, an Apache-2.0 licence, and a dependency on a specific set of runtime choices for marshalling messages.
Who is it for?
Adopt pygls when you are writing a language server in Python and want the protocol plumbing, the workspace document cache and the feature registration handled for you, particularly if you care about supporting editors beyond VS Code. Do not adopt it if you need to remain on an older lsprotocol release, because the dependency is pinned exactly, or if you would rather configure an existing analyzer than implement completion yourself.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A framework for the protocol, not a language server for you

pygls, pronounced like pie glass, is a Python implementation of the Language Server Protocol, described as a foundation for writing your own language servers in just a few lines of code. The distinction matters because a language server is not a product you download, it is a protocol endpoint that an editor talks to. The work is in implementing the behaviour an editor asks for: completion, hover, formatting, navigation, diagnostics.

The project's own framing is generic. Other projects listed as general-purpose servers, the documentation argues, are general only in the sense of having a wide configuration surface. They achieve generality as far as configuration allows, as opposed to what programming in the pygls case achieves. That is a pointed claim and it is the clearest statement of intent in the repository.

The framework is maintained by Open Law Library, a 501(c)(3) tax exempt organisation, and published on PyPI as pygls with the current version at 2.1.1. Documentation and a tutorial are hosted at pygls.readthedocs.io, and the repository also keeps an Implementations.md table of known servers built on top of it, which contributors are asked to extend by pull request.

The package requires Python 3.9 or newer, and the classifiers list support through 3.15, so the floor is deliberately low and the stated range is wide.

The feature decorator and the workspace cache carry the whole design

The quickstart shows the shape of a server. You import the server class and the protocol types, construct a server with a name and a version, register a handler with a decorator, and start the process:

python
from pygls.lsp.server import LanguageServer
from lsprotocol import types

server = LanguageServer("example-server", "v0.1")

@server.feature(types.TEXT_DOCUMENT_COMPLETION)
def completions(params: types.CompletionParams):
    items = []
    document = server.workspace.get_text_document(params.text_document.uri)
    current_line = document.lines[params.position.line].strip()
    if current_line.endswith("hello."):

Two mechanisms are doing the work here. The decorator binds a Python function to a protocol method name, and the function signature tells you what it receives, a CompletionParams in this case. The server object holds a workspace, and the workspace caches text documents by URI so that a handler can ask for a document without re-reading it from disk.

The handler then returns a protocol type, not a dict:

python
        items = [
            types.CompletionItem(label="world"),
            types.CompletionItem(label="friend"),
        ]
    return types.CompletionList(is_incomplete=False, items=items)

server.start_io()

The is_incomplete flag is worth understanding, since it is the difference between a completion list the editor will re-query as the user types and one it will treat as final. And start_io is the transport choice, running the protocol over standard input and output, which is what most editors launch a server with.

lsprotocol is pinned exactly, which fixes both the API and the marshalling

The runtime dependencies are three, and one of them is exact. The package requires attrs at 24.3.0 or newer, cattrs at 23.1.2 or newer, and lsprotocol at exactly 2025.0.0. The version scheme for lsprotocol is a calendar-based release identifier rather than a plain semver, so pinning to 2025.0.0 means pinning to one specific protocol type snapshot.

That is a defensible choice and it has a real cost. The protocol surface moves, and an exact pin means an upgrade is an explicit act rather than something your resolver does for you in a build that has to stay reproducible. For a language server that must interoperate with editors, however, the type definitions are the contract, and a mismatch between your handler signature and the types the editor expects shows up as a runtime error rather than a compile error.

attrs and cattrs are the machinery behind the type objects. They provide the attribute class definitions and the structuring and unstructuring that turns protocol types into JSON payloads on the wire. This is why the framework is called pythonic in the project description: handlers work with typed objects with keyword constructors rather than hand-built dictionaries.

The practical implication for anyone extending pygls is that the type definitions come from lsprotocol, not from pygls itself. When you need a method that seems missing, the place to look is the lsprotocol version you have pinned.

The WebSocket transport is an optional extra, and the default is stdio

Transport is where pygls offers one genuine choice, and it is kept out of the core. The optional dependency group named ws pulls in websockets at 13.0 or newer, and the default install has no WebSocket support at all.

That is a sensible split. An editor launching a language server on a machine uses pipes, and requiring a WebSocket library for that adds a dependency and a port to manage for no benefit. A remote or shared server, where several editors connect to one instance over a network, is a different problem, and that is where the ws extra earns its place.

The starter code chooses the simpler path with server.start_io(), which is the standard input and output transport. The same call is the entry point regardless of which language server you write, so switching transports later is a configuration question rather than a rewrite.

One thing the project does not advertise is a story for multiple clients or per-session state. The workspace model described in the quickstart is a document cache, and the sample server has no notion of a session identifier. If your server needs to distinguish users, you will be building that on top of the protocol yourself.

Testing a language server means running one in a subprocess

The test setup tells you something about the difficulty of this kind of software. Every sub-task requires uv, the package manager and task runner, and the two test entry points are:

bash
uv run --all-extras poe test
uv run --all-extras poe test-pyodide

The task runner is poethepoet, configured in pyproject.toml with the executor type set to uv, and the pytest configuration sets asyncio_mode to auto, so async test functions need no decorator. The marker list defines a single custom marker, e2e, described as tests that run a language server in a subprocess, which is the only honest way to test a protocol server that talks over pipes.

The pyodide variant is the more interesting one. The poe task runs pytest against tests/e2e with an lsp-runtime of pyodide, and the Makefile target for it depends on a dist build plus an npm ci inside tests/pyodide. Pyodide is the Python runtime compiled to WebAssembly, so this is a test that runs a language server inside a browser-grade sandbox, and it implies the framework can be shipped somewhere other than a local process.

Development tooling around that is standard and modern: ruff for linting and formatting, mypy with a package target, black in the dev group, and a coverage-enabled pytest configuration. The Makefile drives all of it through uv with a managed Python version, and there is a separate poetry-free path for building the distribution with git describe and uv build.

Where pygls differs from the Node framework and from the configuration-first servers

The project names its main alternative directly: Microsoft's NodeJS-based Generic Language Server Framework. The argument made about it is that, coming from Microsoft, it is focused on extending VS Code, although in theory it could be used to support any editor. So the argument for pygls is about editor reach rather than about features, and the documentation frames it plainly: if you want to support more editors, pygls is the better choice, because pygls is not focused around VS Code.

The second group of alternatives is different in kind. The projects listed alongside it, diagnostic-languageserver, efm-langserver and null-ls.nvim, are described as other language servers with general in their description or at least in their intentions, but only general in the sense of having a wide configuration surface. The distinction the project draws is between a server whose generality comes from configuration and one whose generality comes from the fact that it is a programming framework. null-ls.nvim is noted as Neovim only, which is itself a reminder that editor support is often where the real specialisation lies.

The practical test is short. If your language server needs behaviour that a config file cannot express, such as completion logic that depends on the surrounding line, you are writing a program either way, and pygls reduces that program to handlers. If you only need to run a linter with different settings, the configuration-first tools are less code for you to own.

Apache-2.0, a 2.x line that broke the old API, and active maintenance

The licence is Apache-2.0, and the file LICENSE.txt sits at the repository root with a matching declaration in the project metadata. For a framework that other people build servers on, that is a permissive licence with an explicit patent grant, and it is the kind of licence that does not force your server to be open source.

Version 2.1.1 was published on 2026-03-25, after 2.1.0 on 2026-03-19 and 2.0.1 on 2026-01-26. That 2.x jump is the thing to look into before you write code, because the repository carries both HISTORY.md and CHANGELOG.md, and the presence of a separate history file alongside a changelog suggests the difference between the pre-2.0 API and the current one is worth reading carefully. Code written against the older pygls will not simply work unchanged.

Maintenance is active. The last push to the repository was on 2026-10-17, the repository is not archived, and the releases landed within a couple of months of each other through the spring of 2026. The project also publishes a docs site, a contributors file, a code of conduct, a pull request template, a RELEASING.md and a cliff.toml for changelog generation, which is the paperwork of a project that expects outside contributions.

The one gap the project is upfront about is support. There is no issue tracker policy or response time in the documentation, only an invitation to contribute and a donation link for Open Law Library, so treat the community channels as the support model.

Editorial conclusion

Adopt pygls when you are writing a language server in Python and want the protocol plumbing, the workspace document cache and the feature registration handled for you, particularly if you care about supporting editors beyond VS Code. Do not adopt it if you need to remain on an older lsprotocol release, because the dependency is pinned exactly, or if you would rather configure an existing analyzer than implement completion yourself. Before you start, check that your editor speaks LSP over stdio, confirm the lsprotocol==2025.0.0 pin matches the method signatures you need, and read the version history in HISTORY.md, since the 2.x line is a recent rewrite of the earlier API.

Frequently asked questions

How do I write a minimal completion server with pygls?

Create a LanguageServer with a name and version, register a function with the @server.feature decorator for types.TEXT_DOCUMENT_COMPLETION, return a types.CompletionList, and call server.start_io() to serve the protocol over standard input and output.

Does pygls work with editors other than VS Code?

That is the point the project argues. The documentation describes the Node-based framework as being focused on extending VS Code, and presents pygls as the better choice if you want to support more editors, since pygls is not focused around VS Code.

What version of lsprotocol does pygls require?

Exactly lsprotocol 2025.0.0, pinned rather than ranged. The other runtime dependencies are attrs at 24.3.0 or newer and cattrs at 23.1.2 or newer.

How do I run the pygls tests?

Every sub-task requires uv, and the documented commands are uv run --all-extras poe test for the main suite and uv run --all-extras poe test-pyodide for the Pyodide end-to-end run. The project uses poethepoet as its task runner and pytest with asyncio_mode set to auto.

Official sources

  1. License: Apache-2.0
  2. openlawlibrary/pygls on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/openlawlibrary-pygls.svg)](https://hysenlabs.com/projects/openlawlibrary-pygls)

Was this useful?