Compare commits

...

5 Commits

Author SHA1 Message Date
dependabot[bot] 960fdbe3df Bump pytest from 9.0.2 to 9.0.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 02:36:25 +00:00
kennethreitz b3f3e985b4 Document missing API features across guides
- chords: open_voicing() alongside other voicings, normal_form() in
  pitch class sets section
- tones: is_natural, is_sharp, is_flat accidental properties
- scales: Key.seventh() for individual degrees, expanded
  Scale.recommend() explanation of how ranking works

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:33:47 -04:00
kennethreitz c1925af69d Add Nashville numbers, blues scales, and tablature guide
New documentation section covering the Nashville number system,
blues scale theory, and tablature export — topics that were
previously scattered across cookbook and fretboard docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:11:29 -04:00
kennethreitz 7883c978f7 Support Fretboard objects in to_tab() — v0.42.1
to_tab(tuning=Fretboard.guitar()) now works, along with bass,
ukulele, mandolin, banjo, and any custom Fretboard with capo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:03:52 -04:00
kennethreitz 36d558573c Remove worktree submodules, add to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:02:17 -04:00
14 changed files with 503 additions and 12 deletions
Submodule .claude/worktrees/agent-a199bb25 deleted from 9404afc1f3
Submodule .claude/worktrees/agent-ae2f8776 deleted from 9404afc1f3
Submodule .claude/worktrees/agent-af0dae97 deleted from 9404afc1f3
+1
View File
@@ -7,3 +7,4 @@ t2.py
__pycache__
pytheory.egg-info
docs/_build
.claude/worktrees/
+7
View File
@@ -2,6 +2,13 @@
All notable changes to PyTheory are documented here.
## 0.42.1
- **Fretboard tuning support** — `to_tab()` now accepts `Fretboard` objects as
the `tuning` parameter. Works with `Fretboard.guitar()`, `Fretboard.bass()`,
`Fretboard.ukulele()`, `Fretboard.mandolin()`, `Fretboard.banjo()`, and any
custom Fretboard with capo.
## 0.42.0
- **LilyPond export** — `Score.to_lilypond()` generates complete LilyPond source
+24
View File
@@ -503,9 +503,16 @@ are standard arranging techniques for spreading chord tones across registers:
>>> cmaj7 = Chord.from_symbol("Cmaj7")
>>> cmaj7.close_voicing()
<Chord C major 7th>
>>> cmaj7.open_voicing()
<Chord C major 7th>
>>> cmaj7.drop2()
<Chord C major 7th>
``open_voicing()`` takes the close voicing and raises every other
non-root tone by an octave, spreading the chord across two octaves.
The result is a wider, more spacious sound — common in orchestral
writing and piano ballads where you want the harmony to breathe.
Chord Extensions
----------------
@@ -596,6 +603,23 @@ music that doesn't follow traditional harmony, this is the tool.
Major and minor triads share the same prime form — they're inversions
of each other in pitch class space.
The **normal form** is the intermediate step — the most compact ascending
arrangement of pitch classes before transposition. It preserves the
actual pitch classes (not transposed to 0), so it tells you which
specific notes are in the set:
.. code-block:: pycon
>>> Chord.from_tones("C", "E", "G").normal_form
(0, 4, 7)
>>> Chord.from_tones("A", "C", "E").normal_form
(9, 0, 4)
Normal form keeps the original pitch classes; prime form transposes to 0
for comparison. Use ``normal_form`` when you care about which notes,
``prime_form`` when you care about the abstract shape.
.. code-block:: pycon
>>> Chord.from_tones("C", "E", "G").forte_number
+404
View File
@@ -0,0 +1,404 @@
Nashville Numbers, Blues Scales, and Tablature
===============================================
Three tools that work together: the Nashville number system for writing
chord charts, blues scales for improvisation, and tablature for seeing
where to put your fingers. This guide covers all three and shows how
they connect.
The Nashville Number System
---------------------------
The `Nashville number system <https://en.wikipedia.org/wiki/Nashville_Number_System>`_
replaces chord names with Arabic numerals (1, 2, 3...) so that a chart
works in **any key**. It's the standard chart format in Nashville
recording studios — a session musician can read a number chart and
transpose on the fly without rewriting anything.
The idea is simple: each number refers to a **scale degree**. In any
major key, 1 is the tonic chord, 4 is the subdominant, 5 is the
dominant, and so on. The chord quality (major, minor, diminished) is
determined by the key — you don't need to write it out.
In C major::
1 = C major 5 = G major
2 = D minor 6 = A minor
3 = E minor 7 = B diminished
4 = F major
In G major::
1 = G major 5 = D major
2 = A minor 6 = E minor
3 = B minor 7 = F# diminished
4 = C major
Same numbers, different key, different chords — but the same harmonic
relationships.
Using Nashville Numbers in PyTheory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Both :class:`~pytheory.scales.Key` and :class:`~pytheory.scales.TonedScale`
support the ``nashville()`` method:
.. code-block:: pycon
>>> from pytheory import Key
>>> key = Key("C", "major")
>>> [c.identify() for c in key.nashville(1, 4, 5, 1)]
['C major', 'F major', 'G major', 'C major']
>>> # Same progression, different key — just change the Key
>>> key_g = Key("G", "major")
>>> [c.identify() for c in key_g.nashville(1, 4, 5, 1)]
['G major', 'C major', 'D major', 'G major']
Nashville numbers and Roman numerals produce the same result — they're
two notations for the same concept:
.. code-block:: pycon
>>> key = Key("G", "major")
>>> nash = [c.identify() for c in key.nashville(1, 5, 6, 4)]
>>> roman = [c.identify() for c in key.progression("I", "V", "vi", "IV")]
>>> nash == roman
True
Seventh Chords
~~~~~~~~~~~~~~
Suffix ``"7"`` to get seventh chords — essential for jazz and blues
charts:
.. code-block:: pycon
>>> key = Key("C", "major")
>>> [c.identify() for c in key.nashville("17", "47", "57")]
['C major 7th', 'F major 7th', 'G dominant 7th']
Nashville vs. Roman Numerals
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When should you use which?
- **Nashville numbers** — faster to type, easier to read at a glance,
standard in studio sessions. Use ``key.nashville(1, 4, 5, 1)``.
- **Roman numerals** — encode chord quality (uppercase = major,
lowercase = minor), standard in theory textbooks. Use
``key.progression("I", "IV", "V", "I")``.
Both are fully supported. Use whichever fits your workflow.
Blues Scales
------------
The `blues scale <https://en.wikipedia.org/wiki/Blues_scale>`_ is a
six-note scale built from the minor pentatonic plus one chromatic
passing tone — the **blue note** (flat 5th). That single added note
gives the blues its tension and character.
The blues system in PyTheory includes several related scales:
==================== ===== ==================================
Scale Notes Character
==================== ===== ==================================
minor pentatonic 5 Foundation of rock and blues soloing
major pentatonic 5 Bright, country, pop
blues 6 Minor pentatonic + blue note (b5)
major blues 6 Major pentatonic + blue note (b3)
dominant 7 Mixolydian — dominant 7th sound
minor 7 Dorian-like — minor with natural 6th
==================== ===== ==================================
Building Blues Scales
~~~~~~~~~~~~~~~~~~~~~
Use ``system="blues"`` when creating a :class:`~pytheory.scales.TonedScale`:
.. code-block:: pycon
>>> from pytheory import TonedScale
>>> c = TonedScale(tonic="C4", system="blues")
>>> c["minor pentatonic"].note_names
['C', 'Eb', 'F', 'G', 'Bb', 'C']
>>> c["blues"].note_names
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
>>> c["major pentatonic"].note_names
['C', 'D', 'E', 'G', 'A', 'C']
>>> c["major blues"].note_names
['C', 'D', 'Eb', 'E', 'G', 'A', 'C']
The Anatomy of a Blues Scale
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The blues scale in C::
C Eb F Gb G Bb C
1 b3 4 b5 5 b7 8
Root ──┐
├── minor 3rd (3 semitones)
├── perfect 4th (5 semitones)
├── diminished 5th (6 semitones) ← the "blue note"
├── perfect 5th (7 semitones)
├── minor 7th (10 semitones)
└── octave (12 semitones)
The blue note (Gb/F#) sits between the 4th and 5th — a dissonant,
unstable pitch that resolves up or down. It's what makes blues sound
like blues.
The 12-Bar Blues
~~~~~~~~~~~~~~~~
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is
the most important chord progression in American music. It uses the
Nashville numbers 1, 4, and 5::
| 1 | 1 | 1 | 1 |
| 4 | 4 | 1 | 1 |
| 5 | 4 | 1 | 5 |
In the key of A:
.. code-block:: pycon
>>> from pytheory import Key
>>> key = Key("A", "major")
>>> bars = key.nashville(1,1,1,1, 4,4,1,1, 5,4,1,5)
>>> [c.identify() for c in bars]
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
For an authentic blues sound, use dominant 7th chords:
.. code-block:: pycon
>>> bars_7 = key.nashville("17","17","17","17", "47","47","17","17", "57","47","17","57")
>>> [c.identify() for c in bars_7]
['A major 7th', 'A major 7th', 'A major 7th', 'A major 7th', 'D major 7th', 'D major 7th', 'A major 7th', 'A major 7th', 'E dominant 7th', 'D major 7th', 'A major 7th', 'E dominant 7th']
Or use the built-in named progression:
.. code-block:: pycon
>>> key = Key("A", "major")
>>> blues = key.common_progressions()["12-bar blues"]
>>> [c.identify() for c in blues]
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
Blues Scale on the Fretboard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Visualize the blues scale on guitar to see the patterns:
.. code-block:: pycon
>>> from pytheory import Fretboard, TonedScale
>>> fb = Fretboard.guitar()
>>> blues = TonedScale(tonic="A4", system="blues")["blues"]
>>> print(fb.scale_diagram(blues, frets=12))
0 1 2 3 4 5 6 7 8 9 10 11 12
E| - | - | - | - | - | A | - | - | C | - | D | Eb| E |
B| - | - | - | D | Eb| E | - | - | - | - | A | - | - |
G| - | - | A | - | - | C | - | D | Eb| E | - | - | - |
D| - | - | - | - | A | - | - | C | - | D | Eb| E | - |
A| A | - | - | C | - | D | Eb| E | - | - | - | - | A |
E| - | - | - | - | - | A | - | - | C | - | D | Eb| E |
The minor pentatonic (same scale without the Eb) is the most-played
scale in rock guitar. Add the blue note and you have the full blues
scale — the same shapes, one extra fret.
Tablature
---------
`Tablature <https://en.wikipedia.org/wiki/Tablature>`_ (tab) shows
**where to put your fingers** rather than what notes to play. Each line
represents a string; numbers indicate fret positions. PyTheory generates
tabs at three levels:
1. **Chord tabs** — single chord fingerings
2. **Part tabs** — full melody/sequence notation
3. **Score tabs** — extract a part from a multi-part score
Chord Tablature
~~~~~~~~~~~~~~~~
Get the tab for any chord on any instrument:
.. code-block:: pycon
>>> from pytheory import Fretboard
>>> fb = Fretboard.guitar()
>>> print(fb.tab("C"))
C major
e|--0--
B|--1--
G|--0--
D|--2--
A|--3--
E|--x--
>>> print(fb.tab("Am"))
A minor
e|--0--
B|--1--
G|--2--
D|--2--
A|--0--
E|--x--
>>> print(fb.tab("E7"))
E dominant 7th
e|--0--
B|--0--
G|--1--
D|--0--
A|--2--
E|--0--
Works with any instrument:
.. code-block:: pycon
>>> uke = Fretboard.ukulele()
>>> print(uke.tab("C"))
C major
A|--3--
E|--0--
C|--0--
G|--0--
Reading Tab Notation
~~~~~~~~~~~~~~~~~~~~~
::
e|--0-- ← open string (don't fret, just pluck)
B|--1-- ← press fret 1
G|--0-- ← open string
D|--2-- ← press fret 2
A|--3-- ← press fret 3
E|--x-- ← muted (don't play this string)
- Each line is a string (highest pitch at top, lowest at bottom)
- Numbers are fret positions (0 = open, 1-24 = fretted)
- ``x`` means the string is muted / not played
- ``|`` marks measure boundaries in sequence tabs
Part Tablature
~~~~~~~~~~~~~~~
Generate tab from a composed part using ``to_tab()``:
.. code-block:: python
from pytheory import Score, Key, Duration
score = Score("4/4", bpm=120)
lead = score.part("lead", synth="saw")
# A simple blues lick
for note in ["A4", "C5", "D5", "Eb5", "E5", "G5", "A5"]:
lead.add(note, Duration.QUARTER)
print(lead.to_tab())
This outputs standard ASCII tab with measure lines, mapping each note
to the most playable string and fret position.
Tuning Options
~~~~~~~~~~~~~~
The ``to_tab()`` method supports multiple tunings:
.. code-block:: python
# Standard guitar (default)
lead.to_tab(tuning="guitar")
# 4-string bass
lead.to_tab(tuning="bass")
# Drop D guitar
lead.to_tab(tuning="drop_d")
# Any Fretboard object — use any of the 25+ instrument presets
from pytheory import Fretboard
lead.to_tab(tuning=Fretboard.mandolin())
lead.to_tab(tuning=Fretboard.banjo())
# Custom tuning as MIDI note numbers (low string first)
lead.to_tab(tuning=[40, 45, 50, 55, 59, 64])
Score Tablature
~~~~~~~~~~~~~~~~
Extract tab from a multi-part score:
.. code-block:: python
score = Score("4/4", bpm=120)
rhythm = score.part("rhythm", synth="saw")
lead = score.part("lead", synth="triangle")
bass = score.part("bass", synth="sine")
# ... compose parts ...
# Tab the lead part
print(score.to_tab("lead"))
# Tab the first non-drum part (if no name given)
print(score.to_tab())
# Bass tab
print(score.to_tab("bass", tuning="bass"))
Putting It All Together
-----------------------
Here's a complete example that uses all three features — Nashville
numbers for the chord progression, the blues scale for the melody, and
tab export to see the fingering:
.. code-block:: python
from pytheory import Key, TonedScale, Fretboard, Score, Duration
# 1. Nashville numbers for the progression
key = Key("A", "major")
chords = key.nashville(1, 1, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5)
# 2. Blues scale for the melody
blues = TonedScale(tonic="A4", system="blues")["blues"]
# 3. Compose a score
score = Score("4/4", bpm=120)
rhythm = score.part("rhythm", synth="saw", envelope="pad")
lead = score.part("lead", synth="triangle", envelope="pluck")
for chord in chords:
rhythm.add(chord, Duration.WHOLE)
for note_name in blues.note_names[:-1]: # walk up the scale
lead.add(f"{note_name}4", Duration.HALF)
# 4. See it as tablature
print(lead.to_tab())
# 5. See the scale on the fretboard
fb = Fretboard.guitar()
print(fb.scale_diagram(blues, frets=12))
Nashville numbers tell you *what chords to play*. The blues scale tells you *what notes to solo with*. Tablature tells you *where to put your fingers*. Together, they're everything you need to play the blues.
+27 -1
View File
@@ -269,6 +269,23 @@ easy:
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
>>> key.seventh_chords
['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th']
Build a seventh chord on any individual degree with ``seventh()``:
.. code-block:: pycon
>>> key.seventh(0) # I7
G major 7th
>>> key.seventh(4) # V7
D dominant 7th
>>> key.seventh(6) # vii7
F# half-diminished 7th
This is the single-degree version of ``seventh_chords`` — useful when
you need one specific chord rather than the full list.
.. code-block:: pycon
>>> Key.detect("C", "E", "G", "A", "D")
C major
@@ -440,7 +457,16 @@ alternative scales to improvise over:
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
[('C', 'blues', 1.0), ...]
Chromatic scales are deprioritized since they match everything.
How it works: ``recommend()`` tests your notes against every scale in
every key (all 12 tonics times all scale types in the Western system).
Each candidate is scored using ``fitness()`` — the fraction of your notes
that belong to that scale (1.0 = perfect match). Results are ranked by
fitness, with chromatic scales deprioritized since they match everything.
Scales whose length is closer to the number of input notes are preferred
when fitness scores tie.
Returns a list of ``(tonic, scale_name, fitness)`` tuples. Pass ``top=``
to control how many results you get back (default 5).
Parallel Modes
~~~~~~~~~~~~~~
+26
View File
@@ -357,6 +357,32 @@ every tone knows its enharmonic spelling:
>>> Tone.from_string("C4", system="western").enharmonic is None
True
Accidental Properties
~~~~~~~~~~~~~~~~~~~~~
Check whether a tone is natural, sharp, or flat:
.. code-block:: pycon
>>> c = Tone.from_string("C4", system="western")
>>> c.is_natural
True
>>> c.is_sharp
False
>>> cs = Tone.from_string("C#4", system="western")
>>> cs.is_sharp
True
>>> cs.is_natural
False
>>> bb = Tone.from_string("Bb4", system="western")
>>> bb.is_flat
True
Useful for filtering — for example, finding all natural notes in a
scale, or counting accidentals in a melody.
Extended Enharmonics
~~~~~~~~~~~~~~~~~~~~
+1
View File
@@ -118,6 +118,7 @@ What's Inside
guide/scales
guide/chords
guide/fretboard
guide/nashville-blues-tabs
guide/systems
guide/sequencing
guide/synths
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.42.0"
version = "0.42.1"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.42.0"
__version__ = "0.42.1"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+7 -2
View File
@@ -3814,8 +3814,8 @@ class Part:
Args:
tuning: ``"guitar"`` (6-string standard), ``"bass"`` (4-string),
``"drop_d"`` (guitar drop D), or a list of MIDI note numbers
for custom tuning (low string first).
``"drop_d"`` (guitar drop D), a ``Fretboard`` object, or a
list of MIDI note numbers for custom tuning (low string first).
frets: Maximum fret number (default 24).
time_signature: A ``TimeSignature`` or ``None`` for 4/4.
@@ -3825,6 +3825,11 @@ class Part:
if isinstance(tuning, str):
open_midis = list(self._TAB_TUNINGS[tuning])
labels = list(self._TAB_LABELS[tuning])
elif hasattr(tuning, "tones"):
# Fretboard object — tones are high-to-low, reverse for low-to-high
fb_tones = list(reversed(tuning.tones))
open_midis = [t.midi for t in fb_tones]
labels = [t.name if len(t.name) <= 2 else t.name[0] for t in fb_tones]
else:
open_midis = list(tuning)
_note_names = ["C", "C#", "D", "D#", "E", "F",
Generated
+4 -4
View File
@@ -672,7 +672,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.2"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -683,14 +683,14 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "pytheory"
version = "0.42.0"
version = "0.42.1"
source = { editable = "." }
dependencies = [
{ name = "rich" },