diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..ca3ca1e0e201a93c23ac2e1348eb6c9f0b2e1486
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+mumpy/_static_version.py export-subst
\ No newline at end of file
diff --git a/mumpy/__init__.py b/mumpy/__init__.py
index 195f661b7a95138866667f852ac0c3d8adf9f011..6211b3078fc910fa6781d9d1a9b442accfc2e710 100644
--- a/mumpy/__init__.py
+++ b/mumpy/__init__.py
@@ -1 +1,2 @@
 from .mumpy import *
+from ._version import __version__
diff --git a/mumpy/_static_version.py b/mumpy/_static_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..5557f9b53452d608e449425b75c13912d54f4118
--- /dev/null
+++ b/mumpy/_static_version.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# This file is part of 'miniver': https://github.com/jbweston/miniver
+#
+# This file will be overwritten by setup.py when a source or binary
+# distribution is made.  The magic value "__use_git__" is interpreted by
+# version.py.
+
+version = "__use_git__"
+
+# These values are only set if the distribution was created with 'git archive'
+refnames = "$Format:%D$"
+git_hash = "$Format:%h$"
diff --git a/mumpy/_version.py b/mumpy/_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..5319eaf83b3d8e820e4ee9ad0d0523fe871e71a9
--- /dev/null
+++ b/mumpy/_version.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+# This file is part of 'miniver': https://github.com/jbweston/miniver
+#
+from collections import namedtuple
+import os
+import subprocess
+
+from distutils.command.build_py import build_py as build_py_orig
+from setuptools.command.sdist import sdist as sdist_orig
+
+Version = namedtuple('Version', ('release', 'dev', 'labels'))
+
+# No public API
+__all__ = []
+
+package_root = os.path.dirname(os.path.realpath(__file__))
+package_name = os.path.basename(package_root)
+distr_root = os.path.dirname(package_root)
+
+STATIC_VERSION_FILE = '_static_version.py'
+
+
+def get_version(version_file=STATIC_VERSION_FILE):
+    version_info = {}
+    with open(os.path.join(package_root, version_file), 'rb') as f:
+        exec(f.read(), {}, version_info)
+    version = version_info['version']
+    if version == "__use_git__":
+        version = get_version_from_git()
+        if not version:
+            version = get_version_from_git_archive(version_info)
+        if not version:
+            version = Version("unknown", None, None)
+        return pep440_format(version)
+    else:
+        return version
+
+
+def pep440_format(version_info):
+    release, dev, labels = version_info
+
+    version_parts = [release]
+    if dev:
+        if release.endswith('-dev') or release.endswith('.dev'):
+            version_parts.append(dev)
+        else:  # prefer PEP440 over stric adhesion to semver
+            version_parts.append('.dev{}'.format(dev))
+
+    if labels:
+        version_parts.append('+')
+        version_parts.append(".".join(labels))
+
+    return "".join(version_parts)
+
+
+def get_version_from_git():
+    try:
+        p = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'],
+                             cwd=distr_root,
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    except OSError:
+        return
+    if p.wait() != 0:
+        return
+    if not os.path.samefile(p.communicate()[0].decode().rstrip('\n'),
+                            distr_root):
+        # The top-level directory of the current Git repository is not the same
+        # as the root directory of the distribution: do not extract the
+        # version from Git.
+        return
+
+    # git describe --first-parent does not take into account tags from branches
+    # that were merged-in. The '--long' flag gets us the 'dev' version and
+    # git hash, '--always' returns the git hash even if there are no tags.
+    for opts in [['--first-parent'], []]:
+        try:
+            p = subprocess.Popen(
+                ['git', 'describe', '--long', '--always'] + opts,
+                cwd=distr_root,
+                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        except OSError:
+            return
+        if p.wait() == 0:
+            break
+    else:
+        return
+
+    description = (p.communicate()[0]
+        .decode()
+        .strip('v')  # Tags can have a leading 'v', but the version should not
+        .rstrip('\n')
+        .rsplit('-', 2))  # Split the latest tag, commits since tag, and hash
+
+    try:
+        release, dev, git = description
+    except ValueError:  # No tags, only the git hash
+        # prepend 'g' to match with format returned by 'git describe'
+        git = 'g{}'.format(*description)
+        release = 'unknown'
+        dev = None
+
+    labels = []
+    if dev == "0":
+        dev = None
+    else:
+        labels.append(git)
+
+    try:
+        p = subprocess.Popen(['git', 'diff', '--quiet'], cwd=distr_root)
+    except OSError:
+        labels.append('confused')  # This should never happen.
+    else:
+        if p.wait() == 1:
+            labels.append('dirty')
+
+    return Version(release, dev, labels)
+
+
+# TODO: change this logic when there is a git pretty-format
+#       that gives the same output as 'git describe'.
+#       Currently we can only tell the tag the current commit is
+#       pointing to, or its hash (with no version info)
+#       if it is not tagged.
+def get_version_from_git_archive(version_info):
+    try:
+        refnames = version_info['refnames']
+        git_hash = version_info['git_hash']
+    except KeyError:
+        # These fields are not present if we are running from an sdist.
+        # Execution should never reach here, though
+        return None
+
+    if git_hash.startswith('$Format') or refnames.startswith('$Format'):
+        # variables not expanded during 'git archive'
+        return None
+
+    VTAG = 'tag: v'
+    refs = set(r.strip() for r in refnames.split(","))
+    version_tags = set(r[len(VTAG):] for r in refs if r.startswith(VTAG))
+    if version_tags:
+        release, *_ = sorted(version_tags)  # prefer e.g. "2.0" over "2.0rc1"
+        return Version(release, dev=None, labels=None)
+    else:
+        return Version('unknown', dev=None, labels=['g{}'.format(git_hash)])
+
+
+__version__ = get_version()
+
+
+# The following section defines a module global 'cmdclass',
+# which can be used from setup.py. The 'package_name' and
+# '__version__' module globals are used (but not modified).
+
+def _write_version(fname):
+    # This could be a hard link, so try to delete it first.  Is there any way
+    # to do this atomically together with opening?
+    try:
+        os.remove(fname)
+    except OSError:
+        pass
+    with open(fname, 'w') as f:
+        f.write("# This file has been created by setup.py.\n"
+                "version = '{}'\n".format(__version__))
+
+
+class _build_py(build_py_orig):
+    def run(self):
+        super().run()
+        _write_version(os.path.join(self.build_lib, package_name,
+                                    STATIC_VERSION_FILE))
+
+
+class _sdist(sdist_orig):
+    def make_release_tree(self, base_dir, files):
+        super().make_release_tree(base_dir, files)
+        _write_version(os.path.join(base_dir, package_name,
+                                    STATIC_VERSION_FILE))
+
+
+cmdclass = dict(sdist=_sdist, build_py=_build_py)
diff --git a/setup.py b/setup.py
index 6937826cea143e19bb902fca4f956a33eace6674..bde7324cf79333421eacf9cde4199b7b701a6885 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,6 @@ import configparser
 import collections
 from setuptools import setup, find_packages, Extension
 from distutils.command.build import build
-from setuptools.command.sdist import sdist
 from setuptools.command.build_ext import build_ext
 
 import jinja2 as j2
@@ -184,6 +183,17 @@ def configure_special_extensions(exts, build_summary):
     return exts
 
 
+# Loads version.py module without importing the whole package.
+def get_version_and_cmdclass(package_path):
+    import os
+    from importlib.util import module_from_spec, spec_from_file_location
+    spec = spec_from_file_location('version',
+                                   os.path.join(package_path, '_version.py'))
+    module = module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module.__version__, module.cmdclass
+
+
 def main():
     mumps = {'mumpy.mumps':
              dict(sources=['mumpy/mumps.pyx'],
@@ -215,6 +225,8 @@ def main():
                           language_level=3,
                           compiler_directives={'linetrace': True})
 
+    version, cmdclass = get_version_and_cmdclass('mumpy')
+
     classifiers = """\
         Development Status :: 3 - Alpha
         Intended Audience :: Science/Research
@@ -227,7 +239,7 @@ def main():
         Operating System :: Microsoft :: Windows"""
 
     setup(name='mumpy',
-          version='0.1.0',
+          version=version,
           author='mumpy authors',
           author_email='authors@kwant-project.org',
           description=("Python bindings for MUMPS "),
@@ -235,9 +247,11 @@ def main():
           url="https://gitlab.kwant-project.org/kwant/mumpy",
           license="BSD",
           packages=find_packages('.'),
-          cmdclass={'build': build,
-                    'sdist': sdist,
-                    'build_ext': build_ext},
+          cmdclass={
+            'build': build,
+            'build_ext': build_ext,
+            **cmdclass,
+          },
           ext_modules=mumps,
           install_requires=['numpy'],
           classifiers=[c.strip() for c in classifiers.split('\n')])