diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7c07058b63ae6bde2d6562aa6933ad5ef7871f10..2025842631711bc383e175e4cb62ba3cab8674b3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,3 +39,14 @@ Flake8:
   - python3.5
   except:
   - tags
+
+Documentation:
+  script:
+  - EXTRA_INSTALL="numpy"
+  - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-docs.sh
+  - ". ./build-docs.sh"
+  tags:
+  - python3.5
+  only:
+  - master
+
diff --git a/README b/README
deleted file mode 100644
index f4ef45f7edb2b80a7cd3f1edfb14b718c8c53115..0000000000000000000000000000000000000000
--- a/README
+++ /dev/null
@@ -1,6 +0,0 @@
-Miscellaneous Python lifesavers.
-
-Andreas Kloeckner <inform@tiker.net>
-
-Includes Michele Simionato's decorator module, from
-http://www.phyast.pitt.edu/~micheles/python/
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b8c9ed9cef0063d797a0a181bb91520affdda3c3
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,18 @@
+Pytools is a big bag of things that are "missing" from the Python standard
+library. This is mainly a dependency of my other software packages, and is
+probably of little interest to you unless you use those. If you're curious
+nonetheless, here's what's on offer:
+
+* A ton of small tool functions such as `len_iterable`, `argmin`,
+  tuple generation, permutation generation, ASCII table pretty printing,
+  GvR's mokeypatch_xxx() hack, the elusive `flatten`, and much more.
+* Michele Simionato's decorator module
+* A time-series logging module, `pytools.log`.
+* Batch job submission, `pytools.batchjob`.
+* A lexer, `pytools.lex`.
+
+Links:
+
+* `Documentation <https://documen.tician.de/pytools>`_
+
+* `Github <https://github.com/inducer/pytools>`_
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e35d8850c9688b1ce82711694692cc574a799396
--- /dev/null
+++ b/doc/.gitignore
@@ -0,0 +1 @@
+_build
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..195e17ce0a2958904630a731b51ecfe4204d129c
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+SPHINXPROJ    = pytools
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..c87091c3ef34558f6bc5d70e82fd3f09a25bac0a
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# pytools documentation build configuration file, created by
+# sphinx-quickstart on Wed Jun 14 16:28:43 2017.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.mathjax',
+    'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'pytools'
+copyright = '2017, Andreas Kloeckner'
+author = 'Andreas Kloeckner'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+ver_dic = {}
+exec(compile(open("../pytools/version.py").read(), "../pytools/version.py", 'exec'), ver_dic)
+version = ".".join(str(x) for x in ver_dic["VERSION"])
+release = ver_dic["VERSION_TEXT"]
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+html_theme_options = {
+        "extra_nav_links": {
+            "🚀 Github": "https://github.com/inducer/pytools",
+            "💾 Download Releases": "https://pypi.python.org/pypi/pytools",
+            }
+        }
+
+html_sidebars = {
+    '**': [
+        'about.html',
+        'navigation.html',
+        'relations.html',
+        'searchbox.html',
+    ]
+}
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'pytoolsdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'pytools.tex', 'pytools Documentation',
+     'Andreas Kloeckner', 'manual'),
+]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'pytools', 'pytools Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'pytools', 'pytools Documentation',
+     author, 'pytools', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+
+
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..fb3dedd0fa7e8e984ef8c06d935d9893819fc6c8
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,18 @@
+Welcome to pytools's documentation!
+===================================
+
+.. toctree::
+    :maxdepth: 2
+    :caption: Contents:
+
+    reference
+    obj_array
+    persistent_dict
+    misc
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/doc/misc.rst b/doc/misc.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4255ef6661a4d2b8139a4ac0659bb277a9d8a3fb
--- /dev/null
+++ b/doc/misc.rst
@@ -0,0 +1,59 @@
+Installation
+============
+
+This command should install :mod:`pytools`::
+
+    pip install pytools
+
+You may need to run this with :command:`sudo`.
+If you don't already have `pip <https://pypi.python.org/pypi/pip>`_,
+run this beforehand::
+
+    curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py
+    python get-pip.py
+
+For a more manual installation, download the source, unpack it,
+and say::
+
+    python setup.py install
+
+User-visible changes
+====================
+
+Version 2017.4
+--------------
+
+.. note::
+
+    This version is currently under development. You can get snapshots from
+    Pytools's `git repository <https://github.com/inducer/pytools>`_
+
+.. _license:
+
+License
+=======
+
+:mod:`pytools` is licensed to you under the MIT/X Consortium license:
+
+Copyright (c) 2008-17 Andreas Klöckner
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/doc/obj_array.rst b/doc/obj_array.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d7a8c46ce994e7027502c6dc5ddf101f5381a046
--- /dev/null
+++ b/doc/obj_array.rst
@@ -0,0 +1 @@
+.. automodule:: pytools.obj_array
diff --git a/doc/persistent_dict.rst b/doc/persistent_dict.rst
new file mode 100644
index 0000000000000000000000000000000000000000..7d10dc1ef3cbd969622479688690f3a600f4b3b7
--- /dev/null
+++ b/doc/persistent_dict.rst
@@ -0,0 +1 @@
+.. automodule:: pytools.persistent_dict
diff --git a/doc/reference.rst b/doc/reference.rst
new file mode 100644
index 0000000000000000000000000000000000000000..fce823adf86e6e9cee927f495da807d75e6e9c1b
--- /dev/null
+++ b/doc/reference.rst
@@ -0,0 +1 @@
+.. automodule:: pytools
diff --git a/doc/upload-docs.sh b/doc/upload-docs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ef523e618ee92bf38fef33d8beae7015d132e58b
--- /dev/null
+++ b/doc/upload-docs.sh
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+rsync --verbose --archive --delete _build/html/* doc-upload:doc/pytools
diff --git a/pytools/__init__.py b/pytools/__init__.py
index 0920e46f2d40dc8e7c25f9110eb13d773abeea20..e5cfa3628fae819ab50ffda5ee861ff0f30f279a 100644
--- a/pytools/__init__.py
+++ b/pytools/__init__.py
@@ -41,6 +41,91 @@ except ImportError:
 else:
     my_decorator = decorator_module.decorator
 
+__doc__ = """
+A Collection of Utilities
+=========================
+
+Math
+----
+
+.. autofunction:: levi_civita
+.. autofunction:: perm
+.. autofunction:: comb
+
+Assertive accessors
+-------------------
+
+.. autofunction:: one
+.. autofunction:: is_single_valued
+.. autofunction:: all_roughly_equal
+.. autofunction:: single_valued
+
+Memoization
+-----------
+
+.. autofunction:: memoize
+.. autofunction:: memoize_on_first_arg
+.. autofunction:: memoize_method
+.. autofunction:: memoize_method_with_uncached
+.. autofunction:: memoize_in
+
+Argmin/max
+----------
+
+.. autofunction:: argmin2
+.. autofunction:: argmax2
+.. autofunction:: argmin
+.. autofunction:: argmax
+
+Cartesian products
+------------------
+.. autofunction:: cartesian_product
+.. autofunction:: distinct_pairs
+
+Permutations, Tuples, Integer sequences
+---------------------------------------
+
+.. autofunction:: wandering_element
+.. autofunction:: indices_in_shape
+.. autofunction:: generate_nonnegative_integer_tuples_below
+.. autofunction:: generate_nonnegative_integer_tuples_summing_to_at_most
+.. autofunction:: generate_all_nonnegative_integer_tuples
+.. autofunction:: generate_all_integer_tuples_below
+.. autofunction:: generate_all_integer_tuples
+.. autofunction:: generate_permutations
+.. autofunction:: generate_unique_permutations
+
+Graph Algorithms
+----------------
+
+.. autofunction:: a_star
+
+Formatting
+----------
+
+.. autoclass:: Table
+.. autofunction:: string_histogram
+.. autofunction:: word_wrap
+
+Debugging
+---------
+
+.. autofunction:: typedump
+.. autofunction:: invoke_editor
+
+Progress bars
+-------------
+
+.. autoclass:: ProgressBar
+
+Name generation
+---------------
+
+.. autofunction:: generate_unique_names
+.. autofunction:: generate_numbered_unique_names
+.. autofunction:: UniqueNameGenerator
+"""
+
 
 # {{{ math --------------------------------------------------------------------
 
@@ -1163,7 +1248,7 @@ def generate_all_integer_tuples(length, least_abs=0):
 
 
 def generate_permutations(original):
-    """Generate all permutations of the list `original'.
+    """Generate all permutations of the list *original*.
 
     Nicked from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252178
     """
@@ -1177,7 +1262,7 @@ def generate_permutations(original):
 
 
 def generate_unique_permutations(original):
-    """Generate all unique permutations of the list `original'.
+    """Generate all unique permutations of the list *original*.
     """
 
     had_those = set()
@@ -1314,7 +1399,13 @@ def a_star(initial_state, goal_state, neighbor_map,
 # {{{ table formatting
 
 class Table:
-    """An ASCII table generator."""
+    """An ASCII table generator.
+
+    .. automethod:: add_row
+    .. automethod:: __str__
+    .. automethod:: latex
+    """
+
     def __init__(self):
         self.rows = []
 
@@ -1402,10 +1493,10 @@ def string_histogram(iterable, min_value=None, max_value=None,
 
 def word_wrap(text, width, wrap_using="\n"):
     # http://code.activestate.com/recipes/148061-one-liner-word-wrap-function/
-    """
+    r"""
     A word-wrap function that preserves existing line breaks
     and most spaces in the text. Expects that existing line
-    breaks are posix newlines (\n).
+    breaks are posix newlines (``\n``).
     """
     space_or_break = [" ", wrap_using]
     return reduce(lambda line, word, width=width: '%s%s%s' %
@@ -1601,6 +1692,14 @@ def invoke_editor(s, filename="edit.txt", descr="the file"):
 # {{{ progress bars
 
 class ProgressBar:
+    """
+    .. automethod:: draw
+    .. automethod:: progress
+    .. automethod:: set_progress
+    .. automethod:: finished
+    .. automethod:: __enter__
+    .. automethod:: __exit__
+    """
     def __init__(self, descr, total, initial=0, length=40):
         import time
         self.description = descr
@@ -1767,6 +1866,12 @@ generate_unique_possibilities = MovedFunctionDeprecationWrapper(
 
 
 class UniqueNameGenerator(object):
+    """
+    .. automethod:: is_name_conflicting
+    .. automethod:: add_name
+    .. automethod:: add_names
+    .. automethod:: __call__
+    """
     def __init__(self, existing_names=set(), forced_prefix=""):
         self.existing_names = existing_names.copy()
         self.forced_prefix = forced_prefix
diff --git a/pytools/obj_array.py b/pytools/obj_array.py
index e425e70a5c6e1f697865239f5187625f65ccd360..f3310e45c57d0540470eed551aeeacc3b61f400d 100644
--- a/pytools/obj_array.py
+++ b/pytools/obj_array.py
@@ -2,6 +2,28 @@ from __future__ import absolute_import, division
 import numpy as np
 from pytools import my_decorator as decorator, MovedFunctionDeprecationWrapper
 
+__doc__ = """
+Handling :mod:`numpy` Object Arrays
+===================================
+
+.. automethod:: oarray_real
+.. automethod:: oarray_imag
+.. automethod:: oarray_real_copy
+.. automethod:: oarray_imag_copy
+
+Creation
+--------
+
+.. automethod:: join_fields
+.. automethod:: make_obj_array
+
+Mapping
+-------
+
+.. automethod:: with_object_array_or_scalar
+.. automethod:: with_object_array_or_scalar_n_args
+"""
+
 
 def gen_len(expr):
     from pytools.obj_array import is_obj_array
diff --git a/pytools/persistent_dict.py b/pytools/persistent_dict.py
index 0a9fe6cdf2e942299d9dfa61fc2aab9b84807074..3fe69482955889499083d459dac68c995470e6a7 100644
--- a/pytools/persistent_dict.py
+++ b/pytools/persistent_dict.py
@@ -1,8 +1,6 @@
 """Generic persistent, concurrent dictionary-like facility."""
 
-from __future__ import division, with_statement
-from __future__ import absolute_import
-import six
+from __future__ import division, with_statement, absolute_import
 
 __copyright__ = "Copyright (C) 2011,2014 Andreas Kloeckner"
 
@@ -30,10 +28,23 @@ import logging
 logger = logging.getLogger(__name__)
 
 
+import six
 import sys
 import os
 import errno
 
+__doc__ = """
+Persistent Hashing
+==================
+
+This module contains functionality that allows hashing with keys that remain
+valid across interpreter invocations, unlike Python's built-in hashes.
+
+.. autoexception:: NoSuchEntryError
+.. autoclass:: KeyBuilder
+.. autoclass:: PersistentDict
+"""
+
 try:
     import hashlib
     new_hash = hashlib.sha256
@@ -269,6 +280,11 @@ class PersistentDict(object):
         :arg identifier: a file-name-compatible string identifying this
             dictionary
         :arg key_builder: a subclass of :class:`KeyBuilder`
+
+        .. automethod:: __getitem__
+        .. automethod:: __setitem__
+        .. automethod:: __delitem__
+        .. automethod:: clear
         """
 
         self.identifier = identifier
diff --git a/setup.py b/setup.py
index ce1838c6a50a7af833f9d2c6baf6235d03f1deb1..42b70c7613564c58f1d99acb3d3cdac6d4587d1a 100644
--- a/setup.py
+++ b/setup.py
@@ -15,20 +15,7 @@ exec(compile(version_file_contents, "pytools/version.py", 'exec'), ver_dic)
 setup(name="pytools",
       version=ver_dic["VERSION_TEXT"],
       description="A collection of tools for Python",
-      long_description="""
-      Pytools is a big bag of things that are "missing" from the Python standard
-      library. This is mainly a dependency of my other software packages, and is
-      probably of little interest to you unless you use those. If you're curious
-      nonetheless, here's what's on offer:
-
-      * A ton of small tool functions such as `len_iterable`, `argmin`,
-        tuple generation, permutation generation, ASCII table pretty printing,
-        GvR's mokeypatch_xxx() hack, the elusive `flatten`, and much more.
-      * Michele Simionato's decorator module
-      * A time-series logging module, `pytools.log`.
-      * Batch job submission, `pytools.batchjob`.
-      * A lexer, `pytools.lex`.
-      """,
+      long_description=open("README.rst", "r").read(),
       classifiers=[
           'Development Status :: 4 - Beta',
           'Intended Audience :: Developers',