diff --git a/python/Makefile b/python/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..e9d2877fb3a1cd297b1327b0b6a2a263f7df20dd
--- /dev/null
+++ b/python/Makefile
@@ -0,0 +1,3 @@
+headers: ../bsspeke.h
+	mkdir -p include
+	$(CPP) -I .. -o include/bsspeke.h ../bsspeke.h
diff --git a/python/bsspeke_build.py b/python/bsspeke_build.py
new file mode 100644
index 0000000000000000000000000000000000000000..93b941ffd4aa8dfbcd8ecf73cbc2553e791e1602
--- /dev/null
+++ b/python/bsspeke_build.py
@@ -0,0 +1,35 @@
+from cffi import FFI
+import subprocess
+
+
+ffibuilder = FFI()
+
+compile_args = ["-I.."]
+link_args = ["-L.."]
+
+# cvw: Borrowed this trick from libolm :)
+headers_build = subprocess.Popen("make headers", shell=True)
+headers_build.wait()
+
+# cdef() expects a single string declaring the C types, functions and
+# globals needed to use the shared object. It must be in valid C syntax.
+#ffibuilder.cdef("""
+#    float pi_approx(int n);
+#""")
+with open("include/bsspeke.h") as f:
+    ffibuilder.cdef(f.read())
+
+# set_source() gives the name of the python extension module to
+# produce, and some C source code as a string.  This C code needs
+# to make the declarated functions, types and globals available,
+# so it is often just the "#include".
+ffibuilder.set_source("_bsspeke_cffi",
+"""
+     #include "bsspeke.h"   // the C header of the library
+""",
+     libraries=['bsspeke'],   # library name, for the linker
+     extra_compile_args=compile_args,
+     extra_link_args=link_args)
+
+if __name__ == "__main__":
+    ffibuilder.compile(verbose=True)
diff --git a/python/demo.py b/python/demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..0cfdb06eed52a35b64db30dd0f12189ed221a680
--- /dev/null
+++ b/python/demo.py
@@ -0,0 +1,49 @@
+#!/bin/env python3
+
+from _bsspeke_cffi import ffi, lib
+
+def bsspeke_setup(user):
+    client = ffi.new("bsspeke_client_ctx *")
+    lib.bsspeke_client_init(client, b"@alice:example.com", 18, b"example.com", 10, b"P@ssword1", 9)
+
+    server = ffi.new("bsspeke_server_ctx *")
+    lib.bsspeke_server_init(server, b"example.com", 10)
+
+    msg1 = ffi.new("bsspeke_setup_msg1_t *")
+    msg2 = ffi.new("bsspeke_setup_msg2_t *")
+    msg3 = ffi.new("bsspeke_setup_msg3_t *")
+
+    lib.bsspeke_client_setup_generate_message1(msg1, client)
+    lib.bsspeke_server_setup_generate_message2(msg2, msg1, user, server)
+    lib.bsspeke_client_setup_generate_message3(msg3, msg2, 100000, 3, client)
+    lib.bsspeke_server_setup_process_message3(msg3, user, server)
+
+
+def bsspeke_login(user):
+    client = ffi.new("bsspeke_client_ctx *")
+    lib.bsspeke_client_init(client, b"@alice:example.com", 18, b"example.com", 10, b"P@ssword1", 9)
+
+    server = ffi.new("bsspeke_server_ctx *")
+    lib.bsspeke_server_init(server, b"example.com", 10)
+
+    msg1 = ffi.new("bsspeke_login_msg1_t *")
+    msg2 = ffi.new("bsspeke_login_msg2_t *")
+    msg3 = ffi.new("bsspeke_login_msg3_t *")
+    msg4 = ffi.new("bsspeke_login_msg4_t *")
+
+    lib.bsspeke_client_login_generate_message1(msg1, client)
+    lib.bsspeke_server_login_generate_message2(msg2, msg1, user, server)
+    lib.bsspeke_client_login_generate_message3(msg3, msg2, client)
+    lib.bsspeke_server_login_generate_message4(msg4, msg3, user, server)
+    lib.bsspeke_client_login_verify_message4(msg4, client)
+
+
+if __name__ == "__main__":
+    print("Creating user info object to represent the server's long-term storage")
+    user = ffi.new("bsspeke_user_info_t *")
+
+    print("\n\nStarting setup...")
+    bsspeke_setup(user)
+
+    print("\n\nRunning login...")
+    bsspeke_login(user)