Apparently, elpy is the way to go when I want to do Python development in emacs. We configure it here.

Elpy automatically activates highlight-identation-mode, which is quite ugly in my opinion. It also uses flymake instead of flycheck by default. So let's only activate the modules we actually want.

(use-package elpy
  :ensure t
  :commands (elpy-enable elpy-disable)
  ;; autoformat on save
  (defun do.python/toggle-autoformat ()
    "Toggle yapf autoformat on save."
    (make-variable-buffer-local 'before-save-hook)
    (if (member #'elpy-yapf-fix-code before-save-hook)
        (progn (remove-hook 'before-save-hook #'elpy-yapf-fix-code)
               (message "Disabled yapf autoformat."))
      (add-hook 'before-save-hook #'elpy-yapf-fix-code)
      (message "Enabled yapf autoformat.")))
  (defun do.python/enable-autoformat ()
    "Enable yapf autoformat on save"
    (make-variable-buffer-local 'before-save-hook)
    (add-hook 'before-save-hook #'elpy-yapf-fix-code)
    (message "Enabled yapf autoformat."))
  (add-hook 'python-mode-hook #'do.python/enable-autoformat)

  (defun do.python/send-region-or-group (&optional arg)
    "Send the active region or the current group to the Python shell."
    (interactive "P")
    (if (use-region-p)
        (elpy-shell-send-region-or-buffer arg)
      (elpy-shell-send-group arg)))

  ;; set the RPC path early, so can change later
  (setq elpy-rpc-virtualenv-path 'system)
  (:map elpy-mode-map ("C-c C-c" . do.python/send-region-or-group))
  (setq elpy-rpc-backend "jedi")
  (setq elpy-modules (quote (elpy-module-company


On windows, I use the conda package manager. To make this work with elpy, I need some extra configuration, including the conda.el package.

(when on-windows
  (defvar ""
    "The path to the anaconda home directory.

Usually C:\\Users\\username\\anaconda3.")

  (defvar "emacs"
    "The name of the default conda env to use for emacs.

This cannot be 'base'. To make an env called 'emacs', do:

conda create --name emacs python=3.8")

  ;; we use conda.el to set up PATH and enable the rest of the user-specified "emacs" environment.
  (use-package conda
    :ensure t
    :after elpy
    (setq conda-anaconda-home
    (setq conda-env-home-directory
    (conda-env-activate "emacs")
    (let ((emacs-venv-path (concat (file-name-as-directory
      ;; make this work with elpy
      (setq elpy-rpc-virtualenv-path emacs-venv-path)
      (pyvenv-activate emacs-venv-path))))

Inferior python

The inferior python shell is set in my site file. Here I just set the hooks and enable font locking.

(add-hook 'inferior-python-mode-hook 'no-trailing-whitespace)
(add-hook 'inferior-python-mode-hook '(lambda () (setq-local ml-interactive? t)))

;; from
(advice-add 'elpy-shell--insert-and-font-lock
            :around (lambda (f string face &optional no-font-lock)
                      (if (not (eq face 'comint-highlight-input))
                          (funcall f string face no-font-lock)
                        (funcall f string face t)

(advice-add 'comint-send-input
            :around (lambda (f &rest args)
                      (if (eq major-mode 'inferior-python-mode)
                          (cl-letf ((g (symbol-function 'add-text-properties))
                                    ((symbol-function 'add-text-properties)
                                     (lambda (start end properties &optional object)
                                       (unless (eq (nth 3 properties) 'comint-highlight-input)
                                         (funcall g start end properties object)))))
                            (apply f args))
                        (apply f args))))

Toggle Python versions (Linux only)

The following functions let me toggle between python 2 and 3 environments. I only use this on my linux machine. Also, this is probably a huge hack and will be removed soon.

(unless on-windows
  (defvar do.python/python-version "3"
    "The current python version we are using.")
  (defun do.python/set-python-version (ver)
    "Set the current python version."
    (setq do.python/python-version ver)
    (setq python-shell-interpreter (concat "ipython" do.python/python-version))
    (setq elpy-rpc-python-command (concat "python" do.python/python-version))
    (setenv "WORKON_HOME" (concat (file-name-as-directory (expand-file-name "~/python_venv"))
                                  "py" do.python/python-version))
    (setenv "VIRTUALENVWRAPPER_PYTHON" (executable-find (concat "python" do.python/python-version)))
    (setenv "VIRTUALENVWRAPPER_VIRTUALENV" (concat "/usr/bin/virtualenv" do.python/python-version))
    (setenv "DO_PYTHON_VERSION" do.python/python-version)
  (defun use-python2 ()
    (do.python/set-python-version "2"))
  (defun use-python3 ()
    (do.python/set-python-version "3"))


When I activate a virtualenv, I also want to set the LD_LIBRARY_PATH variable (mainly for SWIG)

(unless on-windows
  (defun do.python/pyvenv-activate-and-set-ld-path (fun directory)
    "After activating the virtualenv DIRECTORY, set the environment variable LD_LIBRARY_PATH."
    (let ((ld_path (or (getenv "LD_LIBRARY_PATH") "")))
      (apply fun (list directory))
      (setq process-environment (append (list (concat "LD_LIBRARY_PATH="
                                                      ld_path ":"
                                                      directory "/lib"))
  (add-function :around (symbol-function #'pyvenv-activate)