Skip to content

Commit 48cf866

Browse files
authored
Merge pull request #46 from michalmaj/chore/add-mypy
chore: add local mypy checking
2 parents 907033a + be78064 commit 48cf866

13 files changed

Lines changed: 456 additions & 25 deletions

‎CONTRIBUTING.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,9 @@ Delete the corresponding local branch after updating `main`.
171171

172172
## Documentation terminology
173173

174-
Use the official spelling of tool names: `uv` and pytest are lowercase, while
175-
Ruff is capitalized in prose. Keep command names and configuration keys in code
176-
format.
174+
Use the official spelling of tool names: `uv`, pytest, and mypy are lowercase,
175+
while Ruff is capitalized in prose. Keep command names and configuration keys
176+
in code format.
177177

178178
Call the ordered set of core chapters the **main learning path**. Call the
179179
supporting quickstarts, troubleshooting pages, and focused guides

‎README.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
[![CI](https://github.com/michalmaj/modern-python-project-guide/actions/workflows/ci.yml/badge.svg)](https://github.com/michalmaj/modern-python-project-guide/actions/workflows/ci.yml)
44

55
A practical, beginner-friendly guide to building a clean Python project with
6-
`uv`, `pytest`, Ruff, `pyproject.toml`, GitHub Actions, and a pull-request-based
7-
workflow.
6+
`uv`, `pytest`, Ruff, mypy, `pyproject.toml`, GitHub Actions, and a
7+
pull-request-based workflow.
88

99
This repository is both a tutorial and a working example. It introduces each
1010
tool gradually and shows how the pieces fit together in a maintainable project.
@@ -30,7 +30,8 @@ By following the guide, you will learn how to:
3030
- install a package in editable mode and build distributions,
3131
- test code with `pytest`,
3232
- lint and format code with Ruff,
33-
- run the same checks in GitHub Actions,
33+
- check type annotations with mypy,
34+
- run automated quality checks in GitHub Actions,
3435
- work with branches, commits, and pull requests.
3536

3637
You should already know basic Python, terminal usage, Git, and GitHub. Packaging,
@@ -51,7 +52,8 @@ understand why each tool and file is introduced.
5152
8. [Git, commits, branches, and pull requests](docs/07_git_commits_branches_prs.md)
5253
9. [Common beginner mistakes](docs/08_common_mistakes.md)
5354
10. [Building distributions](docs/10_building_distributions.md)
54-
11. [Project checklist](docs/09_checklist.md)
55+
11. [Static type checking with mypy](docs/11_type_checking_with_mypy.md)
56+
12. [Project checklist](docs/09_checklist.md)
5557

5658
## Reference material
5759

‎docs/01_project_structure.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ In this guide, it contains:
115115
- build system configuration,
116116
- dependency configuration,
117117
- pytest configuration,
118+
- mypy configuration,
118119
- Ruff configuration.
119120

120121
Keeping these settings together makes the project easier to inspect and maintain.

‎docs/02_uv.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ uv init --package
104104
uv sync
105105
uv add --group dev pytest
106106
uv add --group dev ruff
107+
uv add --group dev mypy
107108
uv run pytest
108109
uv run ruff check .
109110
uv run ruff format .
111+
uv run mypy
110112
uv build --no-sources
111113
```
112114

‎docs/03_pyproject_toml.md‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,19 @@ build-backend = "uv_build"
3939

4040
[dependency-groups]
4141
dev = [
42+
"mypy>=2.3.1",
4243
"pytest>=9.0.3",
4344
"ruff>=0.15.12",
4445
]
4546

4647
[tool.pytest.ini_options]
4748
testpaths = ["tests"]
4849

50+
[tool.mypy]
51+
python_version = "3.12"
52+
files = ["src", "tests"]
53+
strict = true
54+
4955
[tool.ruff]
5056
line-length = 88
5157
target-version = "py312"
@@ -62,7 +68,7 @@ select = [
6268
```
6369

6470
This describes the project, its build system, its development dependencies, and
65-
the settings used by pytest and Ruff.
71+
the settings used by pytest, mypy, and Ruff.
6672

6773
## The `[project]` section
6874

@@ -130,7 +136,7 @@ This is intentional.
130136

131137
The example package starts with standard library code only.
132138

133-
Development tools such as `pytest` and Ruff are stored separately in the
139+
Development tools such as `pytest`, mypy, and Ruff are stored separately in the
134140
`dev` dependency group.
135141

136142
## The `[build-system]` section
@@ -176,6 +182,7 @@ Development dependencies are needed only while developing the project.
176182
Examples of development dependencies:
177183

178184
- `pytest`,
185+
- `mypy`,
179186
- `ruff`,
180187
- test coverage tools,
181188
- type checkers,
@@ -196,6 +203,12 @@ The current file also contains sections such as:
196203

197204
and:
198205

206+
```toml
207+
[tool.mypy]
208+
```
209+
210+
and:
211+
199212
```toml
200213
[tool.ruff]
201214
```
@@ -217,7 +230,8 @@ This guide was built using a slower approach:
217230
5. add Ruff,
218231
6. configure linting and formatting,
219232
7. add continuous integration,
220-
8. add a build system and install the project.
233+
8. add a build system and install the project,
234+
9. add static type checking.
221235

222236
Each step should explain one idea clearly.
223237

‎docs/09_checklist.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,5 +252,5 @@ Let the project grow gradually.
252252

253253
## Continue
254254

255-
- [Previous: Building Distributions](10_building_distributions.md)
255+
- [Previous: Static Type Checking with mypy](11_type_checking_with_mypy.md)
256256
- [Back to README](../README.md)

‎docs/10_building_distributions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,5 @@ Treat publishing as a separate, deliberate operation.
163163
## Continue
164164

165165
- [Previous: Common Beginner Mistakes](08_common_mistakes.md)
166-
- [Next: Project Checklist](09_checklist.md)
166+
- [Next: Static Type Checking with mypy](11_type_checking_with_mypy.md)
167167
- [Back to README](../README.md)

‎docs/11_type_checking_with_mypy.md‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Static Type Checking with mypy
2+
3+
Python does not require type annotations to run a program.
4+
5+
This project still uses annotations because they document what values functions
6+
accept and return:
7+
8+
```python
9+
def count_words(text: str) -> int:
10+
...
11+
```
12+
13+
The annotation says that `text` should be a string and the result should be an
14+
integer.
15+
16+
## Type hints do not check themselves
17+
18+
Python normally does not reject a call just because it conflicts with an
19+
annotation. Type hints are metadata that tools can inspect.
20+
21+
A static type checker reads the code without running it and reports operations
22+
that are inconsistent with the annotations. This project uses mypy for that
23+
job.
24+
25+
Tests and type checking answer different questions:
26+
27+
- pytest runs code and checks observed behavior,
28+
- mypy analyzes possible value types before the code runs.
29+
30+
Both are useful, and neither replaces the other.
31+
32+
## Add mypy as a development dependency
33+
34+
mypy is needed while developing the project, not while using `text-toolkit` as
35+
a library. Add it to the `dev` dependency group:
36+
37+
```bash
38+
uv add --group dev mypy
39+
```
40+
41+
After cloning this repository, `uv sync` installs mypy with the other
42+
development dependencies.
43+
44+
## Configure mypy
45+
46+
The project keeps its mypy settings in `pyproject.toml`:
47+
48+
```toml
49+
[tool.mypy]
50+
python_version = "3.12"
51+
files = ["src", "tests"]
52+
strict = true
53+
```
54+
55+
These options mean:
56+
57+
- `python_version` checks code using Python 3.12 language rules,
58+
- `files` defines the directories checked when no paths are passed,
59+
- `strict` enables mypy's optional strictness checks.
60+
61+
Strict mode works well here because this is a small project whose functions and
62+
tests already have annotations. Adding strict mode to a larger existing project
63+
may require a gradual approach.
64+
65+
The exact checks included in strict mode can change between mypy releases. The
66+
project therefore runs mypy after dependency updates instead of assuming that a
67+
previous result will always stay valid.
68+
69+
## Run the type checker
70+
71+
Run:
72+
73+
```bash
74+
uv run mypy
75+
```
76+
77+
The `files` setting means that no paths are needed in the command. A successful
78+
check reports output similar to:
79+
80+
```text
81+
Success: no issues found in 4 source files
82+
```
83+
84+
## What an error looks like
85+
86+
Consider code that assigns the integer returned by `count_words` to a variable
87+
declared as a string:
88+
89+
```python
90+
word_count: str = count_words("clean Python project")
91+
```
92+
93+
mypy reports an incompatible assignment because the annotation and return type
94+
do not agree. The program does not need to run for mypy to find that mismatch.
95+
96+
Read the file path, line number, and error code in the message. Fix the type or
97+
the code rather than silencing the warning without understanding it.
98+
99+
## What mypy does not prove
100+
101+
A successful mypy check does not mean that the program is correct.
102+
103+
mypy does not replace:
104+
105+
- tests for runtime behavior,
106+
- Ruff checks for linting and formatting,
107+
- code review,
108+
- clear program design.
109+
110+
Type checking is one layer of feedback in the project workflow.
111+
112+
## Rule of thumb
113+
114+
Use type hints to describe interfaces.
115+
116+
Use mypy to check whether the code follows those descriptions.
117+
118+
Use pytest to check what the code actually does when it runs.
119+
120+
## Continue
121+
122+
- [Previous: Building Distributions](10_building_distributions.md)
123+
- [Next: Project Checklist](09_checklist.md)
124+
- [Back to README](../README.md)

‎docs/ROADMAP.md‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ These parts are already included in the repository.
8585
- [x] Check internal Markdown links with pytest
8686
- [x] Add whitespace edge cases to the example test suite
8787
- [x] Demonstrate pytest parametrization with related inputs
88+
- [x] Add mypy as a development dependency
89+
- [x] Configure strict local type checking
90+
- [x] Run type checks against source code and tests
8891

8992
### Continuous integration
9093

@@ -125,11 +128,16 @@ These parts are already included in the repository.
125128
- [x] Add previous and next navigation to the core chapters
126129
- [x] Review commit history and self-review coverage in the existing guides
127130
- [x] Standardize terminology and dependency examples across the documentation
131+
- [x] Explain type hints and static type checking with mypy
128132

129133
## Next
130134

131135
These are good candidates for upcoming small pull requests.
132136

137+
### Type checking
138+
139+
- [ ] Run mypy in CI and align workflow documentation
140+
133141
### Repository polish
134142

135143
- [ ] Add issue templates only if the repository starts receiving external feedback
@@ -150,13 +158,6 @@ These topics are intentionally not part of the first foundation.
150158

151159
They may be added later as separate chapters.
152160

153-
### Type checking
154-
155-
- [ ] Static type checking with Pyright or mypy
156-
- [ ] Explaining type hints vs type checking
157-
- [ ] Running type checks locally
158-
- [ ] Running type checks in CI
159-
160161
### Test coverage
161162

162163
- [ ] Add coverage tooling
@@ -190,15 +191,14 @@ They may be added later as separate chapters.
190191
- [ ] Explain when Docker is useful
191192
- [ ] Add Docker only if it solves a real problem for the guide
192193

193-
## Not planned for the first version
194+
## Not part of the original foundation
194195

195-
These are deliberately excluded from the main learning path:
196+
These topics were deliberately excluded from the original foundation:
196197

197198
- Docker,
198199
- publishing to PyPI,
199200
- release automation,
200201
- documentation site generation,
201-
- strict type checking,
202202
- pre-commit hooks,
203203
- coverage gates,
204204
- complex multi-job CI,

‎docs/common_questions.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,11 @@ Once readers understand these commands, adding `pre-commit` later will be easier
231231

232232
The guide intentionally avoids too much automation too early.
233233

234-
## Why not add type checking yet?
234+
## Why was type checking added after the foundation?
235235

236236
Type checking with tools such as Pyright or mypy can be valuable.
237237

238-
The example code already uses type hints, but this guide does not add a type checker in the first version.
238+
The example code used type hints before the guide introduced mypy.
239239

240240
That is intentional.
241241

@@ -248,7 +248,9 @@ Before adding type checking, the guide first explains:
248248
- formatting,
249249
- CI.
250250

251-
Type checking is a good candidate for a future chapter.
251+
Readers can now learn those foundations first and then continue to the
252+
[type-checking chapter](11_type_checking_with_mypy.md). This order keeps type
253+
hints and the tool that checks them from becoming another unexplained layer.
252254

253255
## Why not add coverage yet?
254256

0 commit comments

Comments
 (0)