The current (cultural) standard for indentation in Python is four space indent levels and indenting only with spaces, never tabs; this is what GNU Emacs' python mode defaults to and what YAPF and other code formatters use. Our new and updated Python 3 code is written in this official standard, as is some relatively recent Python 2 code. However, I spent a very long time writing Python code using 8-space indent levels and tab-based indentation, which means that I have a great deal of existing Python code in this style, including almost all of our existing Python 2 code at work and all of DWiki . For various reasons I don't want to reformat or reindent all of this code, so I want to work on existing code in its current style, whatever that is. Because Python 3 doesn't like it when you mix spaces and tabs , this should include the use of tabs in indentation.
My existing
.emacs
settings for this across various different systems were basically an inconsistent mess. On my desktop I was reflexively clinging to my old indentation style with various python mode settings; on our Ubuntu login servers, I'd stopped overriding the python mode defaults due to shifting toward the standard style , but that left me with the tab problem. Today, as part of dealing with my
.emacs
in general , I decided that I wanted to have the same
.emacs
everywhere, and that drove me to actively work out a solution.
First, I realized that if I was willing to really commit to shifting my indentation style to the standard on , the only real problem I had was with tabs. GNU Emacs's python mode will automatically detect the current indentation level for existing Python code, and for new files I'll use 4-space indents with spaces no matter what the other files existed in the project. For tabs, I want to continue using tabs if and only if the file is already in my old 8-space tab based indentation style, so the only problem is detecting this.
As far as I can tell there are no existing GNU Emacs features or functions to do this, so I wrote some ELisp to be run as a python-mode hook (which means it happens on a per-file basis). I won't claim it's very good ELisp, but here it is:
(defun cks/leading-tabs-p ()
"Detect if the current buffer has a line with leading tab(s)."
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(if (re-search-forward "^\t+" nil t)
t
nil))))
(add-hook 'python-mode-hook
(lambda ()
(if (and (= python-indent-offset 8) (cks/leading-tabs-p))
(setq indent-tabs-mode t))))
The detection of tab-indented lines here is highly imperfect and can be fooled by all sorts of things, but for my purposes it's good enough; misfires are unlikely in practice. I'm not sure I even have any Python code that uses 8-space indentation but without tabs.
(The start of cks/leading-tabs-p is copied directly from the python-mode function that scans the buffer to determine the indentation level it currently uses. The function naming is superstition based on what I've seen around the Internet.)
I also decided to write some ELisp functions to toggle back and forth between the modern style and my old style and to report the indentation state of a buffer:
(defun cks/python-toggle ()
"Toggle between old-style Python 2 and modern Python 3 settings."
(interactive)
(if (= python-indent-offset 8)
(progn (setq indent-tabs-mode nil) (setq python-indent-offset 4)
(message "Set to modern Python 3 (4-level spaces)"))
(progn (setq indent-tabs-mode t) (setq python-indent-offset 8)
(message "Set to ancient Python 2 with tabs"))))
(defun cks/rep-python ()
"Report the Python indentation status of the current buffer."
(interactive)
(message "Python indentation is %d-space indents with %s %s" python-indent-offset
(if (eq indent-tabs-mode t) "tabs" "spaces only")
(cond ((and (= python-indent-offset 4) (eq indent-tabs-mode nil))
"(Python 3 standard)")
((and (= python-indent-offset 8) (eq indent-tabs-mode t))
"(my Python 2 style)")
(t "(something weird)"))))
It's deliberate that after
cks/python-toggle
, I'm in one or the other of my standard indentation styles, even if the buffer started out in some weird style.
PS: Both python-indent-offset and indent-tabs-mode are buffer-local variables by the time I get my hands on them, so I can just directly use
setq
and so on. There may be a better way to do this these days, but my ELisp knowledge is old and rusty.