diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6240e9796d493c3369d2cf88e6ba4ff368c0462..ac84d1fefda30c60351a7a760aee575f96f9cdbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ changed functionality, fixed bugs).
   already is such a file with an alive PID.
 - Log lines produced by subprocesses are now prefixed with 'PID=nnn'.
 - Moved from pip-installing requirements.txt to Pipenv.
+- Upgraded Python from 3.5 to 3.7
 
 
 ## Version 2.1.0 (2018-01-04)
diff --git a/Pipfile b/Pipfile
index 531fd7ca5ed0d4fb1999db0fced2ff38bdf5c0c0..8742be27630f0c81b28fe915cc53bba64b2bbe4a 100644
--- a/Pipfile
+++ b/Pipfile
@@ -9,7 +9,7 @@ requests = "*"
 psutil = "*"
 
 [dev-packages]
-pytest = "*"
+pytest = "<4"  # pytest-cov uses deprecated function (removed in pytest 4) when using --no-cov
 pytest-cov = "*"
 wheel = "*"
 pyinstaller = "*"
@@ -18,4 +18,4 @@ pathlib2 = {version = "*", markers="python_version < '3.6'"}
 mypy = "*"
 
 [requires]
-python_version = "3.5"
+python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index cbb38b1488283f7eee45dbd3afca6a9503ed87f4..39a4591cf071e111f9fb6e6183b700345cf54d67 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,11 +1,11 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "e426596954522d58f3248f5f4651067486da5aff3799d5568f8843289fdc4de8"
+            "sha256": "b0736d28b87f6c3f3541d2bab2740817ad8f51db7f8e92ce7bd6068ab2ac32c8"
         },
         "pipfile-spec": 6,
         "requires": {
-            "python_version": "3.5"
+            "python_version": "3.7"
         },
         "sources": [
             {
@@ -289,11 +289,11 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb",
-                "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7"
+                "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec",
+                "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"
             ],
             "index": "pypi",
-            "version": "==4.0.0"
+            "version": "==3.10.1"
         },
         "pytest-cov": {
             "hashes": [
diff --git a/flamenco_worker/upstream_update_queue.py b/flamenco_worker/upstream_update_queue.py
index e33e3ea57363b99110169ef79a6aa310c4b0cf8d..280be06abd034ff11b7412c599b757199c149fb7 100644
--- a/flamenco_worker/upstream_update_queue.py
+++ b/flamenco_worker/upstream_update_queue.py
@@ -134,7 +134,7 @@ class TaskUpdateQueue:
         Returns True iff the queue was empty, even before flushing.
         """
 
-        with (await self._queue_lock):
+        async with self._queue_lock:
             queue_is_empty = True
             queue_size_before = self.queue_size()
             handled = 0
diff --git a/flamenco_worker/worker.py b/flamenco_worker/worker.py
index c3b87dfd1e727da9ddfc8be76fa7a24a4e1206a1..4b60c7eba3e0b2d7b78e3dd3ae3a1555ab3e1602 100644
--- a/flamenco_worker/worker.py
+++ b/flamenco_worker/worker.py
@@ -49,10 +49,10 @@ class WorkerState(enum.Enum):
     SHUTTING_DOWN = 'shutting-down'
 
 
-@attr.s()
+@attr.s(auto_attribs=True)
 class PreTaskCheckParams:
-    pre_task_check_write = attr.ib(validator=attr.validators.instance_of(tuple), factory=tuple)
-    pre_task_check_read = attr.ib(validator=attr.validators.instance_of(tuple), factory=tuple)
+    pre_task_check_write: typing.List[str] = []
+    pre_task_check_read: typing.List[str] = []
 
 
 class PreTaskCheckFailed(PermissionError):
@@ -381,7 +381,7 @@ class FlamencoWorker:
         # Prevent outgoing queue overflowing by waiting until it's below the
         # threshold before starting another task.
         # TODO(sybren): introduce another worker state for this, and handle there.
-        with (await self._queue_lock):
+        async with self._queue_lock:
             queue_size = self.tuqueue.queue_size()
         if queue_size > QUEUE_SIZE_THRESHOLD:
             self._log.info('Task Update Queue size too large (%d > %d), waiting until it shrinks.',
@@ -474,7 +474,7 @@ class FlamencoWorker:
                 # Such a failure will always result in a failed task, even when
                 # self.failures_are_acceptable = True; only expected failures are
                 # acceptable then.
-                with (await self._queue_lock):
+                async with self._queue_lock:
                     self._queued_log_entries.append(traceback.format_exc())
                 await self.register_task_update(
                     task_status='failed',
@@ -533,7 +533,7 @@ class FlamencoWorker:
         if self._push_act_to_manager is not None:
             self._push_act_to_manager.cancel()
 
-        with (await self._queue_lock):
+        async with self._queue_lock:
             if self._queued_log_entries:
                 payload['log'] = '\n'.join(self._queued_log_entries)
                 self._queued_log_entries.clear()
@@ -596,7 +596,7 @@ class FlamencoWorker:
             log_entry %= fmt_args
 
         now = datetime.datetime.now(tz.tzutc()).isoformat()
-        with (await self._queue_lock):
+        async with self._queue_lock:
             self._queued_log_entries.append('%s: %s' % (now, log_entry))
             queue_size = len(self._queued_log_entries)
 
diff --git a/setup.cfg b/setup.cfg
index 8a274d060a80911bee79825592bba41437583418..dde7961b0a6cac4da25f1f881342d573c31eb8ab 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,7 +2,7 @@
 addopts = -v --cov flamenco_worker --cov-report term-missing --ignore node_modules
 
 [mypy]
-python_version = 3.5
+python_version = 3.7
 warn_unused_ignores = True
 ignore_missing_imports = True
 follow_imports = skip