Type Checking (mypy)¶
This directory documents how OpenContracts' Python type-checking pipeline is wired up and how to graduate modules out of the initial baseline.
How it is wired¶
- Configuration:
mypy.iniat the repo root. (Pulled out ofsetup.cfgbecause the per-module baseline list below is large.) python_version = 3.12(matches the runtime — Docker images and CI both run 3.12.x). This must not be lowered below the runtime: mypy validates third-party stub syntax against this target, so a stale older value makes it reject modern stubs (e.g. numpy's PEP 695typealiases) and abort before any project code is checked.plugins = mypy_django_plugin.main, mypy_drf_plugin.main— Django- and DRF-aware type inference (models, querysets, serializers, etc.).django_settings_module = config.settings.mypy— a thin wrapper aroundconfig.settings.testthat supplies a dummyDATABASE_URLso mypy runs without needing an env var. The plugin introspectsINSTALLED_APPS, model fields, reverse relations, etc. but never actually connects.check_untyped_defs,warn_unused_ignores,warn_redundant_casts, andwarn_unused_configsare on so the bar rises as modules graduate.ignore_missing_imports = True— most ML/pipeline deps (pdfplumber,docling,sentence-transformers, …) ship no stubs.- Pre-commit hook:
.pre-commit-config.yamlrunspre-commit/mirrors-mypywith stubs + the minimum Django runtime pinned inadditional_dependenciesso contributors don't need the full dev env installed locally. All deps are version-pinned — pre-commit autoupdate only bumpsrev, so leaving stubs unpinned would let them drift independently on every weekly refresh. - CI:
.github/workflows/backend.ymlrunspython -m mypy --config-file mypy.ini opencontractserver configas part of thelinterjob. The precedingInstall dependenciesstep pip-installsrequirements/local.txt, which pinsmypy,django-stubs, anddjangorestframework-stubsalongside the Django runtime — so the plugin has everything it needs in the runner env.
How to run mypy locally¶
Inside the Django Docker container (recommended)¶
The test container already has every runtime dep installed, which keeps the Django plugin happy.
docker compose -f test.yml run --rm django \
python -m mypy --config-file mypy.ini opencontractserver config
Via pre-commit (isolated env)¶
pre-commit run mypy --all-files
Pre-commit builds its own virtualenv from the hook's additional_dependencies on first run (a few minutes) and caches it afterwards.
Via your own dev virtualenv¶
pip install -r requirements/local.txt
python -m mypy --config-file mypy.ini opencontractserver config
No DATABASE_URL env var is required — config.settings.mypy bakes in a dummy one for type-checking only.
Why there is a baseline¶
As of issue #1331 the codebase has ~7.2k pre-existing mypy errors across 357 files (see mypy_baseline.txt). Forcing all of those to be fixed in one go would block the rest of the remediation work.
What the baseline does (and does not) gate¶
mypy.ini lists every file that had an error at the time of the initial wire-up under its own [mypy-<module.path>] section with ignore_errors = True. There is no wildcard pattern covering opencontractserver.* or config.*.
That matters because:
- New files ARE type-checked. A brand-new module at
opencontractserver/new_feature/views.pyis not in the baseline, so mypy will check it and the hook / CI will fail on any errors. - Refactoring an existing baselined file does not silently re-silence it. If a module is renamed or moved, its old
[mypy-…]section becomes dead (warn_unused_configsflags this) and the new path is checked from scratch. - Existing baselined files are silenced. Any error inside
opencontractserver/utils/storages.py(for example) is suppressed until that module is graduated.
Baseline shape (issue #1331)¶
| Metric | Value |
|---|---|
| Files with errors | 357 |
| Total error messages | 7208 |
| Top error code | attr-defined (2403) |
| Next most common | union-attr (355), arg-type (159), assignment (83), valid-type (82), misc (81) |
| Worst offender (file) | opencontractserver/mcp/tests/test_mcp.py (319 errors) |
The full breakdown lives in mypy_baseline.txt (sorted by file + line for stable diffs).
How to graduate a module out of the baseline¶
-
Delete the module's section in
mypy.ini. Example — to graduateopencontractserver/constants/annotations.py:# Before [mypy-opencontractserver.constants.annotations] ignore_errors = True # After (section removed) -
Run mypy and fix what surfaces:
python -m mypy --config-file mypy.ini opencontractserver config -
Prune the corresponding lines from
docs/typing/mypy_baseline.txt. This file is an advisory reference — it is not consumed by mypy itself, so it must be pruned manually. Reviewers: before approving a graduation PR, verify that: - Every pruned entry references the module being graduated (no unrelated drive-by deletions).
-
Every entry for the graduated module has been pruned (no leftover lines that would silently rot in the reference file).
-
Commit with a message referencing the tracker issue, e.g.
typing: graduate opencontractserver.constants.annotations (refs #1331).
Order we'd suggest graduating in¶
Pick small, leaf-ish modules first so the Django plugin has less surface to cover:
opencontractserver/constants/*— pure Python constants, no imports.opencontractserver/utils/*helpers that don't touch models.config/settings/*— narrow and rarely changed.- Apps with small
models.pyfootprints (e.g.feedback,discovery). - Work outwards into GraphQL types and views.
How to suppress a single error¶
Prefer fixing the error. When that isn't realistic in the current PR:
foo = something_mypy_doesnt_like() # type: ignore[assignment]
Always pass the error code in brackets — warn_unused_ignores is on, so bare # type: ignore comments will themselves produce errors once the surrounding module graduates.
If a whole module can't be fixed yet but you want to leave a trail:
[mypy-opencontractserver.legacy_thing]
ignore_errors = True
…with a comment pointing at the tracker issue that will remove it.
Troubleshooting¶
Error constructing plugin instance of NewSemanalDjangoPlugin— the Django plugin importsconfig.settings.mypy, which transitively imports most ofINSTALLED_APPS. Missing one of those apps (e.g.celery,channels,django-storages) is almost always the cause. Install the missing dep or run mypy from inside the test container.Set the DATABASE_URL environment variable— you're pointing mypy atconfig.settings.testor.baseinstead ofconfig.settings.mypy. Double-checkdjango_settings_moduleinmypy.ini.Library stubs not installed for "requests"— addtypes-requeststo the hook'sadditional_dependencies(and to the dev env if you want the error to go away locally outside pre-commit).