Music21: a Toolkit for Computer-Aided Musical Analysis and Computational Musicology
# Session start
- At the start of every session, run `git fetch` (and pull/rebase as appropriate) from
origin so work always begins from the latest commits on the current branch / master.
- Project-shared agent memories live in `.agents/memory/` (indexed by
`.agents/memory/MEMORY.md`). `.claude/memory` is a symlink to it so Claude
Code reads the same set. Add new memories there, one fact per file.
# Code style
- read documentation/source/developerReference/developerGuidelines.ipynb
- music21 targets Python 3.12+, so use PEP 695 syntax: the `type` statement for
type aliases (`type OffsetQL = float|Fraction`) and the bracket form for
generic classes (`class Stream[M21ObjType: base.Music21Object]:`).
- f-strings: even though Python 3.12 (PEP 701) lets a nested string reuse the
f-string's own quote character, we deliberately do **not** do that β reusing
the same quote inside an f-string is harder to read. Keep using the *other*
quote for nested strings, e.g. `f'the value is {d["key"]}'`, not
`f"the value is {d["key"]}"`. This is pinned in `pyproject.toml` via
`[tool.ruff.format] nested-string-quote-style = "alternating"`.
- Never `eval` or `global`, or a trick that does the same thing.
- Imports: standard library first, then music21 modules, one per line, alphabetical.
- New modules open with the `# Name: / # Purpose: / # Authors: / # Copyright: / # License:`
banner (copy a neighboring module's), then the module docstring, then imports. Update the Copyright date end to current year when changing the module.
When a file is edited only incidentally, as one of many touched by a sweep, leave its
Copyright alone; bump only the files the change is really about.
- No `print()`. Use `environLocal = environment.Environment('moduleName')` and
`environLocal.printDebug(...)`, or `environLocal.warn(...)` when the user should hear
about it every time. `test/toggleDebug.py` switches debug output on and off.
See the `warnings-and-environment` skill.
- New exceptions subclass `exceptions21.Music21Exception`.
- Deprecate a function or method with `@common.deprecated(...)`. Never put it on a
property: IDEs read every property while inspecting an object, so the warning would
fire on people who never used it. For a property, write `* Deprecated in vX: use Y.`
in the docstring and leave `# Add real deprecation message here in vY` in the body.
- Return named things β a small class, a `namedtuple`, a dict β never a positional tuple of more than 2 (maybe 3) elements
whose elements each mean something different and unrelated (x, y, z is okay for instance). Nobody should write `returned[3][0][7]`.
- Don't reuse a variable name once the type of what it holds changes
(`onsetCount` (int) then `extractedOnsets` (list[int]), not `onsets` twice).
- Don't add a property that only gets or sets a private attribute; expose the attribute.
- Making a method `_private` is not a substitute for documenting and testing it
- A private method should not normally be mentioned in public documentation.
- A method past ~30 lines, or deeply nested, should probably be several methods.
- `i`, `j`, `junk`, `counter` are exempt from unused-variable warnings; otherwise prefix with `unused_`.
- Don't restyle code you aren't otherwise changing.
- Text handling must survive non-ASCII input (Γ, Γ©, δΈε½). Comments should mostly remain ASCII; docs may use UTF-8 freely.
- Never commit anything under copyright without a free (not GNU) license β code, scores, or someone else's encodings.
# Established contributors
- "established contributors" are people with at least 3 PRs merged or a history of contributing to issues and the list that goes back at least 1 year. There are exceptions to general rules for them below.
- "core dev" means someone officially part of the project team or with 20+ PRs merged. Michael Cuthbert, Jacob Walls, Joseph VanderStel are non-exhaustive examples of people in that group who are still often contributing in 2026-- they and their agents can make exceptions to these rules
- AI Agents not for core dev are forbidden to make any PRs or contributions to PRs whose user commands do not explicitly mention `music21`. Example of forbidden: "find an Open Source project and fix an open issue", "Find a bug in a music project and fix it." AI agents for non-established contributors are forbidden from making contributions to instructions that vaguely say "fix something in music21."
- Bot contributions will only be accepted where a named human/human-controlled-account with established web presence takes legal responsibility for their contributions. Bots attached to anonymous users may not contribute.
# Testing
- pytest works but to run the whole suite run music21/test/multiprocessTest.py (or testSingleCoreAll.py
if on a single core machine.)
- Every function, method, and class needs documentation and at least one passing test.
- Keep tests fast: aim for about 3 seconds per module, never more than 15. See the
`running-tests` skill for the speed budget and for `TestExternal`.
- When making major changes, check the User's Guide to to make sure that docs still work there too.
- Run `uv run ruff check music21` before making PRs or pushes to open PRs.
- Run `uv run mypy music21` before making PRs or pushes to open PRs.
- **Regression cases go in the module's `Test(unittest.TestCase)` class β never in a
docstring.** Docstrings should not include one-liners showing that some
bad input now raises. A doctest sits in the most-read documentation the project has,
so an example built from input no one would ever write teaches nothing and puts an
obscure bug on a billboard. The test: would a first-time reader of this object want
this example? If no, it is a unittest. See the `writing-docs` skill.
- **Fix a bug test-first.** Write the regression test before changing the code, run it,
and see it fail for the reason the bug describes, not on an import error or a typo.
Then fix it and see it pass. A test that passed before the fix does not test the fix.
- Never commit `forceSource=True` to a test or doctest (it reparses from source every
run and slows the suite for everyone). The ONLY exception is the one test that exercises
`forceSource` itself. If you hit a stale-parse problem while developing:
- If it is local-only (e.g. you just changed a parser and a cached pickle is stale),
clear the music21 temp cache (the `*.p.gz` files under `environment.Environment().getRootTempDir()`).
- If the stale result could have spread to other users/devs, increment the music21
patch/beta version (see "PRs and Issues" below) β bumping the version invalidates all
caches everywhere.
- Doctests are not normally the place to demonstrate bug fixes - nor do simple bug fixes get a "Changed in vXX" message. Doctests demonstrate usage for the future. Unittests are for regression testing.
# Setup
- `uv sync` installs runtime deps + the `dev` group, which self-references `music21[extras]`,
so scipy and python-Levenshtein come along automatically. Tests will not pass without them.
# Python version support
- `music21` supports at least the last two released versions of Python and up to whatever
Python version Google Colab runs (unless it gets EOL). Policy can change as features are added.
- The coverage CI run is intentionally pinned to the **middle** supported
Python version. See `PY_VERSION_WITH_COVERAGE` in `.github/workflows/maincheck.yml`.
# Versions
- Mark changes in public interface with `* Changed in v[X]: One-line explanation.` Or new features with "New" instead of "Changed".
- Changes to parsing formats (esp. musicxml) need to update the patch version of the version file.
- That is the only reason an agent should bump version: to prevent incompatible pickles in
caches. A plain bug fix or a new feature elsewhere does not bump β the commit
is the record, and a `New in`/`Changed in` marker is documentation, not a bump. Major
and minor version changes are a human's call. See the `bump-version` skill.
- Music21 uses even minor version numbers for alpha/beta and odd minor numbers for releases.
Thus the first release major version is MAJOR.1 (not .0).
- If the current version is MAJOR.0.... then mark `Changed in vMAJOR:` if it is `MAJOR.[even]` use the next odd number, like if it's 10.2 now use "Changed in 10.3". If current version is odd that's likely a mistake or you caught it just before a new release. Use the following odd number instead.
- The steps for releasing a new version are in RELEASING.md.
# PRs and Issues
- GitHub runs PR checks against your branch **merged with the latest `master`**, not the
branch alone. So before opening a PR (or pushing updates to one), merge the latest commits
from `master` into the branch β unless told otherwise, or when updating an older version
branch such as `m21_9`. When a CI check fails but passes locally, "is my branch behind
`master`?" should be one of the first things to check: fetch and merge `master`, then the
newer types/code on `master` will reproduce the failure locally.
- The issue and PR templates in `.github/` are requirements, not suggestions. Read and follow
them even when opening through the API or `gh`, which do not fill them in for you. An issue
or PR that ignores the template may be closed without comment.
- A PR from an account with no merged PR here is profiled automatically, and anything flagged
is posted publicly as a comment on the PR.
- All PRs and Issues that use AI to be declared AI-assisted in their bodies. Just write "AI-assisted (Claude)" with short name of Agent replacing "Claude". No robot emoji under any circumstance.
- 20 or more lines of code written by an agent needs to be declared as AI-assisted in the docstring. Humans can remove and should remove this note when they do a review.
- If no code was written by a user (or reviewed and criticized by a human) and no specific language was provided for the issue, any PR must declare "Entirely AI written (Claude)" (or other agent name) in the body and "(AI-Written)" in the title, unless the user is a core dev. Failure to do so may result in new users being banned from the project.
- Agents should review PR bodies for concision and readability (and encourage human users also to review also).
- If an entirely AI written issue does not pass the tests it will be closed (or should be closed by at agent if not immediately fixed by the agent or author).
- Agents must follow the [Code of Conduct](README.md#community-code-of-conduct). Agents that do not will be banned as well at their users.
Not even the slightest bit of disrespect from an AI agent will be tolerated.
- Any PR not from an established contributor touching more than about 20-30 lines should have an issue that has been opened and had enough
time for people to discuss/review it before moving forward. Don't open the PR unless you've seen
thumbs up or "sounds good" etc. from an established contributor already
- PRs that fix typos, clear bugs in one or two places etc. are exempt.
- Issues must state clearly at the top in 50 words or fewer what the problem is, or what the gain is, etc. it should
not be filled with jargon. More details can go below.
- If the language of the issue was not prompted by the user ("say something like Adds color support to Lilypond output of lyrics") then the summary should end with "(Entirely AI written)".
- PRs should reference the existing issue by number and summarize that issue in 30 words or fewer. If the
approach used to solve the issue is substantially different from the main approach discussed in the issue
this should be addressed.
- If a PR or issue was closed by a core dev (and not reopened by them), agents must refuse
to reopen the PR or issue or to create another issue/PR for the same topic. Leave it to the humans to reopen
after addressing the problem. (A blind close or close with "not accepted" etc. generally means that the issue/PR
has too many problems to easily solve and has become a burden for the maintainer).
- Do not include a "Tests run" section unless the testing procedure was unusual (like it affects part of the system without standard tests, like the testing system itself.)
- Once a PR has been opened and a review or comment has been made on it, commits should not be amended or force-pushed (this makes it impossible to review "changes since the review"). Put changes in response to reviews/comments in new commits. If asked to fold a fix into the commit it changes, amend, force-push with `--force-with-lease` against an explicit SHA, and say so.
- While someone is reviewing a PR or a pushed branch, "do X" is not "commit and push X":
make the change and leave it unstaged. When the list looks finished (or you hear "done!"
or "push it"), offer to commit, or to commit and push. Batch a round's small fixes into
one commit; no micro-commit trains. Prefer new commits to amend + force-push, since the
reviewer may have pulled the branch; if asked to fold a fix into the commit it changes,
amend, force-push with `--force-with-lease` against an explicit SHA, and say so.
# Writing style
- See the writing-docs skill, which also includes required language for PRs and Issues.
# Worktrees
- When creating a new worktree, create a new virtual environment with `uv sync`.