Skip to content

Commit b0386b3

Browse files
Naaremanclaude
andcommitted
Add common mistakes reference + token budget script
- New references/07-common-mistakes.md: 15 inline anti-patterns in SKILL.md with full reference doc covering structure, imports, deps, publishing, testing - New scripts/count-tokens.py: token/line budget checker (tiktoken or estimate) - Remove LICENSE (not needed for Claude skills) - Update README with new file structure and budget section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ddcf260 commit b0386b3

File tree

5 files changed

+445
-33
lines changed

5 files changed

+445
-33
lines changed

LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,32 @@ Before diving into details, you should be able to see the whole thing working en
9292
pyckage/
9393
├── SKILL.md # Main skill definition (philosophy + routing)
9494
├── README.md # This file
95-
├── LICENSE # MIT
96-
└── references/
97-
├── 01-scaffold.md # Package scaffolding (the "whole game")
98-
├── 02-api-design.md # Naming, messages, errors
99-
├── 03-testing.md # pytest conventions and patterns
100-
├── 04-docs.md # Docstrings + mkdocs-material
101-
├── 05-lifecycle.md # Deprecation ceremony + versioning
102-
└── 06-release.md # PyPI publishing + GitHub Actions
95+
├── references/
96+
│ ├── 01-scaffold.md # Package scaffolding (the "whole game")
97+
│ ├── 02-api-design.md # Naming, messages, errors
98+
│ ├── 03-testing.md # pytest conventions and patterns
99+
│ ├── 04-docs.md # Docstrings + mkdocs-material
100+
│ ├── 05-lifecycle.md # Deprecation ceremony + versioning
101+
│ ├── 06-release.md # PyPI publishing + GitHub Actions
102+
│ └── 07-common-mistakes.md # Python packaging anti-patterns
103+
└── scripts/
104+
└── count-tokens.py # Token budget checker
103105
```
104106

105-
## Contributing
107+
## Token Budget
106108

107-
This project is opinionated by design. If you think a convention is wrong, open an issue — but bring a reason, not just a preference. The goal is a *philosophy*, not a menu of options.
109+
pyckage follows [posit-dev/skills](https://github.com/posit-dev/skills) conventions for skill size:
110+
111+
- SKILL.md description: under 100 tokens
112+
- SKILL.md body: under 5,000 tokens / 500 lines
113+
- Reference files: loaded on demand (no hard limit)
114+
115+
Check with:
116+
```bash
117+
python3 scripts/count-tokens.py .
118+
# Install tiktoken for exact counts: pip install tiktoken
119+
```
108120

109-
## License
121+
## Contributing
110122

111-
MIT
123+
This project is opinionated by design. If you think a convention is wrong, open an issue — but bring a reason, not just a preference. The goal is a *philosophy*, not a menu of options.

SKILL.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,33 @@ When invoked without a subcommand (auto-triggered or plain `/pyckage`), use the
139139

140140
---
141141

142+
## Common Mistakes — Catch These Early
143+
144+
When reviewing or generating Python package code, watch for these. If you see any, fix them
145+
immediately and explain why.
146+
147+
| Mistake | Why it's bad | Fix |
148+
|---|---|---|
149+
| Flat layout (no `src/`) | Imports source dir instead of installed package — tests pass locally, fail for users | Always use `src/` layout |
150+
| `from .module import *` in `__init__.py` | Slow imports, polluted namespace, no control over public API | Explicit imports + `__all__` |
151+
| `--cov=src` in pytest | Measures directory path, not importable module — confusing reports | `--cov=my_package` (the importable name) |
152+
| `dependencies = ["requests"]` (no version) | Breaking release silently breaks your package | Lower bound: `"requests>=2.28"` |
153+
| `dependencies = ["requests>=2.28,<3"]` | Blocks users from upgrading — #1 cause of dependency conflicts | Lower bound only for libraries |
154+
| `requires-python = ">=3.10,<3.13"` | Blocks install on new Python versions that almost certainly work | Lower bound only: `">=3.10"` |
155+
| `__version__ = "0.1.0"` hardcoded in two places | Version drift between pyproject.toml and code | Use `importlib.metadata.version()` |
156+
| Missing `py.typed` marker | Type checkers ignore all your annotations for downstream users | `touch src/my_package/py.typed` |
157+
| `print()` for user messages | Not styled, not catchable, not centralized | Use `_messages.py` with `rich` |
158+
| `raise Exception("bad")` | Too broad — users can't catch specific errors | Custom exception hierarchy in `errors.py` |
159+
| Dev deps in `[project] dependencies` | Users get pytest, ruff installed as transitive deps | Use `[dependency-groups]` (PEP 735) |
160+
| `tests/__init__.py` exists | Confuses package discovery, may ship tests in wheel | Remove it — use `conftest.py` instead |
161+
| `MANIFEST.in` for wheel contents | MANIFEST.in only affects sdist, not wheels | Use build backend config for wheel contents |
162+
| Pinned deps in library (`"requests==2.31.0"`) | Impossible to install alongside anything else needing requests | Pins are for apps (lock files), not libraries |
163+
| `mkdocs.yml` inside `docs/` | `mkdocs serve` looks in project root by default | Keep `mkdocs.yml` at project root |
164+
165+
For the full list with detailed explanations, see [references/07-common-mistakes.md](references/07-common-mistakes.md).
166+
167+
---
168+
142169
## Tone When Helping
143170

144171
- Be opinionated. This skill exists to have opinions. Don't offer five options when one is right.

references/07-common-mistakes.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# 07 — Common Mistakes in Python Packaging
2+
3+
These are real-world mistakes that trip up both beginners and experienced developers.
4+
When reviewing or writing package code, check for these actively.
5+
6+
---
7+
8+
## Table of Contents
9+
10+
- [Project Structure](#project-structure)
11+
- [pyproject.toml Configuration](#pyprojecttoml-configuration)
12+
- [Imports](#imports)
13+
- [Version Management](#version-management)
14+
- [Dependencies](#dependencies)
15+
- [Files Missing from Builds](#files-missing-from-builds)
16+
- [Publishing](#publishing)
17+
- [Testing](#testing)
18+
19+
---
20+
21+
## Project Structure
22+
23+
### Using flat layout instead of src layout
24+
```
25+
# BAD — source is importable from project root
26+
my-package/
27+
├── my_package/ ← Python imports this instead of installed version
28+
├── tests/
29+
└── pyproject.toml
30+
31+
# GOOD — src layout prevents accidental imports
32+
my-package/
33+
├── src/
34+
│ └── my_package/
35+
├── tests/
36+
└── pyproject.toml
37+
```
38+
**Why:** The current working directory is first on `sys.path`. Without `src/`, tests pass
39+
locally but the installed package may be broken (missing files, wrong imports).
40+
41+
### Including tests/ in the wheel
42+
**Mistake:** Build includes a top-level `tests/` in the wheel.
43+
**Why:** Writes to `site-packages/tests/`, colliding with other packages.
44+
**Fix:** With hatchling, use `[tool.hatch.build.targets.wheel] exclude = ["tests"]`.
45+
46+
### Putting `__init__.py` in tests/
47+
**Why:** Confuses package discovery. Tests should never be importable.
48+
**Fix:** Remove it. Use `conftest.py` for shared fixtures.
49+
50+
---
51+
52+
## pyproject.toml Configuration
53+
54+
### Missing `[build-system]` table
55+
**Why:** Build tools fall back to legacy setuptools behavior, which is unpredictable.
56+
**Fix:** Always include:
57+
```toml
58+
[build-system]
59+
requires = ["hatchling"]
60+
build-backend = "hatchling.build"
61+
```
62+
63+
### Deprecated license table format
64+
**Mistake:** `license = {text = "MIT"}` or `license = {file = "LICENSE"}`.
65+
**Why:** PEP 639 deprecated the table format.
66+
**Fix:** Use `license = "MIT"` (SPDX string) + `license-files = ["LICENSE"]`.
67+
68+
### Not reinstalling after changing pyproject.toml
69+
**Why:** `pyproject.toml` is read at install time. Changes are ignored until reinstall.
70+
**Fix:** Run `uv sync` or `uv pip install -e .` after changes.
71+
72+
---
73+
74+
## Imports
75+
76+
### Running modules directly instead of `python -m`
77+
**Mistake:** `python src/mypackage/cli.py`
78+
**Why:** Sets `__package__` to `None`, breaking all relative imports.
79+
**Fix:** Use `python -m mypackage.cli`, or define entry points:
80+
```toml
81+
[project.scripts]
82+
mycli = "mypackage.cli:main"
83+
```
84+
85+
### "Import the world" in `__init__.py`
86+
```python
87+
# BAD
88+
from .models import *
89+
from .utils import *
90+
from .core import *
91+
92+
# GOOD
93+
from my_package.core import read_data, write_data
94+
from my_package.errors import MyPackageError
95+
96+
__all__ = ["read_data", "write_data", "MyPackageError"]
97+
```
98+
**Why:** Slows import time, wastes memory, pollutes namespace.
99+
100+
### Missing `__all__` for re-exports
101+
**Why:** Type checkers (mypy, pyright) treat imports without `__all__` as private.
102+
Users get "module has no attribute" type errors.
103+
104+
### `TYPE_CHECKING` guard without string annotations
105+
```python
106+
# BAD — crashes at runtime
107+
from typing import TYPE_CHECKING
108+
if TYPE_CHECKING:
109+
from .models import User
110+
111+
def get_user() -> User: ... # NameError!
112+
113+
# GOOD — use string annotation or future annotations
114+
from __future__ import annotations
115+
116+
from typing import TYPE_CHECKING
117+
if TYPE_CHECKING:
118+
from .models import User
119+
120+
def get_user() -> User: ... # works
121+
```
122+
123+
---
124+
125+
## Version Management
126+
127+
### Version in multiple places
128+
**Mistake:** Hardcoding in both `pyproject.toml` and `__init__.py`.
129+
**Fix:** Single source:
130+
```python
131+
# src/my_package/__init__.py
132+
from importlib.metadata import version
133+
__version__ = version("my-package")
134+
```
135+
136+
### Dynamic version that isn't a string literal
137+
**Mistake:** `__version__ = f"{major}.{minor}.{patch}"` or `__version__ = get_version()`.
138+
**Why:** Build backends parse source statically — they won't execute your code.
139+
**Fix:** Always a plain string: `__version__ = "1.2.3"` (if not using importlib.metadata).
140+
141+
### importlib.metadata with no fallback
142+
**Mistake:** Fails when code is copied without installing (Lambda, Docker, vendoring).
143+
**Fix:**
144+
```python
145+
try:
146+
from importlib.metadata import version
147+
__version__ = version("my-package")
148+
except Exception:
149+
__version__ = "0.0.0"
150+
```
151+
152+
---
153+
154+
## Dependencies
155+
156+
### No version bounds
157+
**Mistake:** `dependencies = ["requests", "pydantic"]`
158+
**Fix:** Lower bounds: `dependencies = ["requests>=2.28", "pydantic>=2.0"]`
159+
160+
### Overly strict upper bounds
161+
**Mistake:** `dependencies = ["requests>=2.28,<3"]`
162+
**Why:** When a compatible release comes out, your package blocks users from upgrading.
163+
This is the single biggest cause of dependency resolution conflicts in Python.
164+
**Fix:** Lower bounds only for libraries. Reserve upper bounds for *known* incompatibilities.
165+
166+
### Upper bound on `requires-python`
167+
**Mistake:** `requires-python = ">=3.10,<3.13"`
168+
**Why:** pip/uv will refuse to install on Python 3.13 even though it almost certainly works.
169+
**Fix:** `requires-python = ">=3.10"`
170+
171+
### Pinned exact versions in library deps
172+
**Mistake:** `dependencies = ["requests==2.31.0"]`
173+
**Why:** Makes it impossible to install alongside other packages needing a different version.
174+
**Fix:** Pins are for applications (lock files), not libraries.
175+
176+
### Dev deps in `[project] dependencies`
177+
**Mistake:** pytest, ruff, mypy in `dependencies`.
178+
**Why:** Users installing your library get all your dev tools as transitive deps.
179+
**Fix:** Use `[dependency-groups]` (PEP 735):
180+
```toml
181+
[dependency-groups]
182+
dev = ["pytest>=8.0", "ruff>=0.4", "mypy>=1.0"]
183+
```
184+
185+
---
186+
187+
## Files Missing from Builds
188+
189+
### `py.typed` not shipping in wheel
190+
**Why:** Type checkers treat your package as untyped and ignore all annotations.
191+
**Fix:** Verify: `unzip -l dist/*.whl | grep py.typed`
192+
193+
### Non-Python data files missing
194+
**Mistake:** JSON schemas, templates, SQL files at project root instead of inside package dir.
195+
**Fix:** Put data files inside `src/my_package/`. Hatchling includes them by default.
196+
197+
### Confusing MANIFEST.in with wheel contents
198+
**Why:** `MANIFEST.in` only controls sdist (source distribution). Zero effect on wheels.
199+
**Fix:** Use build backend config for wheel contents.
200+
201+
---
202+
203+
## Publishing
204+
205+
### Not building both sdist and wheel
206+
**Why:** Without wheel: slow source builds. Without sdist: users can't audit source.
207+
**Fix:** `uv build` produces both by default.
208+
209+
### Classifiers not matching `requires-python`
210+
**Mistake:** `requires-python = ">=3.10"` but classifiers only list 3.10, 3.11.
211+
**Why:** PyPI uses classifiers for search filtering. Not auto-generated.
212+
**Fix:** Keep classifiers in sync with every Python version you test against.
213+
214+
### Not using Trusted Publishing
215+
**Mistake:** Long-lived API tokens in CI secrets.
216+
**Why:** Tokens can leak. Compromised CI = malicious package versions.
217+
**Fix:** Use PyPI Trusted Publishing (OIDC) with GitHub Actions.
218+
219+
---
220+
221+
## Testing
222+
223+
### Testing source checkout instead of installed package
224+
**Why:** Missing files, wrong `__init__.py`, broken entry points — none caught.
225+
**Fix:** src layout + editable install (`uv pip install -e .`) + pytest.
226+
227+
### Importing from conftest.py explicitly
228+
**Mistake:** `from conftest import some_fixture`
229+
**Why:** conftest.py is auto-loaded by pytest. Explicit imports cause double-loading.
230+
**Fix:** Just use fixture names — pytest injects them automatically.

0 commit comments

Comments
 (0)