Repository: titus-ong/chordparser

Issue:
Roman editor
Create a separate class to create roman numerals from chords, and create chords from roman numerals + scale/key. This makes it easier to convert from chords to roman numerals and back by only storing the key the song is in.

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index d0d5faf..f8ef5c6 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
 chordparser
 ===========
 
-A simple Python 3 package to analyse chords.
+A Python 3 package that provides a musical framework to analyse chords.
 
 .. image:: https://travis-ci.org/titus-ong/chordparser.svg?branch=master
    :alt: build status
@@ -20,9 +20,13 @@ A simple Python 3 package to analyse chords.
    :target: https://pypi.org/pypi/chordparser
    :alt: downloads
 
-chordparser aims to parse chord notation and provide harmonic analysis of chords based on keys and scales. This helps in understanding how each chord functions, and allows for the conversion to roman numeral chord notation.
+chordparser aims to act as a framework for harmonic analysis of chords based on keys and scales. This helps in understanding how each chord functions, and allows for the conversion to roman numeral chord notation.
 
-The parser can be used to analyse the following chords: diatonic chords, mode mixture/ borrowed chords (from other modes), secondary dominant chords (e.g. V/V), secondary leading tone chords.
+Musical classes have been grouped into Notes, Keys, Scales, Chords and Romans as the foundation for analysing chords. Chord notation can be parsed as string input and converted to Chords, which have been built on the other classes.
+
+The Parser is a one-stop shop for creating, manipulating and analysing the musical classes. It can be used to analyse the following chords: diatonic chords, mode mixture/ borrowed chords (from other modes), secondary dominant chords (e.g. V/V), secondary leading tone chords.
+
+chordparser can be incorporated into Songbooks easily with its ability to transpose chords correctly. Check out the `Colab notebook <https://colab.research.google.com/drive/1T5WcH2WMHqpqbJrzxDt_Mg03lw1aXho7?usp=sharing>`_ on how you can do that and perform more advanced manipulation with other functions available in chordparser.
 
 -------
 Install
diff --git a/src/chordparser/__init__.py b/src/chordparser/__init__.py
index 39612ef..a9cd9c3 100644
--- a/src/chordparser/__init__.py
+++ b/src/chordparser/__init__.py
@@ -6,8 +6,10 @@ from chordparser.notes import Note
 from chordparser.keys import Key
 from chordparser.scales import Scale
 from chordparser.chords import Chord
+from chordparser.quality import Quality
+from chordparser.roman import Roman
 
 
 __author__ = 'Titus Ong'
 __email__ = 'titusongyl@gmail.com'
-__version__ = '0.2.2'
+__version__ = '0.3.0'
diff --git a/src/chordparser/chord_roman_converter.py b/src/chordparser/chord_roman_converter.py
new file mode 100644
index 0000000..e7ae9e8
--- /dev/null
+++ b/src/chordparser/chord_roman_converter.py
@@ -0,0 +1,93 @@
+from chordparser.scales import Scale
+from chordparser.roman import Roman
+from chordparser.notes_editor import NoteEditor
+from chordparser.scales_editor import ScaleEditor
+from chordparser.chords_editor import ChordEditor
+import warnings
+import re
+
+
+class ChordRomanConverter:
+    _symbols = {
+        -1: '\u266d', -2: '\U0001D12B',
+        +1: '\u266f', +2: '\U0001D12A',
+        0: '',
+        }
+    _inversions = {
+        3: (6,), 5: (6, 4),
+    }
+    _inversions_ext = {
+        3: (6, 5), 5: (4, 3),
+        7: (4, 2),
+    }
+    _roman_deg = {
+        1: 'I', 2: 'II', 3: 'III',
+        4: 'IV', 5: 'V', 6: 'VI', 7: 'VII',
+    }
+    _q_dict = {
+        'diminished': '\u00B0',
+        'augmented': '+',
+        'half-diminished': '\u00f8',
+    }
+    NE = NoteEditor()
+    CE = ChordEditor()
+    SE = ScaleEditor()
+
+    def to_roman(self, chord, scale_key):
+        """Return roman numeral notation."""
+        if chord.quality.value in {"power", "sus2", "sus4"}:
+            warnings.warn(
+                "Warning: Power and sus chords are defaulted to major chords",
+                UserWarning
+            )
+            chord = self.CE.change_chord(chord, quality="Maj", inplace=False)
+        if isinstance(scale_key, Scale):
+            scale_root = scale_key.key.root
+        else:
+            scale_root = scale_key.root
+        scale = self.SE.create_scale(scale_root, "major")
+        root = self._get_roman_root(chord, scale)
+        quality = self._get_roman_quality(chord)
+        inversion = self._get_roman_inversion(chord)
+        return Roman(root, quality, inversion)
+
+    def _get_roman_root(self, chord, scale):
+        """Get Roman notation of the chord root."""
+        degree = min(
+            scale.notes.index(x)
+            for x in scale.notes
+            if x.letter() == chord.root.letter()
+        ) + 1
+        # lower/uppercase numeral
+        if chord.quality.value in {"major", "augmented", "dominant"}:
+            quality_fn = str.upper
+        else:
+            quality_fn = str.lower
+        root = quality_fn(ChordRomanConverter._roman_deg[degree])
+        (shift,) = self.NE.get_min_intervals(scale.notes[degree-1], chord.root)
+        sym = ChordRomanConverter._symbols[shift]  # accidental of root
+        return sym + root
+
+    def _get_roman_inversion(self, chord):
+        """Get Roman inversion notation of the chord."""
+        notes = len(chord.base_notes)
+        if chord.inversion and notes <= 4:  # get inversion notation
+            if chord.quality.ext:
+                inv_dict = ChordRomanConverter._inversions_ext
+            else:
+                inv_dict = ChordRomanConverter._inversions
+            return inv_dict.get(chord.inversion, (notes*2-1,))
+        elif notes > 3:
+            return (notes*2-1,)  # chord extension
+        return ()
+
+    def _get_roman_quality(self, chord):
+        """Get Roman notation for chord quality."""
+        q_str = ""
+        if chord.quality.ext and re.match("major", chord.quality.ext):
+            q_str += "M"
+        q_str += ChordRomanConverter._q_dict.get(chord.quality.value, "")
+        return q_str
+
+    def to_chord(self, roman, scale_key):
+        pass
diff --git a/src/chordparser/chords.py b/src/chordparser/chords.py
index bc82867..0f0c76b 100644
--- a/src/chordparser/chords.py
+++ b/src/chordparser/chords.py
@@ -1,14 +1,14 @@
 from chordparser.notes import Note
-from chordparser.notes_editor import NoteEditor
 from chordparser.keys import Key
+from chordparser.quality import Quality
+from chordparser.notes_editor import NoteEditor
 from chordparser.scales_editor import ScaleEditor
-from typing import Union, List
-import copy
+from typing import Union, List, Tuple
 
 
 class Chord:
     """
-    Chord class that is composed of a root note, chord quality, optional suspended and/or added notes, and an optional bass note.
+    Chord class that is composed of a root note, chord quality, optional added notes and an optional bass note.
 
     The Chord class accepts the chord components and builds a tuple of notes provided by the 'notes' method.
 
@@ -20,216 +20,129 @@ class Chord:
     def __init__(
             self,
             root: Note,
-            quality: str,
-            sus: Union[int, None] = None,
-            add: Union[List[str], None] = None,
+            quality: Quality,
+            add: Union[List[Tuple[str, int]], None] = None,
             bass: Union[Note, None] = None,
             string: str = None,
     ):
         self.root = root
         self.quality = quality
-        self.sus = sus
         self.add = add
         self.bass = bass
         self.string = string
         self.build()
 
     def build(self):
-        """Build the chord notes."""
-        self._build_base_triad()
-        self._build_quality()
+        """Build the chord."""
         self._build_base_chord()
-        if self.sus:
-            self._build_sus()
-        if self.add:
-            self._build_add()
-        if self.bass:
-            self._build_bass_note()
+        self._build_full_chord()
         self._build_notation()
 
-    def _build_base_triad(self):
-        """Build the base triad of the chord."""
-        triad_quality = {
-            'major': ("major", 0),
-            'minor': ("minor", 0),
-            'diminished': ("minor", -1),
-            'augmented': ("major", 1),
-            'dominant': ("major", 0),
-            'minor-major': ("minor", 0),
-            'half-diminished': ("minor", -1),
-            'augmented-major': ("major", 1),
-        }
-        quality = self.quality.split()[0]
-        if quality == 'power':
-            self.base_triad = None
-            self._scale = self.SE.create_scale(self.root)
-            return
-        (triad_q, adjust) = triad_quality[quality]
-        self._scale = self.SE.create_scale(self.root, triad_q)
-        last_note = copy.copy(self._scale.notes[4])
-        self.base_triad = (
-                self._scale.notes[0],
-                self._scale.notes[2],
-                last_note.shift_s(adjust),
-                )
-        return
-
-    def _build_quality(self):
-        """Build the chord notes based on its quality."""
-        intervals = {
-            'dominant': -1,
-            'major': 0,
-            'minor': 0,
-            'minor-major': 1,
-            'diminished': -1,
-            'half-diminished': 0,
-            'augmented': -1,
-            'augmented-major': 0,
-        }
-        q_list = self.quality.split()
-        if q_list[0] == 'power':
-            self.notes = [
-                self._scale.notes[0],
-                self._scale.notes[4],
-                ]
-            self.tones = [[None, 1], [None, 5]]
-            return
-        self.notes = list(self.base_triad)
-        self.tones = [[None, 1], [None, 3], [None, 5]]
-        if len(q_list) == 1:
-            return
-        self.notes.append(self._scale.notes[6].shift_s(intervals[q_list[0]]))
-        self.tones.append([None, 7])
-        if q_list[-1] == 'ninth':
-            self.notes.append(self._scale.notes[8])
-            self.tones.append([None, 9])
-        if q_list[-1] == 'eleventh':
-            self.notes += [
-                self._scale.notes[8],
-                self._scale.notes[10],
-                ]
-            self.tones += [[None, 9], [None, 11]]
-        if q_list[-1] == 'thirteenth':
-            self.notes += [
-                self._scale.notes[8],
-                self._scale.notes[10],
-                self._scale.notes[12],
-                ]
-            self.tones += [[None, 9], [None, 11], [None, 13]]
-        if q_list[1] == 'minor':
-            self.notes[-1].shift_s(-1)
-            self.tones[-1][0] = '\u266D'
-        return
-
     def _build_base_chord(self):
-        """Set the base chord notes and tones."""
-        self.base_chord = tuple(self.notes)
-        self.base_tones = tuple(self.tones)
-        return
-
-    def _build_sus(self):
-        """Change notes for suspended chords."""
-        if self.sus == 2:
-            self.notes[1] = self._scale.notes[1]
-            self.tones[1][1] -= 1
-        else:
-            self.notes[1] = self._scale.notes[3]
-            self.tones[1][1] += 1
-        return
+        """Build the chord without any added or bass notes."""
+        self.base_scale = self.SE.create_scale(self.root.value)
+        self.base_intervals = self.quality.intervals
+        self.base_degrees = self.quality.degrees
+        self.base_symbols = self.quality.symbols
+        # get a copy of the root
+        root = self.NE.create_note(self.root.value)
+        notes = [root]
+        idx = len(self.base_intervals)
+        for i in range(idx):
+            new = self.NE.create_note(notes[-1].value)
+            new.transpose(
+                self.base_intervals[i],
+                self.base_degrees[i+1] - self.base_degrees[i]
+                )
+            notes.append(new)
+        self.base_notes = tuple(notes)
+
+    def _build_full_chord(self):
+        self.notes = list(self.base_notes)
+        self.degrees = list(self.base_degrees)
+        self.symbols = list(self.base_symbols)
+        self._build_add()
+        self._build_bass_note()
+        self.notes = tuple(self.notes)
+        self.degrees = tuple(self.degrees)
+        self.symbols = tuple(self.symbols)
+        self.intervals = self.NE.get_intervals(*self.notes)
 
     def _build_add(self):
         """Add notes for chords with added notes."""
+        if not self.add:
+            return
         symbols = {
             '\u266d': -1, '\U0001D12B': -2,
             '\u266f': +1, '\U0001D12A': +2,
+            '': 0,
             }
-        pos_list = []
         for each in self.add:
-            adjustment = 0
-            accidental = None
-            if each[0] in symbols.keys():
-                adjustment = symbols[each[0]]
-                accidental = each[0]
-                each = each[1:]
-            tone = int(each)
-            pos = max(self.tones.index(i) for i in self.tones if i[1] < tone)+1
-            self.tones.insert(pos, [accidental, tone])
-            self.notes.insert(pos, self._scale.notes[tone-1].shift_s(adjustment))
-            pos_list.append(pos)
-        self.add = [x for _, x in sorted(zip(pos_list, self.add))]
-        return
+            sym = each[0]
+            tone = each[1]
+            shift = symbols[sym]
+            pos = max(
+                self.degrees.index(i)
+                for i in self.degrees
+                if i < tone
+                ) + 1
+            self.symbols.insert(pos, sym)
+            self.degrees.insert(pos, tone)
+            # copy the note
+            new_note = self.NE.create_note(self.base_scale.notes[tone-1].value)
+            new_note.shift_s(shift)
+            self.notes.insert(pos, new_note)
 
     def _build_bass_note(self):
         """Build the bass note."""
+        self.inversion = None
+        if not self.bass:
+            return
         if self.bass in self.notes:
             idx = self.notes.index(self.bass)
-            self.notes.pop(idx)
-            self.notes.insert(0, self.bass)
-            tone = self.tones.pop(idx)
-            self.tones.insert(0, tone)
+            self.notes.insert(0, self.notes.pop(idx))
+            self.symbols.insert(0, self.symbols.pop(idx))
+            self.degrees.insert(0, self.degrees.pop(idx))
+            self.inversion = self.degrees[0]
             return
         self.notes.insert(0, self.bass)
         degree = min(
-            self._scale.notes.index(x) for x in self._scale.notes if x.letter() == self.bass.letter()
-            )+1
-        (interval,) = self.NE.get_min_intervals(self._scale.notes[degree-1], self.bass)
+            self.base_scale.notes.index(x)
+            for x in self.base_scale.notes
+            if x.letter() == self.bass.letter()
+            ) + 1
+        self.degrees.insert(0, degree)
+        (shift,) = self.NE.get_min_intervals(
+            self.base_scale.notes[degree-1],
+            self.bass
+        )
         symbols = {
             -1: '\u266d', -2: '\U0001D12B',
             +1: '\u266f', +2: '\U0001D12A',
-            0: None,
+            0: '',
             }
-        accidental = symbols[interval]
-        self.tones.insert(0, [accidental, degree])
-        return
+        sym = symbols[shift]
+        self.symbols.insert(0, sym)
 
     def _build_notation(self):
         """Build a standardised chord notation."""
-        q_dict = {
-            'power': ('5', ''),
-            'major': ('maj', ''),
-            'minor': ('m', ''),
-            'diminished': ('dim', ''),
-            'augmented': ('aug', ''),
-            'dominant': ('', ''),
-            'minor-major': ('minmaj', ''),
-            'half-diminished': ('m', '\u266D5'),
-            'augmented-major': ('maj', '\u266F5'),
-        }
-        ext_dict = {
-            '': '',
-            'seventh': '7',
-            'ninth': '9',
-            'eleventh': '11',
-            'thirteenth': '13',
-            'minor ninth': '\u266D9',
-            'minor eleventh': '\u266D11',
-            'minor thirteenth': '\u266D13',
-        }
-        if self.quality == 'major':
-            q_short = ''
-        else:
-            q_list = self.quality.split()
-            (first, last) = q_dict[q_list.pop(0)]
-            mid = ext_dict[' '.join(q_list)]
-            q_short = first+mid+last
-        if self.sus:
-            sus = 'sus'+str(self.sus)
-        else:
-            sus = ''
-        add = ''
+        add = ""
         if self.add:
             for each in self.add:
-                if not each[0] in {'\u266d', '\U0001D12B', '\u266f', '\U0001D12A'}:
-                    add += 'add'
-                add += each
+                if not add and not self.quality.short():
+                    string = 'add'  # when quality is blank
+                elif not each[0]:
+                    string = 'add'  # when there's no accidental
+                else:
+                    string = ''
+                add += string + each[0] + str(each[1])
         if self.bass:
             bass = '/'+str(self.bass)
         else:
             bass = ''
-        self.notation = str(self.root)+q_short+sus+add+str(bass)
+        self.notation = str(self.root) + self.quality.short() + add + bass
         if not self.string:
             self.string = self.notation
-        return
 
     def transpose(self, semitones: int, letter: int):
         """Transpose the chord by specifying the semitone and letter intervals."""
@@ -239,10 +152,6 @@ class Chord:
         self.build()
         return self
 
-    # def format(self, options):
-    #     """Specify options to format string output."""
-    #     pass
-
     def _xstr(self, value):
         # To print blank for None values
         if value is None:
@@ -253,13 +162,12 @@ class Chord:
         return f'{self.notation} chord'
 
     def __eq__(self, other):
-        # Allow comparison between Keys by checking their basic attributes
+        # Allow comparison between Chords by checking their basic attributes
         if not isinstance(other, Chord):
             return NotImplemented
         return (
             self.root == other.root
             and self.quality == other.quality
-            and self.sus == other.sus
             and self.add == other.add
             and self.bass == other.bass
             )
diff --git a/src/chordparser/chords_analyser.py b/src/chordparser/chords_analyser.py
index 0cf7e1b..5948762 100644
--- a/src/chordparser/chords_analyser.py
+++ b/src/chordparser/chords_analyser.py
@@ -1,7 +1,8 @@
 from chordparser.notes_editor import NoteEditor
 from chordparser.scales_editor import ScaleEditor
 from chordparser.chords_editor import ChordEditor
-from typing import Union, List
+from chordparser.chord_roman_converter import ChordRomanConverter
+from typing import List
 import warnings
 
 
@@ -11,36 +12,6 @@ class ChordAnalyser:
 
     The ChordAnalyser can return the roman numeral notation of a chord using the 'roman' method. It also analyses the chord's relationship to a scale using the 'analyse_diatonic' and 'analyse_all' methods, which account for only diatonic chords and chords from all modes respectively.
     """
-    intervals = {
-        (4, 3): str.upper,
-        (3, 4): str.lower,
-        (3, 3): str.lower,
-        (4, 4): str.upper,
-    }
-    symbols = {
-        -1: '\u266d', -2: '\U0001D12B',
-        +1: '\u266f', +2: '\U0001D12A',
-        0: '',
-        }
-    inversions = {
-        (1, 3): '6', (2, 3): '64',
-        (1, 4): '65', (2, 4): '43',
-        (3, 4): '42',
-    }
-    roman_deg = {
-        1: 'I', 2: 'II', 3: 'III',
-        4: 'IV', 5: 'V', 6: 'VI', 7: 'VII',
-    }
-    q_dict = {
-        'major': 'M',
-        'minor': '',
-        'diminished': '\u00B0',
-        'augmented': '+',
-        'dominant': '',
-        'minor-major': 'M',
-        'half-diminished': '\u00f8',
-        'augmented-major': '+M',
-    }
     mode_list = [
         'major',
         'minor',
@@ -53,38 +24,23 @@ class ChordAnalyser:
     NE = NoteEditor()
     CE = ChordEditor()
     SE = ScaleEditor()
+    CRC = ChordRomanConverter()
 
-    def roman(self, chord, scale):
-        """Return roman numeral notation."""
-        if chord.quality == 'power':
-            raise ValueError("Cannot parse power chords")
-        if chord.sus:
-            warnings.warn("Warning: sus chords may not be parsed correctly", UserWarning)
-        c_int = (self.NE.get_intervals(*chord.base_chord[0:3]))
-        scale = self.SE.create_scale(scale.key.root, "major")
-        degree = min(scale.notes.index(x) for x in scale.notes if x.letter()==chord.root.letter())+1
-        q_fn = self.intervals[c_int]
-        c_qual = q_fn(self.roman_deg[degree])  # lower/uppercase numeral
-        (shift,) = self.NE.get_min_intervals(scale.notes[degree-1], chord.root)
-        symb = self.symbols[shift]  # accidental of root
-        notes = len(chord.base_tones)
-        if chord.bass and chord.tones[0] in chord.base_tones:
-            inversion = chord.base_tones.index(chord.tones[0])
-            inv_str = self.inversions.get((inversion, notes), str(notes*2-1))  # inversion notation
-        elif notes > 3:
-            inv_str = str(notes*2-1)
-        else:
-            inv_str = ''
-        qual = chord.quality.split()[0]
-        if notes > 3 or qual == 'augmented' or qual == 'diminished':
-            q_str = self.q_dict[qual]  # quality notation
-        else:
-            q_str = ''
-        numeral = symb+c_qual+q_str+inv_str
-        return numeral
-
-    def analyse_diatonic(self, chord, scale, incl_submodes: bool = False) -> List[tuple]:
-        """Return all possible chord function (None if not found). Format: List[(Roman numeral, scale mode, scale submode)]."""
+    def analyse_diatonic(
+            self, chord, scale,
+            incl_submodes: bool = False,
+            allow_power_sus=False,
+            default_power_sus="M",
+    ) -> List[tuple]:
+        """Return all possible chord function (None if not found). Format: List[(Roman numeral, scale mode, scale submode)]. To convert power and sus chords to be parsed as Roman notation, use allow_power_sus=True and set default_power_sus ("M" for major, "m" for minor)."""
+        if chord.quality.value in {"power", "sus2", "sus4"}:
+            if not allow_power_sus:
+                return []
+            chord = self.CE.change_chord(
+                chord,
+                quality=default_power_sus,
+                inplace=False
+            )
         if not incl_submodes:
             j = [scale.key.submode]
         elif scale.key.mode in {'minor', 'aeolian'}:
@@ -98,18 +54,30 @@ class ChordAnalyser:
             for i in range(7):
                 nscale = self.SE.create_scale(scale.key.root, scale.key.mode, submode)
                 diatonic = self.CE.create_diatonic(nscale, i+1)
-                if chord.base_triad == diatonic.base_triad:
-                    chords.append((self.roman(chord, nscale), nscale.key.mode, submode))
+                if chord.base_notes[0:3] == diatonic.base_notes:
+                    chords.append(
+                        (self.CRC.to_roman(chord, nscale),
+                         nscale.key.mode,
+                         submode)
+                    )
         return chords
 
-    def analyse_all(self, chord, scale, incl_submodes: bool = False):
+    def analyse_all(
+            self, chord, scale,
+            incl_submodes: bool = False,
+            allow_power_sus=False,
+            default_power_sus="M",
+    ):
         """Return all possible chord function accounting for all modes (None if not found)."""
         self.mode_list.remove(scale.key.mode)
         self.mode_list.insert(0, scale.key.mode)  # shift to the front
         chords = []
         for mode in self.mode_list:
             nscale = self.SE.create_scale(scale.key.root, mode)
-            result = self.analyse_diatonic(chord, nscale, incl_submodes)
+            result = self.analyse_diatonic(
+                chord, nscale, incl_submodes,
+                allow_power_sus, default_power_sus
+            )
             if result:
                 chords += result
         return chords
diff --git a/src/chordparser/chords_editor.py b/src/chordparser/chords_editor.py
index b47ab3b..cdf3273 100644
--- a/src/chordparser/chords_editor.py
+++ b/src/chordparser/chords_editor.py
@@ -1,11 +1,10 @@
-from chordparser.notes import Note
 from chordparser.keys import Key
 from chordparser.scales import Scale
 from chordparser.chords import Chord
+from chordparser.quality import Quality
 from chordparser.notes_editor import NoteEditor
-from chordparser.keys_editor import KeyEditor
-from chordparser.scales_editor import ScaleEditor
-from typing import Union, List
+from chordparser.quality_editor import QualityEditor
+from typing import Union
 import re
 
 
@@ -15,221 +14,73 @@ class ChordEditor:
 
     The ChordEditor class can parse a chord notation string to create a Chord using the 'create_chord' method, or create a Chord by specifying a Scale/Key and scale degree using the 'create_diatonic' method. The ChordEditor can also change a chord using the 'change_chord' method.
     """
+    NE = NoteEditor()
+    QE = QualityEditor()
     _letter_pattern = '[a-gA-G]'
     _flat_pattern = '\u266D|\U0001D12B|bb|b'
     _sharp_pattern = '\u266F|\U0001D12A|##|#'
     _symbol_pattern = f"{_flat_pattern}|{_sharp_pattern}"
     _note_pattern = f"(?:{_letter_pattern})(?:{_symbol_pattern}){{0,1}}"
-    _major_pattern = 'Maj|Ma|M|maj|\u0394'
-    _minor_pattern = 'min|m|-'
-    _dim_pattern = 'dim|o|\u00B0'
-    _aug_pattern = 'aug|\+'
-    _halfdim_pattern = '\u00f8|\u00d8'
-    _dom_pattern = 'dom'
-    _minmaj_pattern = f"(?:{_minor_pattern})(?:{_major_pattern})"
-    _augmaj_pattern = f"(?:{_aug_pattern})(?:{_major_pattern})"
-    _power_chord = "5"
-    _extended_str = f"({_flat_pattern}){{0,1}}(7|9|11|13)"
-    _altered_5 = f"({_dim_pattern}|{_aug_pattern}|{_symbol_pattern})5"
     _others = f"[^/]+"
-    _suspended = f"sus(2|4){{0,1}}"
     _added = f"(?:add){{0,1}}({_symbol_pattern}){{0,1}}(2|4|6|9|11|13)"
-
-    # Pattern notation [regex group]
-    # Match string [0]:
-    #     note [1], {
-    #         power chord [3] |
-    #         extended chords [4] {
-    #             (
-    #                 dominant [5-none; minor if note is lowercase] |
-    #                 minmaj [6] |
-    #                 augmaj [7] |
-    #                 aug [8] |
-    #                 dim [9] |
-    #                 halfdim [10] |
-    #                 major [11] |
-    #                 minor [12]
-    #                 ) +
-    #             (symbol [14] + degree [15]) [13]
-    #             } |
-    #         triads [16] (
-    #             aug [17] |
-    #             dim [18] |
-    #             major [19] |
-    #             minor [20]
-    #             )
-    #         } [2],
-    #         altered 5ths [21],
-    #         others (sus/add) [22],
-    #         bass [23]
     _pattern = (
         f"({_note_pattern})"
-        f"(({_power_chord})|"
-        f"((({_minmaj_pattern})|({_augmaj_pattern})|"
-        f"({_aug_pattern})|({_dim_pattern})|"
-        f"({_halfdim_pattern})|({_major_pattern})|"
-        f"({_minor_pattern})){{0,1}}"  # 0 for dominant/minor ext (lowercase note)
-        f"({_extended_str}))|"
-        f"(({_aug_pattern})|({_dim_pattern})|"
-        f"({_major_pattern})|({_minor_pattern}))){{0,1}}"
-        f"(?:{_altered_5}){{0,1}}"
+        f"({QE.quality_pattern})"
         f"({_others}){{0,1}}"
         f"(?:/({_note_pattern})){{0,1}}"
-        )
+    )
     _symbols = {
         'b': '\u266D', 'bb': '\U0001D12B',
         '#': '\u266F', '##': '\U0001D12A',
         None: '',
-        }
-    _quality_intervals = {
-        (7,): 'power',
-        (4, 3): 'major',
-        (3, 4): 'minor',
-        (3, 3): 'diminished',
-        (4, 4): 'augmented',
-        (4, 3, 3): 'dominant',
-        (4, 3, 4): 'major',
-        (3, 4, 3): 'minor',
-        (3, 4, 4): 'minor-major',
-        (3, 3, 3): 'diminished',
-        (3, 3, 4): 'half-diminished',
-        (4, 4, 2): 'augmented',
-        (4, 4, 3): 'augmented-major',
-    }
-    _ext_words = {
-        None: '',
-        'M7': 'seventh',
-        'M9': 'ninth',
-        'M11': 'eleventh',
-        'M13': 'thirteenth',
-        'm9': 'minor ninth',
-        'm11': 'minor eleventh',
-        'm13': 'minor thirteenth',
     }
-    NE = NoteEditor()
 
     def create_chord(self, value):
         """Create a chord from a string (do not use any spaces)."""
-        if not isinstance(value, str):
-            raise TypeError("Only strings are accepted")
         rgx = re.match(ChordEditor._pattern, value, re.UNICODE)
         if not rgx:
-            raise ValueError("Chord could not be recognised")
-        root, quality, sus, add, bass = self._parse_rgx(rgx)
-        return Chord(root, quality, sus, add, bass, string=rgx.group(0))
+            raise SyntaxError(f"'{value}' could not be parsed")
+        root, quality, add, bass = self._parse_rgx(rgx)
+        return Chord(root, quality, add, bass, string=rgx.group(0))
 
     def _parse_rgx(self, rgx):
-        """Parse chord notation regex."""
-        root = self._parse_root(rgx)
-        temp_q, extension = self._parse_quality(rgx)
-        q = self._parse_alt5(rgx, temp_q)  # altered 5th may change quality
-        quality = "{} {}".format(
-            ChordEditor._quality_intervals[tuple(q)],
-            ChordEditor._ext_words[extension],
-            ).strip()
-        sus, add = self._parse_others(rgx)
-        bass_note = self._parse_bass(rgx)
-        return root, quality, sus, add, bass_note
-
-    def _parse_root(self, rgx):
+        """Distribute regex groups and form chord notation."""
+        root = self._parse_root(rgx.group(1))
+        quality = self._parse_quality(rgx.group(2), rgx.group(1)[0].isupper())
+        add = self._parse_add(rgx.groups()[-2])
+        bass_note = self._parse_bass(rgx.groups()[-1])
+        return root, quality, add, bass_note
+
+    def _parse_root(self, root):
         """Return chord root."""
-        return self.NE.create_note(rgx.group(1))
-
-    def _parse_quality(self, rgx):
-        """Return chord quality and extension."""
-        if rgx.group(3):
-            return [7], None
-        if rgx.group(4):
-            return self._parse_ext(rgx)
-        if rgx.group(16):
-            return self._parse_triad(rgx)
-        if rgx.group(1)[0].isupper():
-            return [4, 3], None
-        return [3, 4], None
+        return self.NE.create_note(root)
 
-    def _parse_ext(self, rgx):
-        """Parse quality of extended chords."""
-        if not rgx.group(5):
-            if rgx.group(1)[0].isupper():
-                quality = [4, 3, 3]
-            else:
-                quality = [3, 4, 3]
-        elif rgx.group(6):
-            quality = [3, 4, 4]
-        elif rgx.group(7):
-            quality = [4, 4, 3]
-        elif rgx.group(8):
-            quality = [4, 4, 2]
-        elif rgx.group(9):
-            quality = [3, 3, 3]
-        elif rgx.group(10):
-            quality = [3, 3, 4]
-        elif rgx.group(11):
-            quality = [4, 3, 4]
-        elif rgx.group(12):
-            quality = [3, 4, 3]
-        if rgx.group(14):
-            ext = 'm'
-        else:
-            ext = 'M'
-        ext += rgx.group(15)
-        return quality, ext
-
-    def _parse_triad(self, rgx):
-        """Parse quality of triads."""
-        if rgx.group(17):
-            return [4, 4], None
-        if rgx.group(18):
-            return [3, 3], None
-        if rgx.group(19):
-            return [4, 3], None
-        return [3, 4], None
+    def _parse_quality(self, string, capital_note=True):
+        """Return chord quality."""
+        return self.QE.create_quality(string, capital_note)
 
-    def _parse_alt5(self, rgx, temp_q):
-        """Change the quality for chords with altered fifths."""
-        if not rgx.group(21):
-            return temp_q
-        if rgx.group(21) in ChordEditor._dim_pattern+ChordEditor._flat_pattern:
-            temp_q[1] -= 1
-            if len(temp_q) > 2:
-                temp_q[2] += 1
-        else:
-            temp_q[1] += 1
-            if len(temp_q) > 2:
-                temp_q[2] -= 1
-        return temp_q
-
-    def _parse_others(self, rgx):
-        """Parse suspended and added notes."""
-        if not rgx.group(22):
-            return None, None
-        string = rgx.group(22)
-        # sus
-        reg = re.search(ChordEditor._suspended, string, re.UNICODE)
-        if reg:
-            note = reg.group(1) or 4  # if 'sus'
-            sus = int(note)
-            string = ''.join(string.split(reg.group(0)))
-        else:
-            sus = None
-        # add
+    def _parse_add(self, string):
+        """Parse added notes."""
+        if string is None:
+            return None
         add = []
         while True:
             reg = re.search(ChordEditor._added, string, re.UNICODE)
             if not reg:
                 break
-            foo = ChordEditor._symbols[reg.group(1)]+reg.group(2)
+            foo = (ChordEditor._symbols[reg.group(1)], int(reg.group(2)))
             add.append(foo)
             string = ''.join(string.split(reg.group(0)))
         if string.strip():
-            raise SyntaxError(f"String '{string.strip()}' could not be parsed")
-        return sus, add or None
+            raise SyntaxError(f"'{string.strip()}' could not be parsed")
+        add.sort(key=lambda x: x[1])  # sort by scale degree
+        return add
 
-    def _parse_bass(self, rgx):
+    def _parse_bass(self, string):
         """Parse the bass note."""
-        if not rgx.group(23):
+        if not string:
             return None
-        return self.NE.create_note(rgx.group(23))
+        return self.NE.create_note(string)
 
     def _xstr(self, value):
         # To print blank for None values
@@ -237,90 +88,73 @@ class ChordEditor:
             return ''
         return value
 
-    def create_diatonic(
-            self,
-            value: Union[Scale, Key],
-            degree: int = 1,
-    ):
+    def create_diatonic(self, value: Union[Scale, Key], degree: int = 1):
         """Create a diatonic chord from a scale/key by specifying the scale degree."""
-        if degree not in {1, 2, 3, 4, 5, 6, 7}:
+        if degree not in range(1, 8):
             raise ValueError("Scale degree must be between 1 and 7")
-        if isinstance(value, Key):
+        if not isinstance(value, Scale):
             scale_ = Scale(value)
         else:
             scale_ = value
-        root = scale_.notes[degree-1]
+        root = scale_.notes[degree - 1]
         bass = None
-        sus = None
         add = None
         base_chord = (
-            scale_.notes[degree-1],
-            scale_.notes[degree+1],
-            scale_.notes[degree+3],
-            )
+            scale_.notes[degree - 1],
+            scale_.notes[degree + 1],
+            scale_.notes[degree + 3],
+        )
+        quality_intervals = {
+            (4, 3): 'M',
+            (3, 4): 'm',
+            (3, 3): 'dim',
+            (4, 4): 'aug',
+        }
         interval = self.NE.get_intervals(*base_chord)
-        quality = ChordEditor._quality_intervals[interval]
-        return Chord(root, quality, sus, add, bass)
+        q_str = quality_intervals[interval]
+        quality = self.QE.create_quality(q_str)
+        return Chord(root, quality, add, bass)
 
     def change_chord(
             self,
             chord,
-            root: Union[Note, None] = None,
+            root: Union[str, None] = None,
             quality: Union[str, None] = None,
-            sus: Union[int, bool, None] = None,
-            add: Union[List[str], None] = None,
-            remove: Union[List[str], None] = None,
-            bass: Union[Note, bool, None] = None,
+            add: Union[str, None] = None,
+            remove: Union[str, None] = None,
+            bass: Union[str, bool, None] = None,
+            inplace=True,
     ):
-        """Change the chord by specifying root, quality, sus, add, remove (you can only remove added notes), and/or bass. To remove the bass or sus note, use the argument False. To remove all added notes, use remove=True."""
-        if not isinstance(chord, Chord):
-            raise TypeError(f"Object {chord} is not a 'Chord'")
+        """Change the chord by specifying root, quality, add, remove (you can only remove added notes), and/or bass. To remove the bass note, use bass=False. To remove all added notes, use remove=True."""
+        if not inplace:
+            chord = Chord(
+                chord.root, chord.quality,
+                chord.add, chord.bass, chord.string
+            )
         if root:
-            chord.root = self.NE.create_note(root)
+            chord.root = self._parse_root(root)
         if quality:
-            q_list = quality.split()
-            words = len(q_list)
-            if q_list[0] not in ChordEditor._quality_intervals.values():
-                raise ValueError(f"Could not parse {quality}")
-            if words == 2:
-                if q_list[1] not in ChordEditor._ext_words.values():
-                    raise ValueError(f"Could not parse {quality}")
-            if words == 3:
-                if f"{q_list[1]} {q_list[2]}" not in ChordEditor._ext_words.values():
-                    raise ValueError(f"Could not parse {quality}")
-            if words < 1 or words > 3:
-                raise ValueError(f"Could not parse {quality}")
-            if words != 1 and q_list[0] == 'power':
-                raise ValueError(f"Could not parse {quality}")
-            chord.quality = quality
-        if sus not in {2, 4, False, None}:
-            raise ValueError(f"Could not parse {sus}")
-        elif sus is not None:
-            chord.sus = sus or None
+            chord.quality = self._parse_quality(quality)
         if remove is True:
             chord.add = None
         elif remove:
-            for each in remove:
-                rgx = re.match(f"^{ChordEditor._added}$", each)
-                if not rgx:
-                    raise ValueError(f"Could not parse {each}")
-                new_each = ChordEditor._symbols[rgx.group(1)] + rgx.group(2)
-                if not chord.add or new_each not in chord.add:
-                    raise ValueError(f"{each} is not an added note")
-                chord.add.remove(new_each)
+            removed = self._parse_add(remove)
+            for each in removed:
+                try:
+                    chord.add.remove(each)
+                except ValueError:
+                    raise ValueError(f"'{each}' is not an added note")
+                except AttributeError:
+                    raise IndexError("No added notes to be removed")
             chord.add = chord.add or None
         if add:
             chord.add = chord.add or []
-            for each in add:
-                rgx = re.match(f"^{ChordEditor._added}$", each)
-                if not rgx:
-                    raise ValueError(f"Could not parse {each}")
-                new_each = ChordEditor._symbols[rgx.group(1)] + rgx.group(2)
-                chord.add.append(new_each)
+            chord.add += self._parse_add(add)
+            # re-sort after adding new added notes
+            chord.add.sort(key=lambda x: x[1])
         if bass:
-            chord.bass = self.NE.create_note(bass)
+            chord.bass = self._parse_bass(bass)
         if bass is False:
             chord.bass = None
         chord.build()
         return chord
-
diff --git a/src/chordparser/keys.py b/src/chordparser/keys.py
index e8a9ab9..8e293dd 100644
--- a/src/chordparser/keys.py
+++ b/src/chordparser/keys.py
@@ -8,6 +8,7 @@ class Key:
 
     The Key class composes of a Note class with the additional attributes 'mode' and 'submode' (e.g. types of minor keys). It is created by the KeyEditor. Keys have the same methods as Notes.
     """
+
     def __init__(
             self, root, mode: str,
             submode: Union[str, None]):
@@ -35,15 +36,15 @@ class Key:
                 self.root == other.root
                 and other.mode in {'major', 'ionian'}
                 and self.submode == other.submode
-                )
+            )
         if self.mode in {'minor', 'aeolian'}:
             return (
                 self.root == other.root
                 and other.mode in {'minor', 'aeolian'}
                 and self.submode == other.submode
-                )
+            )
         return (
-                self.root == other.root
-                and self.mode == other.mode
-                and self.submode == other.submode
-                )
+            self.root == other.root
+            and self.mode == other.mode
+            and self.submode == other.submode
+        )
diff --git a/src/chordparser/keys_editor.py b/src/chordparser/keys_editor.py
index 4efb83e..e139a62 100644
--- a/src/chordparser/keys_editor.py
+++ b/src/chordparser/keys_editor.py
@@ -4,23 +4,24 @@ from chordparser.notes_editor import NoteEditor
 from typing import Union
 
 
+class ModeError(Exception):
+    """Raise when a key's mode is invalid."""
+    pass
+
+
 class KeyEditor:
     """
     KeyEditor class that can create/change a Key, and change keys to their relative major/minor key.
 
-    The KeyEditor class can create a Key using the 'create_key' method by accepting a Note or string, an optional mode (default 'major') and optional submode. Submodes are only available for the minor/aeolian mode (default 'natural'). The 'relative_major' and 'relative_minor' methods change the key into its relative major/minor (submode can be specified for 'relative_minor'). The 'change_key' method can be used to alter the root, mode and submode all at once.
+    The KeyEditor class can create a Key using the 'create_key' method by accepting a Note or string, an optional mode (default 'major') and optional submode. Submodes are only available for the minor/aeolian mode (default 'natural'). The 'relative_major' and 'relative_minor' methods change the key into its relative major/minor (submode can be specified for 'relative_minor'). The 'change_key' method can be used to alter the root, mode and/or submode.
     """
     _modes = (
         'major', 'minor', 'ionian', 'dorian', 'phrygian',
         'lydian', 'mixolydian', 'aeolian', 'locrian'
-        )
-    _submodes = {
-        'minor': ('harmonic', 'melodic', 'natural'),
-        'aeolian': ('harmonic', 'melodic', 'natural'),
-        }
-    _notes_tuple = (
-        'C', 'D', 'E', 'F', 'G', 'A', 'B',
-        'C', 'D', 'E', 'F', 'G', 'A', 'B')
+    )
+    _modes_with_submodes = ('minor', 'aeolian')
+    _submodes = ('harmonic', 'melodic', 'natural')
+    NE = NoteEditor()
 
     def create_key(
             self,
@@ -44,39 +45,30 @@ class KeyEditor:
 
     def _check_root(self, root):
         if not isinstance(root, Note):
-            try:
-                NE = NoteEditor()
-                root = NE.create_note(root)
-            except TypeError:
-                raise TypeError("Only Notes and strings are accepted for root note")
+            root = KeyEditor.NE.create_note(root)
         return root
 
     def _check_mode(self, mode):
-        if not isinstance(mode, str):
-            raise TypeError("Only strings are accepted for mode")
         if mode.lower() not in KeyEditor._modes:
-            raise KeyError("Mode could not be found")
+            raise SyntaxError(f"'{mode}' could not be parsed")
         return mode.lower()
 
     def _check_submode(self, mode, submode):
-        if submode is None and mode not in KeyEditor._submodes.keys():
-            return submode
+        if mode not in KeyEditor._modes_with_submodes:
+            if submode is None:
+                return submode
+            raise ModeError(f"'{mode}' does not have any submodes")
+        # minor modes
         if submode is None:
             return 'natural'
-        if not isinstance(submode, str):
-            raise TypeError("Only strings are accepted")
-        submode_tuple = KeyEditor._submodes.get(mode)
-        if submode_tuple is None:
-            raise KeyError("Mode does not have any submodes")
-        if submode.lower() not in submode_tuple:
-            raise KeyError("Submode could not be found")
+        if submode.lower() not in KeyEditor._submodes:
+            raise SyntaxError(f"'{submode}' could not be parsed")
         return submode.lower()
 
     def relative_major(self, key):
         """Change a key to its relative major."""
-        if not key.mode in {'minor', 'aeolian'}:
-            raise KeyError("Key is not minor")
-        idx = KeyEditor._notes_tuple.index(key.letter())
+        if key.mode not in {'minor', 'aeolian'}:
+            raise ModeError(f"'{key}' is not minor")
         key.transpose(3, 2)
         key.submode = None
         key.mode = 'major'
@@ -84,26 +76,23 @@ class KeyEditor:
 
     def relative_minor(self, key, submode='natural'):
         """Change a key to its relative minor."""
-        if not key.mode in {'major', 'ionian'}:
-            raise KeyError("Key is not major")
-        if submode not in KeyEditor._submodes['minor']:
-            raise KeyError("Submode could not be found")
-        idx = KeyEditor._notes_tuple.index(key.letter())
+        if key.mode not in {'major', 'ionian'}:
+            raise ModeError(f"'{key}' is not major")
+        if submode.lower() not in KeyEditor._submodes:
+            raise SyntaxError(f"'{submode}' could not be parsed")
         key.transpose(-3, -2)
         key.submode = submode
         key.mode = 'minor'
         return key
 
-    def change_key(self, key, root: Union[Note, None] = None, mode: Union[str, None] = None, submode: Union[str, None] = None):
-        """Change the key by specifying root, mode and/or submode."""
-        if not isinstance(key, Key):
-            raise TypeError(f"Object {key} is not a 'Key'")
+    def change_key(self, key, root=None, mode=None, submode=None, inplace=True):
+        """Change the key by specifying root, mode and/or submode. Use inplace=False to return a new key."""
+        if not inplace:
+            key = self.create_key(key.root, key.mode, key.submode)
         if root:
             key.root = self._check_root(root)
         if mode:
             key.mode = self._check_mode(mode)
-        if not submode and key.mode in KeyEditor._submodes.keys():
-            key.submode = self._check_submode(key.mode, key.submode)
-        else:
-            key.submode = self._check_submode(key.mode, submode)
+        submode = submode or key.submode
+        key.submode = self._check_submode(key.mode, submode)
         return key
diff --git a/src/chordparser/notes.py b/src/chordparser/notes.py
index dbe2e49..f368efe 100644
--- a/src/chordparser/notes.py
+++ b/src/chordparser/notes.py
@@ -4,7 +4,7 @@ class Note:
 
     The Note class consists of notation a-g or A-G, with optional unicode accidental symbols \u266d, \u266f, \U0001D12B, or \U0001D12A. It is created by the NoteEditor.
 
-    Notes can have accidentals set using the 'accidental' method, and can be shifted by semitones using the 'shift_s' method. The letter of the Note can be shifted using the 'shift_l' method. Notes also have 'letter' and 'symbol' methods to get their respective values. Numerical representation of the Note value can be accessed via the 'num_value' and 'symbol_value' methods. Notes can be transposed using the 'transpose' method.
+    Notes can have accidentals set using the 'accidental' method, and can be shifted by semitones using the 'shift_s' method. The letter of the Note can be shifted using the 'shift_l' method. Notes also have 'letter' and 'symbol' methods to get their respective values. Numerical representation of the Note value can be accessed via the 'num_value', 'letter_value' and 'symbol_value' methods. Notes can be transposed using the 'transpose' method.
 
     Notes can be compared either with other Notes or with strings.
     """
@@ -13,49 +13,24 @@ class Note:
     _doubleflat = '\U0001D12B'
     _doublesharp = '\U0001D12A'
     _symbols = {
-        _flat: -1, _doubleflat: -2,
-        _sharp: +1, _doublesharp: +2,
         -1: _flat, -2: _doubleflat,
         +1: _sharp, +2: _doublesharp,
-        0: '', None: 0,
-        }
+        0: '',
+    }
+    _symbol_signs = {
+        _flat: -1, _doubleflat: -2,
+        _sharp: 1, _doublesharp: 2,
+        '': 0,
+    }
     _note_values = {  # Basis: C = 0
-        ('C' + _doubleflat): 10,
-        ('C' + _flat): 11,
         'C': 0,
-        ('C' + _sharp): 1,
-        ('C' + _doublesharp): 2,
-        ('D' + _doubleflat): 0,
-        ('D' + _flat): 1,
         'D': 2,
-        ('D' + _sharp): 3,
-        ('D' + _doublesharp): 4,
-        ('E' + _doubleflat): 2,
-        ('E' + _flat): 3,
         'E': 4,
-        ('E' + _sharp): 5,
-        ('E' + _doublesharp): 6,
-        ('F' + _doubleflat): 3,
-        ('F' + _flat): 4,
         'F': 5,
-        ('F' + _sharp): 6,
-        ('F' + _doublesharp): 7,
-        ('G' + _doubleflat): 5,
-        ('G' + _flat): 6,
         'G': 7,
-        ('G' + _sharp): 8,
-        ('G' + _doublesharp): 9,
-        ('A' + _doubleflat): 7,
-        ('A' + _flat): 8,
         'A': 9,
-        ('A' + _sharp): 10,
-        ('A' + _doublesharp): 11,
-        ('B' + _doubleflat): 9,
-        ('B' + _flat): 10,
         'B': 11,
-        ('B' + _sharp): 0,
-        ('B' + _doublesharp): 1,
-        }
+    }
     _notes_tuple = (
         'C', 'D', 'E', 'F', 'G', 'A', 'B',
         'C', 'D', 'E', 'F', 'G', 'A', 'B')
@@ -65,44 +40,43 @@ class Note:
 
     def accidental(self, value: int):
         """Change a note's accidental by specifying a value from -2(doubleflat) to 2(doublesharp)."""
-        if not isinstance(value, int):
-            raise TypeError("Only integers are accepted")
-        if value not in {-2, -1, 0, 1, 2}:
+        if value not in range(-2, 3):
             raise ValueError(
                 "Only integers between -2 and 2 are accepted"
-                )
-        self.value = self.letter() + Note._symbols.get(value)
+            )
+        self.value = self.letter() + Note._symbols[value]
         return self
 
     def shift_s(self, value: int):
         """Shift a note's accidental."""
-        if not isinstance(value, int):
-            raise TypeError("Only integers are accepted")
         value += self.symbol_value()
-        if value not in {-2, -1, 0, 1, 2}:
+        if value not in range(-2, 3):
             raise ValueError(
                 "Only symbols up to doublesharps and doubleflats are accepted"
-                )
-        self.value = self.letter() + Note._symbols.get(value)
+            )
+        self.value = self.letter() + Note._symbols[value]
         return self
 
     def shift_l(self, value: int):
         """Shift a note's letter."""
-        if not isinstance(value, int):
-            raise TypeError("Only integers are accepted")
-        pos = self._notes_tuple.index(self.letter()) + value % 7
-        new_letter = self._notes_tuple[pos]
+        pos = (Note._notes_tuple.index(self.letter()) + value) % 7
+        new_letter = Note._notes_tuple[pos]
         self.value = new_letter + (self.symbol() or '')
         return self
 
     def num_value(self) -> int:
         """Return numerical value (basis: C = 0)."""
-        return Note._note_values[self.value]
+        num = (self.letter_value() + self.symbol_value()) % 12
+        return num
 
     def letter(self) -> str:
         """Return note letter."""
         return self.value[0]
 
+    def letter_value(self) -> int:
+        """Return note letter as an integer value (Basis: C = 0)."""
+        return Note._note_values[self.letter()]
+
     def symbol(self) -> str:
         """Return note symbol (None if no symbol)."""
         if len(self.value) > 1:
@@ -110,24 +84,16 @@ class Note:
         return None
 
     def symbol_value(self) -> int:
-        """Return note symbol as a integer value."""
-        if len(self.value) > 1:
-            symbol = self.value[1]
-        else:
-            symbol = None
-        return Note._symbols.get(symbol)
+        """Return note symbol as an integer value."""
+        return Note._symbol_signs.get(self.symbol(), 0)
 
     def transpose(self, semitones: int, letter: int):
         """Transpose a note by specifying the change in semitone and letter intervals."""
-        if not isinstance(semitones, int) or not isinstance(letter, int):
-            raise TypeError("Only integers are accepted for value")
         new_val = (self.num_value() + semitones) % 12
         self.shift_l(letter)
         curr_val = self.num_value()
-        if (new_val-curr_val) % 12 < 12 - ((new_val-curr_val) % 12):
-            shift = abs((new_val-curr_val) % 12)
-        else:
-            shift = -abs(12 - (new_val-curr_val) % 12)
+        shift = (new_val - curr_val) % 12
+        shift = shift - 12 if shift > 6 else shift  # shift downwards if closer
         self.shift_s(shift)
         return self
 
@@ -135,7 +101,7 @@ class Note:
         return self.value
 
     def __eq__(self, other):
-        # Allow comparison between note values and strings
+        # Allow comparison between other Notes and strings
         if isinstance(other, Note):
             return self.value == other.value
         elif isinstance(other, str):
diff --git a/src/chordparser/notes_editor.py b/src/chordparser/notes_editor.py
index 8eb15c4..6dfeca6 100644
--- a/src/chordparser/notes_editor.py
+++ b/src/chordparser/notes_editor.py
@@ -8,7 +8,7 @@ class NoteEditor:
 
     The NoteEditor class can create a Note using the 'create_note' method by accepting a string with notation a-g or A-G and optional accidental symbols. The symbols that can be used are b (flat), bb (doubleflat), # (sharp), ## (doublesharp) and their respective unicode characters.
 
-    The NoteEditor can also find the intervals between Notes using the 'get_intervals', 'get_min_intervals' and 'get_tone_notes' methods.
+    The NoteEditor can also find the intervals between Notes using the 'get_intervals', 'get_min_intervals' and 'get_tone_notes' methods. The 'change_note' method allows for changing a note's value.
     """
     _flat = '\u266d'
     _sharp = '\u266f'
@@ -27,28 +27,26 @@ class NoteEditor:
 
     def create_note(self, value):
         """Create a Note from a string."""
-        if not isinstance(value, str):
-            raise TypeError("Only strings are accepted")
         pattern = (
             '^([a-gA-G])(\u266F|\u266D|\U0001D12B|\U0001D12A'
             '|bb|##|b|#){0,1}$'
-            )
+        )
         rgx = re.match(pattern, value, re.UNICODE)
         if not rgx:
-            raise ValueError("Note is invalid")
+            raise SyntaxError(f"'{value}' could not be parsed")
         letter = rgx.group(1).upper()
         symbol = NoteEditor._symbol_converter.get(rgx.group(2))
         notation = letter + symbol
         return Note(notation)
 
     def get_tone_letter(self, *notes):
-        """Return a nested tuple of (semitone, letter) differences between notes."""
+        """Return a nested tuple of (semitone interval, letter interval) between notes."""
         semitones = self.get_intervals(*notes)
-        old_note = self._notes_tuple.index(notes[0].letter())
+        old_note = NoteEditor._notes_tuple.index(notes[0].letter())
         note_diff = []
         for each in notes:
-            new_note = self._notes_tuple.index(each.letter())
-            note_diff.append((new_note-old_note) % 8)
+            new_note = NoteEditor._notes_tuple.index(each.letter())
+            note_diff.append((new_note - old_note) % 8)
             old_note = new_note
         note_diff.pop(0)
         return tuple(zip(semitones, note_diff))
@@ -59,7 +57,7 @@ class NoteEditor:
         intervals = []
         for each in notes:
             new_val = each.num_value()
-            intervals.append((new_val-old_val) % 12)
+            intervals.append((new_val - old_val) % 12)
             old_val = new_val
         intervals.pop(0)
         return tuple(intervals)
@@ -69,9 +67,17 @@ class NoteEditor:
         intervals = self.get_intervals(*notes)
         min_int = []
         for each in intervals:
-            if each > (12-each):
-                shift = each-12
+            if each > (12 - each):
+                shift = each - 12
             else:
                 shift = each
             min_int.append(shift)
         return tuple(min_int)
+
+    def change_note(self, note, value, inplace=True):
+        """Change a note's value. Use inplace=True to return a new note."""
+        new = self.create_note(value)
+        if inplace:
+            note.value = new.value
+            return note
+        return new
diff --git a/src/chordparser/parser.py b/src/chordparser/parser.py
index 0e483dc..5759cc8 100644
--- a/src/chordparser/parser.py
+++ b/src/chordparser/parser.py
@@ -3,10 +3,11 @@ from chordparser.keys_editor import KeyEditor
 from chordparser.scales_editor import ScaleEditor
 from chordparser.chords_editor import ChordEditor
 from chordparser.chords_analyser import ChordAnalyser
+from chordparser.chord_roman_converter import ChordRomanConverter
 import os.path
 
 
-class Parser:
+class Parser(KeyEditor, NoteEditor, ScaleEditor, ChordEditor, ChordAnalyser, ChordRomanConverter):
     """
     Parser class that acts as a central collection for easy access to the Editors and Analysers.
 
@@ -14,24 +15,3 @@ class Parser:
     """
     _path = os.path.join(os.path.dirname(__file__), 'sample_sheet.cho')
     sample = open(_path, 'r')
-
-    def __init__(self):
-        self.NE = NoteEditor()
-        self.KE = KeyEditor()
-        self.SE = ScaleEditor()
-        self.CE = ChordEditor()
-        self.CA = ChordAnalyser()
-
-    def __getattr__(self, attribute):
-        # So Editor/Analyser methods can be used on Parser
-        if attribute in NoteEditor.__dict__:
-            return getattr(self.NE, attribute)
-        if attribute in KeyEditor.__dict__:
-            return getattr(self.KE, attribute)
-        if attribute in ScaleEditor.__dict__:
-            return getattr(self.SE, attribute)
-        if attribute in ChordEditor.__dict__:
-            return getattr(self.CE, attribute)
-        if attribute in ChordAnalyser.__dict__:
-            return getattr(self.CA, attribute)
-        raise AttributeError(f"'Parser' object has no attribute '{attribute}'")
diff --git a/src/chordparser/quality.py b/src/chordparser/quality.py
new file mode 100644
index 0000000..8e6bb2b
--- /dev/null
+++ b/src/chordparser/quality.py
@@ -0,0 +1,182 @@
+class Quality:
+    """
+    Quality class that is built from a chord's quality and provides intervals and scale degrees.
+
+    The Quality class is built from the base chord quality, extensions, and optional flat extension. It has the 'base_intervals' and 'base_degrees' attributes for the intervals and scale degrees of the base chord, and 'intervals' and 'degrees' attributes for that of the proper chord.
+
+    The Quality is represented by its full name by default. The short form of the Quality can be accessed via the 'short' method.
+    """
+    _flat = '\u266d'
+    _sharp = '\u266f'
+    _doubleflat = '\U0001D12B'
+    _doublesharp = '\U0001D12A'
+    _symbols = {
+        -1: _flat, -2: _doubleflat,
+        +1: _sharp, +2: _doublesharp,
+        0: '',
+    }
+    _heptatonic_base = (2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1)
+    _base_int = {
+        'power': (7,),
+        'major': (4, 3),
+        'minor': (3, 4),
+        'diminished': (3, 3),
+        'augmented': (4, 4),
+        'dominant': (4, 3),
+        'half-diminished': (3, 3),
+        'sus2': (2, 5),
+        'sus4': (5, 2),
+    }
+    _base_deg = {
+        'power': (1, 5),
+        'sus2': (1, 2, 5),
+        'sus4': (1, 4, 5),
+    }
+    _ext_int = {
+        'seventh': (10,),
+        'ninth': (10, 14),
+        'eleventh': (10, 14, 17),
+        'thirteenth': (10, 14, 17, 21),
+        'major seventh': (11,),
+        'major ninth': (11, 14),
+        'major eleventh': (11, 14, 17),
+        'major thirteenth': (11, 14, 17, 21),
+        'diminished seventh': (9,),
+    }
+    _shortform = {
+        'power': "5",
+        'major': "",
+        'minor': 'm',
+        'diminished': 'dim',
+        'augmented': 'aug',
+        'dominant': "",
+        'half-diminished': "m",
+        'sus2': "sus2",
+        'sus4': "sus",
+    }
+    _ext_shortform = {
+        'seventh': "7",
+        'ninth': "9",
+        'eleventh': "11",
+        'thirteenth': "13",
+        'major seventh': "maj7",
+        'major ninth': "maj9",
+        'major eleventh': "maj11",
+        'major thirteenth': "maj13",
+        'diminished seventh': "7",
+    }
+
+    def __init__(self, quality_str, ext_str=None, flat_ext=False):
+        """Quality that composes of chord quality, chord extension and any alterations to the extension.
+
+        Arguments:
+        quality_str -- chord quality e.g. major, minor (str)
+        ext_str -- chord extension e.g. seventh, major ninth (str)
+        flat_ext -- if flat extension e.g. flat ninth (bool)
+        """
+        self.value = quality_str
+        self.ext = ext_str
+        self.flat_ext = flat_ext
+        self.build()
+
+    def build(self):
+        """Build intervals, scale degrees, and symbols."""
+        self._check()
+        self._base()
+        self._ext()
+        self._sym()
+
+    def _check(self):
+        if not self.ext:
+            return
+        # Check dominant chords do not have a value of 'major'
+        if self.value == "major":
+            if self.ext.split()[0] != "major":
+                raise ValueError("'Major' can either be dominant or major extended chord")
+        # Check diminished chords are only diminished sevenths
+        if self.value == "diminished" or self.ext == "diminished seventh":
+            if self.ext != "diminished seventh" or self.value != "diminished":
+                raise ValueError("Diminished chords can only be seventh")
+        # Check flat extension does not work with sevenths
+        if self.ext.split()[-1] == "seventh" and self.flat_ext:
+            raise ValueError("Flat extension does not apply to seventh chords")
+
+    def _base(self):
+        """Set base intervals and degrees."""
+        self.base_intervals = Quality._base_int[self.value]
+        self.base_degrees = Quality._base_deg.get(self.value, (1, 3, 5))
+
+    def _ext(self):
+        """Set intervals and degrees with chord extensions."""
+        if not self.ext:
+            self.intervals = self.base_intervals
+            self.degrees = self.base_degrees
+            return
+        intervals = list(self.base_intervals)
+        degrees = list(self.base_degrees)
+        for idx, semitone in enumerate(Quality._ext_int[self.ext]):
+            intervals.append(semitone - sum(intervals))
+            degrees.append(7 + 2 * idx)
+        if self.flat_ext:
+            intervals[-1] = intervals[-1] - 1
+        self.intervals = tuple(intervals)
+        self.degrees = tuple(degrees)
+
+    def _sym(self):
+        """Build base symbols and symbols with extension."""
+        symbols = [""]
+        for i in range(len(self.intervals)):
+            sum_interval = sum(self.intervals[:i+1])
+            next_ = self.degrees[i+1]
+            diff = sum(Quality._heptatonic_base[:next_-1])
+            symbols.append(Quality._symbols[sum_interval-diff])
+        if self.value == "power":
+            self.base_symbols = tuple(symbols[0:2])
+        else:
+            self.base_symbols = tuple(symbols[0:3])
+        self.symbols = tuple(symbols)
+
+    def short(self):
+        """Return short form for quality."""
+        string = Quality._shortform[self.value]
+        if not self.ext:
+            return string
+        ext_string = Quality._ext_shortform[self.ext]
+        if self.flat_ext:
+            # add flat symbols
+            if len(ext_string) < 3:
+                ext_string = '\u266d' + ext_string
+            else:
+                ext_string = ext_string[0:3] + '\u266d' + ext_string[3:]
+        if self.value == "half-diminished":
+            ext_string += "\u266d5"
+        if self.value == "augmented":
+            string = ""
+            ext_string += "\u266f5"
+        if self.value in {"sus2", "sus4"}:
+            # sus at the back
+            return ext_string + string
+        return string + ext_string
+
+    def __repr__(self):
+        if not self.ext:
+            return self.value
+        if self.value in {"major", "diminished"}:
+            # avoid word overlap
+            string = self.ext
+        else:
+            string = self.value + " " + self.ext
+        if not self.flat_ext:
+            return string
+        split_string = string.split()
+        split_string.insert(-1, "flat")
+        return " ".join(split_string)
+
+    def __eq__(self, other):
+        if not isinstance(other, Quality):
+            return NotImplemented
+        return (
+            self.value == other.value
+            and self.ext == other.ext
+            and self.flat_ext == other.flat_ext
+            )
diff --git a/src/chordparser/quality_editor.py b/src/chordparser/quality_editor.py
new file mode 100644
index 0000000..58cf6cf
--- /dev/null
+++ b/src/chordparser/quality_editor.py
@@ -0,0 +1,156 @@
+from chordparser.quality import Quality
+import re
+
+
+class QualityEditor:
+    _flat_pattern = '\u266D|\U0001D12B|bb|b'
+    _sharp_pattern = '\u266F|\U0001D12A|##|#'
+    _symbol_pattern = f"{_flat_pattern}|{_sharp_pattern}"
+    _major_pattern = 'Maj|Ma|M|maj|\u0394'
+    _minor_pattern = 'min|m|-'
+    _dim_pattern = 'dim|o|\u00B0'
+    _aug_pattern = r'aug|\+'
+    _halfdim_pattern = '\u00f8|\u00d8'
+    _dom_pattern = 'dom'
+    _minmaj_pattern = f"(?:{_minor_pattern})(?:{_major_pattern})"
+    _augmaj_pattern = f"(?:{_aug_pattern})(?:{_major_pattern})"
+    _power_chord = "5"
+    _extended_str = f"({_flat_pattern}){{0,1}}(7|9|11|13)"
+    _altered_5 = f"({_dim_pattern}|{_aug_pattern}|{_symbol_pattern})5"
+    _suspended = f"sus(2|4){{0,1}}"
+    quality_pattern = (
+        f"(({_power_chord})|"
+        f"((({_minmaj_pattern})|({_augmaj_pattern})|"
+        f"({_aug_pattern})|({_dim_pattern})|"
+        f"({_halfdim_pattern})|({_major_pattern})|"
+        f"({_minor_pattern})){{0,1}}"  # 0 for dominant/minor ext (lowercase note)
+        f"({_extended_str}))|"
+        f"(({_aug_pattern})|({_dim_pattern})|"
+        f"({_major_pattern})|({_minor_pattern}))){{0,1}}"
+        f"(?:{_altered_5}){{0,1}}"
+        f"({_suspended}){{0,1}}"
+    )
+
+    # Pattern notation [regex group]
+    # Match string [0]:
+    #     {
+    #         power chord [2] |
+    #         extended chords [3] {
+    #             (
+    #                 dominant [4-none; minor if note is lowercase] |
+    #                 minmaj [5] |
+    #                 augmaj [6] |
+    #                 aug [7] |
+    #                 dim [8] |
+    #                 halfdim [9] |
+    #                 major [10] |
+    #                 minor [11]
+    #                 ) +
+    #             (symbol [13] + degree [14]) [12]
+    #             } |
+    #         triads [15] (
+    #             aug [16] |
+    #             dim [17] |
+    #             major [18] |
+    #             minor [19]
+    #             )
+    #         } [1],
+    #         altered 5ths [20],
+    #         sus [21] 2/4 [22]
+
+    def create_quality(self, string, capital_note=True):
+        """Return chord quality."""
+        if string is None:
+            if capital_note:
+                return Quality("major")
+            return Quality("minor")
+        rgx = re.match(QualityEditor.quality_pattern, string, re.UNICODE)
+        if rgx.group(2):  # power
+            return Quality("power")
+        alt5 = self._parse_alt5(rgx.group(20))
+        quality_str = self._parse_base(rgx, alt5, capital_note)
+        ext_str = self._parse_ext(rgx, alt5, capital_note)
+        flat_ext = self._parse_flat_ext(rgx.group(13))
+        return Quality(quality_str, ext_str, flat_ext)
+
+    def _parse_alt5(self, alt5):
+        """Return state of altered 5: 0 if none, -1 if flat 5, 1 if aug 5."""
+        if not alt5:
+            return 0
+        if alt5 in QualityEditor._dim_pattern + QualityEditor._flat_pattern:
+            return -1
+        return 1
+
+    def _parse_base(self, rgx, alt5, capital_note):
+        if rgx.group(21):
+            return self._parse_sus(rgx)
+        if rgx.group(15):
+            return self._parse_triad(rgx, alt5)
+        if rgx.group(3):  # extended chord
+            return self._parse_base_ext(rgx, alt5, capital_note)
+        if not capital_note:
+            return "diminished" if alt5 < 0 else "minor"
+        return "augmented" if alt5 > 0 else "major"
+
+    def _parse_sus(self, rgx):
+        note = rgx.group(22) or "4"  # if 'sus'
+        return "sus" + note
+
+    def _parse_triad(self, rgx, alt5):
+        if rgx.group(16):
+            return "augmented"
+        if rgx.group(17):
+            return "diminished"
+        if rgx.group(18):
+            if alt5 > 0:
+                return "augmented"
+            return "major"
+        if alt5 < 0:
+            return "diminished"
+        return "minor"
+
+    def _parse_base_ext(self, rgx, alt5, capital_note):
+        """Parse base quality of extended chords."""
+        if not rgx.group(4):
+            if capital_note:
+                quality = "dominant"
+            else:
+                quality = "minor"
+        elif rgx.group(5) or rgx.group(11):
+            quality = "minor"
+        elif rgx.group(6) or rgx.group(7):
+            quality = "augmented"
+        elif rgx.group(8):
+            quality = "diminished"
+        elif rgx.group(9):
+            quality = "half-diminished"
+        elif rgx.group(10):
+            quality = "major"
+        if alt5 < 0 and quality == "minor":
+            quality = "half-diminished"
+        if alt5 > 0 and quality in {"major", "dominant"}:
+            quality = "augmented"
+        return quality
+
+    def _parse_ext(self, rgx, alt5, capital_note):
+        """Parse quality of extended chords."""
+        if not rgx.group(3):
+            return None
+        num_to_words = {
+            '7': "seventh",
+            '9': "ninth",
+            '11': "eleventh",
+            '13': "thirteenth",
+        }
+        ext_str = ""
+        if rgx.group(5) or rgx.group(6) or rgx.group(10):
+            ext_str += "major "
+        if rgx.group(8):
+            ext_str += "diminished "
+        ext_str += num_to_words[rgx.group(14)]
+        return ext_str
+
+    def _parse_flat_ext(self, flat):
+        if flat:
+            return True
+        return False
diff --git a/src/chordparser/roman.py b/src/chordparser/roman.py
new file mode 100644
index 0000000..67db9de
--- /dev/null
+++ b/src/chordparser/roman.py
@@ -0,0 +1,24 @@
+class Roman:
+    def __init__(self, root, quality, inversion):
+        self.root = root
+        self.quality = quality
+        self.inversion = inversion
+        self._build_notation()
+
+    def _build_notation(self):
+        inv_str = "".join(map(str, self.inversion))
+        self.notation = self.root + self.quality + inv_str
+
+    def __repr__(self):
+        return self.notation
+
+    def __eq__(self, other):
+        if isinstance(other, Roman):
+            return (
+                self.root == other.root and
+                self.quality == other.quality and
+                self.inversion == other.inversion
+            )
+        if isinstance(other, str):
+            return str(self) == other
+        return NotImplemented
diff --git a/src/chordparser/scales.py b/src/chordparser/scales.py
index 2f76be2..cf447c1 100644
--- a/src/chordparser/scales.py
+++ b/src/chordparser/scales.py
@@ -9,7 +9,7 @@ class Scale:
     The Scale class accepts a Key and generates a 2-octave Note tuple in its 'notes' attribute. The Scale can be changed by transposing its key using the 'transpose' method.
     """
     _heptatonic_base = (2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1)
-    _SCALES = {
+    _scales = {
         "major": 0,
         "ionian": 0,
         "dorian": 1,
@@ -19,15 +19,6 @@ class Scale:
         "aeolian": 5,
         "minor": 5,
         "locrian": 6,
-        }
-    _SCALE_DEGREE = {
-        0: "ionian",
-        1: "dorian",
-        2: "phrygian",
-        3: "lydian",
-        4: "mixolydian",
-        5: "aeolian",
-        6: "locrian",
     }
     _submodes = {
         None: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
@@ -35,9 +26,6 @@ class Scale:
         "melodic": (0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 1, 0, -1),
         "harmonic": (0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 1, -1),
     }
-    _notes_tuple = (
-        'C', 'D', 'E', 'F', 'G', 'A', 'B',
-        'C', 'D', 'E', 'F', 'G', 'A', 'B')
     NE = NoteEditor()
 
     def __init__(self, key: Key):
@@ -46,61 +34,24 @@ class Scale:
 
     def build(self):
         """Build the scale from its key."""
-        self.notes = self._get_notes()
-
-    def _get_notes(self):
-        """Get notes in the scale."""
-        self.scale_intervals = self._get_scale_intervals()
-        note_no_symbol = self.NE.create_note(self.key.letter())
-        self._idx = Scale._notes_tuple.index(note_no_symbol)
-        self._note_order = self._get_note_order()
-        notes = self._shift_notes()
-        return notes
-
-    def _get_scale_intervals(self):
-        """Get note-by-note intervals in the scale."""
-        intervals = self._get_intervals(self.key.mode)
-        # Account for submode
-        submode_intervals = Scale._submodes.get(self.key.submode)
-        scale_intervals = []
-        for scale, subm in zip(intervals, submode_intervals):
-            scale_intervals.append(scale + subm)
-        return tuple(scale_intervals)
+        self.scale_intervals = self._get_intervals()
+        self.notes = [self.NE.create_note(self.key.root.value)]
+        for interval in self.scale_intervals:
+            new_note = self.NE.create_note(self.notes[-1].value)
+            self.notes.append(new_note.transpose(interval, 1))
+        self.notes = tuple(self.notes)
+        return self
 
-    def _get_intervals(self, mode):
+    def _get_intervals(self):
         """Get intervals based on mode."""
-        shift = Scale._SCALES[mode]
-        intervals = (
+        shift = Scale._scales[self.key.mode]
+        mode_intervals = (
             Scale._heptatonic_base[shift:]
             + Scale._heptatonic_base[:shift]
-            )
-        return intervals
-
-    def _get_note_order(self):
-        """Re-arrange note order based on key."""
-        note_order = (
-            Scale._notes_tuple[self._idx:]
-            + Scale._notes_tuple[:self._idx]
-            )
-        return note_order
-
-    def _shift_notes(self):
-        """Shift notes with reference to original mode intervals."""
-        base_intervals = self._get_intervals(
-                Scale._SCALE_DEGREE[self._idx]
-                )
-        symbol_increment = self.key.symbol_value()
-        note_list = []
-        for num, note in enumerate(self._note_order):
-            new_note = self.NE.create_note(note)
-            total_increment = (
-                symbol_increment
-                + sum(self.scale_intervals[:num])
-                - sum(base_intervals[:num])
-                )
-            new_note.shift_s(total_increment)
-            note_list.append(new_note)
-        return tuple(note_list)
+        )
+        submode_intervals = Scale._submodes[self.key.submode]
+        intervals = [x + y for x, y in zip(mode_intervals, submode_intervals)]
+        return tuple(intervals)
 
     def transpose(self, semitones: int, letter: int):
         """Transpose the key of the scale."""
@@ -115,4 +66,4 @@ class Scale:
         # Allow comparison between Keys by checking their basic attributes
         if not isinstance(other, Scale):
             return NotImplemented
-        return self.key == other.key
+        return self.key == other.key and self.notes == other.notes
diff --git a/src/chordparser/scales_editor.py b/src/chordparser/scales_editor.py
index e49d649..25cd1f8 100644
--- a/src/chordparser/scales_editor.py
+++ b/src/chordparser/scales_editor.py
@@ -1,8 +1,6 @@
-from chordparser.notes import Note
 from chordparser.keys import Key
 from chordparser.scales import Scale
 from chordparser.keys_editor import KeyEditor
-from typing import Union
 
 
 class ScaleEditor:
@@ -21,10 +19,10 @@ class ScaleEditor:
             key = value
         return Scale(key)
 
-    def change_scale(self, scale, *args):
+    def change_scale(self, scale, *args, inplace=True, **kwargs):
         """Change the scale by specifying root, mode and/or submode."""
-        if not isinstance(scale, Scale):
-            raise TypeError(f"Object {scale} is not a 'Scale'")
-        self.KE.change_key(scale.key, *args)
+        if not inplace:
+            scale = self.create_scale(scale.key)
+        self.KE.change_key(scale.key, *args, **kwargs)
         scale.build()
         return scale


Test patch:
diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml
deleted file mode 100644
index de66c8d..0000000
--- a/.github/workflows/publish-to-test-pypi.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-# This is a basic workflow to help you get started with Actions
-
-name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
-
-# Controls when the action will run. Triggers the workflow on push or pull request
-# events but only for the master branch
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    branches: [ master ]
-
-# A workflow run is made up of one or more jobs that can run sequentially or in parallel
-jobs:
-  # This workflow contains a single job called "build"
-  build:
-    name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
-    # The type of runner that the job will run on
-    runs-on: ubuntu-latest
-
-    # Steps represent a sequence of tasks that will be executed as part of the job
-    steps:
-    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
-    - uses: actions/checkout@v2
-
-    - name: Set up Python 3.8
-      uses: actions/setup-python@v1
-      with:
-        python-version: 3.8
-        
-    # Install pep517
-    - name: Install pep517
-      run: >-
-        python -m
-        pip install
-        pep517
-        --user
-         
-    - name: Build a binary wheel and a source tarball
-      run: >-
-        python -m
-        pep517.build
-        --source
-        --binary
-        --out-dir dist/
-        .
-
-    - name: Publish distribution 📦 to Test PyPI
-      uses: pypa/gh-action-pypi-publish@master
-      with:
-        password: ${{ secrets.test_pypi_password }}
-        repository_url: https://test.pypi.org/legacy/
-    - name: Publish distribution 📦 to PyPI
-      if: startsWith(github.ref, 'refs/tags')
-      uses: pypa/gh-action-pypi-publish@master
-      with:
-        password: ${{ secrets.pypi_password }}
diff --git a/tests/test_chords.py b/tests/test_chords.py
index 7edbb7e..8c9d5a2 100644
--- a/tests/test_chords.py
+++ b/tests/test_chords.py
@@ -13,102 +13,69 @@ CE = chords_editor.ChordEditor()
 
 
 @pytest.mark.parametrize(
-    "name, quality, chord", [
-        ("C", 'major', (NE.create_note('C'), NE.create_note('E'), NE.create_note('G'))),
-        ("C", 'diminished', (NE.create_note('C'), NE.create_note('Eb'), NE.create_note('Gb'))),
+    "name, chord", [
+        ("C", ('C', 'E', 'G')),
+        ('Cdim', ('C', 'E\u266d', 'G\u266d')),
+        ('Cmaj9', ('C', 'E', 'G', 'B', 'D')),
+        ('C5', ('C', 'G')),
         ])
-def test_base_triad(name, quality, chord):
-    c = Chord(name, quality)
-    assert c.base_triad == chord
+def test_base_notes(name, chord):
+    c = CE.create_chord(name)
+    assert c.base_notes == chord
 
 
 @pytest.mark.parametrize(
-    "name, quality, q", [
-        ("C", 'major seventh', [NE.create_note('C'), NE.create_note('E'), NE.create_note('G'), NE.create_note('B')]),
-        ("C", 'diminished ninth', [NE.create_note('C'), NE.create_note('Eb'), NE.create_note('Gb'), NE.create_note('Bbb'), NE.create_note('D')]),
-        ("C", 'augmented-major minor eleventh', [NE.create_note('C'), NE.create_note('E'), NE.create_note('G#'), NE.create_note('B'), NE.create_note('D'), NE.create_note('Fb')]),
+    "name, notes, sym, deg, intervals", [
+        ("Cmaj7add2addb11",
+            ('C', 'D', 'E', 'G', 'B', 'F\u266D'),
+            ('', '', '', '', '', '\u266D'),
+            (1, 2, 3, 5, 7, 11),
+            (2, 2, 3, 4, 5)),
+        ("Cdimadd11",
+            ('C', 'E\u266D', 'G\u266D', 'F'),
+            ('', '\u266D', '\u266D', ''),
+            (1, 3, 5, 11),
+            (3, 3, 11)),
         ])
-def test_base_notes(name, quality, q):
-    c = Chord(name, quality)
-    assert c.notes == q
+def test_add_notes(name, notes, sym, deg, intervals):
+    c = CE.create_chord(name)
+    assert c.notes == notes
+    assert c.symbols == sym
+    assert c.degrees == deg
+    assert c.intervals == intervals
 
 
 @pytest.mark.parametrize(
-    "name, quality, q", [
-        ("C", 'major seventh', ([[None, 1], [None, 3], [None, 5], [None, 7]])),
-        ("C", 'diminished ninth', ([[None, 1], [None, 3], [None, 5], [None, 7], [None, 9]])),
-        ("C", 'augmented-major minor eleventh', ([[None, 1], [None, 3], [None, 5], [None, 7], [None, 9], ['\u266D', 11]])),
+    "name, notes, sym, deg, intervals, inversion", [
+        ("Cmaj7/G",
+            ('G', 'C', 'E', 'B'),
+            ('', '', '', ''),
+            (5, 1, 3, 7),
+            (5, 4, 7),
+            (5)),
+        ("Cdim/G#",
+            ('G\u266F', 'C', 'E\u266D', 'G\u266D'),
+            ('\u266F', '', '\u266D', '\u266D'),
+            (5, 1, 3, 5),
+            (4, 3, 3),
+            (None)),
         ])
-def test_base_tones(name, quality, q):
-    c = Chord(name, quality)
-    assert c.tones == q
-
-
-@pytest.mark.parametrize(
-    "name, quality, sus, q", [
-        ("C", 'major seventh', 2, [NE.create_note('C'), NE.create_note('D'), NE.create_note('G'), NE.create_note('B')]),
-        ("C", 'diminished ninth', 4, [NE.create_note('C'), NE.create_note('F'), NE.create_note('Gb'), NE.create_note('Bbb'), NE.create_note('D')]),
-        ])
-def test_sus_notes(name, quality, sus, q):
-    c = Chord(name, quality, sus=sus)
-    assert c.notes == q
-
-
-@pytest.mark.parametrize(
-    "name, quality, sus, q", [
-        ("C", 'major seventh', 2, ([[None, 1], [None, 2], [None, 5], [None, 7]])),
-        ("C", 'diminished ninth', 4, ([[None, 1], [None, 4], [None, 5], [None, 7], [None, 9]])),
-        ])
-def test_sus_tones(name, quality, sus, q):
-    c = Chord(name, quality, sus=sus)
-    assert c.tones == q
-
-
-@pytest.mark.parametrize(
-    "name, quality, add, q", [
-        ("C", 'major seventh', ['2', '\u266D11'], [NE.create_note('C'), NE.create_note('D'), NE.create_note('E'), NE.create_note('G'), NE.create_note('B'), NE.create_note('F\u266D')]),
-        ("C", 'diminished', ['11'], [NE.create_note('C'), NE.create_note('Eb'), NE.create_note('Gb'), NE.create_note('F')]),
-        ])
-def test_add_notes(name, quality, add, q):
-    c = Chord(name, quality, add=add)
-    assert c.notes == q
-
-
-@pytest.mark.parametrize(
-    "name, quality, add, q", [
-        ("C", 'major seventh', ['2', '\u266D11'],  ([[None, 1], [None, 2], [None, 3], [None, 5], [None, 7], ['\u266D', 11]])),
-        ("C", 'diminished', ['11'], ([[None, 1], [None, 3], [None, 5], [None, 11]])),
-        ])
-def test_add_tones(name, quality, add, q):
-    c = Chord(name, quality, add=add)
-    assert c.tones == q
-
-
-@pytest.mark.parametrize(
-    "name, quality, bass, q", [
-        ("C", 'major seventh', NE.create_note('G'), [NE.create_note('G'), NE.create_note('C'), NE.create_note('E'), NE.create_note('B')]),
-        ("C", 'diminished', NE.create_note('G#'), [NE.create_note('G#'), NE.create_note('C'), NE.create_note('Eb'), NE.create_note('Gb')]),
-        ])
-def test_bass_notes(name, quality, bass, q):
-    c = Chord(name, quality, bass=bass)
-    assert c.notes == q
-
-
-@pytest.mark.parametrize(
-    "name, quality, bass, q", [
-        ("C", 'major seventh', NE.create_note('G'),  ([[None, 5], [None, 1], [None, 3], [None, 7]])),
-        ("C", 'diminished', NE.create_note('G#'), ([['\u266F', 5], [None, 1], [None, 3], [None, 5]])),
-        ])
-def test_bass_tones(name, quality, bass, q):
-    c = Chord(name, quality, bass=bass)
-    assert c.tones == q
+def test_bass_notes(name, notes, sym, deg, intervals, inversion):
+    c = CE.create_chord(name)
+    assert c.notes == notes
+    assert c.symbols == sym
+    assert c.degrees == deg
+    assert c.intervals == intervals
+    assert c.inversion is inversion
 
 
 @pytest.mark.parametrize(
     "string, notation", [
         ("Cmajsus2add9/G", "Csus2add9/G chord"),
-        ("Cminmaj9susadd#9#13/G#", "Cminmaj9sus4\u266F9\u266F13/G\u266F chord"),
+        ("Cmaj9sus4add#9#13/G#", "Cmaj9sus\u266F9\u266F13/G\u266F chord"),
         ("Caugmaj11", "Cmaj11\u266F5 chord"),
+        ("Cadd2", "Cadd2 chord"),
+        ("Cadd2addb6", "Cadd2\u266d6 chord"),
         ]
     )
 def test_notation(string, notation):
diff --git a/tests/test_chords_analyser.py b/tests/test_chords_analyser.py
index 8b18a08..3c73e09 100644
--- a/tests/test_chords_analyser.py
+++ b/tests/test_chords_analyser.py
@@ -13,37 +13,6 @@ CE = ChordEditor()
 CA = ChordAnalyser()
 
 
-@pytest.mark.parametrize(
-    "chord, roman", [
-        ("C", "I"),
-        ("Cm", "i"),
-        ("Cb", '\u266dI'),
-        ("C7", "I7"),
-        ("C7/G", "I43"),
-        ("C9/G", "I9"),
-        ("Cdim7", "i\u00B07"),
-        ]
-    )
-def test_roman(chord, roman):
-    c = CE.create_chord(chord)
-    s = SE.create_scale("C", "major")
-    assert CA.roman(c, s) == roman
-
-
-def test_power_error():
-    c = CE.create_chord("C5")
-    s = SE.create_scale("C", "major")
-    with pytest.raises(ValueError):
-        CA.roman(c, s)
-
-
-def test_sus_warning():
-    c = CE.create_chord("Csus")
-    s = SE.create_scale("C", "major")
-    with pytest.warns(UserWarning):
-        CA.roman(c, s)
-
-
 @pytest.mark.parametrize(
     "chord, mode, incl_submodes, result", [
         ("C", "major", True, [('I', 'major', None)]),
diff --git a/tests/test_chords_editor.py b/tests/test_chords_editor.py
index 6d6b96d..a1e1d66 100644
--- a/tests/test_chords_editor.py
+++ b/tests/test_chords_editor.py
@@ -11,17 +11,10 @@ SE = scales_editor.ScaleEditor()
 CE = chords_editor.ChordEditor()
 
 
-@pytest.mark.parametrize(
-    "value", [1, len, True, [], ()])
-def test_chord_typeerror(value):
-    with pytest.raises(TypeError):
-        CE.create_chord(value)
-
-
 @pytest.mark.parametrize(
     "value", ["1", "H", "\u266fG"])
 def test_chord_valueerror(value):
-    with pytest.raises(ValueError):
+    with pytest.raises(SyntaxError):
         CE.create_chord(value)
 
 
@@ -29,74 +22,36 @@ def test_chord_valueerror(value):
     "value, root", [
         ('C', NE.create_note('C')),
         ('E\u266f', NE.create_note('E\u266f')),
-        ]
-    )
+    ]
+)
 def test_chord_root(value, root):
     new_chord = CE.create_chord(value)
     assert new_chord.root == root
 
 
-@pytest.mark.parametrize(
-    "string, quality", [
-        ('C', 'major'),
-        ('Cm', 'minor'),
-        ('Cdim', 'diminished'),
-        ('C+', 'augmented'),
-        ('C7', 'dominant seventh'),
-        ('C5', 'power'),
-        ('c7', 'minor seventh'),
-        ('Cminmaj9', 'minor-major ninth'),
-        ('Caugmaj11', 'augmented-major eleventh'),
-        ('Caugb13', 'augmented minor thirteenth'),
-        ('Cmaj9', 'major ninth'),
-        ('Cdim7', 'diminished seventh'),
-        ('C\u00f87', 'half-diminished seventh'),
-        ('Cm9', 'minor ninth'),
-        ]
-    )
-def test_quality(string, quality):
-    new_chord = CE.create_chord(string)
-    assert new_chord.quality == quality
-
-
-@pytest.mark.parametrize(
-    "string, quality", [
-        ('Cmb5', 'diminished'),
-        ('CM7#5', 'augmented-major seventh'),
-        ('Cmin7dim5', 'half-diminished seventh'),
-        ]
-    )
-def test_quality_alt5(string, quality):
-    new_chord = CE.create_chord(string)
-    assert new_chord.quality == quality
-
-
-@pytest.mark.parametrize(
-    "string, sus", [
-        ('Csus2', 2),
-        ('Csus', 4),
-        ('Csus4', 4),
-        ('C', None),
-        ]
-    )
-def test_sus(string, sus):
-    c = CE.create_chord(string)
-    assert c.sus == sus
+def test_quality():
+    c = CE.create_chord("Cminmaj7")
+    assert "minor major seventh" == str(c.quality)
 
 
 @pytest.mark.parametrize(
     "string, add", [
-        ('Cadd13', ['13']),
-        ('cadd2#6', ['2', '\u266f6']),
-        ('cminmaj9b11', ['\u266d11']),
+        ('Cadd13', [('', 13)]),
+        ('cadd2#6', [('', 2), ('\u266f', 6)]),
+        ('cminmaj9b11', [('\u266d', 11)]),
         ('C', None),
-        ]
-    )
+    ]
+)
 def test_add(string, add):
     c = CE.create_chord(string)
     assert c.add == add
 
 
+def test_add_2():
+    c = CE.create_chord("C")
+    assert c.add is None
+
+
 def test_parse_error():
     with pytest.raises(SyntaxError):
         c = CE.create_chord('Cadd21')
@@ -107,24 +62,29 @@ def test_parse_error():
         ('C', None),
         ('C/G', NE.create_note('G')),
         ('C/G#', NE.create_note('G\u266f')),
-        ]
-    )
+    ]
+)
 def test_bass(string, bass):
     c = CE.create_chord(string)
     assert c.bass == bass
 
 
+def test_bass_2():
+    c = CE.create_chord("C")
+    assert None is c.bass
+
+
 @pytest.mark.parametrize(
     "degree, quality", [
         (7, 'diminished'),
         (4, 'major'),
         (6, 'minor'),
-        ]
-    )
+    ]
+)
 def test_diatonic(degree, quality):
     s = SE.create_scale('C', 'major')
     c = CE.create_diatonic(s, degree)
-    assert c.quality == quality
+    assert str(c.quality) == quality
 
 
 @pytest.mark.parametrize(
@@ -132,12 +92,12 @@ def test_diatonic(degree, quality):
         (7, 'diminished'),
         (4, 'major'),
         (6, 'minor'),
-        ]
-    )
+    ]
+)
 def test_diatonic_2(degree, quality):
     s = KE.create_key('C', 'major')
     c = CE.create_diatonic(s, degree)
-    assert c.quality == quality
+    assert str(c.quality) == quality
 
 
 @pytest.mark.parametrize("degree", [0, len])
@@ -150,17 +110,12 @@ def test_diatonic_value_error(degree):
 @pytest.mark.parametrize(
     "input, output", [
         ("hey", "hey"), (None, '')
-        ]
-    )
+    ]
+)
 def test_xstr(input, output):
     assert CE._xstr(input) == output
 
 
-def test_change_chord_error():
-    with pytest.raises(TypeError):
-        CE.change_chord(len)
-
-
 def test_change_chord_root():
     o = CE.create_chord('C')
     n = CE.create_chord('D')
@@ -170,66 +125,38 @@ def test_change_chord_root():
 def test_change_chord_q():
     o = CE.create_chord('C')
     n = CE.create_chord('Cminmajb9')
-    assert n == CE.change_chord(o, quality="minor-major minor ninth")
-
-
-@pytest.mark.parametrize(
-    "quality", [
-        "powr", "power ninth", "major sixth", "augmented major ninth", "augmented minor tenth", "major minor ninth ten",
-        ]
-    )
-def test_change_chord_q_error(quality):
-    o = CE.create_chord('C')
-    with pytest.raises(ValueError):
-        CE.change_chord(o, quality=quality)
-
-
-def test_change_chord_sus():
-    o = CE.create_chord('Csus2')
-    n = CE.create_chord('C')
-    assert n == CE.change_chord(o, sus=False)
-
-
-def test_change_chord_sus_error():
-    o = CE.create_chord('C')
-    with pytest.raises(ValueError):
-        CE.change_chord(o, sus=3)
+    assert n == CE.change_chord(o, quality="minmajb9")
 
 
 def test_change_chord_add():
     o = CE.create_chord('Cadd9')
     n = CE.create_chord('Caddb2add9')
-    assert n == CE.change_chord(o, add=['b2'])
-
-
-def test_change_chord_add_error():
-    o = CE.create_chord('C')
-    with pytest.raises(ValueError):
-        CE.change_chord(o, add=['b22'])
+    assert n == CE.change_chord(o, add='b2')
 
 
 def test_change_chord_rem():
     o = CE.create_chord('Caddb2')
     n = CE.create_chord('C')
-    assert n == CE.change_chord(o, remove=['b2'])
+    assert n == CE.change_chord(o, remove='b2')
 
 
 def test_change_chord_rem_2():
     o = CE.create_chord('Caddb2add6add9')
     n = CE.create_chord('C')
     assert n == CE.change_chord(o, remove=True)
+    assert None is o.add
 
 
 def test_change_chord_rem_error():
     o = CE.create_chord('C')
-    with pytest.raises(ValueError):
-        CE.change_chord(o, remove=['b2'])
+    with pytest.raises(IndexError):
+        CE.change_chord(o, remove='b2')
 
 
 def test_change_chord_rem_error_2():
     o = CE.create_chord('Cb2')
     with pytest.raises(ValueError):
-        CE.change_chord(o, remove=['b22'])
+        CE.change_chord(o, remove='b22')
 
 
 def test_change_chord_bass():
@@ -242,3 +169,11 @@ def test_change_chord_bass_2():
     o = CE.create_chord('Cadd9/G')
     n = CE.create_chord('Cadd9')
     assert n == CE.change_chord(o, bass=False)
+    assert None is o.bass
+
+
+def test_change_chord_not_inplace():
+    o = CE.create_chord("Csus4add9/G")
+    n = CE.change_chord(o, bass='G', inplace=False)
+    assert n == o
+    assert n is not o
diff --git a/tests/test_crc.py b/tests/test_crc.py
new file mode 100644
index 0000000..de68fb3
--- /dev/null
+++ b/tests/test_crc.py
@@ -0,0 +1,42 @@
+from chordparser.scales_editor import ScaleEditor
+from chordparser.chords_editor import ChordEditor
+from chordparser.chord_roman_converter import ChordRomanConverter
+import pytest
+
+
+SE = ScaleEditor()
+CE = ChordEditor()
+CRC = ChordRomanConverter()
+
+
+@pytest.mark.parametrize(
+    "chord, roman", [
+        ("C", "I"),
+        ("Cm", "i"),
+        ("Cb", '\u266dI'),
+        ("C7", "I7"),
+        ("C7/G", "I43"),
+        ("C9/G", "I9"),
+        ("Cdim7", "i\u00B07"),
+        ]
+    )
+def test_roman(chord, roman):
+    c = CE.create_chord(chord)
+    s = SE.create_scale("C", "major")
+    assert str(CRC.to_roman(c, s)) == roman
+
+
+def test_power_error():
+    c = CE.create_chord("C5")
+    s = SE.create_scale("C", "major")
+    with pytest.warns(UserWarning):
+        CRC.to_roman(c, s)
+
+
+def test_sus_warning():
+    c = CE.create_chord("Csus")
+    s = SE.create_scale("C", "major")
+    with pytest.warns(UserWarning):
+        CRC.to_roman(c, s)
+
+
diff --git a/tests/test_keys.py b/tests/test_keys.py
index ecfba7b..db93eaa 100644
--- a/tests/test_keys.py
+++ b/tests/test_keys.py
@@ -16,7 +16,7 @@ def test_note_transpose(key, semitone, letter, new_key):
 
 @pytest.mark.parametrize(
     "value", [
-        "H#", 10.0, "Z", len])
+        "H#", "Z", len])
 def test_root_transpose_error(value):
     nkey = KE.create_key('C')
     with pytest.raises(TypeError):
@@ -36,8 +36,8 @@ def test_root_transpose_error_2(value):
     "root, mode, submode", [
         ('C', 'dorian', None),
         ('C', 'aeolian', 'harmonic'),
-        ]
-    )
+    ]
+)
 def test_key_representation(root, mode, submode):
     nkey = KE.create_key(root, mode, submode)
     if submode:
@@ -61,8 +61,8 @@ def test_key_attribute_error():
     "root, mode, submode", [
         ('C', 'major', None),
         ('C', 'ionian', None),
-        ]
-    )
+    ]
+)
 def test_key_equality(root, mode, submode):
     nkey = KE.create_key('C')
     assert nkey == KE.create_key(root, mode, submode)
@@ -77,8 +77,8 @@ def test_key_equality_2():
     "root, mode, submode", [
         ('C', 'major', None),
         ('C', 'minor', 'harmonic'),
-        ]
-    )
+    ]
+)
 def test_key_inequality(root, mode, submode):
     nkey = KE.create_key('C', 'aeolian', 'natural')
     assert nkey != KE.create_key(root, mode, submode)
diff --git a/tests/test_keys_editor.py b/tests/test_keys_editor.py
index 8d16caf..6e84b9a 100644
--- a/tests/test_keys_editor.py
+++ b/tests/test_keys_editor.py
@@ -1,22 +1,8 @@
-from chordparser import keys_editor
+from chordparser.keys_editor import KeyEditor, ModeError
 import pytest
 
 
-KE = keys_editor.KeyEditor()
-
-
-@pytest.mark.parametrize(
-    "key", [1, True, len, [], ()])
-def test_keys_typeerror(key):
-    with pytest.raises(TypeError):
-        nkey = KE.create_key(key)
-
-
-@pytest.mark.parametrize(
-    "key", ["ABC", "G\u266f\u266f", "H"])
-def test_keys_valueerror(key):
-    with pytest.raises(ValueError):
-        nkey = KE.create_key(key)
+KE = KeyEditor()
 
 
 @pytest.mark.parametrize(
@@ -38,31 +24,24 @@ def test_key_mode_submode_2():
     assert nkey.submode == 'natural'
 
 
-@pytest.mark.parametrize(
-    "mode", [1, True, len])
-def test_key_mode_typeerror(mode):
-    with pytest.raises(TypeError):
-        nkey = KE.create_key('C', mode)
-
-
 @pytest.mark.parametrize(
     "mode", ["ionia", "hello", "1rh9"])
 def test_key_mode_error(mode):
-    with pytest.raises(KeyError):
+    with pytest.raises(SyntaxError):
         nkey = KE.create_key('C', mode)
 
 
 @pytest.mark.parametrize(
-    "mode, submode", [("major", "harmonic"), ("minor", "nothing")])
+    "mode, submode", [("minor", "nothing")])
 def test_key_submode_key_error(mode, submode):
-    with pytest.raises(KeyError):
+    with pytest.raises(SyntaxError):
         nkey = KE.create_key('C', mode, submode)
 
 
 @pytest.mark.parametrize(
-    "mode, submode", [("major", 1), ("minor", True)])
-def test_key_submode_type_error(mode, submode):
-    with pytest.raises(TypeError):
+    "mode, submode", [("major", "harmonic")])
+def test_key_submode_key_error_2(mode, submode):
+    with pytest.raises(ModeError):
         nkey = KE.create_key('C', mode, submode)
 
 
@@ -70,8 +49,8 @@ def test_key_submode_type_error(mode, submode):
     "root, mode, newroot, newmode", [
         ('C', 'minor', 'E\u266d', 'major'),
         ('F', 'aeolian', 'A\u266d', 'major'),
-        ]
-    )
+    ]
+)
 def test_relative_major(root, mode, newroot, newmode):
     nkey = KE.create_key(root, mode)
     rel_key = KE.relative_major(nkey)
@@ -80,19 +59,19 @@ def test_relative_major(root, mode, newroot, newmode):
 
 def test_relative_major_incorrect_mode():
     nkey = KE.create_key('C', 'dorian')
-    with pytest.raises(KeyError):
+    with pytest.raises(ModeError):
         KE.relative_major(nkey)
 
 
 def test_relative_minor_incorrect_mode():
     nkey = KE.create_key('C', 'dorian')
-    with pytest.raises(KeyError):
+    with pytest.raises(ModeError):
         KE.relative_minor(nkey)
 
 
 def test_relative_minor_incorrect_submode():
     nkey = KE.create_key('C', 'major')
-    with pytest.raises(KeyError):
+    with pytest.raises(SyntaxError):
         KE.relative_minor(nkey, 'blah')
 
 
@@ -100,8 +79,8 @@ def test_relative_minor_incorrect_submode():
     "root, mode, submode, newroot, newmode, newsub", [
         ('A', 'major', 'harmonic', 'F\u266f', 'minor', 'harmonic'),
         ('F\u266d', 'ionian', 'natural', 'D\u266d', 'minor', 'natural'),
-        ]
-    )
+    ]
+)
 def test_relative_minor(root, mode, submode, newroot, newmode, newsub):
     nkey = KE.create_key(root, mode)
     rel_key = KE.relative_minor(nkey, submode)
@@ -119,6 +98,8 @@ def test_change_key_2():
     nkey = KE.create_key('D\u266d', 'minor', 'melodic')
     assert nkey == KE.change_key(okey, 'D\u266d', 'minor', None)
 
-def test_change_key_error():
-    with pytest.raises(TypeError):
-        KE.change_key(len)
+
+def test_change_key_not_inplace():
+    okey = KE.create_key("C")
+    nkey = KE.change_key(okey, root="C", inplace=False)
+    assert nkey is not okey
diff --git a/tests/test_notes.py b/tests/test_notes.py
index 55b7151..c0f4709 100644
--- a/tests/test_notes.py
+++ b/tests/test_notes.py
@@ -6,14 +6,6 @@ import pytest
 NE = notes_editor.NoteEditor()
 
 
-@pytest.mark.parametrize(
-    "accidental", ['CA', 'D\u266F', 2.0])
-def test_note_accidental_wrong_type(accidental):
-    new_note = NE.create_note('C')
-    with pytest.raises(TypeError):
-        new_note.accidental(accidental)
-
-
 @pytest.mark.parametrize(
     "accidental", [-3, 3])
 def test_note_accidental_wrong_value(accidental):
@@ -32,14 +24,6 @@ def test_note_accidental_positive(accidental, note):
     assert new_note.value == note
 
 
-@pytest.mark.parametrize(
-    "shift", ['CA', 'D\u266F', 2.0, len, [], ()])
-def test_note_shift_wrong_type(shift):
-    new_note = NE.create_note('C')
-    with pytest.raises(TypeError):
-        new_note.shift_s(shift)
-
-
 @pytest.mark.parametrize(
     "shift", [300, -10, 3, -3])
 def test_note_shift_wrong_value(shift):
@@ -58,14 +42,6 @@ def test_note_shift_positive(shift, note):
     assert new_note.value == note
 
 
-@pytest.mark.parametrize(
-    "shift", ['CA', 'D\u266F', 2.0, len, [], ()])
-def test_note_shift_wrong_type_2(shift):
-    new_note = NE.create_note('C')
-    with pytest.raises(TypeError):
-        new_note.shift_l(shift)
-
-
 @pytest.mark.parametrize(
     "shift, note", [
         (-2, 'A'), (-1, 'B'),
@@ -94,6 +70,15 @@ def test_note_letter(note):
     assert new_note.letter() == note[0]
 
 
+@pytest.mark.parametrize(
+    "note, value", [
+        ('C', 0), ('D\u266F', 2), ('G\u266D', 7),
+        ('A\U0001D12B', 9), ('B\U0001D12A', 11)])
+def test_note_letter_value(note, value):
+    new_note = NE.create_note(note)
+    assert new_note.letter_value() == value
+
+
 @pytest.mark.parametrize(
     "note", ['C', 'D\u266F', 'G\u266D', 'A\U0001D12B', 'B\U0001D12A'])
 def test_note_symbol(note):
@@ -109,7 +94,7 @@ def test_note_symbol(note):
     "note, value", [
         ('C', 0), ('D\u266F', 1), ('G\u266D', -1),
         ('A\U0001D12B', -2), ('B\U0001D12A', 2)])
-def test_note_value(note, value):
+def test_note_symbol_value(note, value):
     new_note = NE.create_note(note)
     if len(note) > 1:
         symbol = note[1]
@@ -146,21 +131,3 @@ def test_note_transpose(note, semitone, letter, new_note):
     nnote = NE.create_note(note)
     nnote.transpose(semitone, letter)
     assert nnote == new_note
-
-
-@pytest.mark.parametrize(
-    "value", [
-        "H#", 10.0, "Z", len])
-def test_root_transpose_error(value):
-    nnote = NE.create_note('C')
-    with pytest.raises(TypeError):
-        nnote.transpose(value, 1)
-
-
-@pytest.mark.parametrize(
-    "value", [
-        "H#", 10.0, "Z", len])
-def test_root_transpose_error_2(value):
-    nnote = NE.create_note('C')
-    with pytest.raises(TypeError):
-        nnote.transpose(1, value)
diff --git a/tests/test_notes_editor.py b/tests/test_notes_editor.py
index ec81b2a..f9359ab 100644
--- a/tests/test_notes_editor.py
+++ b/tests/test_notes_editor.py
@@ -11,23 +11,16 @@ NE = notes_editor.NoteEditor()
         ('A\U0001D12B', 'A\U0001D12B'), ('B\U0001D12A', 'B\U0001D12A'),
         ('F#', 'F\u266F'), ('Eb', 'E\u266d'),
         ('Gbb', 'G\U0001D12B'), ('C##', 'C\U0001D12A'),
-        ])
+    ])
 def test_note_creation_positive(note, expected):
     new_note = NE.create_note(note)
     assert new_note.value == expected
 
 
-@pytest.mark.parametrize(
-    "note", [True, 1, len, [], ()])
-def test_note_creation_typeerror(note):
-    with pytest.raises(TypeError):
-        new_note = NE.create_note(note)
-
-
 @pytest.mark.parametrize(
     "note", ['CA', 'D\u266F\u266F', '\u266DG', '\U0001D12A', 'F###'])
-def test_note_creation_valueerror(note):
-    with pytest.raises(ValueError):
+def test_note_creation_syntaxerror(note):
+    with pytest.raises(SyntaxError):
         new_note = NE.create_note(note)
 
 
@@ -35,8 +28,8 @@ def test_note_creation_valueerror(note):
     "notes, interval", [
         (('C', 'D#'), (3,)),
         (('B', 'E', 'F#'), (5, 2)),
-        ]
-    )
+    ]
+)
 def test_intervals(notes, interval):
     note_list = []
     for each in notes:
@@ -48,8 +41,8 @@ def test_intervals(notes, interval):
     "notes, interval", [
         (('C', 'B'), (-1,)),
         (('B', 'E', 'D'), (5, -2)),
-        ]
-    )
+    ]
+)
 def test_min_intervals(notes, interval):
     note_list = []
     for each in notes:
@@ -61,8 +54,8 @@ def test_min_intervals(notes, interval):
     "notes, difference", [
         (('C', 'B'), ((11, 6),)),
         (('B', 'E', 'D'), ((5, 4), (10, 7))),
-        ]
-    )
+    ]
+)
 def test_tone_letter(notes, difference):
     note_list = []
     for each in notes:
@@ -70,4 +63,13 @@ def test_tone_letter(notes, difference):
     assert difference == NE.get_tone_letter(*note_list)
 
 
+def test_change_note():
+    note = NE.create_note("C")
+    NE.change_note(note, "d")
+    assert "D" == note
+
 
+def test_change_note_not_inplace():
+    note = NE.create_note("C")
+    new = NE.change_note(note, "C", inplace=False)
+    assert new is not note
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 26cafbd..67c504e 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -35,7 +35,7 @@ def test_chords():
 def test_CA():
     c = CE.create_chord("C")
     s = SE.create_scale("C")
-    assert cp.roman(c, s) == "I"
+    assert cp.to_roman(c, s) == "I"
 
 
 def test_attribute_error():
diff --git a/tests/test_quality.py b/tests/test_quality.py
new file mode 100644
index 0000000..609c88a
--- /dev/null
+++ b/tests/test_quality.py
@@ -0,0 +1,126 @@
+from chordparser.quality import Quality
+import pytest
+
+
+@pytest.mark.parametrize(
+    "quality, interval", [
+        ("major", (4, 3)),
+        ("sus2", (2, 5)),
+    ]
+)
+def test_base_interval(quality, interval):
+    q = Quality(quality)
+    assert interval == q.base_intervals
+
+
+@pytest.mark.parametrize(
+    "quality, degree", [
+        ("major", (1, 3, 5)),
+        ("sus2", (1, 2, 5)),
+    ]
+)
+def test_base_degree(quality, degree):
+    q = Quality(quality)
+    assert degree == q.base_degrees
+
+
+@pytest.mark.parametrize(
+    "quality, ext, interval", [
+        ("dominant", "seventh", (4, 3, 3)),
+        ("sus2", "major ninth", (2, 5, 4, 3)),
+        ("diminished", "diminished seventh", (3, 3, 3)),
+        ("half-diminished", "seventh", (3, 3, 4)),
+    ]
+)
+def test_interval(quality, ext, interval):
+    q = Quality(quality, ext)
+    assert interval == q.intervals
+
+
+@pytest.mark.parametrize(
+    "quality, ext, degree", [
+        ("major", "major ninth", (1, 3, 5, 7, 9)),
+        ("sus2", "thirteenth", (1, 2, 5, 7, 9, 11, 13)),
+    ]
+)
+def test_degree(quality, ext, degree):
+    q = Quality(quality, ext)
+    assert degree == q.degrees
+
+
+@pytest.mark.parametrize(
+    "quality, ext, sym", [
+        ("dominant", "seventh", ("", "", "", "\u266d")),
+        ("minor", "major ninth", ("", "\u266d", "", "", "")),
+        ("diminished", "diminished seventh", ("", "\u266d", "\u266d", "\U0001D12B")),
+        ("augmented", "seventh", ("", "", "\u266f", "\u266d")),
+    ]
+)
+def test_sym(quality, ext, sym):
+    q = Quality(quality, ext)
+    assert sym == q.symbols
+
+
+@pytest.mark.parametrize(
+    "quality, ext, sym", [
+        ("dominant", "seventh", ("", "", "")),
+        ("power", None, ("", "")),
+    ]
+)
+def test_sym_base(quality, ext, sym):
+    q = Quality(quality, ext)
+    assert sym == q.base_symbols
+
+
+@pytest.mark.parametrize(
+    "quality, name", [
+        ("major", "major"),
+        ("sus2", "sus2"),
+    ]
+)
+def test_long_repr(quality, name):
+    q = Quality(quality)
+    assert name == str(q)
+
+
+@pytest.mark.parametrize(
+    "quality, ext, name", [
+        ("dominant", "ninth", "dominant ninth"),
+        ("major", "major seventh", "major seventh"),
+        ("diminished", "diminished seventh", "diminished seventh"),
+    ]
+)
+def test_long_repr_with_ext(quality, ext, name):
+    q = Quality(quality, ext)
+    assert name == str(q)
+
+
+@pytest.mark.parametrize(
+    "quality, ext, name", [
+        ("dominant", "ninth", "dominant flat ninth"),
+        ("major", "major eleventh", "major flat eleventh"),
+    ]
+)
+def test_long_repr_with_flat_ext(quality, ext, name):
+    q = Quality(quality, ext, True)
+    assert name == str(q)
+
+
+def test_short():
+    q = Quality("diminished")
+    assert "dim" == q.short()
+
+
+def test_short_flat():
+    q = Quality("minor", "ninth", True)
+    assert "m\u266d9" == q.short()
+
+
+def test_short_halfdim():
+    q = Quality("half-diminished", "eleventh")
+    assert "m11\u266d5" == q.short()
+
+
+def test_short_sus():
+    q = Quality("sus4", "seventh")
+    assert "7sus" == q.short()
diff --git a/tests/test_quality_editor.py b/tests/test_quality_editor.py
new file mode 100644
index 0000000..337ff33
--- /dev/null
+++ b/tests/test_quality_editor.py
@@ -0,0 +1,98 @@
+from chordparser.quality_editor import QualityEditor
+import pytest
+
+
+QE = QualityEditor()
+
+
+def test_power():
+    q = QE.create_quality("5")
+    assert "power" == q.value
+
+
+def test_sus():
+    q = QE.create_quality("sus")
+    assert "sus4" == q.value
+
+
+def test_sus_2():
+    q = QE.create_quality("sus2")
+    assert "sus2" == q.value
+
+
+def test_triad():
+    q = QE.create_quality("aug")
+    assert "augmented" == q.value
+
+
+def test_alt5():
+    q = QE.create_quality("mb5")
+    assert "diminished" == q.value
+
+
+def test_alt5_2():
+    q = QE.create_quality("7#5")
+    assert "augmented" == q.value
+
+
+@pytest.mark.parametrize(
+    "quality, value", [
+        ("7", "dominant"),
+        ("m9", "minor"),
+        ("augmaj7", "augmented"),
+        ("maj7#5", "augmented"),
+        ("m7b5", "half-diminished"),
+    ]
+)
+def test_base_ext(quality, value):
+    q = QE.create_quality(quality)
+    assert value == q.value
+
+
+@pytest.mark.parametrize(
+    "quality, value", [
+        ("7", "seventh"),
+        ("m9", "ninth"),
+        ("augmaj7", "major seventh"),
+        ("dim7", "diminished seventh"),
+        ("dim", None),
+    ]
+)
+def test_base_ext(quality, value):
+    q = QE.create_quality(quality)
+    assert value == q.ext
+
+
+def test_flat_ext():
+    q = QE.create_quality("majb9")
+    assert True is q.flat_ext
+
+
+def test_capital():
+    q = QE.create_quality("7", False)
+    assert "minor" == q.value
+
+
+def test_capital_2():
+    q = QE.create_quality("", False)
+    assert "minor" == q.value
+
+
+def test_empty():
+    q = QE.create_quality("+")
+    assert "augmented" == q.value
+
+
+def test_empty_2():
+    q = QE.create_quality("")
+    assert "major" == q.value
+
+
+def test_none():
+    q = QE.create_quality(None)
+    assert "major" == q.value
+
+
+def test_none_2():
+    q = QE.create_quality(None, False)
+    assert "minor" == q.value
diff --git a/tests/test_roman.py b/tests/test_roman.py
new file mode 100644
index 0000000..3f95b49
--- /dev/null
+++ b/tests/test_roman.py
@@ -0,0 +1,28 @@
+from chordparser.roman import Roman
+import pytest
+
+
+def test_notation():
+    r = Roman("I", "M+", (6, 4))
+    assert "IM+64" == str(r)
+
+
+def test_notation_2():
+    r = Roman("v", "\u00B0", ())
+    assert "v\u00B0" == str(r)
+
+
+def test_notation_3():
+    r = Roman("IV", "+", (6,))
+    assert "IV+6" == str(r)
+
+
+def test_equality():
+    r = Roman("I", "+", (6,))
+    o = Roman("I", "+", (6,))
+    assert r == o
+
+
+def test_equality_2():
+    r = Roman("IV", "+", (6,))
+    assert "IV+6" == r
diff --git a/tests/test_scales.py b/tests/test_scales.py
index dbb52ab..b949063 100644
--- a/tests/test_scales.py
+++ b/tests/test_scales.py
@@ -9,67 +9,43 @@ SE = scales_editor.ScaleEditor()
 
 @pytest.mark.parametrize(
     "mode, submode, intervals", [
-         ("ionian", None, (2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1)),
-         ("MAJOR", None, (2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1)),
-         ("dorian", None, (2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2)),
-         ("PhRyGiAn", None, (1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2)),
-         ("lydian", None, (2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1)),
-         ("mixolydian", None, (2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2)),
-         ("aeolian", None, (2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2)),
-         ("minor", None, (2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2)),
-         ("minor", "natural", (2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2)),
-         ("minor", "melodic", (2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 1)),
-         ("minor", "harmonic", (2, 1, 2, 2, 1, 3, 1, 2, 1, 2, 2, 1, 3, 1)),
-         ("locrian", None, (1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2)),
-         ]
-    )
+        ("ionian", None, (2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1)),
+        ("MAJOR", None, (2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1)),
+        ("dorian", None, (2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2)),
+        ("PhRyGiAn", None, (1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2)),
+        ("lydian", None, (2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1)),
+        ("mixolydian", None, (2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2)),
+        ("aeolian", None, (2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2)),
+        ("minor", None, (2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2)),
+        ("minor", "natural", (2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2)),
+        ("minor", "melodic", (2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 1)),
+        ("minor", "harmonic", (2, 1, 2, 2, 1, 3, 1, 2, 1, 2, 2, 1, 3, 1)),
+        ("locrian", None, (1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2)),
+    ]
+)
 def test_scale_modes(mode, submode, intervals):
     nkey = KE.create_key('D', mode=mode, submode=submode)
     scale = SE.create_scale(nkey)
     assert scale.scale_intervals == intervals
 
 
-@pytest.mark.parametrize(
-    "key, note_order", [
-        ('C', ('C', 'D', 'E', 'F', 'G', 'A', 'B',
-               'C', 'D', 'E', 'F', 'G', 'A', 'B')),
-        ('D', ('D', 'E', 'F', 'G', 'A', 'B', 'C',
-               'D', 'E', 'F', 'G', 'A', 'B', 'C')),
-        ('E', ('E', 'F', 'G', 'A', 'B', 'C', 'D',
-               'E', 'F', 'G', 'A', 'B', 'C', 'D')),
-        ('F', ('F', 'G', 'A', 'B', 'C', 'D', 'E',
-               'F', 'G', 'A', 'B', 'C', 'D', 'E')),
-        ('G', ('G', 'A', 'B', 'C', 'D', 'E', 'F',
-               'G', 'A', 'B', 'C', 'D', 'E', 'F')),
-        ('A', ('A', 'B', 'C', 'D', 'E', 'F', 'G',
-               'A', 'B', 'C', 'D', 'E', 'F', 'G')),
-        ('B', ('B', 'C', 'D', 'E', 'F', 'G', 'A',
-               'B', 'C', 'D', 'E', 'F', 'G', 'A')),
-        ]
-    )
-def test_scale_note_order(key, note_order):
-    nkey = KE.create_key(key)
-    scale = SE.create_scale(nkey)
-    assert scale._note_order == note_order
-
-
 @pytest.mark.parametrize(
     "key, mode, note", [
         ('C', 'major', (
             'C', 'D', 'E', 'F', 'G', 'A', 'B',
-            'C', 'D', 'E', 'F', 'G', 'A', 'B')),
+            'C', 'D', 'E', 'F', 'G', 'A', 'B', 'C')),
         ('C\u266f', 'minor', (
             'C\u266f', 'D\u266f', 'E', 'F\u266f', 'G\u266f', 'A', 'B',
-            'C\u266f', 'D\u266f', 'E', 'F\u266f', 'G\u266f', 'A', 'B')),
+            'C\u266f', 'D\u266f', 'E', 'F\u266f', 'G\u266f', 'A', 'B', 'C\u266f')),
         ('F', 'mixolydian', (
             'F', 'G', 'A', 'B\u266d', 'C', 'D', 'E\u266d',
-            'F', 'G', 'A', 'B\u266d', 'C', 'D', 'E\u266d')),
+            'F', 'G', 'A', 'B\u266d', 'C', 'D', 'E\u266d', 'F')),
         ('G\u266d', 'dorian', (
             'G\u266d', 'A\u266d', 'B\U0001D12B',
             'C\u266d', 'D\u266d', 'E\u266d', 'F\u266d',
             'G\u266d', 'A\u266d', 'B\U0001D12B',
-            'C\u266d', 'D\u266d', 'E\u266d', 'F\u266d')),
-        ])
+            'C\u266d', 'D\u266d', 'E\u266d', 'F\u266d', 'G\u266d')),
+    ])
 def test_scale_notes(key, mode, note):
     nkey = KE.create_key(key, mode=mode)
     scale = SE.create_scale(nkey)
@@ -80,14 +56,14 @@ def test_scale_notes(key, mode, note):
     "key, mode, submode, note", [
         ('C', 'minor', 'natural', (
             'C', 'D', 'E\u266d', 'F', 'G', 'A\u266d', 'B\u266d',
-            'C', 'D', 'E\u266d', 'F', 'G', 'A\u266d', 'B\u266d',)),
+            'C', 'D', 'E\u266d', 'F', 'G', 'A\u266d', 'B\u266d', 'C')),
         ('C', 'minor', 'harmonic', (
             'C', 'D', 'E\u266d', 'F', 'G', 'A\u266d', 'B',
-            'C', 'D', 'E\u266d', 'F', 'G', 'A\u266d', 'B',)),
+            'C', 'D', 'E\u266d', 'F', 'G', 'A\u266d', 'B', 'C')),
         ('C', 'minor', 'melodic', (
             'C', 'D', 'E\u266d', 'F', 'G', 'A', 'B',
-            'C', 'D', 'E\u266d', 'F', 'G', 'A', 'B',)),
-        ])
+            'C', 'D', 'E\u266d', 'F', 'G', 'A', 'B', 'C')),
+    ])
 def test_scale_submode_notes(key, mode, submode, note):
     nkey = KE.create_key(key, mode=mode, submode=submode)
     scale = SE.create_scale(nkey)
diff --git a/tests/test_scales_editor.py b/tests/test_scales_editor.py
index b48f698..a559df6 100644
--- a/tests/test_scales_editor.py
+++ b/tests/test_scales_editor.py
@@ -17,19 +17,12 @@ def test_scale_keys(key):
 
 @pytest.mark.parametrize(
     "root, mode, submode", [('C', 'phrygian', None)]
-    )
+)
 def test_scale_key_args(root, mode, submode):
     new_scale = SE.create_scale(root, mode, submode)
     assert new_scale.key == KE.create_key(root, mode, submode)
 
 
-@pytest.mark.parametrize(
-    "key", [1, True, len])
-def test_scale_keys_typeerror(key):
-    with pytest.raises(TypeError):
-        SE.create_scale(key)
-
-
 def test_change_scale():
     o = SE.create_scale('C', 'major', None)
     n = SE.create_scale('D', 'minor', 'harmonic')
@@ -41,6 +34,8 @@ def test_change_scale_2():
     n = SE.create_scale('D\u266d', 'minor', 'melodic')
     assert n == SE.change_scale(o, 'D\u266d', 'minor', None)
 
-def test_change_scale_error():
-    with pytest.raises(TypeError):
-        SE.change_scale(len)
+
+def test_change_scale_not_inplace():
+    o = SE.create_scale("C")
+    n = SE.change_scale(o, root="C", inplace=False)
+    assert n is not o