diff --git a/cmd/HocusPocus.hs b/cmd/HocusPocus.hs
--- a/cmd/HocusPocus.hs
+++ b/cmd/HocusPocus.hs
@@ -4,7 +4,8 @@
 -- [magic-wormhole](https://github.com/warner/magic-wormhole).
 module Main (main) where
 
-import Protolude
+import Protolude hiding (toS)
+import Protolude.Conv (toS)
 
 import qualified Options.Applicative as Opt
 
diff --git a/magic-wormhole.cabal b/magic-wormhole.cabal
--- a/magic-wormhole.cabal
+++ b/magic-wormhole.cabal
@@ -1,11 +1,13 @@
--- This file has been generated from package.yaml by hpack version 0.28.2.
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.2.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 4734e9879f3640e17bc21f36a81592698c5c136e6046390958f24fbf41c1b0d7
+-- hash: c4c1ddfbe11ba22263ee25a7aa801d30f3177d8fcf3835d05597dc6fbf6eec70
 
 name:           magic-wormhole
-version:        0.3.3
+version:        0.3.4
 synopsis:       Interact with Magic Wormhole
 description:    Magic Wormhole is a scheme to get things from one computer to another,
                 safely.
@@ -21,7 +23,6 @@
 license:        Apache
 license-file:   LICENSE
 build-type:     Simple
-cabal-version:  >= 1.10
 data-files:
     tests/python/derive_phase_key.py
     tests/python/nacl_exchange.py
@@ -48,9 +49,9 @@
     , network
     , network-uri
     , pqueue
-    , protolude >=0.2
+    , protolude >=0.3.0 && <0.4
     , saltine
-    , spake2 >=0.4
+    , spake2 >=0.4.3
     , stm
     , unordered-containers
     , websockets >=0.8.0.0
@@ -82,8 +83,8 @@
     , base >=4.6 && <5
     , magic-wormhole
     , optparse-applicative
-    , protolude >=0.2
-    , spake2 >=0.4
+    , protolude >=0.3.0 && <0.4
+    , spake2 >=0.4.3
     , text
   default-language: Haskell2010
 
@@ -102,9 +103,9 @@
     , magic-wormhole
     , memory
     , process
-    , protolude >=0.2
+    , protolude >=0.3.0 && <0.4
     , saltine
-    , spake2 >=0.3
+    , spake2 >=0.4.3
     , stm
     , tasty
     , tasty-hedgehog >=0.2 && <1.0
diff --git a/src/MagicWormhole/Internal/ClientProtocol.hs b/src/MagicWormhole/Internal/ClientProtocol.hs
--- a/src/MagicWormhole/Internal/ClientProtocol.hs
+++ b/src/MagicWormhole/Internal/ClientProtocol.hs
@@ -22,7 +22,8 @@
   , phasePurpose
   ) where
 
-import Protolude hiding (phase)
+import Protolude hiding (phase, toS)
+import Protolude.Conv (toS)
 
 import Crypto.Hash (SHA256(..), hashWith)
 import qualified Crypto.KDF.HKDF as HKDF
diff --git a/src/MagicWormhole/Internal/Messages.hs b/src/MagicWormhole/Internal/Messages.hs
--- a/src/MagicWormhole/Internal/Messages.hs
+++ b/src/MagicWormhole/Internal/Messages.hs
@@ -18,7 +18,8 @@
   , Mood(..)
   ) where
 
-import Protolude
+import Protolude hiding (toS)
+import Protolude.Conv (toS)
 
 import Control.Monad (fail)
 import Crypto.Random (MonadRandom(..))
diff --git a/src/MagicWormhole/Internal/Pake.hs b/src/MagicWormhole/Internal/Pake.hs
--- a/src/MagicWormhole/Internal/Pake.hs
+++ b/src/MagicWormhole/Internal/Pake.hs
@@ -14,7 +14,8 @@
   , messageBodyToSpakeBytes
   ) where
 
-import Protolude
+import Protolude hiding (toS)
+import Protolude.Conv (toS)
 
 import Control.Monad (fail)
 import Crypto.Hash (SHA256(..))
diff --git a/src/MagicWormhole/Internal/Versions.hs b/src/MagicWormhole/Internal/Versions.hs
--- a/src/MagicWormhole/Internal/Versions.hs
+++ b/src/MagicWormhole/Internal/Versions.hs
@@ -11,7 +11,8 @@
     , VersionsError(..)
     ) where
 
-import Protolude hiding (phase)
+import Protolude hiding (phase, toS)
+import Protolude.Conv (toS)
 
 import Data.Aeson (FromJSON, ToJSON, (.=), object, Value(..), (.:))
 import Data.Aeson.Types (typeMismatch)
diff --git a/tests/Generator.hs b/tests/Generator.hs
--- a/tests/Generator.hs
+++ b/tests/Generator.hs
@@ -10,7 +10,8 @@
   , offerMessages
   ) where
 
-import Protolude
+import Protolude hiding (toS)
+import Protolude.Conv (toS)
 
 import Hedgehog (MonadGen(..))
 import qualified Hedgehog.Gen as Gen
diff --git a/tests/Integration.hs b/tests/Integration.hs
--- a/tests/Integration.hs
+++ b/tests/Integration.hs
@@ -7,7 +7,8 @@
 -- * if magic-wormhole is not present, these tests will pass
 module Integration (tests) where
 
-import Protolude hiding (phase, stdin, stdout)
+import Protolude hiding (phase, stdin, stdout, toS)
+import Protolude.Conv (toS)
 
 import Control.Concurrent.STM.TChan
   ( newTChan
diff --git a/tests/python/derive_phase_key.py b/tests/python/derive_phase_key.py
--- a/tests/python/derive_phase_key.py
+++ b/tests/python/derive_phase_key.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 """Derive a one-off key for NaCl given a SPAKE2 key."""
 
 import argparse
@@ -11,18 +11,18 @@
 def main():
     parser = argparse.ArgumentParser(prog='version_exchange')
     parser.add_argument(
-        '--spake-key', dest='spake_key', type=unicode,
+        '--spake-key', dest='spake_key', type=str,
         help='SPAKE2 key to generate the one-off key, hex-encoded')
     parser.add_argument(
-        '--side', dest='side', type=unicode,
+        '--side', dest='side', type=str,
         help='Identifier for this side of the exchange')
     parser.add_argument(
-        '--phase', dest='phase', type=unicode,
+        '--phase', dest='phase', type=str,
         help='The phase of the message we need a one-off key for')
     params = parser.parse_args(sys.argv[1:])
     key = derive_phase_key(
         util.hexstr_to_bytes(params.spake_key), params.side, params.phase)
-    print util.bytes_to_hexstr(key)
+    print(util.bytes_to_hexstr(key))
 
 
 if __name__ == '__main__':
diff --git a/tests/python/nacl_exchange.py b/tests/python/nacl_exchange.py
--- a/tests/python/nacl_exchange.py
+++ b/tests/python/nacl_exchange.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python
+
 """Decrypt and then re-encrypt a message using NaCl."""
 
 import attr
@@ -11,10 +13,10 @@
 def main():
     parser = argparse.ArgumentParser(prog='nacl')
     parser.add_argument(
-        '--key', dest='key', type=unicode,
+        '--key', dest='key', type=str,
         help='NaCl key for secret box message, hex-encoded')
     parser.add_argument(
-        '--nonce', dest='nonce', type=unicode,
+        '--nonce', dest='nonce', type=str,
         help='NaCl nonce for secret box message, hex-encoded')
     params = parser.parse_args(sys.argv[1:])
     run_exchange(
@@ -26,7 +28,7 @@
     box = SecretBox(key)
     line = transport.receive_line()
     decrypted = box.decrypt(util.hexstr_to_bytes(line))
-    transport.send_line(decrypted)
+    transport.send_line(decrypted.decode('utf-8'))
     encrypted = util.bytes_to_hexstr(box.encrypt(decrypted, nonce))
     transport.send_line(encrypted)
 
@@ -38,12 +40,13 @@
     output_stream = attr.ib()
 
     def send_line(self, line):
-        self.output_stream.write(line.rstrip().encode('utf8'))
+        output = line.rstrip()
+        self.output_stream.write(output)
         self.output_stream.write('\n')
         self.output_stream.flush()
 
     def receive_line(self):
-        return self.input_stream.readline().strip().decode('utf8')
+        return self.input_stream.readline().strip()
 
 
 if __name__ == '__main__':
diff --git a/tests/python/spake2_exchange.py b/tests/python/spake2_exchange.py
--- a/tests/python/spake2_exchange.py
+++ b/tests/python/spake2_exchange.py
@@ -1,4 +1,5 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+
 """Exchange SPAKE2 keys and print out the session key.
 
 This does the ``pake`` phase of the magic-wormhole client over stdin and
@@ -17,7 +18,7 @@
 try:
     import wormhole
 except ImportError as e:
-    print "Could not find Magic Wormhole: %s" % (e,)
+    print("Could not find Magic Wormhole: %s" % (e,))
     sys.exit(0)
 
 from wormhole import util
@@ -29,13 +30,13 @@
 def main():
     parser = argparse.ArgumentParser(prog='spake2_exchange')
     parser.add_argument(
-        '--code', dest='code', type=unicode,
+        '--code', dest='code', type=str,
         help='Password to use to connect to other side')
     parser.add_argument(
-        '--side', dest='side', type=unicode,
+        '--side', dest='side', type=str,
         help='Identifier for this side of the exchange')
     parser.add_argument(
-        '--app-id', dest='app_id', type=unicode,
+        '--app-id', dest='app_id', type=str,
         help='Identifier for the application')
     params = parser.parse_args(sys.argv[1:])
     transport = Transport(input_stream=sys.stdin, output_stream=sys.stdout)
@@ -48,7 +49,7 @@
         util.to_bytes(code), idSymmetric=util.to_bytes(app_id))
     outbound = spake.start()
     transport.send_json({
-        'phase': u'pake',
+        'phase': 'pake',
         'body': util.bytes_to_hexstr(
             util.dict_to_bytes({
                 'pake_v1': util.bytes_to_hexstr(outbound),
@@ -76,7 +77,7 @@
     output_stream = attr.ib()
 
     def send_line(self, line):
-        self.output_stream.write(line.rstrip().encode('utf8'))
+        self.output_stream.write(line.rstrip())
         self.output_stream.write('\n')
         self.output_stream.flush()
 
@@ -84,7 +85,7 @@
         self.send_line(json.dumps(json_value))
 
     def receive_line(self):
-        return self.input_stream.readline().strip().decode('utf8')
+        return self.input_stream.readline().strip()
 
     def receive_json(self):
         return json.loads(self.receive_line())
diff --git a/tests/python/version_exchange.py b/tests/python/version_exchange.py
--- a/tests/python/version_exchange.py
+++ b/tests/python/version_exchange.py
@@ -1,4 +1,5 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+
 """Exchange SPAKE2 keys and versions.
 
 This is a fake magic-wormhole client that receives messages from STDIN and
@@ -21,7 +22,7 @@
 try:
     import wormhole
 except ImportError as e:
-    print "Could not find Magic Wormhole: %s" % (e,)
+    print("Could not find Magic Wormhole: %s" % (e,))
     sys.exit(0)
 
 from wormhole._key import decrypt_data, encrypt_data, derive_phase_key
@@ -34,13 +35,13 @@
 def main():
     parser = argparse.ArgumentParser(prog='version_exchange')
     parser.add_argument(
-        '--code', dest='code', type=unicode,
+        '--code', dest='code', type=str,
         help='Password to use to connect to other side')
     parser.add_argument(
-        '--side', dest='side', type=unicode,
+        '--side', dest='side', type=str,
         help='Identifier for this side of the exchange')
     parser.add_argument(
-        '--app-id', dest='app_id', type=unicode,
+        '--app-id', dest='app_id', type=str,
         help='Identifier for the application')
     params = parser.parse_args(sys.argv[1:])
     transport = Transport(input_stream=sys.stdin, output_stream=sys.stdout)
@@ -53,7 +54,7 @@
         util.to_bytes(code), idSymmetric=util.to_bytes(app_id))
     outbound = spake.start()
     transport.send_json({
-        'phase': u'pake',
+        'phase': 'pake',
         'body': util.bytes_to_hexstr(
             util.dict_to_bytes({
                 'pake_v1': util.bytes_to_hexstr(outbound),
@@ -73,7 +74,7 @@
     spake_key = spake.finish(inbound)
 
     # Send the versions message
-    version_phase = u'version'
+    version_phase = 'version'
     transport.send_json({
         'phase': version_phase,
         'body': util.bytes_to_hexstr(
