Configuring Emacs, lsp-mode and Microsoft’s Visual Studio Code Python language server.

In a previous post I showed how to get Palantir’s Python Language Server working together with Emacs and lsp-mode.

In this post, we look at the brand new elephant in the room, Microsoft’s arguably far more powerful own Python Language Server, and how to integrate it with Emacs.


Since that previous post on Palantir’s language server, I’ve been using Emacs far more intensively for Python coding in tmux on remote machines with GPUs for deep learning.

The interactive programming possibilities of Emacs (remember that Lisp programmers have been doing this since the 60s) make for a great development solution: I can interact with my remotely running neural network code, start a long-running training, and then detach from the running tmux. When I re-attach the next morning (or the next), I can continue interactively experimenting with the still-running Python instance, for example further fine-tuning the training.

Palantir’s Python Language Server can become sluggish at times, so I switched back to elpy which is usually quite snappy, and affords an impressive suite of code intelligence features for such a small package.

However, the elpy RPC Python process has a tendency to die quite often, and even M-x elpy-rpc-restart simply stops working at some point.

This, together with the fact that Microsoft’s own Python Language Server, the one used for both Visual Studio Code and also in Microsoft’s flagship Visual Studio IDE, has enjoyed and will continue to enjoy a far larger share of developer mindshare and attention, encouraged me to try and get this language server also working with Emacs.

This exercise cost many more hours than I was planning to spend, but everything seems to be working now.

/In this post, I will explain step-by-step how you too can enjoy the highly multi-threaded and actively developed Microsoft Python Language Server in your Emacs./

The Goal

If you follow the steps set out further down in this blog post, your Emacs too could end up looking like the one showed in the following screenshots.

LSP showing the blue documentation overlay box, with Emacs in a tmux, i.e. terminal mode, on a remote Linux machine, editing PyTorch / fastai training code.
The same documentation showed locally, i.e. in GUI mode, on the macOS desktop.
lsp-describe-thing-at-point on the right for more persistent documentation, blue overlay box on the left.
C#’s strong multi-threading support comes in handy when parsing Python code! Ironic?

Four steps to combining Emacs with Microsoft’s Python Language Server

Read through all of the steps first, and then follow them carefully.

Let me know in the comments if anything should be further clarified.

Build the Microsoft Python Language Server using dotnet

If you don’t have this yet, download the .NET Core SDK for your platform from Microsoft’s .NET download site and install it.

Next, build the language server:

mkdir ~/build
cd ~/build
git clone
cd python-language-server/src/LanguageServer/Impl
dotnet build -c Release

Install required Emacs packages

In Emacs, install the required and some optional packages using for example M-x package-install:

  • lsp-mode – the main language server protocol package
  • lsp-ui – UI-related LSP extras, such as the sideline info, docs, flycheck, etc.
  • company-lsp – company-backend for LSP-based code completion.
  • projectile or find-file-in-project – we use a single function from here to determine the root directory of a project.

Required change to lsp-ui-doc.el

Microsoft’s Python Language Server likes to replace spaces in the documentation it returns with   HTML entities.

Furthermore, there seems to be an additional misunderstanding between Emacs lsp-mode and the MS PyLS with regard to the interpretation of markdown and plaintext docstrings.

Both of these issues impact the blue documentation overlay, and should be worked around by editing the lsp-ui-doc-extract function in lsp-ui-doc.el.

Right before the line with

((gethash "kind" contents) (gethash "value" contents)) ;; MarkupContent

add the following sexp:

;; cpbotha: with numpy functions, e.g. np.array for example,
;; kind=markdown and docs are in markdown, but in default
;; lsp-ui-20181031 this is rendered as plaintext see

;; not only that, MS PyLS turns all spaces into   instances,
;; which we remove here this single additional cond clause fixes all
;; of this for hover

;; as if that was not enough: import pandas as pd - pandas is returned
;; with kind plaintext but contents markdown, whereas pd is returned
;; with kind markdown. fortunately, handling plaintext with the
;; markdown viewer still looks good, so here we are.
((member (gethash "kind" contents) '("markdown" "plaintext"))
 (replace-regexp-in-string " " " " (lsp-ui-doc--extract-marked-string contents)))

Emacs configuration

Add the following to your Emacs init.el, and don’t forget to read the comments.

If you’re not yet using use-package now would be a good time to upgrade.

(use-package lsp-mode
  :ensure t

  ;; change nil to 't to enable logging of packets between emacs and the LS
  ;; this was invaluable for debugging communication with the MS Python Language Server
  ;; and comparing this with what vs.code is doing
  (setq lsp-print-io nil)

  ;; lsp-ui gives us the blue documentation boxes and the sidebar info
  (use-package lsp-ui
    :ensure t
    (setq lsp-ui-sideline-ignore-duplicate t)
    (add-hook 'lsp-mode-hook 'lsp-ui-mode))

  ;; make sure we have lsp-imenu everywhere we have LSP
  (require 'lsp-imenu)
  (add-hook 'lsp-after-open-hook 'lsp-enable-imenu)

  ;; install LSP company backend for LSP-driven completion
  (use-package company-lsp
    :ensure t
    (push 'company-lsp company-backends))

  ;; dir containing Microsoft.Python.LanguageServer.dll
  (setq ms-pyls-dir (expand-file-name "~/build/python-language-server/output/bin/Release/"))

  ;; this gets called when we do lsp-describe-thing-at-point in lsp-methods.el
  ;; we remove all of the " " entities that MS PYLS adds
  ;; this is mostly harmless for other language servers
  (defun render-markup-content (kind content)
    (message kind)
    (replace-regexp-in-string " " " " content))
  (setq lsp-render-markdown-markup-content #'render-markup-content)

  ;; it's crucial that we send the correct Python version to MS PYLS, else it returns no docs in many cases
  ;; furthermore, we send the current Python's (can be virtualenv) sys.path as searchPaths
  (defun get-python-ver-and-syspath (workspace-root)
    "return list with pyver-string and json-encoded list of python search paths."
    (let ((python (executable-find python-shell-interpreter))
          (init "from __future__ import print_function; import sys; import json;")
          (ver "print(\"%s.%s\" % (sys.version_info[0], sys.version_info[1]));")
          (sp (concat "sys.path.insert(0, '" workspace-root "'); print(json.dumps(sys.path))")))
        (call-process python nil t nil "-c" (concat init ver sp))
        (subseq (split-string (buffer-string) "\n") 0 2))))

  ;; I based most of this on the vs.code implementation:
  ;; (it still took quite a while to get right, but here we are!)
  (defun ms-pyls-extra-init-params (workspace)
    (destructuring-bind (pyver pysyspath) (get-python-ver-and-syspath (lsp--workspace-root workspace))
      `(:interpreter (
                      :properties (
                                   :InterpreterPath ,(executable-find python-shell-interpreter)
                                   ;; this database dir will be created if required
                                   :DatabasePath ,(expand-file-name (concat ms-pyls-dir "db/"))
                                   :Version ,pyver))
                     ;; preferredFormat "markdown" or "plaintext"
                     ;; experiment to find what works best -- over here mostly plaintext
                     :displayOptions (
                                      :preferredFormat "plaintext"
                                      :trimDocumentationLines :json-false
                                      :maxDocumentationLineLength 0
                                      :trimDocumentationText :json-false
                                      :maxDocumentationTextLength 0)
                     :searchPaths ,(json-read-from-string pysyspath))))  

  (lsp-define-stdio-client lsp-python "python"
                           `("dotnet" ,(concat ms-pyls-dir "Microsoft.Python.LanguageServer.dll"))
                           :extra-init-params #'ms-pyls-extra-init-params)

  ;; lsp-python-enable is created by macro above 
  (add-hook 'python-mode-hook
            (lambda ()


Although I would have preferred to do this with the two lsp-mode work-arounds, I am pretty satisfied with this setup.

With the number of users and development effort Microsoft’s Python Language Server has been enjoying and will probably continue to enjoy, it’s great knowing we can make use of this functionality in Emacs.

I am curious how well eglot, a smaller Emacs LSP package than lsp-mode, would do based on the integration above. (hint hint…)



Uploaded new version of emacs-lisp init code with two improvements:

  • Thanks to reddit user cyanj for the print_function import suggestion, and commenter Erik Hetzner below for the old-style string interpolation suggestion, this setup should now also work with Python 2, in addition to 3.
  • The ms-pyls database directory is now created as a subdirectory of the bindir.


So far, I have logged two issues, one with MS PyLS and one with lsp-mode, so that we can hopefully one day remove some of the work-arounds detailed above:

12 thoughts on “Configuring Emacs, lsp-mode and Microsoft’s Visual Studio Code Python language server.”

  1. In Emacs 26.1, there is project.el, which can find the project root without projectile or ffip: (cdr-safe (project-current)). I still think it’s worth installing projectile, but it’s always nice to remove a dep.

    1. Thank you very much for that suggestion!

      It would indeed be great to remove the projectile or ffip dependency. However, I could not get this working on a simple project with a .git in its root, a common use-case for ffip and projectile.

      In an IELM connected to a Python buffer visiting a file in a project dir, this happened:

      ELISP> default-directory
      ELISP> (project-current)

      Am I doing it wrong?

      1. Not sure. It returns nil if it can’t find a project. Just having a .git dir seems to be enough (it can even be empty). It needs vc to work, so you need ‘Git to be in vc-handled-backends. I use magit, but I still have ‘Git enabled for vc for diff-hl-mode.

        1. As I mentioned, the project I tested it on very definitely has a .git dir (it’s fully managed in git), and it is found by ffip.

          I just checked: my vc-handled-backends is indeed nil. No idea why, because I’ve always used magit.

          Anyways, it seems that using the built-in project.el *can* also be at least as complicated as using an additional package. 😉

          1. Magit is independent of vc. Sometimes it’s recommended to remove ‘Git from vc-handled-backends if you use magit, because presumably you’re using magit for all of your git needs and you don’t want vc doing extra stuff, slowing things down.

            Something in your config is setting vc-handled-backends, because the default includes ‘Git.

            1. Which is why I think I will continue suggesting that people install ffip or projectile for above-mentioned lsp setup to work: It’s easier than diagnosing why project.el is not working. 🙂

              1. Fair enough, but a fresh Emacs install will work. A user needs to do extra work (changing vc-handled-backends) to break it. If you installed a package that sets it to nil, I’d argue that package is broken.

  2. Thank you!!

    F-strings were introduced in python 3.6, so to support older versions it might be better to use:

    (defun get-python-ver-and-syspath (workspace-root)
    “Return list with pyver-string and json-encoded list of python search paths for python project at WORKSPACE-ROOT.”
    (let ((python (executable-find python-shell-interpreter))
    (ver “import sys; print(\”%s.%s\”%(sys.version_info[0], sys.version_info[1]));”)
    (sp (concat “import json; sys.path.insert(0, ‘” workspace-root “‘); print(json.dumps(sys.path))”)))
    (call-process python nil t nil “-c” (concat ver sp))
    (subseq (split-string (buffer-string) “\n”) 0 2))))

    1. Thank you very much for the suggestion, I have updated the post with new code that incorporates yours and another suggestion for full Python 2 compatibility.

    1. Wow, that’s amazing work, thank you very much for putting it together!

      I especially like the 10x cleaner way that you applied the nbsp filter.

      Only thing we’re missing now, is the hack to the lsp-mode source for rendering docs with lsp-ui-doc–extract-marked-string — however, I logged a bug about this — and was invited to submit a PR, so we might get that into the lsp-mode source eventually.

      I will just need to update this post to point people at your package rather.

  3. This was giving me a warning “Unknown method: python/languageServerStarted” whenever it started up. Here’s the fix (adding to the use-package config):

    (defun lsp-python–language-server-started-callback (workspace params)
    (lsp-workspace-status “::Started” workspace)
    (message “Python language server started”))

    (defun lsp-python–client-initialized (client)
    (lsp-client-on-notification client “python/languageServerStarted” ‘lsp-python–language-server-started-callback))

    (lsp-define-stdio-client lsp-python “python”
    `(“dotnet” ,(concat ms-pyls-dir “Microsoft.Python.LanguageServer.dll”))
    :extra-init-params #’ms-pyls-extra-init-params
    ;;Add this next line:
    :initialize ‘lsp-python–client-initialized)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.