Python indentation

The subject of Python source code indentation can be a contentious one. The Python style guide (PEP8) suggests that using four spaces per indentation level is the proper was to indent Python code. However, I have discovered I prefer to use tabs instead of spaces. Could I customize my text editor of choice, Vim, to edit using tabs and save to disk using spaces? Doing so would allow me to adhere to PEP8 when exchanging source code files with other programmers.

Rendering tabs in Vim

Vim can be configured to display a unique, visible, character on screen at the position of each tab character. I have this set to a unicode, vertical bar character. This makes indentation errors easy to see, and indentation easier to follow over large code blocks.

vim tab rendering example

In my case I chose a nice vertical bar U+2502 from the box drawing set of characters (https://en.wikipedia.org/wiki/Box_Drawing). In order to activate this feature, the vim list configuration options are added to the .vimrc configuration file.

set list
set listchars=tab:│\ ,eol:¬,extends:❯,precedes:\<

Listchars takes two characters for tab, so an escaped white space character is appended to the unicode character. I have also defined characters to show end-of-line markers and vim-specific line continuation markers extends and precedes.

On-the-fly re-indentation

Now tab characters will be rendered in the editor window, but there is still a problem. Some source files use spaces for indentation, not tabs. Any Python source file that uses spaces for indentation will look like this:

vim Python code with spaces

However, Vim autocommands can be used to replace spaces with tabs when reading in a Python source file, and reverse the operation when writing it back to disk. This can be achieved by defining two Vim functions and some autocommands in the .vimrc configuration file.

function! PyExpandTabs()
        silent setlocal expandtab
        silent %retab!
endfunction

function! PyContractTabs()
        silent setlocal noexpandtab
        silent %retab!
endfunction

augroup python_files
        autocmd!
        autocmd FileType python setlocal noexpandtab
        autocmd FileType python set tabstop=4
        autocmd FileType python set shiftwidth=4
        autocmd BufWritePre *.py :call PyExpandTabs()
        autocmd BufWritePost *.py :call PyContractTabs()
        autocmd BufReadPost *.py :call PyContractTabs()
augroup END

The first 3 autocmd statements sets up using tabs, equivalent to 4 spaces, when editing a Python source file. The last 3 statements trigger upon reading and writing a Python source file to disk. BufWritePre calls the user-defined Vim function PyExpandTabs() just before writing an edited buffer to disk. The PyExpandTabs() function replaces all the tabs in the source file being edited with spaces. BufWritePost triggers after that file has been written to disk to reverse the change and replace the spaces with tabs to allow us to continue editing with our favorite indentation method. BufReadPost also calls PyContractTabs() right after a file has been loaded into a Vim edit buffer, but before passing control to the user. PyContractTabs() replaces leading spaces with tabs.