diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index acebe2f..9821e0a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -5,5 +5,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: chartboost/ruff-action@v1
-      - uses: psf/black@stable
+      - name: Setup Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: 3.11
+      - name: Install tox
+        run: pip install tox
+      - name: Lint with ruff, black and mypy
+        run: tox -e lint
diff --git a/git_hg_sync/__main__.py b/git_hg_sync/__main__.py
index 5b773ae..334469f 100644
--- a/git_hg_sync/__main__.py
+++ b/git_hg_sync/__main__.py
@@ -1,5 +1,6 @@
 import argparse
 import sys
+import logging
 from pathlib import Path
 
 import sentry_sdk
@@ -45,7 +46,9 @@ def get_queue(config):
     )
 
 
-def start_app(config, logger, *, one_shot=False):
+def start_app(
+    config: Config, logger: logging.Logger, *, one_shot: bool = False
+) -> None:
     pulse_config = config.pulse
     connection = get_connection(pulse_config)
 
diff --git a/pyproject.toml b/pyproject.toml
index 90b60c2..b76d206 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,3 +11,11 @@ dependencies = ['kombu', 'mozillapulse', 'GitPython', 'mozlog', "pydantic", "sen
 
 [tool.ruff]
 line-length = 100
+
+[[tool.mypy.overrides]]
+module = [
+  'kombu.*',
+  'mozlog'
+]
+
+ignore_missing_imports = true
diff --git a/tests/conftest.py b/tests/conftest.py
index b3a43c6..cb84835 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,22 +5,20 @@
 
 
 @pytest.fixture(autouse=True)
-def mozlog_logging():
+def mozlog_logging() -> None:
     logger = mozlog.structuredlog.StructuredLogger("tests")
     mozlog.structuredlog.set_default_logger(logger)
 
 
 @pytest.fixture
-def pulse_config():
+def pulse_config() -> PulseConfig:
     return PulseConfig(
-        **{
-            "userid": "guest",
-            "host": "pulse",
-            "port": 5672,
-            "exchange": "exchange/guest/test",
-            "routing_key": "#",
-            "queue": "queue/guest/test",
-            "password": "guest",
-            "ssl": False,
-        }
+        userid="guest",
+        host="pulse",
+        port=5672,
+        exchange="exchange/guest/test",
+        routing_key="#",
+        queue="queue/guest/test",
+        password="guest",
+        ssl=False,
     )
diff --git a/tests/pulse_utils.py b/tests/pulse_utils.py
index 70c7408..739d1ac 100644
--- a/tests/pulse_utils.py
+++ b/tests/pulse_utils.py
@@ -2,15 +2,18 @@
 import sys
 from datetime import datetime
 from pathlib import Path
+from typing import Any
 
 import kombu
 
-from git_hg_sync.config import Config
+from git_hg_sync.config import Config, PulseConfig
 
 HERE = Path(__file__).parent
 
 
-def send_pulse_message(pulse_config, payload, purge=False):
+def send_pulse_message(
+    pulse_config: PulseConfig, payload: Any, purge: bool = False
+) -> None:
     """Send a pulse message
     The routing key will be constructed from the repository URL.
     The Pulse message will be constructed from the specified payload
@@ -22,7 +25,7 @@ def send_pulse_message(pulse_config, payload, purge=False):
     host = pulse_config.host
     port = pulse_config.port
     exchange = pulse_config.exchange
-    queue = pulse_config.queue
+    queue_name = pulse_config.queue
     print(f"connecting to pulse at {host}:{port} as {userid}")
 
     connection = kombu.Connection(
@@ -38,7 +41,7 @@ def send_pulse_message(pulse_config, payload, purge=False):
     with connection:
         ex = kombu.Exchange(exchange, type="direct")
         queue = kombu.Queue(
-            name=queue,
+            name=queue_name,
             exchange=exchange,
             routing_key=routing_key,
             durable=True,
diff --git a/tests/test_config.py b/tests/test_config.py
index b483fd9..ecab986 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -5,7 +5,7 @@
 HERE = Path(__file__).parent
 
 
-def test_load_config():
+def test_load_config() -> None:
     config = Config.from_file(HERE / "data" / "config.toml")
     assert not config.pulse.ssl
     assert config.mappings[0].destination.branch == "default"
diff --git a/tests/test_integration.py b/tests/test_integration.py
index ccc2e88..782b1dd 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -9,14 +9,14 @@
 from utils import hg_export_tip
 
 from git_hg_sync.__main__ import get_connection, get_queue, start_app
-from git_hg_sync.config import Config
+from git_hg_sync.config import Config, PulseConfig
 
 NO_RABBITMQ = not os.getenv("RABBITMQ") == "true"
 HERE = Path(__file__).parent
 
 
 @pytest.mark.skipif(NO_RABBITMQ, reason="This test doesn't work without rabbitMq")
-def test_send_and_receive(pulse_config):
+def test_send_and_receive(pulse_config: PulseConfig) -> None:
 
     payload = {
         "type": "tag",
diff --git a/tests/test_pulse_worker.py b/tests/test_pulse_worker.py
index 58fa223..24bdb1d 100644
--- a/tests/test_pulse_worker.py
+++ b/tests/test_pulse_worker.py
@@ -29,13 +29,13 @@ def raw_tag_entity():
     }
 
 
-def test_parse_entity_valid():
+def test_parse_entity_valid() -> None:
     push_entity = PulseWorker.parse_entity(raw_push_entity())
     assert isinstance(push_entity, Push)
     tag_entity = PulseWorker.parse_entity(raw_tag_entity())
     assert isinstance(tag_entity, Tag)
 
 
-def test_parse_invalid_type():
+def test_parse_invalid_type() -> None:
     with pytest.raises(EntityTypeError):
         PulseWorker.parse_entity({"type": "unknown"})
diff --git a/tests/test_repo_synchronizer.py b/tests/test_repo_synchronizer.py
index bd734dd..31746ce 100644
--- a/tests/test_repo_synchronizer.py
+++ b/tests/test_repo_synchronizer.py
@@ -6,7 +6,7 @@
 from utils import hg_export_tip
 
 from git_hg_sync.__main__ import get_connection, get_queue
-from git_hg_sync.config import TrackedRepository
+from git_hg_sync.config import TrackedRepository, PulseConfig
 from git_hg_sync.repo_synchronizer import RepoSynchronizer
 
 
@@ -46,7 +46,7 @@ def test_sync_process_(
     assert "FOO CONTENT" in hg_export_tip(hg_remote_repo_path)
 
 
-def test_get_connection_and_queue(pulse_config):
+def test_get_connection_and_queue(pulse_config: PulseConfig) -> None:
     connection = get_connection(pulse_config)
     queue = get_queue(pulse_config)
     assert connection.userid == pulse_config.userid
diff --git a/tests/utils.py b/tests/utils.py
index c32a216..bd085ed 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,7 +1,8 @@
+from pathlib import Path
 import subprocess
 
 
-def hg_export_tip(repo_path: str):
+def hg_export_tip(repo_path: Path):
     process = subprocess.run(
         ["hg", "export", "tip"],
         cwd=repo_path,
diff --git a/tox.ini b/tox.ini
index f05ed26..baf2429 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,6 +19,10 @@ description = lint source code
 deps =
     ruff
     black
+    mypy
+    pytest # dev dependency
+ignore_errors = true # Run commands even if an earlier command failed
 commands =
     ruff check git_hg_sync/ tests/
     black --check git_hg_sync tests
+    mypy git_hg_sync/ tests/