Add distilled package standards page and review skill - #41
Conversation
The course content is spread across seven slide decks, which makes it hard to point someone at a specific rule. standards.md distils it into ~45 normative rules, one linkable heading each, published at standards.html. Exposition is deliberately left in the slides: the page holds only rules you would assert at someone, including ones no tool can check. Also folds in seven issues that never made it into the slides: #40 (the underscore convention, with the public-API-only amendment to breaking changes), #37 (cost of dependencies), #36 (long signatures), #35 (inappropriate intimacy), #34 (comments say why), #33/#26 (deprecation), #22 (changelog). The review-python-package skill audits a repository against the page and links each finding to the rule it breaks.
| `FutureWarning` is for end users (always visible). `mypy --enable-error-code=deprecated` | ||
| catches uses of `@deprecated` at type-check time. | ||
|
|
||
| ### Changelog |
There was a problem hiding this comment.
This is nice to have, but is tedious to curate manually. Think twice before spending time on this.
There was a problem hiding this comment.
| success — a non-empty string is truthy. | ||
|
|
||
| ```python | ||
| if is_operable(height=12.0, period=5.0): # returns "No way!" — and this runs |
There was a problem hiding this comment.
This function returns None. Fix it.
There was a problem hiding this comment.
Fixed in 0055f53 — the example only had the call site, so nothing showed the mixed return. It now defines is_operable, with the str / None / bool paths visible, plus a line making the implicit-None case explicit as the same bug.
I read this as being about the One return type example. If you meant the clip above it, that one returns None on purpose — it is the bad variant showing mutation — so say the word and I will handle it differently.
| ``` | ||
|
|
||
| ### Keyword-only arguments | ||
| *Nice.* `def f(*, x, y)` forces callers to be explicit and lets you reorder parameters later |
There was a problem hiding this comment.
This should be more nuanced. One or two positional arguments are fine, that is usually the data the function is operating upon. More than two positional arguments are a strong code smell.
There was a problem hiding this comment.
Adopted your wording in 0055f53 and promoted the rule to Recommended. It now says one or two positional parameters is fine (the data being operated on), everything after belongs behind the *, and three or more positionals is a strong smell — resample(df, 3, 0, True) as the counter-example.
This collided with When a long signature is a smell, which said "the smell is not the count". Scoped that one to the total count and cross-linked the two so the page does not argue with itself.
| ``` | ||
|
|
||
| ### Examples that are tested | ||
| *Nice.* `doctest` runs the examples in your docstrings. Documentation that is wrong is worse |
There was a problem hiding this comment.
Using Quarto for documentation, where the snippets are run as part of the build is another option.
There was a problem hiding this comment.
Added in 0055f53 — doctest for docstring examples, Quarto for prose pages where the build executes every snippet and fails first.
| ``` | ||
|
|
||
| ### Published API documentation | ||
| *Recommended.* `mkdocs` + `mkdocstrings` + GitHub Pages, at |
There was a problem hiding this comment.
Quarto or the new framework Great Docs (wrapping Quarto), or zensical are all viable options.
| ### Published API documentation | ||
| *Recommended.* `mkdocs` + `mkdocstrings` + GitHub Pages, at | ||
| `https://dhi.github.io/<repository>/`. Note that a private repository can still have a public | ||
| website — `robots.txt` hides it from search engines but is not security. |
There was a problem hiding this comment.
No longer relevant. With GitHub Enterprise, we can have private pages.
There was a problem hiding this comment.
Removed in 0055f53. The robots.txt sentence is gone; the rule now says a private repository can have access-controlled Pages on GitHub Enterprise, and to use that rather than relying on the URL not being found.
| ``` | ||
|
|
||
| ### Lint and format with ruff | ||
| *Recommended.* `ruff check` finds unused imports, undefined names and dead variables — usually |
There was a problem hiding this comment.
There is no reason not use ruff for linting and formatting.
There was a problem hiding this comment.
Your sentence is now the opening of the rule in 0055f53, with the reason spelled out: one binary, no configuration required, replaces flake8 + black + isort.
I left the severity at Recommended rather than promoting it. Blocker is defined at the top of the page as "not fit to share", and an unlinted package is still shareable — it would be the first rule to stretch that definition. Happy to promote it if you would rather the legend bend.
| ``` | ||
|
|
||
| ### Makefile | ||
| *Nice.* One source of truth for how to run the project's tools, and the fastest onboarding |
There was a problem hiding this comment.
Consider just instead of make, more modern and better cross platform support.
There was a problem hiding this comment.
Done in 0055f53 — the heading is now A task runner: Makefile if everyone is on Linux or macOS, just if anyone is on Windows, where make is absent by default and just is a single binary (uv tool install rust-just). Both recipe files are shown, both using uv run. Anchor changed to #a-task-runner; nothing linked to the old one.
| strategy: | ||
| matrix: | ||
| os: [ubuntu-latest, windows-latest] | ||
| python-version: ["3.10", "3.13"] |
There was a problem hiding this comment.
Matrix testing is powerful, but don't go overboard. If your package is actually an application, targeting a single Python version might be ok. CI is great, but it is not free.
There was a problem hiding this comment.
Added in 0055f53 — "test what you claim and no more", CI is not free, every cell costs minutes on every push. Uses the library/application split the page already makes, cross-linked: an application has one deployment target, so one cell is the honest matrix.
| ## Release | ||
|
|
||
| ### Publish from a GitHub release | ||
| *Recommended.* Tag a release, let a workflow build and publish. Use Trusted Publishers so |
There was a problem hiding this comment.
Using git tags to track released versions makes your life easy. Just do it.
There was a problem hiding this comment.
Promoted to its own rule in 0055f53 — Tag every release, Recommended, annotated vX.Y.Z pushed, framed as what makes "which commit is 1.2.0?" answerable a year later. Two commands, so "just do it" is in there.
|
|
||
| ```yaml | ||
| on: | ||
| release: |
There was a problem hiding this comment.
A viable alternative is to trigger automated releases via git tags.
There was a problem hiding this comment.
Added in 0055f53 — the rule is now Publish from a tag or a release and shows on: push: tags: ["v*"], with the release trigger as the inline alternative and one clause on the tradeoff: the release gives you somewhere for release notes, the tag is one step fewer. Trusted Publishers works with either.
standards.md: - Keyword-only arguments: rewritten around positional count — one or two for the data, config after the `*`, three+ is the smell. Now Recommended. - Changelog demoted to Nice; hand curation is real work, so point at towncrier/git-cliff instead of prescribing it. - One return type: show the function, so the None path is visible. - New rules: Tag every release, Type checking in CI. - Publish from a tag or a release, showing the `on: push: tags` trigger. - Test the matrix: CI is not free; test only what you claim to support. - Docs: name Quarto, Great Docs and zensical as alternatives; replace the robots.txt note with access-controlled Pages on GitHub Enterprise. - Makefile becomes A task runner, covering `just` for Windows. - Fix checkout@v3 -> v4 and a backwards `min`/`max` in the clip example. review-python-package skill: - Use Glob/Read/Grep instead of `ls`, `cat` and piped `grep` — the reviewer may be on Windows with no POSIX shell. Table-escaped pipes had also made the mutable-default pattern match nothing. - `uvx ruff`, and say so when the repo has no ruff config.
There was a problem hiding this comment.
Pull request overview
Adds a linkable standards reference and a Claude Code skill for auditing Python packages.
Changes:
- Adds distilled package-development standards.
- Adds an automated package-review skill.
- Links and documents both resources across the site.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
standards.md |
Defines the package standards. |
README.md |
Documents the standards and review skill. |
index.qmd |
Links the standards from the course homepage. |
CLAUDE.md |
Records the new repository structure. |
.claude/skills/review-python-package/SKILL.md |
Implements the package-audit workflow. |
Suppressed comments (7)
standards.md:69
- Missing these two tables does not necessarily make a package un-installable: pip can still build legacy
setup.py/setup.cfgprojects, and some PEP 517 backends use backend-specific metadata. State this as the DHI standard rather than an installability fact to avoid false blocker findings.
### pyproject.toml
*Blocker.* A package without `[build-system]` and `[project]` is not installable. `uv init
--lib` gives you a working one.
standards.md:128
- A copyright notice does not grant internal users permission to use or redistribute the package, so it cannot substitute for a license. Internal packages still need applicable proprietary/internal license terms or organization-wide terms granting those rights.
### License
*Blocker.* Without a license the package is "all rights reserved" and legally unusable by
others. MIT for open, a copyright notice for internal-only. Check your dependencies' licenses
too.
standards.md:427
- Quarto does not execute every fenced snippet automatically. Only executable code cells are run when an execution engine is configured, so this rule currently promises documentation testing that ordinary Markdown examples do not receive.
### Examples that are tested
*Nice.* Documentation that is wrong is worse than documentation that is missing. `doctest`
runs the examples in your docstrings. For prose pages, [Quarto](https://quarto.org/) executes
every snippet as part of the build, so the docs cannot ship broken — the build fails first.
standards.md:437
- The PR description says issue #39 remains open because Great Docs still needs evaluation, but this text already endorses it as a viable alternative and links a different repository than the issue. Remove the endorsement until that evaluation is complete.
`https://dhi.github.io/<repository>/`. [Quarto](https://quarto.org/),
[Great Docs](https://github.com/machow/great-docs) (which wraps Quarto) and
[zensical](https://zensical.org/) are viable alternatives.
standards.md:557
- Pre-release versions are still indexed and visible on package pages/search; installers merely avoid selecting them in normal resolution. Describe pip's opt-in behavior instead of claiming the release is hidden.
### Pre-releases for anything unfinished
*Nice.* `1.0.0rc1` is not installed by default and does not appear in search — the safe way to
put something in front of users before committing to it.
.claude/skills/review-python-package/SKILL.md:72
- This regex requires two dots, so it matches
self.child._privatebut misses the common direct accessother._private. Searching any direct private attribute access and then applying the existingself._exclusion catches both forms.
\w+\.\w*\._[a-z] # reaching into another object's internals
.claude/skills/review-python-package/SKILL.md:105
- This repeats the incorrect assumption that pip cannot build without an explicit
[build-system]; legacy setuptools projects remain buildable. Phrase the example as noncompliance with the DHI standard rather than failed installability.
**my_library** — not installable yet
Blockers
✗ pyproject.toml — no [build-system], so pip cannot build this
https://dhi.github.io/python-package-development/standards.html#pyproject.toml
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ### Underscore means internal | ||
| *Recommended.* A leading underscore says "not part of the public API". You may change or | ||
| remove `_foo` without it counting as a [breaking change](#breaking-changes-bump-major) — | ||
| anyone importing it did so at their own risk. Declare the public surface with `__all__` so | ||
| the boundary is explicit rather than implied. (Double underscore, `__foo`, is name mangling — |
| | Layout, `.gitignore`, `LICENSE`, `README` | Glob `*` and `.*` at the root | | ||
| | Build system, metadata, dependencies, versioning | Read `pyproject.toml` | | ||
| | CI | Glob `.github/workflows/*`, then Read each | | ||
| | Data committed to git | `git ls-files "*.csv" "*.nc" "*.dfs*" "*.xlsx" "*.zip" "*.parquet"` | | ||
| | Commit hygiene | `git log --oneline -20` | |
The course is seven slide decks, which makes it awkward to point someone at a specific rule mid-conversation.
standards.mddistils it into ~45 normative rules, one linkable heading each, published atstandards.html:Only rules you'd assert at someone — the exposition stays in the slides. That includes rules no tool can check (keep PRs small, composition over inheritance), since the point is lecturing, not just linting.
Issues folded in
Seven that never made it into the slides:
Left open deliberately: #39 (great-docs is still "worth evaluating"), #31 and #32 (pedagogy, belongs in the slides), #25 and #24 (questions you haven't settled).
Review skill
.claude/skills/review-python-package/audits a package repo against the page and links every finding to the rule it breaks, so the report is something you can paste into a PR and the reader gets the explanation without you there. It runsrufffor ground truth but deliberately won't runpytest/mypy/uv syncunprompted on an unfamiliar repo.The finding URLs 404 until this merges and Pages rebuilds.