diff --git a/roles/upload-pypi/README.rst b/roles/upload-pypi/README.rst
new file mode 100644
index 000000000..819a21ceb
--- /dev/null
+++ b/roles/upload-pypi/README.rst
@@ -0,0 +1,32 @@
+Upload python packages to PyPI
+
+**Role Variables**
+
+.. zuul:rolevar:: pypi_info
+
+   Complex argument which contains the information about the PyPI
+   server as well as the authentication information needed. It is
+   expected that this argument comes from a `Secret`.
+
+  .. zuul:rolevar:: username
+
+     Username to use to log in to PyPI.
+
+  .. zuul:rolevar:: password
+
+     Password to use to log in to PyPI.
+
+  .. zuul:rolevar:: repository
+     :default: pypi
+
+     Name of the repository to upload to
+
+  .. zuul:rolevar:: repository_url
+     :default: https://pypi.python.org/pypi
+
+     URL of the PyPI repostory
+
+.. zuul:rolevar:: pypi_path
+   :default: src/{{ zuul.project.canonical_name }}/dist
+
+   Path containing artifacts to upload.
diff --git a/roles/upload-pypi/defaults/main.yaml b/roles/upload-pypi/defaults/main.yaml
new file mode 100644
index 000000000..da44daffa
--- /dev/null
+++ b/roles/upload-pypi/defaults/main.yaml
@@ -0,0 +1,4 @@
+---
+pypi_path: "src/{{ zuul.project.canonical_name }}/dist"
+pypi_repository: "{{ pypi_info.repository|default('pypi') }}"
+pypi_repository_url: "{{ pypi_info.repository_url|default('https://pypi.python.org/pypi') }}"
diff --git a/roles/upload-pypi/tasks/main.yaml b/roles/upload-pypi/tasks/main.yaml
new file mode 100644
index 000000000..e73a62e42
--- /dev/null
+++ b/roles/upload-pypi/tasks/main.yaml
@@ -0,0 +1,36 @@
+- name: Check for twine install
+  command: which twine
+  ignore_errors: yes
+  register: twine_command_which
+
+- name: Ensure twine is installed
+  command: pip install --user twine
+  when: not twine_command_which|succeeded
+
+- name: Install .pypirc configuration file
+  template:
+    dest: ~/.pypirc
+    mode: 0400
+    src: .pypirc.j2
+
+- name: Find wheels to upload
+  find:
+    paths: "{{ pypi_path }}"
+    patterns: "*.whl"
+  register: found_wheels
+
+- name: Upload wheel with twine before tarballs
+  command: "twine upload -r {{ pypi_repository }} {{ item }}"
+  with_items: "{{ found_wheels.files }}"
+  when: found_wheels.matched|bool
+
+- name: Find tarballs to upload
+  find:
+    paths: "{{ pypi_path }}"
+    patterns: "*.tar.gz"
+  register: found_tarballs
+
+- name: Upload tarballs with twine
+  command: "twine upload -r {{ pypi_repository }} {{ item }}"
+  with_items: "{{ found_tarballs.files }}"
+  when: found_tarballs.matched|bool
diff --git a/roles/upload-pypi/templates/.pypirc.j2 b/roles/upload-pypi/templates/.pypirc.j2
new file mode 100644
index 000000000..3cc8ab2a2
--- /dev/null
+++ b/roles/upload-pypi/templates/.pypirc.j2
@@ -0,0 +1,8 @@
+[distutils]
+index-servers=
+  {{ pypi_repository }}
+
+[{{ pypi_repository }}]
+repository:{{ pypi_repository_url }}
+username:{{ pypi_info.username }}
+password:{{ pypi_info.password }}