diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
new file mode 100644
index 0000000..6517fd8
--- /dev/null
+++ b/.github/workflows/python-package.yml
@@ -0,0 +1,79 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
+
+name: Python package
+
+on:
+  push:
+    branches: [ "master" ]
+  pull_request:
+    branches: [ "master" ]
+
+jobs:
+  test:
+
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+        python-version:
+        - "3.7"
+        - "3.8"
+        - "3.9"
+        - "3.10"
+        - "3.11"
+        - "3.12"
+        - "pypy3.6"
+        - "pypy3.7"
+        - "pypy3.8"
+        - "pypy3.9"
+        - "pypy3.10"
+        include:
+        # Python 3.6 is not available on ubuntu-latest,
+        # so use ubuntu-20.04 for it:
+        - python-version: "3.6"
+          os: ubuntu-20.04
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v5
+      with:
+        python-version: ${{ matrix.python-version }}
+        allow-prereleases: true
+    - name: Install dependencies
+      run: |
+        python -m pip install -U pip
+        pip install .
+        pip install -r requirements-test.txt
+    - name: Test with pytest
+      run: |
+        pytest
+
+  lint:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+    - uses: actions/setup-python@v5
+      with:
+        python-version: "3.12"
+    - uses: pre-commit/action@v3.0.1
+
+  type:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+    - uses: actions/setup-python@v5
+      with:
+        python-version: "3.12"
+    - run: |
+        python -m pip install -U pip
+        pip install .
+        pip install -r requirements-type.txt
+    - run: |
+        mypy src/
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
deleted file mode 100644
index 3aab7ea..0000000
--- a/.github/workflows/pythonpackage.yml
+++ /dev/null
@@ -1,99 +0,0 @@
-# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-
-name: Python package
-
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    branches: [ master ]
-
-jobs:
-  build:
-
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        python-version:
-          - "3.7"
-          - "3.8"
-          - "3.9"
-          - "3.10"
-          - "3.11"
-          - "3.12"
-          - "pypy3.6"
-          - "pypy3.7"
-          - "pypy3.8"
-          - "pypy3.9"
-          - "pypy3.10"
-        os: [ubuntu-latest]
-        include:
-          # Python 3.6 is not available on Ubuntu 22.04,
-          # so use Ubuntu 20.04:
-          - python-version: "3.6"
-            os: ubuntu-20.04
-
-
-    steps:
-    - uses: actions/checkout@v4
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
-      with:
-        python-version: ${{ matrix.python-version }}
-        allow-prereleases: true
-    - uses: actions/cache@v2
-      with:
-        path: ~/.cache/pip
-        key: ${{ runner.os }}-pip-${{ matrix.python }}-${{ hashFiles('requirements*.txt') }}
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install flake8
-        pip install -e .
-        pip install -r requirements.txt
-        pip install -r requirements-test.txt
-    - name: Lint with flake8
-      run: |
-        # stop the build if there are Python syntax errors or undefined names
-        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
-        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-        flake8 . --count --exit-zero --statistics
-    - name: Test with pytest
-      run: |
-        pytest
-
-  linters:
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v2
-      - name: Set up Python
-        uses: actions/setup-python@v2
-        with:
-          python-version: "3.9"
-      - uses: actions/cache@v2
-        with:
-          path: ~/.cache/pip
-          key: ${{ runner.os }}-pip-${{ hashFiles('requirements-linters.txt') }}
-      - name: Install deps
-        run: |
-          python -m pip install --upgrade pip
-          pip install -r requirements-linters.txt
-      - name: Run isort
-        run: |
-          isort --version
-          isort -c .
-      - name: Run flake8
-        if: failure() || success()
-        run: |
-          flake8 --version
-          flake8
-      - name: Run check-manifest
-        if: failure() || success()
-        run: |
-          check-manifest
-      - uses: psf/black@stable
-        with:
-          version: "22.3.0"
-        if: failure() || success()
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 12c971e..7ed51d5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,13 @@
+default_language_version:
+    python: python3.12
 repos:
   - repo: 'https://github.com/pre-commit/pre-commit-hooks'
-    rev: v3.2.0
+    rev: v4.5.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
   - repo: 'https://github.com/PyCQA/flake8'
-    rev: 4.0.1
+    rev: 7.0.0
     hooks:
       - id: flake8
   - repo: 'https://github.com/pre-commit/mirrors-isort'
@@ -13,35 +15,22 @@ repos:
     hooks:
       - id: isort
   - repo: 'https://github.com/mgedmin/check-manifest'
-    rev: '0.46'
+    rev: '0.49'
     hooks:
       - id: check-manifest
   - repo: 'https://github.com/myint/autoflake'
-    rev: v1.4
+    rev: v2.3.0
     hooks:
       - id: autoflake
         args:
           - '--remove-all-unused-imports'
           - '-i'
-        language_version: python3.9
-  - repo: 'https://github.com/pre-commit/mirrors-autopep8'
-    rev: v1.5.7
-    hooks:
-      - id: autopep8
-        language_version: python3.9
-  - repo: https://github.com/psf/black
-    rev: 22.3.0
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 24.2.0
     hooks:
       - id: black
-        language_version: python3.9
-  - repo: https://github.com/ikamensh/flynt/
-    rev: '0.76'
-    hooks:
-      - id: flynt
-        language_version: python3.9
   - repo: https://github.com/asottile/pyupgrade
-    rev: v2.31.1
+    rev: v3.15.1
     hooks:
       - id: pyupgrade
         entry: pyupgrade --py3-plus --py36-plus --keep-runtime-typing
-        language_version: python3.9
diff --git a/pyproject.toml b/pyproject.toml
index 3472177..9e5b62a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,6 @@
 [tool.black]
 line-length = 120
-# required-version = '22.3.0' # see https://github.com/psf/black/issues/2493
-target-version = ['py39']
+target-version = ["py36", "py37", "py38", "py39", "py310", "py311", "py312"]
 
 [tool.isort]
 profile = "black"
diff --git a/requirements-linters.txt b/requirements-linters.txt
deleted file mode 100644
index 60d9024..0000000
--- a/requirements-linters.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-isort==5.10.1
-flake8==4.0.1
-check-manifest
diff --git a/requirements-type.txt b/requirements-type.txt
new file mode 100644
index 0000000..b055c06
--- /dev/null
+++ b/requirements-type.txt
@@ -0,0 +1,2 @@
+mypy>=1.8.0
+types-pytz
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 14cddf9..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-fluent.syntax>=0.14
-attrs==19.3.0
-babel==2.9.1
-pytz==2018.9
diff --git a/src/fluent_compiler/ast_compat.py b/src/fluent_compiler/ast_compat.py
index c82eb6b..bdac2e6 100644
--- a/src/fluent_compiler/ast_compat.py
+++ b/src/fluent_compiler/ast_compat.py
@@ -23,6 +23,7 @@ def NewAst(...):
       NewAst = ast.NewAst
 
 """
+
 import ast
 import sys
 
diff --git a/src/fluent_compiler/codegen.py b/src/fluent_compiler/codegen.py
index 2f2088c..3cd4f42 100644
--- a/src/fluent_compiler/codegen.py
+++ b/src/fluent_compiler/codegen.py
@@ -675,7 +675,7 @@ def as_ast(self):
         return ast.Name(id=self.name, ctx=ast.Load(), **DEFAULT_AST_ARGS)
 
     def __eq__(self, other):
-        return type(other) == type(self) and other.name == self.name
+        return isinstance(other, type(self)) and other.name == self.name
 
     def __repr__(self):
         return f"VariableReference({repr(self.name)})"
diff --git a/src/fluent_compiler/types.py b/src/fluent_compiler/types.py
index 177454e..5a49461 100644
--- a/src/fluent_compiler/types.py
+++ b/src/fluent_compiler/types.py
@@ -88,7 +88,6 @@ class NumberFormatOptions:
 
 
 class FluentNumber(FluentType):
-
     default_number_format_options = NumberFormatOptions()
 
     def __new__(cls, value, **kwargs):
diff --git a/src/fluent_compiler/utils.py b/src/fluent_compiler/utils.py
index 839df57..74a72ec 100644
--- a/src/fluent_compiler/utils.py
+++ b/src/fluent_compiler/utils.py
@@ -48,7 +48,6 @@ def attribute_ast_to_id(attribute, parent_ast):
 
 
 def allowable_name(ident, for_method=False, allow_builtin=False):
-
     if keyword.iskeyword(ident):
         return False
 
diff --git a/tests/format/test_escapers.py b/tests/format/test_escapers.py
index a6283bc..840dc99 100644
--- a/tests/format/test_escapers.py
+++ b/tests/format/test_escapers.py
@@ -40,6 +40,7 @@ def join(self, parts):
 # that, unlike HtmlEscaper above, the output type is not a subclass of
 # str, in order to test the implementation handles this properly.
 
+
 # We also test whether the implementation can handle subclasses
 class Markdown:
     def __init__(self, text):
diff --git a/tests/test_types.py b/tests/test_types.py
index 5cf392c..d768a19 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -14,7 +14,6 @@ def currency(amount, *args, **kwargs):
 
 
 class TestFluentNumber(unittest.TestCase):
-
     locale = Locale.parse("en_US")
 
     def setUp(self):
@@ -189,7 +188,6 @@ def test_copy_attributes(self):
 
 
 class TestFluentDate(unittest.TestCase):
-
     locale = Locale.parse("en_US")
 
     def setUp(self):
diff --git a/tox.ini b/tox.ini
index db945a9..cb7378a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,31 +1,24 @@
 # This config is for local testing. Where needed changes should be duplicated into
 # .github/workflows/pythonpackage.yml
 [tox]
-envlist = py36,py37,py38,py39,py310,py311,py312,pypy3.6,flake8,isort
+envlist =
+    py{36,37,38,39,310,311,312},
+    pypy{3.6,3.7,3.8,3.9,3.10},
+    lint,
+    type,
 # For Python 3.6 support:
 requires = virtualenv<20.22.0
 
 [testenv]
-deps =
-     # Just '.[develop]' would be nice here.
-     # Unfortunately it is super slow: https://github.com/pypa/pip/issues/2195
-     # So we duplicate deps from setup.py for now.
-     -r{toxinidir}/requirements.txt
-     -r{toxinidir}/requirements-test.txt
-     -r{toxinidir}/requirements-linters.txt
+deps = -r{toxinidir}/requirements-test.txt
 commands = pytest
 
+[testenv:lint]
+base_python = py312
+deps = pre-commit
+commands = pre-commit run --all-files --show-diff-on-failure
 
-[testenv:flake8]
-basepython = python3.9
-commands = flake8 src tests
-
-
-[testenv:isort]
-basepython = python3.9
-commands = isort -c src tests
-
-[testenv:check-manifest]
-basepython = python3.9
-deps = check-manifest
-commands = check-manifest
+[testenv:type]
+base_python = py312
+deps = -r{toxinidir}/requirements-type.txt
+commands = mypy src/