diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index 5f4e5685c896fcd4ed7bbc262b5c07780179529b..28670659bb8fde7d0b2cda20a2770f8a24b175b6 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -373,7 +373,7 @@ class ZKAPAuthorizerStorageClient(object):
                 None,
             )
         ).values()
-        num_passes = required_passes(self._pass_value, share_sizes)
+        num_passes = required_passes(self._pass_value, list(share_sizes))
 
         result = yield call_with_passes(
             lambda passes: rref.callRemote(
diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py
index 809b385118b2a2f27c32e623c5d97bea2b8fc3be..2d774aca410ebe7e9eb507d8ed3f14d34809c4f4 100644
--- a/src/_zkapauthorizer/_storage_server.py
+++ b/src/_zkapauthorizer/_storage_server.py
@@ -22,6 +22,13 @@ implemented in ``_storage_client.py``.
 """
 
 from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
 
 from datetime import timedelta
 from errno import ENOENT
@@ -122,13 +129,13 @@ class _ValidationResult(object):
         """
         Cryptographically check the validity of a single pass.
 
-        :param unicode message: The shared message for pass validation.
+        :param str message: The shared message for pass validation.
         :param Pass pass_: The pass to validate.
 
         :return bool: ``False`` (invalid) if the pass includes a valid
             signature, ``True`` (valid) otherwise.
         """
-        assert isinstance(message, unicode), "message %r not unicode" % (message,)
+        assert isinstance(message, str), "message %r not str" % (message,)
         assert isinstance(pass_, Pass), "pass %r not a Pass" % (pass_,)
         try:
             preimage = TokenPreimage.decode_base64(pass_.preimage)
@@ -148,7 +155,7 @@ class _ValidationResult(object):
         """
         Check all of the given passes for validity.
 
-        :param unicode message: The shared message for pass validation.
+        :param str message: The shared message for pass validation.
         :param list[bytes] passes: The encoded passes to validate.
         :param SigningKey signing_key: The signing key to use to check the passes.
 
@@ -398,7 +405,7 @@ class ZKAPAuthorizerStorageServer(Referenceable):
 
     def remote_share_sizes(self, storage_index_or_slot, sharenums):
         with start_action(
-            action_type=u"zkapauthorizer:storage-server:remote:share-sizes",
+            action_type="zkapauthorizer:storage-server:remote:share-sizes",
             storage_index_or_slot=storage_index_or_slot,
         ):
             return dict(
@@ -443,7 +450,7 @@ class ZKAPAuthorizerStorageServer(Referenceable):
           Note that the lease is *not* renewed in this case (see #254).
         """
         with start_action(
-            action_type=u"zkapauthorizer:storage-server:remote:slot-testv-and-readv-and-writev",
+            action_type="zkapauthorizer:storage-server:remote:slot-testv-and-readv-and-writev",
             storage_index=b2a(storage_index),
             path=storage_index_to_dir(storage_index),
         ):
@@ -877,7 +884,7 @@ def get_share_path(storage_server, storage_index, sharenum):
     return (
         FilePath(storage_server.sharedir)
         .preauthChild(storage_index_to_dir(storage_index))
-        .child(u"{}".format(sharenum))
+        .child("{}".format(sharenum))
     )
 
 
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index ab52c5bc98a8eec0d253b364945effe31f396ce0..c4f202ee5b228591f0b998111fd19d9c14db6f12 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -18,6 +18,13 @@ for the client side of the storage plugin.
 """
 
 from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
 
 from base64 import b64decode, b64encode
 from datetime import timedelta
@@ -97,7 +104,7 @@ class RedemptionResult(object):
     :ivar list[UnblindedToken] unblinded_tokens: The tokens which resulted
         from the redemption.
 
-    :ivar unicode public_key: The public key which the server proved was
+    :ivar str public_key: The public key which the server proved was
         involved in the redemption process.
     """
 
@@ -238,13 +245,13 @@ class ErrorRedeemer(object):
     configured error.
     """
 
-    details = attr.ib(validator=attr.validators.instance_of(unicode))
+    details = attr.ib(validator=attr.validators.instance_of(str))
 
     @classmethod
     def make(cls, section_name, node_config, announcement, reactor):
         details = node_config.get_config(
             section=section_name,
-            option=u"details",
+            option="details",
         ).decode("ascii")
         return cls(details)
 
@@ -325,7 +332,7 @@ def dummy_random_tokens(voucher, counter, count):
             # Padding is 96 (random token length) - 32 (decoded voucher
             # length) - 4 (fixed-width counter)
             b64encode(
-                v + u"{:0>4}{:0>60}".format(counter, n).encode("ascii"),
+                v + "{:0>4}{:0>60}".format(counter, n).encode("ascii"),
             ),
         )
 
@@ -340,7 +347,7 @@ class DummyRedeemer(object):
     really redeeming them, it makes up some fake ZKAPs and pretends those are
     the result.
 
-    :ivar unicode _public_key: The base64-encoded public key to return with
+    :ivar str _public_key: The base64-encoded public key to return with
         all successful redemption results.  As with the tokens returned by
         this redeemer, chances are this is not actually a valid public key.
         Its corresponding private key certainly has not been used to sign
@@ -348,7 +355,7 @@ class DummyRedeemer(object):
     """
 
     _public_key = attr.ib(
-        validator=attr.validators.instance_of(unicode),
+        validator=attr.validators.instance_of(str),
     )
 
     @classmethod
@@ -356,8 +363,8 @@ class DummyRedeemer(object):
         return cls(
             node_config.get_config(
                 section=section_name,
-                option=u"issuer-public-key",
-            ).decode(u"utf-8"),
+                option="issuer-public-key",
+            ).decode("utf-8"),
         )
 
     def random_tokens_for_voucher(self, voucher, counter, count):
@@ -461,7 +468,7 @@ class RistrettoRedeemer(object):
     def make(cls, section_name, node_config, announcement, reactor):
         configured_issuer = node_config.get_config(
             section=section_name,
-            option=u"ristretto-issuer-root-url",
+            option="ristretto-issuer-root-url",
         ).decode("ascii")
         if announcement is not None:
             # Don't let us talk to a storage server that has a different idea
@@ -473,7 +480,7 @@ class RistrettoRedeemer(object):
             # If we aren't given an announcement then we're not being used in
             # the context of a specific storage server so the check is
             # unnecessary and impossible.
-            announced_issuer = announcement[u"ristretto-issuer-root-url"]
+            announced_issuer = announcement["ristretto-issuer-root-url"]
             if announced_issuer != configured_issuer:
                 raise IssuerConfigurationMismatch(announced_issuer, configured_issuer)
 
@@ -498,12 +505,12 @@ class RistrettoRedeemer(object):
         )
         blinded_tokens = list(token.blind() for token in random_tokens)
         response = yield self._treq.post(
-            self._api_root.child(u"v1", u"redeem").to_text(),
+            self._api_root.child("v1", "redeem").to_text(),
             dumps(
                 {
-                    u"redeemVoucher": voucher.number.decode("ascii"),
-                    u"redeemCounter": counter,
-                    u"redeemTokens": list(
+                    "redeemVoucher": voucher.number.decode("ascii"),
+                    "redeemCounter": counter,
+                    "redeemTokens": list(
                         token.encode_base64() for token in blinded_tokens
                     ),
                 }
@@ -517,26 +524,26 @@ class RistrettoRedeemer(object):
         except ValueError:
             raise UnexpectedResponse(response.code, response_body)
 
-        success = result.get(u"success", False)
+        success = result.get("success", False)
         if not success:
-            reason = result.get(u"reason", None)
-            if reason == u"double-spend":
+            reason = result.get("reason", None)
+            if reason == "double-spend":
                 raise AlreadySpent(voucher)
-            elif reason == u"unpaid":
+            elif reason == "unpaid":
                 raise Unpaid(voucher)
 
             raise UnrecognizedFailureReason(result)
 
         self._log.info(
             "Redeemed: {public_key} {proof} {count}",
-            public_key=result[u"public-key"],
-            proof=result[u"proof"],
-            count=len(result[u"signatures"]),
+            public_key=result["public-key"],
+            proof=result["proof"],
+            count=len(result["signatures"]),
         )
 
-        marshaled_signed_tokens = result[u"signatures"]
-        marshaled_proof = result[u"proof"]
-        marshaled_public_key = result[u"public-key"]
+        marshaled_signed_tokens = result["signatures"]
+        marshaled_proof = result["proof"]
+        marshaled_public_key = result["public-key"]
 
         public_key = challenge_bypass_ristretto.PublicKey.decode_base64(
             marshaled_public_key.encode("ascii"),
@@ -660,17 +667,17 @@ class PaymentController(object):
         redeeming a voucher, if no other count is given when the redemption is
         started.
 
-    :ivar set[unicode] allowed_public_keys: The base64-encoded public keys for
+    :ivar set[str] allowed_public_keys: The base64-encoded public keys for
         which to accept tokens.
 
-    :ivar dict[unicode, Redeeming] _active: A mapping from voucher identifiers
+    :ivar dict[str, Redeeming] _active: A mapping from voucher identifiers
         which currently have redemption attempts in progress to a
         ``Redeeming`` state representing the attempt.
 
-    :ivar dict[unicode, datetime] _error: A mapping from voucher identifiers
+    :ivar dict[str, datetime] _error: A mapping from voucher identifiers
         which have recently failed with an unrecognized, transient error.
 
-    :ivar dict[unicode, datetime] _unpaid: A mapping from voucher identifiers
+    :ivar dict[str, datetime] _unpaid: A mapping from voucher identifiers
         which have recently failed a redemption attempt due to an unpaid
         response from the redemption server to timestamps when the failure was
         observed.
@@ -732,7 +739,7 @@ class PaymentController(object):
         )
 
     def _retry_redemption(self):
-        for voucher in self._error.keys() + self._unpaid.keys():
+        for voucher in list(self._error.keys()) + list(self._unpaid.keys()):
             if voucher in self._active:
                 continue
             if self.get_voucher(voucher).state.should_start_redemption():
@@ -982,22 +989,22 @@ class PaymentController(object):
 
 
 def get_redeemer(plugin_name, node_config, announcement, reactor):
-    section_name = u"storageclient.plugins.{}".format(plugin_name)
+    section_name = "storageclient.plugins.{}".format(plugin_name)
     redeemer_kind = node_config.get_config(
         section=section_name,
-        option=u"redeemer",
-        default=u"ristretto",
+        option="redeemer",
+        default="ristretto",
     )
     return _REDEEMERS[redeemer_kind](section_name, node_config, announcement, reactor)
 
 
 _REDEEMERS = {
-    u"non": NonRedeemer.make,
-    u"dummy": DummyRedeemer.make,
-    u"double-spend": DoubleSpendRedeemer.make,
-    u"unpaid": UnpaidRedeemer.make,
-    u"error": ErrorRedeemer.make,
-    u"ristretto": RistrettoRedeemer.make,
+    "non": NonRedeemer.make,
+    "dummy": DummyRedeemer.make,
+    "double-spend": DoubleSpendRedeemer.make,
+    "unpaid": UnpaidRedeemer.make,
+    "error": ErrorRedeemer.make,
+    "ristretto": RistrettoRedeemer.make,
 }
 
 
diff --git a/src/_zkapauthorizer/eliot.py b/src/_zkapauthorizer/eliot.py
index 8f607d8a0e66e7605ff4d66cd5640d759bba3cdb..ccd847276fbc90a5c055b34979011c8076e20443 100644
--- a/src/_zkapauthorizer/eliot.py
+++ b/src/_zkapauthorizer/eliot.py
@@ -17,90 +17,97 @@ Eliot field, message, and action definitions for ZKAPAuthorizer.
 """
 
 from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
 
 from eliot import ActionType, Field, MessageType
 
 PRIVACYPASS_MESSAGE = Field(
-    u"message",
-    unicode,
-    u"The PrivacyPass request-binding data associated with a pass.",
+    "message",
+    str,
+    "The PrivacyPass request-binding data associated with a pass.",
 )
 
 INVALID_REASON = Field(
-    u"reason",
-    unicode,
-    u"The reason given by the server for rejecting a pass as invalid.",
+    "reason",
+    str,
+    "The reason given by the server for rejecting a pass as invalid.",
 )
 
 PASS_COUNT = Field(
-    u"count",
+    "count",
     int,
-    u"A number of passes.",
+    "A number of passes.",
 )
 
 GET_PASSES = MessageType(
-    u"zkapauthorizer:get-passes",
+    "zkapauthorizer:get-passes",
     [PRIVACYPASS_MESSAGE, PASS_COUNT],
-    u"An attempt to spend passes is beginning.",
+    "An attempt to spend passes is beginning.",
 )
 
 SPENT_PASSES = MessageType(
-    u"zkapauthorizer:spent-passes",
+    "zkapauthorizer:spent-passes",
     [PASS_COUNT],
-    u"An attempt to spend passes has succeeded.",
+    "An attempt to spend passes has succeeded.",
 )
 
 INVALID_PASSES = MessageType(
-    u"zkapauthorizer:invalid-passes",
+    "zkapauthorizer:invalid-passes",
     [INVALID_REASON, PASS_COUNT],
-    u"An attempt to spend passes has found some to be invalid.",
+    "An attempt to spend passes has found some to be invalid.",
 )
 
 RESET_PASSES = MessageType(
-    u"zkapauthorizer:reset-passes",
+    "zkapauthorizer:reset-passes",
     [PASS_COUNT],
-    u"Some passes involved in a failed spending attempt have not definitely been spent and are being returned for future use.",
+    "Some passes involved in a failed spending attempt have not definitely been spent and are being returned for future use.",
 )
 
 SIGNATURE_CHECK_FAILED = MessageType(
-    u"zkapauthorizer:storage-client:signature-check-failed",
+    "zkapauthorizer:storage-client:signature-check-failed",
     [PASS_COUNT],
-    u"Some passes the client tried to use were rejected for having invalid signatures.",
+    "Some passes the client tried to use were rejected for having invalid signatures.",
 )
 
 CALL_WITH_PASSES = ActionType(
-    u"zkapauthorizer:storage-client:call-with-passes",
+    "zkapauthorizer:storage-client:call-with-passes",
     [PASS_COUNT],
     [],
-    u"A storage operation is being started which may spend some passes.",
+    "A storage operation is being started which may spend some passes.",
 )
 
 CURRENT_SIZES = Field(
-    u"current_sizes",
+    "current_sizes",
     dict,
-    u"A dictionary mapping the numbers of existing shares to their existing sizes.",
+    "A dictionary mapping the numbers of existing shares to their existing sizes.",
 )
 
 TW_VECTORS_SUMMARY = Field(
-    u"tw_vectors_summary",
+    "tw_vectors_summary",
     dict,
-    u"A dictionary mapping share numbers from tw_vectors to test and write vector summaries.",
+    "A dictionary mapping share numbers from tw_vectors to test and write vector summaries.",
 )
 
 NEW_SIZES = Field(
-    u"new_sizes",
+    "new_sizes",
     dict,
-    u"A dictionary like that of CURRENT_SIZES but for the sizes computed for the shares after applying tw_vectors.",
+    "A dictionary like that of CURRENT_SIZES but for the sizes computed for the shares after applying tw_vectors.",
 )
 
 NEW_PASSES = Field(
-    u"new_passes",
+    "new_passes",
     int,
-    u"The number of passes computed as being required for the change in size.",
+    "The number of passes computed as being required for the change in size.",
 )
 
 MUTABLE_PASSES_REQUIRED = MessageType(
-    u"zkapauthorizer:storage:mutable-passes-required",
+    "zkapauthorizer:storage:mutable-passes-required",
     [CURRENT_SIZES, TW_VECTORS_SUMMARY, NEW_SIZES, NEW_PASSES],
-    u"Some number of passes has been computed as the cost of updating a mutable.",
+    "Some number of passes has been computed as the cost of updating a mutable.",
 )
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 7530d2b2613538fdbe26f5a3e05b6a1b8d35eacf..57b7cb4aa8f63c8264d1ab7ff23f830c8ceb5469 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -17,6 +17,16 @@ This module implements models (in the MVC sense) for the client side of
 the storage plugin.
 """
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
+from past.builtins import long
+
 from datetime import datetime
 from functools import wraps
 from json import dumps, loads
@@ -41,12 +51,12 @@ from .validators import greater_than, has_length, is_base64_encoded
 
 def parse_datetime(s, **kw):
     """
-    Like ``aniso8601.parse_datetime`` but accept unicode as well.
+    Like ``aniso8601.parse_datetime`` but accept str as well.
     """
-    if isinstance(s, unicode):
+    if isinstance(s, str):
         s = s.encode("utf-8")
     assert isinstance(s, bytes)
-    if "delimiter" in kw and isinstance(kw["delimiter"], unicode):
+    if "delimiter" in kw and isinstance(kw["delimiter"], str):
         kw["delimiter"] = kw["delimiter"].encode("utf-8")
     return _parse_datetime(s, **kw)
 
@@ -86,7 +96,7 @@ class NotEnoughTokens(Exception):
     """
 
 
-CONFIG_DB_NAME = u"privatestorageio-zkapauthz-v1.sqlite3"
+CONFIG_DB_NAME = "privatestorageio-zkapauthz-v1.sqlite3"
 
 
 def open_and_initialize(path, connect=None):
@@ -380,7 +390,7 @@ class VoucherStore(object):
         Store some unblinded tokens, for example as part of a backup-restore
         process.
 
-        :param list[unicode] unblinded_tokens: The unblinded tokens to store.
+        :param list[str] unblinded_tokens: The unblinded tokens to store.
 
         :param int group_id: The unique identifier of the redemption group to
             which these tokens belong.
@@ -398,7 +408,7 @@ class VoucherStore(object):
             tokens.  This voucher will be marked as redeemed to indicate it
             has fulfilled its purpose and has no further use for us.
 
-        :param unicode public_key: The encoded public key for the private key
+        :param str public_key: The encoded public key for the private key
             which was used to sign these tokens.
 
         :param list[UnblindedToken] unblinded_tokens: The unblinded tokens to
@@ -411,9 +421,9 @@ class VoucherStore(object):
             inserted tokens, ``False`` otherwise.
         """
         if completed:
-            voucher_state = u"redeemed"
+            voucher_state = "redeemed"
         else:
-            voucher_state = u"pending"
+            voucher_state = "pending"
 
         if spendable:
             token_count_increase = len(unblinded_tokens)
@@ -683,7 +693,7 @@ class VoucherStore(object):
         )
         tokens = cursor.fetchall()
         return {
-            u"unblinded-tokens": list(token for (token,) in tokens),
+            "unblinded-tokens": list(token for (token,) in tokens),
         }
 
     def start_lease_maintenance(self):
@@ -720,9 +730,9 @@ class VoucherStore(object):
             return None
         [(started, count, finished)] = activity
         return LeaseMaintenanceActivity(
-            parse_datetime(started, delimiter=u" "),
+            parse_datetime(started, delimiter=" "),
             count,
-            parse_datetime(finished, delimiter=u" "),
+            parse_datetime(finished, delimiter=" "),
         )
 
 
@@ -923,8 +933,8 @@ class Pending(object):
 
     def to_json_v1(self):
         return {
-            u"name": u"pending",
-            u"counter": self.counter,
+            "name": "pending",
+            "counter": self.counter,
         }
 
 
@@ -944,9 +954,9 @@ class Redeeming(object):
 
     def to_json_v1(self):
         return {
-            u"name": u"redeeming",
-            u"started": self.started.isoformat(),
-            u"counter": self.counter,
+            "name": "redeeming",
+            "started": self.started.isoformat(),
+            "counter": self.counter,
         }
 
 
@@ -969,9 +979,9 @@ class Redeemed(object):
 
     def to_json_v1(self):
         return {
-            u"name": u"redeemed",
-            u"finished": self.finished.isoformat(),
-            u"token-count": self.token_count,
+            "name": "redeemed",
+            "finished": self.finished.isoformat(),
+            "token-count": self.token_count,
         }
 
 
@@ -984,8 +994,8 @@ class DoubleSpend(object):
 
     def to_json_v1(self):
         return {
-            u"name": u"double-spend",
-            u"finished": self.finished.isoformat(),
+            "name": "double-spend",
+            "finished": self.finished.isoformat(),
         }
 
 
@@ -1004,8 +1014,8 @@ class Unpaid(object):
 
     def to_json_v1(self):
         return {
-            u"name": u"unpaid",
-            u"finished": self.finished.isoformat(),
+            "name": "unpaid",
+            "finished": self.finished.isoformat(),
         }
 
 
@@ -1018,16 +1028,16 @@ class Error(object):
     """
 
     finished = attr.ib(validator=attr.validators.instance_of(datetime))
-    details = attr.ib(validator=attr.validators.instance_of(unicode))
+    details = attr.ib(validator=attr.validators.instance_of(str))
 
     def should_start_redemption(self):
         return True
 
     def to_json_v1(self):
         return {
-            u"name": u"error",
-            u"finished": self.finished.isoformat(),
-            u"details": self.details,
+            "name": "error",
+            "finished": self.finished.isoformat(),
+            "details": self.details,
         }
 
 
@@ -1089,15 +1099,15 @@ class Voucher(object):
     @classmethod
     def from_row(cls, row):
         def state_from_row(state, row):
-            if state == u"pending":
+            if state == "pending":
                 return Pending(counter=row[3])
-            if state == u"double-spend":
+            if state == "double-spend":
                 return DoubleSpend(
-                    parse_datetime(row[0], delimiter=u" "),
+                    parse_datetime(row[0], delimiter=" "),
                 )
-            if state == u"redeemed":
+            if state == "redeemed":
                 return Redeemed(
-                    parse_datetime(row[0], delimiter=u" "),
+                    parse_datetime(row[0], delimiter=" "),
                     row[1],
                 )
             raise ValueError("Unknown voucher state {}".format(state))
@@ -1112,54 +1122,54 @@ class Voucher(object):
             # value represents a leap second.  However, since we also use
             # Python to generate the data in the first place, it should never
             # represent a leap second... I hope.
-            created=parse_datetime(created, delimiter=u" "),
+            created=parse_datetime(created, delimiter=" "),
             state=state_from_row(state, row[4:]),
         )
 
     @classmethod
     def from_json(cls, json):
         values = loads(json)
-        version = values.pop(u"version")
+        version = values.pop("version")
         return getattr(cls, "from_json_v{}".format(version))(values)
 
     @classmethod
     def from_json_v1(cls, values):
-        state_json = values[u"state"]
-        state_name = state_json[u"name"]
-        if state_name == u"pending":
-            state = Pending(counter=state_json[u"counter"])
-        elif state_name == u"redeeming":
+        state_json = values["state"]
+        state_name = state_json["name"]
+        if state_name == "pending":
+            state = Pending(counter=state_json["counter"])
+        elif state_name == "redeeming":
             state = Redeeming(
-                started=parse_datetime(state_json[u"started"]),
-                counter=state_json[u"counter"],
+                started=parse_datetime(state_json["started"]),
+                counter=state_json["counter"],
             )
-        elif state_name == u"double-spend":
+        elif state_name == "double-spend":
             state = DoubleSpend(
-                finished=parse_datetime(state_json[u"finished"]),
+                finished=parse_datetime(state_json["finished"]),
             )
-        elif state_name == u"redeemed":
+        elif state_name == "redeemed":
             state = Redeemed(
-                finished=parse_datetime(state_json[u"finished"]),
-                token_count=state_json[u"token-count"],
+                finished=parse_datetime(state_json["finished"]),
+                token_count=state_json["token-count"],
             )
-        elif state_name == u"unpaid":
+        elif state_name == "unpaid":
             state = Unpaid(
-                finished=parse_datetime(state_json[u"finished"]),
+                finished=parse_datetime(state_json["finished"]),
             )
-        elif state_name == u"error":
+        elif state_name == "error":
             state = Error(
-                finished=parse_datetime(state_json[u"finished"]),
-                details=state_json[u"details"],
+                finished=parse_datetime(state_json["finished"]),
+                details=state_json["details"],
             )
         else:
             raise ValueError("Unrecognized state {!r}".format(state_json))
 
         return cls(
-            number=values[u"number"].encode("ascii"),
-            expected_tokens=values[u"expected-tokens"],
+            number=values["number"].encode("ascii"),
+            expected_tokens=values["expected-tokens"],
             created=None
-            if values[u"created"] is None
-            else parse_datetime(values[u"created"]),
+            if values["created"] is None
+            else parse_datetime(values["created"]),
             state=state,
         )
 
@@ -1172,9 +1182,9 @@ class Voucher(object):
     def to_json_v1(self):
         state = self.state.to_json_v1()
         return {
-            u"number": self.number.decode("ascii"),
-            u"expected-tokens": self.expected_tokens,
-            u"created": None if self.created is None else self.created.isoformat(),
-            u"state": state,
-            u"version": 1,
+            "number": self.number.decode("ascii"),
+            "expected-tokens": self.expected_tokens,
+            "created": None if self.created is None else self.created.isoformat(),
+            "state": state,
+            "version": 1,
         }
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index f5db0531f7d9563ddcee051475e0c9cf2d6ff1ba..89b0a11fb26e21cb623c22a4f14a51826cb73c96 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -21,6 +21,16 @@ vouchers for fresh tokens.
 In the future it should also allow users to read statistics about token usage.
 """
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
+from past.builtins import long
+
 from itertools import islice
 from json import dumps, load, loads
 from sys import maxint
@@ -65,18 +75,18 @@ def get_token_count(
     Retrieve the configured voucher value, in number of tokens, from the given
     configuration.
 
-    :param unicode plugin_name: The plugin name to use to choose a
+    :param str plugin_name: The plugin name to use to choose a
         configuration section.
 
     :param _Config node_config: See ``from_configuration``.
 
     :param int default: The value to return if none is configured.
     """
-    section_name = u"storageclient.plugins.{}".format(plugin_name)
+    section_name = "storageclient.plugins.{}".format(plugin_name)
     return int(
         node_config.get_config(
             section=section_name,
-            option=u"default-token-count",
+            option="default-token-count",
             default=NUM_TOKENS,
         )
     )
@@ -109,7 +119,7 @@ def from_configuration(
     :return IZKAPRoot: The root of the resource hierarchy presented by the
         client side of the plugin.
     """
-    plugin_name = u"privatestorageio-zkapauthz-v1"
+    plugin_name = "privatestorageio-zkapauthz-v1"
     if redeemer is None:
         redeemer = get_redeemer(
             plugin_name,
@@ -219,7 +229,7 @@ class _CalculatePrice(Resource):
         Calculate the price in ZKAPs to store or continue storing files specified
         sizes.
         """
-        if wrong_content_type(request, u"application/json"):
+        if wrong_content_type(request, "application/json"):
             return NOT_DONE_YET
 
         application_json(request)
@@ -235,8 +245,8 @@ class _CalculatePrice(Resource):
             )
 
         try:
-            version = body_object[u"version"]
-            sizes = body_object[u"sizes"]
+            version = body_object["version"]
+            sizes = body_object["sizes"]
         except (TypeError, KeyError):
             request.setResponseCode(BAD_REQUEST)
             return dumps(
@@ -268,8 +278,8 @@ class _CalculatePrice(Resource):
         price = self._price_calculator.calculate(sizes)
         return dumps(
             {
-                u"price": price,
-                u"period": self._lease_period,
+                "price": price,
+                "period": self._lease_period,
             }
         )
 
@@ -280,14 +290,14 @@ def wrong_content_type(request, required_type):
 
     :param request: The request object to check.
 
-    :param unicode required_type: The required content-type (eg
-        ``u"application/json"``).
+    :param str required_type: The required content-type (eg
+        ``"application/json"``).
 
     :return bool: ``True`` if the content-type is wrong and an error response
         has been generated.  ``False`` otherwise.
     """
     actual_type = request.requestHeaders.getRawHeaders(
-        u"content-type",
+        "content-type",
         [None],
     )[0]
     if actual_type != required_type:
@@ -303,7 +313,7 @@ def application_json(request):
 
     :param twisted.web.iweb.IRequest request: The request to modify.
     """
-    request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"])
+    request.responseHeaders.setRawHeaders("content-type", ["application/json"])
 
 
 class _ProjectVersion(Resource):
@@ -339,7 +349,7 @@ class _UnblindedTokenCollection(Resource):
         """
         application_json(request)
         state = self._store.backup()
-        unblinded_tokens = state[u"unblinded-tokens"]
+        unblinded_tokens = state["unblinded-tokens"]
 
         limit = request.args.get(b"limit", [None])[0]
         if limit is not None:
@@ -349,14 +359,14 @@ class _UnblindedTokenCollection(Resource):
 
         return dumps(
             {
-                u"total": len(unblinded_tokens),
-                u"spendable": self._store.count_unblinded_tokens(),
-                u"unblinded-tokens": list(
+                "total": len(unblinded_tokens),
+                "spendable": self._store.count_unblinded_tokens(),
+                "unblinded-tokens": list(
                     islice(
                         (token for token in unblinded_tokens if token > position), limit
                     )
                 ),
-                u"lease-maintenance-spending": self._lease_maintenance_activity(),
+                "lease-maintenance-spending": self._lease_maintenance_activity(),
             }
         )
 
@@ -365,7 +375,7 @@ class _UnblindedTokenCollection(Resource):
         Store some unblinded tokens.
         """
         application_json(request)
-        unblinded_tokens = load(request.content)[u"unblinded-tokens"]
+        unblinded_tokens = load(request.content)["unblinded-tokens"]
         self._store.insert_unblinded_tokens(unblinded_tokens, group_id=0)
         return dumps({})
 
@@ -374,8 +384,8 @@ class _UnblindedTokenCollection(Resource):
         if activity is None:
             return activity
         return {
-            u"when": activity.finished.isoformat(),
-            u"count": activity.passes_required,
+            "when": activity.finished.isoformat(),
+            "count": activity.passes_required,
         }
 
 
@@ -401,14 +411,14 @@ class _VoucherCollection(Resource):
         try:
             payload = loads(request.content.read())
         except Exception:
-            return bad_request(u"json request body required").render(request)
-        if payload.keys() != [u"voucher"]:
+            return bad_request("json request body required").render(request)
+        if payload.keys() != ["voucher"]:
             return bad_request(
-                u"request object must have exactly one key: 'voucher'"
+                "request object must have exactly one key: 'voucher'"
             ).render(request)
-        voucher = payload[u"voucher"]
+        voucher = payload["voucher"]
         if not is_syntactic_voucher(voucher):
-            return bad_request(u"submitted voucher is syntactically invalid").render(
+            return bad_request("submitted voucher is syntactically invalid").render(
                 request
             )
 
@@ -422,7 +432,7 @@ class _VoucherCollection(Resource):
         application_json(request)
         return dumps(
             {
-                u"vouchers": list(
+                "vouchers": list(
                     self._controller.incorporate_transient_state(voucher).marshal()
                     for voucher in self._store.list()
                 ),
@@ -444,12 +454,12 @@ def is_syntactic_voucher(voucher):
     """
     :param voucher: A candidate object to inspect.
 
-    :return bool: ``True`` if and only if ``voucher`` is a unicode string
+    :return bool: ``True`` if and only if ``voucher`` is a text string
         containing a syntactically valid voucher.  This says **nothing** about
         the validity of the represented voucher itself.  A ``True`` result
-        only means the unicode string can be **interpreted** as a voucher.
+        only means the string can be **interpreted** as a voucher.
     """
-    if not isinstance(voucher, unicode):
+    if not isinstance(voucher, str):
         return False
     if len(voucher) != 44:
         # TODO.  44 is the length of 32 bytes base64 encoded.  This model
@@ -480,7 +490,7 @@ class VoucherView(Resource):
         return self._voucher.to_json()
 
 
-def bad_request(reason=u"Bad Request"):
+def bad_request(reason="Bad Request"):
     """
     :return IResource: A resource which can be rendered to produce a **BAD
         REQUEST** response.
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
index bbe326a292b502024eef0b77b4181977c4f2509d..aeb861841946a05a2448937712ee030b4270c2f2 100644
--- a/src/_zkapauthorizer/storage_common.py
+++ b/src/_zkapauthorizer/storage_common.py
@@ -16,7 +16,15 @@
 Functionality shared between the storage client and server.
 """
 
+from __future__ import absolute_import
 from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
+from past.builtins import long
 
 from base64 import b64encode
 
@@ -50,7 +58,7 @@ class MorePassesRequired(Exception):
 
 def _message_maker(label):
     def make_message(storage_index):
-        return u"{label} {storage_index}".format(
+        return "{label} {storage_index}".format(
             label=label,
             storage_index=b64encode(storage_index),
         )
@@ -60,10 +68,10 @@ def _message_maker(label):
 
 # Functions to construct the PrivacyPass request-binding message for pass
 # construction for different Tahoe-LAFS storage operations.
-allocate_buckets_message = _message_maker(u"allocate_buckets")
-add_lease_message = _message_maker(u"add_lease")
+allocate_buckets_message = _message_maker("allocate_buckets")
+add_lease_message = _message_maker("add_lease")
 slot_testv_and_readv_and_writev_message = _message_maker(
-    u"slot_testv_and_readv_and_writev"
+    "slot_testv_and_readv_and_writev"
 )
 
 # The number of bytes we're willing to store for a lease period for each pass
@@ -80,8 +88,8 @@ def get_configured_shares_needed(node_config):
     """
     return int(
         node_config.get_config(
-            section=u"client",
-            option=u"shares.needed",
+            section="client",
+            option="shares.needed",
             default=3,
         )
     )
@@ -96,8 +104,8 @@ def get_configured_shares_total(node_config):
     """
     return int(
         node_config.get_config(
-            section=u"client",
-            option=u"shares.total",
+            section="client",
+            option="shares.total",
             default=10,
         )
     )
@@ -111,11 +119,11 @@ def get_configured_pass_value(node_config):
     value is read from the **pass-value** option of the ZKAPAuthorizer plugin
     client section.
     """
-    section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1"
+    section_name = "storageclient.plugins.privatestorageio-zkapauthz-v1"
     return int(
         node_config.get_config(
             section=section_name,
-            option=u"pass-value",
+            option="pass-value",
             default=BYTES_PER_PASS,
         )
     )
@@ -125,17 +133,19 @@ def get_configured_allowed_public_keys(node_config):
     """
     Read the set of allowed issuer public keys from the given configuration.
     """
-    section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1"
+    section_name = "storageclient.plugins.privatestorageio-zkapauthz-v1"
     return set(
         node_config.get_config(
             section=section_name,
-            option=u"allowed-public-keys",
+            option="allowed-public-keys",
         )
         .strip()
         .split(",")
     )
 
 
+_dict_values = type(dict().values())
+
 def required_passes(bytes_per_pass, share_sizes):
     """
     Calculate the number of passes that are required to store shares of the
@@ -148,9 +158,9 @@ def required_passes(bytes_per_pass, share_sizes):
 
     :return int: The number of passes required to cover the storage cost.
     """
-    if not isinstance(share_sizes, list):
+    if not isinstance(share_sizes, (list, _dict_values)):
         raise TypeError(
-            "Share sizes must be a list of integers, got {!r} instead".format(
+            "Share sizes must be a list (or dict_values) of integers, got {!r} instead".format(
                 share_sizes,
             ),
         )
@@ -253,7 +263,7 @@ def get_required_new_passes_for_mutable_write(pass_value, current_sizes, tw_vect
     """
     current_passes = required_passes(
         pass_value,
-        current_sizes.values(),
+        list(current_sizes.values()),
     )
 
     new_sizes = current_sizes.copy()
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 61b401967aa131463f7e83f03ba03bceda85c430..6ce17b9381921e08ea0805991ab247d0baca6053 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -16,6 +16,15 @@
 Hypothesis strategies for property testing.
 """
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
+
 from base64 import b64encode, urlsafe_b64encode
 from datetime import datetime, timedelta
 from urllib import quote
@@ -142,7 +151,7 @@ def tahoe_config_texts(storage_client_plugins, shares):
     def merge_shares(shares, the_rest):
         for (k, v) in zip(("needed", "happy", "total"), shares):
             if v is not None:
-                the_rest["shares." + k] = u"{}".format(v)
+                the_rest["shares." + k] = "{}".format(v)
         return the_rest
 
     client_section = builds(
@@ -151,7 +160,7 @@ def tahoe_config_texts(storage_client_plugins, shares):
         fixed_dictionaries(
             {
                 "storage.plugins": just(
-                    u",".join(storage_client_plugins.keys()),
+                    ",".join(storage_client_plugins.keys()),
                 ),
             },
         ),
@@ -186,8 +195,8 @@ def minimal_tahoe_configs(storage_client_plugins=None, shares=just((None, None,
 
     :param shares: See ``tahoe_config_texts``.
 
-    :return SearchStrategy[unicode]: A strategy that builds unicode strings
-        which are Tahoe-LAFS configuration file contents.
+    :return SearchStrategy[str]: A strategy that builds text strings which are
+        Tahoe-LAFS configuration file contents.
     """
     if storage_client_plugins is None:
         storage_client_plugins = {}
@@ -207,9 +216,9 @@ def node_nicknames():
         alphabet=characters(
             blacklist_categories={
                 # Surrogates
-                u"Cs",
+                "Cs",
                 # Unnamed and control characters
-                u"Cc",
+                "Cc",
             },
         ),
     )
@@ -241,23 +250,23 @@ def server_configurations(signing_key_path):
     """
     Build configuration values for the server-side plugin.
 
-    :param unicode signing_key_path: A value to insert for the
+    :param str signing_key_path: A value to insert for the
         **ristretto-signing-key-path** item.
     """
     return one_of(
         fixed_dictionaries(
             {
-                u"pass-value":
+                "pass-value":
                 # The configuration is ini so everything is always a byte string!
-                integers(min_value=1).map(bytes),
+                integers(min_value=1).map(lambda v: u"{}".format(v).encode("ascii")),
             }
         ),
         just({}),
     ).map(
         lambda config: config.update(
             {
-                u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
-                u"ristretto-signing-key-path": signing_key_path.path,
+                "ristretto-issuer-root-url": "https://issuer.example.invalid/",
+                "ristretto-signing-key-path": signing_key_path.path,
             }
         )
         or config,
@@ -294,8 +303,8 @@ def zkapauthz_configuration(
         allowed_public_keys,
     ):
         config = {
-            u"default-token-count": u"32",
-            u"allowed-public-keys": u",".join(allowed_public_keys),
+            "default-token-count": "32",
+            "allowed-public-keys": ",".join(allowed_public_keys),
         }
         config.update(extra_configuration)
         return config
@@ -314,8 +323,8 @@ def client_ristrettoredeemer_configurations():
     return zkapauthz_configuration(
         just(
             {
-                u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
-                u"redeemer": u"ristretto",
+                "ristretto-issuer-root-url": "https://issuer.example.invalid/",
+                "redeemer": "ristretto",
             }
         )
     )
@@ -351,10 +360,10 @@ def client_dummyredeemer_configurations(
         extra_config = lease_configs.map(
             lambda config: config.update(
                 {
-                    u"redeemer": u"dummy",
+                    "redeemer": "dummy",
                     # Pick out one of the allowed public keys so that the dummy
                     # appears to produce usable tokens.
-                    u"issuer-public-key": next(iter(allowed_keys)),
+                    "issuer-public-key": next(iter(allowed_keys)),
                 }
             )
             or config,
@@ -382,7 +391,7 @@ def client_doublespendredeemer_configurations(default_token_counts=token_counts(
     return zkapauthz_configuration(
         just(
             {
-                u"redeemer": u"double-spend",
+                "redeemer": "double-spend",
             }
         )
     )
@@ -395,7 +404,7 @@ def client_unpaidredeemer_configurations():
     return zkapauthz_configuration(
         just(
             {
-                u"redeemer": u"unpaid",
+                "redeemer": "unpaid",
             }
         )
     )
@@ -408,7 +417,7 @@ def client_nonredeemer_configurations():
     return zkapauthz_configuration(
         just(
             {
-                u"redeemer": u"non",
+                "redeemer": "non",
             }
         )
     )
@@ -421,8 +430,8 @@ def client_errorredeemer_configurations(details):
     return zkapauthz_configuration(
         just(
             {
-                u"redeemer": u"error",
-                u"details": details,
+                "redeemer": "error",
+                "details": details,
             }
         )
     )
@@ -492,14 +501,14 @@ def direct_tahoe_configs(
     """
     config_texts = minimal_tahoe_configs(
         {
-            u"privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration,
+            "privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration,
         },
         shares,
     )
     return config_texts.map(
         lambda config_text: config_from_string(
-            u"/dev/null/illegal",
-            u"",
+            "/dev/null/illegal",
+            "",
             config_text.encode("utf-8"),
         ),
     )
@@ -845,7 +854,7 @@ def bytes_for_share(sharenum, size):
         given share number
     """
     if 0 <= sharenum <= 255:
-        return (unichr(sharenum) * size).encode("latin-1")
+        return (chr(sharenum) * size).encode("latin-1")
     raise ValueError("Sharenum must be between 0 and 255 inclusive.")
 
 
@@ -949,7 +958,7 @@ def announcements():
     """
     return just(
         {
-            u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
+            "ristretto-issuer-root-url": "https://issuer.example.invalid/",
         }
     )
 
@@ -977,7 +986,7 @@ class _DirectoryNode(object):
     _storage_index = attr.ib()
     _children = attr.ib()
 
-    def list(self):
+    def list(self):  # noqa: F811
         return succeed(self._children)
 
     def get_storage_index(self):
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 7e3e9abf1067d27a2c3a00b44bc94c1f47c9526a..26f0786a9f11efdc7d156141284459d60f6ccc86 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -18,6 +18,13 @@ plugin.
 """
 
 from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
 
 from datetime import datetime
 from io import BytesIO
@@ -110,7 +117,7 @@ from .strategies import (
     vouchers,
 )
 
-TRANSIENT_ERROR = u"something went wrong, who knows what"
+TRANSIENT_ERROR = "something went wrong, who knows what"
 
 # Helper to work-around https://github.com/twisted/treq/issues/161
 def uncooperator(started=True):
@@ -137,7 +144,7 @@ def is_not_json(bytestring):
 
 def not_vouchers():
     """
-    Builds unicode strings which are not legal vouchers.
+    Builds text strings which are not legal vouchers.
     """
     return one_of(
         text().filter(
@@ -147,7 +154,7 @@ def not_vouchers():
             # Turn a valid voucher into a voucher that is invalid only by
             # containing a character from the base64 alphabet in place of one
             # from the urlsafe-base64 alphabet.
-            lambda voucher: u"/"
+            lambda voucher: "/"
             + voucher[1:],
         ),
     )
@@ -155,7 +162,7 @@ def not_vouchers():
 
 def is_urlsafe_base64(text):
     """
-    :param unicode text: A candidate unicode string to inspect.
+    :param str text: A candidate text string to inspect.
 
     :return bool: ``True`` if and only if ``text`` is urlsafe-base64 encoded
     """
@@ -174,13 +181,13 @@ def invalid_bodies():
         # The wrong key but the right kind of value.
         fixed_dictionaries(
             {
-                u"some-key": vouchers(),
+                "some-key": vouchers(),
             }
         ).map(dumps),
         # The right key but the wrong kind of value.
         fixed_dictionaries(
             {
-                u"voucher": one_of(
+                "voucher": one_of(
                     integers(),
                     not_vouchers(),
                 ),
@@ -242,7 +249,7 @@ def authorized_request(api_auth_token, agent, method, uri, headers=None, data=No
     else:
         headers = Headers(headers)
     headers.setRawHeaders(
-        u"authorization",
+        "authorization",
         [b"tahoe-lafs {}".format(api_auth_token)],
     )
     return agent.request(
@@ -323,24 +330,24 @@ class GetTokenCountTests(TestCase):
         ``get_token_count`` returns the integer value of the
         ``default-token-count`` item from the given configuration object.
         """
-        plugin_name = u"hello-world"
+        plugin_name = "hello-world"
         if token_count is None:
             expected_count = NUM_TOKENS
             token_config = {}
         else:
             expected_count = token_count
-            token_config = {u"default-token-count": u"{}".format(expected_count)}
+            token_config = {"default-token-count": "{}".format(expected_count)}
 
         config_text = config_string_from_sections(
             [
                 {
-                    u"storageclient.plugins." + plugin_name: token_config,
+                    "storageclient.plugins." + plugin_name: token_config,
                 }
             ]
         )
         node_config = config_from_string(
             self.useFixture(TempDir()).join(b"tahoe"),
-            u"tub.port",
+            "tub.port",
             config_text.encode("utf-8"),
         )
         self.assertThat(
@@ -492,7 +499,7 @@ class UnblindedTokenTests(TestCase):
         data = BytesIO(
             dumps(
                 {
-                    u"unblinded-tokens": list(
+                    "unblinded-tokens": list(
                         token.unblinded_token.decode("ascii")
                         for token in unblinded_tokens
                     )
@@ -514,7 +521,7 @@ class UnblindedTokenTests(TestCase):
             ),
         )
 
-        stored_tokens = root.controller.store.backup()[u"unblinded-tokens"]
+        stored_tokens = root.controller.store.backup()["unblinded-tokens"]
 
         self.assertThat(
             stored_tokens,
@@ -560,8 +567,8 @@ class UnblindedTokenTests(TestCase):
             b"http://127.0.0.1/unblinded-token",
         )
         self.addDetail(
-            u"requesting result",
-            text_content(u"{}".format(vars(requesting.result))),
+            "requesting result",
+            text_content("{}".format(vars(requesting.result))),
         )
         self.assertThat(
             requesting,
@@ -608,8 +615,8 @@ class UnblindedTokenTests(TestCase):
             b"http://127.0.0.1/unblinded-token?limit={}".format(limit),
         )
         self.addDetail(
-            u"requesting result",
-            text_content(u"{}".format(vars(requesting.result))),
+            "requesting result",
+            text_content("{}".format(vars(requesting.result))),
         )
         self.assertThat(
             requesting,
@@ -663,8 +670,8 @@ class UnblindedTokenTests(TestCase):
             ),
         )
         self.addDetail(
-            u"requesting result",
-            text_content(u"{}".format(vars(requesting.result))),
+            "requesting result",
+            text_content("{}".format(vars(requesting.result))),
         )
         self.assertThat(
             requesting,
@@ -674,7 +681,7 @@ class UnblindedTokenTests(TestCase):
                 AllMatch(
                     MatchesAll(
                         GreaterThan(position),
-                        IsInstance(unicode),
+                        IsInstance(str),
                     ),
                 ),
                 matches_lease_maintenance_spending(),
@@ -715,7 +722,7 @@ class UnblindedTokenTests(TestCase):
             )
             d.addCallback(readBody)
             d.addCallback(
-                lambda body: loads(body)[u"unblinded-tokens"],
+                lambda body: loads(body)["unblinded-tokens"],
             )
             return d
 
@@ -756,7 +763,7 @@ class UnblindedTokenTests(TestCase):
             succeeded(
                 MatchesPredicate(
                     check_tokens,
-                    u"initial, after (%s): initial[1:] != after",
+                    "initial, after (%s): initial[1:] != after",
                 ),
             ),
         )
@@ -803,7 +810,7 @@ class UnblindedTokenTests(TestCase):
         )
         d.addCallback(readBody)
         d.addCallback(
-            lambda body: loads(body)[u"lease-maintenance-spending"],
+            lambda body: loads(body)["lease-maintenance-spending"],
         )
         self.assertThat(
             d,
@@ -845,10 +852,10 @@ def succeeded_with_unblinded_tokens_with_matcher(
                 succeeded(
                     ContainsDict(
                         {
-                            u"total": Equals(all_token_count),
-                            u"spendable": match_spendable_token_count,
-                            u"unblinded-tokens": match_unblinded_tokens,
-                            u"lease-maintenance-spending": match_lease_maint_spending,
+                            "total": Equals(all_token_count),
+                            "spendable": match_spendable_token_count,
+                            "unblinded-tokens": match_unblinded_tokens,
+                            "lease-maintenance-spending": match_lease_maint_spending,
                         }
                     ),
                 ),
@@ -873,7 +880,7 @@ def succeeded_with_unblinded_tokens(all_token_count, returned_token_count):
         match_spendable_token_count=Equals(all_token_count),
         match_unblinded_tokens=MatchesAll(
             HasLength(returned_token_count),
-            AllMatch(IsInstance(unicode)),
+            AllMatch(IsInstance(str)),
         ),
         match_lease_maint_spending=matches_lease_maintenance_spending(),
     )
@@ -889,8 +896,8 @@ def matches_lease_maintenance_spending():
         Is(None),
         ContainsDict(
             {
-                u"when": matches_iso8601_datetime(),
-                u"amount": matches_positive_integer(),
+                "when": matches_iso8601_datetime(),
+                "amount": matches_positive_integer(),
             }
         ),
     )
@@ -905,11 +912,11 @@ def matches_positive_integer():
 
 def matches_iso8601_datetime():
     """
-    :return: A matcher which matches unicode strings which can be parsed as an
+    :return: A matcher which matches text strings which can be parsed as an
         ISO8601 datetime string.
     """
     return MatchesAll(
-        IsInstance(unicode),
+        IsInstance(str),
         AfterPreprocessing(
             parse_datetime,
             lambda d: Always(),
@@ -942,7 +949,7 @@ class VoucherTests(TestCase):
         )
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
-        data = BytesIO(dumps({u"voucher": voucher.decode("ascii")}))
+        data = BytesIO(dumps({"voucher": voucher.decode("ascii")}))
         requesting = authorized_request(
             api_auth_token,
             agent,
@@ -951,8 +958,8 @@ class VoucherTests(TestCase):
             data=data,
         )
         self.addDetail(
-            u"requesting result",
-            text_content(u"{}".format(vars(requesting.result))),
+            "requesting result",
+            text_content("{}".format(vars(requesting.result))),
         )
         self.assertThat(
             requesting,
@@ -983,8 +990,8 @@ class VoucherTests(TestCase):
             data=BytesIO(body),
         )
         self.addDetail(
-            u"requesting result",
-            text_content(u"{}".format(vars(requesting.result))),
+            "requesting result",
+            text_content("{}".format(vars(requesting.result))),
         )
         self.assertThat(
             requesting,
@@ -1006,7 +1013,7 @@ class VoucherTests(TestCase):
         )
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
-        url = u"http://127.0.0.1/voucher/{}".format(
+        url = "http://127.0.0.1/voucher/{}".format(
             quote(
                 not_voucher.encode("utf-8"),
                 safe=b"",
@@ -1043,7 +1050,7 @@ class VoucherTests(TestCase):
             api_auth_token,
             agent,
             b"GET",
-            u"http://127.0.0.1/voucher/{}".format(voucher).encode("ascii"),
+            "http://127.0.0.1/voucher/{}".format(voucher).encode("ascii"),
         )
         self.assertThat(
             requesting,
@@ -1235,7 +1242,7 @@ class VoucherTests(TestCase):
             agent,
             b"PUT",
             b"http://127.0.0.1/voucher",
-            data=BytesIO(dumps({u"voucher": voucher.decode("ascii")})),
+            data=BytesIO(dumps({"voucher": voucher.decode("ascii")})),
         )
         self.assertThat(
             putting,
@@ -1248,7 +1255,7 @@ class VoucherTests(TestCase):
             api_auth_token,
             agent,
             b"GET",
-            u"http://127.0.0.1/voucher/{}".format(
+            "http://127.0.0.1/voucher/{}".format(
                 quote(
                     voucher.encode("utf-8"),
                     safe=b"",
@@ -1292,7 +1299,7 @@ class VoucherTests(TestCase):
             vouchers,
             Equals(
                 {
-                    u"vouchers": list(
+                    "vouchers": list(
                         Voucher(
                             number=voucher,
                             expected_tokens=count,
@@ -1329,7 +1336,7 @@ class VoucherTests(TestCase):
             vouchers,
             Equals(
                 {
-                    u"vouchers": list(
+                    "vouchers": list(
                         Voucher(
                             number=voucher,
                             expected_tokens=count,
@@ -1362,7 +1369,7 @@ class VoucherTests(TestCase):
         note("{} vouchers".format(len(vouchers)))
 
         for voucher in vouchers:
-            data = BytesIO(dumps({u"voucher": voucher.decode("ascii")}))
+            data = BytesIO(dumps({"voucher": voucher.decode("ascii")}))
             putting = authorized_request(
                 api_auth_token,
                 agent,
@@ -1479,22 +1486,22 @@ def bad_calculate_price_requests():
 
     good_data = fixed_dictionaries(
         {
-            u"version": good_version,
-            u"sizes": good_sizes,
+            "version": good_version,
+            "sizes": good_sizes,
         }
     ).map(dumps)
 
     bad_data_version = fixed_dictionaries(
         {
-            u"version": bad_version,
-            u"sizes": good_sizes,
+            "version": bad_version,
+            "sizes": good_sizes,
         }
     ).map(dumps)
 
     bad_data_sizes = fixed_dictionaries(
         {
-            u"version": good_version,
-            u"sizes": bad_sizes,
+            "version": good_version,
+            "sizes": bad_sizes,
         }
     ).map(dumps)
 
@@ -1638,7 +1645,7 @@ class CalculatePriceTests(TestCase):
                 b"POST",
                 self.url,
                 headers={b"content-type": [b"application/json"]},
-                data=BytesIO(dumps({u"version": 1, u"sizes": sizes})),
+                data=BytesIO(dumps({"version": 1, "sizes": sizes})),
             ),
             succeeded(
                 matches_response(
@@ -1648,8 +1655,8 @@ class CalculatePriceTests(TestCase):
                         loads,
                         Equals(
                             {
-                                u"price": expected_price,
-                                u"period": 60 * 60 * 24 * 31 - min_time_remaining,
+                                "price": expected_price,
+                                "period": 60 * 60 * 24 * 31 - min_time_remaining,
                             }
                         ),
                     ),
@@ -1660,8 +1667,8 @@ class CalculatePriceTests(TestCase):
 
 def application_json():
     return AfterPreprocessing(
-        lambda h: h.getRawHeaders(u"content-type"),
-        Equals([u"application/json"]),
+        lambda h: h.getRawHeaders("content-type"),
+        Equals(["application/json"]),
     )
 
 
@@ -1701,8 +1708,8 @@ class _MatchResponse(object):
     def match(self, response):
         self._details.update(
             {
-                u"code": response.code,
-                u"headers": response.headers.getAllRawHeaders(),
+                "code": response.code,
+                "headers": response.headers.getAllRawHeaders(),
             }
         )
         return MatchesStructure(
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index e4f08b1ae621d7ca86d81a6698a768106eb1a176..1dc17948d60620eedc151506d8937610d9d715ec 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -43,6 +43,7 @@ from testtools.matchers import (
     Equals,
     HasLength,
     IsInstance,
+    Is,
     MatchesAll,
     MatchesStructure,
 )
@@ -73,6 +74,7 @@ from ..controller import (
     UnpaidRedeemer,
     UnrecognizedFailureReason,
     token_count_for_group,
+    bracket,
 )
 from ..model import DoubleSpend as model_DoubleSpend
 from ..model import Error as model_Error
@@ -1289,3 +1291,54 @@ def bad_content_type(request):
         b"Unsupported media type",
         b"Unsupported media type",
     ).render(request)
+
+
+class BracketTests(TestCase):
+    """
+    Tests for ``bracket``.
+    """
+    def test_success(self):
+        """
+        ``bracket`` calls ``first`` then ``between`` then ``last`` and returns a
+        ``Deferred`` that fires with the result of ``between``.
+        """
+        result = object()
+        actions = []
+        first = partial(actions.append, "first")
+        def between():
+            actions.append("between")
+            return result
+        last = partial(actions.append, "last")
+        self.assertThat(
+            bracket(first, last, between),
+            succeeded(
+                Is(result),
+            ),
+        )
+        self.assertThat(
+            actions,
+            Equals(["first", "between", "last"]),
+        )
+
+    def test_failure(self):
+        """
+        ``bracket`` calls ``first`` then ``between`` then ``last`` and returns a
+        ``Deferred`` that fires with the failure result of ``between``.
+        """
+        class SomeException(Exception):
+            pass
+        actions = []
+        first = partial(actions.append, "first")
+        def between():
+            actions.append("between")
+            raise SomeException()
+        last = partial(actions.append, "last")
+        self.assertThat(
+            bracket(first, last, between),
+            failed(
+                AfterPreprocessing(
+                    lambda failure: failure.value,
+                    IsInstance(SomeException),
+                ),
+            ),
+        )
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 93605cd83f2f373021707a0edb5cf7d49df81024..6c0e360cdd0ac810604fcb997fc394f4ee978d5b 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -269,7 +269,7 @@ class VoucherStoreTests(TestCase):
             ),
             Raises(
                 AfterPreprocessing(
-                    lambda (type, exc, tb): exc,
+                    lambda exc_info: exc_info[1],
                     MatchesAll(
                         IsInstance(StoreOpenError),
                         MatchesStructure(
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index f300d3a6ece15d8ba48479bb437c300a2057fc0d..56c63aa6f41b28b336d5410b95ee3946496f871c 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -17,6 +17,13 @@ Tests for the Tahoe-LAFS plugin.
 """
 
 from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
 
 from datetime import timedelta
 from functools import partial
@@ -99,7 +106,7 @@ from .strategies import (
     vouchers,
 )
 
-SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key")
+SIGNING_KEY_PATH = FilePath(__file__).sibling("testing-signing.key")
 
 
 def get_rref(interface=None):
@@ -281,12 +288,12 @@ class ServerPluginTests(TestCase):
         and an interval how often to do so, test that metrics are actually
         written there after the configured interval.
         """
-        metrics_path = self.useFixture(TempDir()).join(u"metrics")
+        metrics_path = self.useFixture(TempDir()).join("metrics")
         configuration = {
-            u"prometheus-metrics-path": metrics_path,
-            u"prometheus-metrics-interval": str(int(metrics_interval.total_seconds())),
-            u"ristretto-issuer-root-url": "foo",
-            u"ristretto-signing-key-path": SIGNING_KEY_PATH.path,
+            "prometheus-metrics-path": metrics_path,
+            "prometheus-metrics-interval": str(int(metrics_interval.total_seconds())),
+            "ristretto-issuer-root-url": "foo",
+            "ristretto-signing-key-path": SIGNING_KEY_PATH.path,
         }
         announceable = extract_result(
             storage_server.get_storage_server(
@@ -341,8 +348,8 @@ tahoe_configs_with_dummy_redeemer = tahoe_configs(client_dummyredeemer_configura
 
 tahoe_configs_with_mismatched_issuer = minimal_tahoe_configs(
     {
-        u"privatestorageio-zkapauthz-v1": just(
-            {u"ristretto-issuer-root-url": u"https://another-issuer.example.invalid/"}
+        "privatestorageio-zkapauthz-v1": just(
+            {"ristretto-issuer-root-url": "https://another-issuer.example.invalid/"}
         ),
     }
 )
@@ -401,8 +408,8 @@ class ClientPluginTests(TestCase):
         # switch to an io.StringIO here.
         config_text = StringIO()
         node_config.config.write(config_text)
-        self.addDetail(u"config", text_content(config_text.getvalue()))
-        self.addDetail(u"announcement", text_content(unicode(announcement)))
+        self.addDetail("config", text_content(config_text.getvalue()))
+        self.addDetail("announcement", text_content(str(announcement)))
         self.assertThat(
             lambda: storage_server.get_storage_client(
                 node_config,
@@ -521,12 +528,12 @@ class ClientPluginTests(TestCase):
         # tests, at least until creating a real server doesn't involve so much
         # complex setup.  So avoid using any of the client APIs that make a
         # remote call ... which is all of them.
-        pass_group = storage_client._get_passes(u"request binding message", num_passes)
+        pass_group = storage_client._get_passes("request binding message", num_passes)
         pass_group.mark_spent()
 
         # There should be no unblinded tokens left to extract.
         self.assertThat(
-            lambda: storage_client._get_passes(u"request binding message", 1),
+            lambda: storage_client._get_passes("request binding message", 1),
             raises(NotEnoughTokens),
         )
 
@@ -540,8 +547,8 @@ class ClientPluginTests(TestCase):
                         lambda logged_message: logged_message.message,
                         ContainsDict(
                             {
-                                u"message": Equals(u"request binding message"),
-                                u"count": Equals(num_passes),
+                                "message": Equals("request binding message"),
+                                "count": Equals(num_passes),
                             }
                         ),
                     ),
diff --git a/src/_zkapauthorizer/tests/test_pricecalculator.py b/src/_zkapauthorizer/tests/test_pricecalculator.py
index baadd9119d73a37988d90baa5bdb62f106405e73..2dd7ab918fbe0ca2736b62a73e92405cfcf38df3 100644
--- a/src/_zkapauthorizer/tests/test_pricecalculator.py
+++ b/src/_zkapauthorizer/tests/test_pricecalculator.py
@@ -17,6 +17,16 @@
 Tests for ``_zkapauthorizer.pricecalculator``.
 """
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from future.utils import PY2
+if PY2:
+    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401
+from past.builtins import long
+
 from functools import partial
 
 from hypothesis import given