Repository: yuma-m/pychord

Issue:
Drop Python 2.7 support
Thinking about dropping support for Python 2.7, 3.4.

Discussion:

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a5ace89..f5d5464 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
+        python-version: [3.6, 3.7, 3.8, 3.9]
 
     steps:
     - uses: actions/checkout@v2
@@ -18,3 +18,11 @@ jobs:
     - name: Run test
       run: |
         python setup.py test
+    - name: Style check
+      run: |
+        pip install flake8
+        flake8 --ignore=E501 pychord test
+    - name: Type check
+      run: |
+        pip install mypy
+        mypy pychord
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e217c64..b12bf75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 ## Forthcoming
 
+- Drop compatibility for Python 2.7 and 3.5
+- Refactor whole library to optimize for Python 3.x
+
 ## v0.6.3
 
 - Add modes to `RELATIVE_KEY_DICT`.
diff --git a/README.md b/README.md
index 800d12c..8045570 100644
--- a/README.md
+++ b/README.md
@@ -63,16 +63,16 @@ True
 True
 ```
 
-### Find Chords
+### Find Chords from notes
 
 ```python
->>> from pychord import note_to_chord
->>> note_to_chord(["C", "E", "G"])
-[<Chord: C>]
->>> note_to_chord(["F#", "A", "C", "D"])
-[<Chord: D7/F#>]
->>> note_to_chord(["F", "G", "C"])
-[<Chord: Fsus2>, <Chord: Csus4/F>]
+>>> from pychord import find_chords_from_notes
+>>> find_chords_from_notes(["C", "E", "G"])
+[ <Chord: C>]
+>>> find_chords_from_notes(["F#", "A", "C", "D"])
+[ <Chord: D7/F#>]
+>>> find_chords_from_notes(["F", "G", "C"])
+[ <Chord: Fsus2>, <Chord: Csus4/F>]
 ```
 
 ### Create and handle chord progressions
@@ -127,8 +127,9 @@ True
 
 ## Supported Python Versions
 
-- 2.7
-- 3.5 and above
+- 3.6 and above
+
+Python 2.7 and 3.5 compatibility was dropped from version 1.0.0.
 
 ## Links
 
@@ -144,4 +145,4 @@ True
 
 - MIT License
 
-Icon is made by [Freepik](https://www.flaticon.com/authors/freepik")
+Icon is made by [Freepik](https://www.flaticon.com/authors/freepik).
diff --git a/pychord/__init__.py b/pychord/__init__.py
index 14766fe..bea5067 100644
--- a/pychord/__init__.py
+++ b/pychord/__init__.py
@@ -1,4 +1,5 @@
-from .analyzer import note_to_chord
+# flake8: noqa
+from .analyzer import find_chords_from_notes
 from .chord import Chord
 from .progression import ChordProgression
 from .quality import Quality, QualityManager
diff --git a/pychord/analyzer.py b/pychord/analyzer.py
index b033bcd..cbf9819 100644
--- a/pychord/analyzer.py
+++ b/pychord/analyzer.py
@@ -1,16 +1,15 @@
-# -*- coding: utf-8 -*-
+from typing import List
 
 from .chord import Chord
 from .quality import QualityManager
 from .utils import note_to_val
 
 
-def note_to_chord(notes):
-    """ Convert note list to chord list
+def find_chords_from_notes(notes: List[str]) -> List[Chord]:
+    """ Find possible chords consisted from notes
 
-    :param list[str] notes: list of note arranged from lower note. ex) ["C", "Eb", "G"]
-    :rtype: list[pychord.Chord]
-    :return: list of chord
+    :param notes: List of note arranged from lower note. ex) ["C", "Eb", "G"]
+    :return: List of chord
     """
     if not notes:
         raise ValueError("Please specify notes which consist a chord.")
@@ -18,7 +17,7 @@ def note_to_chord(notes):
     root_and_positions = []
     for rotated_notes in get_all_rotated_notes(notes):
         rotated_root = rotated_notes[0]
-        root_and_positions.append([rotated_root, notes_to_positions(rotated_notes, rotated_notes[0])])
+        root_and_positions.append((rotated_root, notes_to_positions(rotated_notes, rotated_notes[0])))
     chords = []
     for temp_root, positions in root_and_positions:
         quality = QualityManager().find_quality_from_components(positions)
@@ -32,15 +31,15 @@ def note_to_chord(notes):
     return chords
 
 
-def notes_to_positions(notes, root):
-    """ Get notes positions.
+def notes_to_positions(notes: List[str], root: str) -> List[int]:
+    """ Get notes positions from the root note
 
-    ex) notes_to_positions(["C", "E", "G"], "C") -> [0, 4, 7]
+    >>> notes_to_positions(["C", "E", "G"], "C")
+    [0, 4, 7]
 
-    :param list[str] notes: list of notes
-    :param str root: the root note
-    :rtype: list[int]
-    :return: list of note positions
+    :param notes: List of notes
+    :param root: Root note
+    :return: List of note positions
     """
     root_pos = note_to_val(root)
     current_pos = root_pos
@@ -54,13 +53,10 @@ def notes_to_positions(notes, root):
     return positions
 
 
-def get_all_rotated_notes(notes):
+def get_all_rotated_notes(notes: List[str]) -> List[List[str]]:
     """ Get all rotated notes
 
-    get_all_rotated_notes([1,3,5]) -> [[1,3,5],[3,5,1],[5,1,3]]
-
-    :type notes: list[str]
-    :rtype: list[list[str]]
+    get_all_rotated_notes([A,C,E]) -> [[A,C,E],[C,E,A],[E,A,C]]
     """
     notes_list = []
     for x in range(len(notes)):
diff --git a/pychord/chord.py b/pychord/chord.py
index 468923e..8bc59c8 100644
--- a/pychord/chord.py
+++ b/pychord/chord.py
@@ -1,28 +1,34 @@
-# -*- coding: utf-8 -*-
+from typing import List, Union
+
 from .constants import NOTE_VAL_DICT, VAL_NOTE_DICT
 from .constants.scales import RELATIVE_KEY_DICT
 from .parser import parse
-from .quality import QualityManager
+from .quality import QualityManager, Quality
 from .utils import transpose_note, display_appended, display_on, note_to_val, val_to_note
 
 
-class Chord(object):
+class Chord:
     """ Class to handle a chord.
 
-    :param str _chord: Name of the chord. (e.g. C, Am7, F#m7-5/A)
-    :param str _root: The root note of chord.
-    :param pychord.Quality _quality: The quality of chord. (e.g. m7, 6, M9, ...)
-    :param list[str] _appended: The appended notes on chord.
-    :param str _on: The base note of slash chord.
+    Attributes:
+        _chord: Name of the chord. (e.g. C, Am7, F#m7-5/A)
+        _root: The root note of chord. (e.g. C, A, F#)
+        _quality: The quality of chord. (e.g. maj, m7, m7-5)
+        _appended: The appended notes on chord.
+        _on: The base note of slash chord.
     """
-    def __init__(self, chord):
+
+    def __init__(self, chord: str):
         """ Constructor of Chord instance
 
-        :param str chord: Name of chord.
+        :param chord: Name of chord (e.g. C, Am7, F#m7-5/A).
         """
-        self._chord = chord
-        self._root, self._quality, self._appended, self._on = "", "", "", ""
-        self._parse(chord)
+        root, quality, appended, on = parse(chord)
+        self._chord: str = chord
+        self._root: str = root
+        self._quality: Quality = quality
+        self._appended: List[str] = appended
+        self._on: str = on
 
     def __unicode__(self):
         return self._chord
@@ -31,11 +37,11 @@ class Chord(object):
         return self._chord
 
     def __repr__(self):
-        return "<Chord: {}>".format(self._chord)
+        return f"<Chord: {self._chord}>"
 
     def __eq__(self, other):
         if not isinstance(other, Chord):
-            raise TypeError("Cannot compare Chord object with {} object".format(type(other)))
+            raise TypeError(f"Cannot compare Chord object with {type(other)} object")
         if note_to_val(self._root) != note_to_val(other.root):
             return False
         if self._quality != other.quality:
@@ -51,20 +57,20 @@ class Chord(object):
         return not self.__eq__(other)
 
     @classmethod
-    def from_note_index(cls, note, quality, scale, diatonic=False):
+    def from_note_index(cls, note: int, quality: str, scale: str, diatonic: bool = False) -> 'Chord':
         """ Create a Chord from note index in a scale
 
         Chord.from_note_index(1, "", "Cmaj") returns I of C major => Chord("C")
         Chord.from_note_index(3, "m7", "Fmaj") returns IIImin of F major => Chord("Am7")
         Chord.from_note_index(5, "7", "Amin") returns Vmin of A minor => Chord("E7")
 
-        :param int note: Note index in a Scale I, II, ..., VIII
-        :param str quality: Quality of a chord (m7, sus4, ...)
-        :param str scale: Base scale (Cmaj, Amin, F#maj, Ebmin, ...)
-        :rtype: Chord
+        :param note: Note index in a Scale I, II, ..., VIII
+        :param quality: Quality of a chord (m7, sus4, ...)
+        :param scale: Base scale (Cmaj, Amin, F#maj, Ebmin, ...)
+        :param diatonic: Adjust certain chord qualities according to the scale
         """
         if not 1 <= note <= 8:
-            raise ValueError("Invalid note {}".format(note))
+            raise ValueError(f"Invalid note {note}")
         relative_key = RELATIVE_KEY_DICT[scale[-3:]][note - 1]
         root_num = NOTE_VAL_DICT[scale[:-3]]
         root = VAL_NOTE_DICT[(root_num + relative_key) % 12][0]
@@ -104,9 +110,9 @@ class Chord(object):
             quality_manager = QualityManager()
             quality = quality_manager.find_quality_from_components(q)
             if not quality:
-                raise RuntimeError("Quality with components {} not found".format(q))
+                raise RuntimeError(f"Quality with components {q} not found")
 
-        return cls("{}{}".format(root, quality))
+        return cls(f"{root}{quality}")
 
     @property
     def chord(self):
@@ -135,31 +141,29 @@ class Chord(object):
 
     def info(self):
         """ Return information of chord to display """
-        return """{}
-root={}
-quality={}
-appended={}
-on={}""".format(self._chord, self._root, self._quality, self._appended, self._on)
+        return f"""{self._chord}
+root={self._root}
+quality={self._quality}
+appended={self._appended}
+on={self._on}"""
 
-    def transpose(self, trans, scale="C"):
+    def transpose(self, trans: int, scale: str = "C") -> None:
         """ Transpose the chord
 
-        :param int trans: Transpose key
-        :param str scale: key scale
-        :return:
+        :param trans: Transpose key
+        :param scale: key scale
         """
         if not isinstance(trans, int):
-            raise TypeError("Expected integers, not {}".format(type(trans)))
+            raise TypeError(f"Expected integers, not {type(trans)}")
         self._root = transpose_note(self._root, trans, scale)
         if self._on:
             self._on = transpose_note(self._on, trans, scale)
         self._reconfigure_chord()
 
-    def components(self, visible=True):
+    def components(self, visible: bool = True) -> Union[List[str], List[int]]:
         """ Return the component notes of chord
 
-        :param bool visible: returns the name of notes if True else list of int
-        :rtype: list[(str or int)]
+        :param visible: returns the name of notes if True else list of int
         :return: component notes of chord
         """
         if self._on:
@@ -167,11 +171,10 @@ on={}""".format(self._chord, self._root, self._quality, self._appended, self._on
 
         return self._quality.get_components(root=self._root, visible=visible)
 
-    def components_with_pitch(self, root_pitch):
+    def components_with_pitch(self, root_pitch: int) -> List[str]:
         """ Return the component notes of chord formatted like ["C4", "E4", "G4"]
 
-        :param int root_pitch: the pitch of the root note
-        :rtype: list[str]
+        :param root_pitch: the pitch of the root note
         :return: component notes of chord
         """
         if self._on:
@@ -180,38 +183,11 @@ on={}""".format(self._chord, self._root, self._quality, self._appended, self._on
         components = self._quality.get_components(root=self._root)
         if components[0] < 0:
             components = [c + 12 for c in components]
-        return ["{}{}".format(val_to_note(c, scale=self._root), root_pitch + c // 12) for c in components]
-
-    def _parse(self, chord):
-        """ parse a chord
-
-        :param str chord: Name of chord.
-        """
-        root, quality, appended, on = parse(chord)
-        self._root = root
-        self._quality = quality
-        self._appended = appended
-        self._on = on
+        return [f"{val_to_note(c, scale=self._root)}{root_pitch + c // 12}" for c in components]
 
     def _reconfigure_chord(self):
         # TODO: Use appended
         self._chord = "{}{}{}{}".format(self._root,
-                                        self._quality._quality,
+                                        self._quality.quality,
                                         display_appended(self._appended),
                                         display_on(self._on))
-
-
-def as_chord(chord):
-    """ convert from str to Chord instance if input is str
-
-    :type chord: str|pychord.Chord
-    :param chord: Chord name or Chord instance
-    :rtype: pychord.Chord
-    :return: Chord instance
-    """
-    if isinstance(chord, Chord):
-        return chord
-    elif isinstance(chord, str):
-        return Chord(chord)
-    else:
-        raise TypeError("input type should be str or Chord instance.")
diff --git a/pychord/constants/__init__.py b/pychord/constants/__init__.py
index ad46688..5789bad 100644
--- a/pychord/constants/__init__.py
+++ b/pychord/constants/__init__.py
@@ -1,1 +1,2 @@
+# flake8: noqa
 from .scales import NOTE_VAL_DICT, VAL_NOTE_DICT, SCALE_VAL_DICT
diff --git a/pychord/parser.py b/pychord/parser.py
index fdbdf4c..9391c2e 100644
--- a/pychord/parser.py
+++ b/pychord/parser.py
@@ -1,14 +1,13 @@
-# -*- coding: utf-8 -*-
+from typing import Tuple, List
 
-from .quality import QualityManager
+from .quality import QualityManager, Quality
 from .utils import NOTE_VAL_DICT
 
 
-def parse(chord):
+def parse(chord: str) -> Tuple[str, Quality, List[str], str]:
     """ Parse a string to get chord component
 
-    :param str chord: str expression of a chord
-    :rtype: (str, pychord.Quality, str, str)
+    :param chord: str expression of a chord
     :return: (root, quality, appended, on)
     """
 
@@ -19,27 +18,20 @@ def parse(chord):
         root = chord[:1]
         rest = chord[1:]
 
-    check_note(root, chord)
+    def check_note(note: str):
+        """ Raise ValueError if note is invalid """
+        if note not in NOTE_VAL_DICT:
+            raise ValueError(f"Invalid note {note}")
+
+    check_note(root)
     on_chord_idx = rest.find("/")
     if on_chord_idx >= 0:
         on = rest[on_chord_idx + 1:]
         rest = rest[:on_chord_idx]
-        check_note(on, chord)
+        check_note(on)
     else:
-        on = None
+        on = ""
     quality = QualityManager().get_quality(rest)
     # TODO: Implement parser for appended notes
-    appended = []
+    appended: List[str] = []
     return root, quality, appended, on
-
-
-def check_note(note, chord):
-    """ Return True if the note is valid.
-
-    :param str note: note to check its validity
-    :param str chord: the chord which includes the note
-    :rtype: bool
-    """
-    if note not in NOTE_VAL_DICT:
-        raise ValueError("Invalid chord {}: Unknown note {}".format(chord, note))
-    return True
diff --git a/pychord/progression.py b/pychord/progression.py
index 146db48..35207b7 100644
--- a/pychord/progression.py
+++ b/pychord/progression.py
@@ -1,30 +1,31 @@
-# -*- coding: utf-8 -*-
+from typing import Union, List
 
-from .chord import as_chord, Chord
+from .chord import Chord
 
 
-class ChordProgression(object):
+class ChordProgression:
     """ Class to handle chord progressions.
 
-    :param list[pychord.Chord] _chords: component chords of chord progression.
+    Attributes:
+        _chords: component chords of chord progression.
     """
 
-    def __init__(self, initial_chords=None):
+    def __init__(self, initial_chords: Union[str, Chord, List[Union[str, Chord]]] = None):
         """ Constructor of ChordProgression instance.
 
-        :type initial_chords: str|pychord.Chord|list
         :param initial_chords: Initial chord or chords of the chord progressions
         """
         if initial_chords is None:
             initial_chords = []
         if isinstance(initial_chords, Chord):
-            self._chords = [initial_chords]
+            chords = [initial_chords]
         elif isinstance(initial_chords, str):
-            self._chords = [as_chord(initial_chords)]
+            chords = [self._as_chord(initial_chords)]
         elif isinstance(initial_chords, list):
-            self._chords = [as_chord(chord) for chord in initial_chords]
+            chords = [self._as_chord(chord) for chord in initial_chords]
         else:
-            raise TypeError("Cannot initialize ChordProgression with argument of {} type".format(type(initial_chords)))
+            raise TypeError(f"Cannot initialize ChordProgression with argument of {type(initial_chords)} type")
+        self._chords: List[Chord] = chords
 
     def __unicode__(self):
         return " | ".join([chord.chord for chord in self._chords])
@@ -33,7 +34,7 @@ class ChordProgression(object):
         return " | ".join([chord.chord for chord in self._chords])
 
     def __repr__(self):
-        return "<ChordProgression: {}>".format(" | ".join([chord.chord for chord in self._chords]))
+        return f"<ChordProgression: {' | '.join([chord.chord for chord in self._chords])}>"
 
     def __add__(self, other):
         self._chords += other.chords
@@ -50,7 +51,7 @@ class ChordProgression(object):
 
     def __eq__(self, other):
         if not isinstance(other, ChordProgression):
-            raise TypeError("Cannot compare ChordProgression object with {} object".format(type(other)))
+            raise TypeError(f"Cannot compare ChordProgression object with {type(other)} object")
         if len(self) != len(other):
             return False
         for c, o in zip(self, other):
@@ -62,45 +63,50 @@ class ChordProgression(object):
         return not self.__eq__(other)
 
     @property
-    def chords(self):
-        """ Get component chords of chord progression
-
-        :rtype: list[pychord.Chord]
-        """
+    def chords(self) -> List[Chord]:
+        """ Get component chords of chord progression """
         return self._chords
 
-    def append(self, chord):
+    def append(self, chord: Union[str, Chord]) -> None:
         """ Append a chord to chord progressions
 
-        :type chord: str|pychord.Chord
         :param chord: A chord to append
-        :return:
         """
-        self._chords.append(as_chord(chord))
+        self._chords.append(self._as_chord(chord))
 
-    def insert(self, index, chord):
+    def insert(self, index: int, chord: Union[str, Chord]) -> None:
         """ Insert a chord to chord progressions
 
-        :param int index: Index to insert a chord
-        :type chord: str|pychord.Chord
+        :param index: Index to insert a chord
         :param chord: A chord to insert
-        :return:
         """
-        self._chords.insert(index, as_chord(chord))
+        self._chords.insert(index, self._as_chord(chord))
 
-    def pop(self, index=-1):
+    def pop(self, index: int = -1) -> Chord:
         """ Pop a chord from chord progressions
 
-        :param int index: Index of the chord to pop (default: -1)
-        :return: pychord.Chord
+        :param index: Index of the chord to pop (default: -1)
         """
         return self._chords.pop(index)
 
-    def transpose(self, trans):
+    def transpose(self, trans: int) -> None:
         """ Transpose whole chord progressions
 
-        :param int trans: Transpose key
-        :return:
+        :param trans: Transpose key
         """
         for chord in self._chords:
             chord.transpose(trans)
+
+    @staticmethod
+    def _as_chord(chord: Union[str, Chord]) -> Chord:
+        """ Convert from str to Chord instance if input is str
+
+        :param chord: Chord name or Chord instance
+        :return: Chord instance
+        """
+        if isinstance(chord, Chord):
+            return chord
+        elif isinstance(chord, str):
+            return Chord(chord)
+        else:
+            raise TypeError("input type should be str or Chord instance.")
diff --git a/pychord/quality.py b/pychord/quality.py
index 6dfc92c..dd8b6ee 100644
--- a/pychord/quality.py
+++ b/pychord/quality.py
@@ -1,24 +1,22 @@
-# -*- coding: utf-8 -*-
 import copy
 from collections import OrderedDict
+from typing import Tuple, List
 
 from .constants.qualities import DEFAULT_QUALITIES
 from .utils import note_to_val, val_to_note
 
 
-class Quality(object):
-    """ Chord quality
+class Quality:
+    """ Chord quality """
 
-    :param str _quality: str expression of chord quality
-    """
-    def __init__(self, name, components):
+    def __init__(self, name: str, components: Tuple[int, ...]):
         """ Constructor of chord quality
 
-        :param str name: name of quality
-        :param Tuple[int]: components of quality
+        :param name: name of quality
+        :param components: components of quality
         """
-        self._quality = name
-        self.components = list(components)
+        self._quality: str = name
+        self.components: Tuple[int, ...] = components
 
     def __unicode__(self):
         return self._quality
@@ -28,7 +26,7 @@ class Quality(object):
 
     def __eq__(self, other):
         if not isinstance(other, Quality):
-            raise TypeError("Cannot compare Quality object with {} object".format(type(other)))
+            raise TypeError(f"Cannot compare Quality object with {type(other)} object")
         return self.components == other.components
 
     def __ne__(self, other):
@@ -68,43 +66,22 @@ class Quality(object):
         root_val = note_to_val(root)
         on_chord_val = note_to_val(on_chord) - root_val
 
-        list_ = list(self.components)
-        for idx, val in enumerate(list_):
+        components = list(self.components)
+        for idx, val in enumerate(self.components):
             if val % 12 == on_chord_val:
-                self.components.remove(val)
+                components.remove(val)
                 break
 
         if on_chord_val > root_val:
             on_chord_val -= 12
 
-        if on_chord_val not in self.components:
-            self.components.insert(0, on_chord_val)
+        if on_chord_val not in components:
+            components.insert(0, on_chord_val)
 
-    def append_note(self, note, root, scale=0):
-        """ Append a note to quality
-
-        :param str note: note to append on quality
-        :param str root: root note of chord
-        :param int scale: key scale
-        """
-        root_val = note_to_val(root)
-        note_val = note_to_val(note) - root_val + scale * 12
-        if note_val not in self.components:
-            self.components.append(note_val)
-            self.components.sort()
-
-    def append_notes(self, notes, root, scale=0):
-        """ Append notes to quality
-
-        :param list[str] notes: notes to append on quality
-        :param str root: root note of chord
-        :param int scale: key scale
-        """
-        for note in notes:
-            self.append_note(note, root, scale)
+        self.components = tuple(components)
 
 
-class QualityManager(object):
+class QualityManager:
     """ Singleton class to manage the qualities """
 
     def __new__(cls, *args, **kwargs):
@@ -118,27 +95,27 @@ class QualityManager(object):
             (q, Quality(q, c)) for q, c in DEFAULT_QUALITIES
         ])
 
-    def get_quality(self, name):
+    def get_quality(self, name: str) -> Quality:
         if name not in self._qualities:
-            raise ValueError("Unknown quality: {}".format(name))
+            raise ValueError(f"Unknown quality: {name}")
         # Create a new instance not to affect any existing instances
         return copy.deepcopy(self._qualities[name])
 
-    def set_quality(self, name, components):
+    def set_quality(self, name: str, components: Tuple[int, ...]):
         """ Set a Quality
 
         This method will not affect any existing Chord instances.
-        :param str name: name of quality
-        :param Tuple[int] components: components of quality
+        :param name: name of quality
+        :param components: components of quality
         """
         self._qualities[name] = Quality(name, components)
 
-    def find_quality_from_components(self, components):
+    def find_quality_from_components(self, components: List[int]):
         """ Find a quality from components
 
-        :param Tuple[int] components: components of quality
+        :param components: components of quality
         """
         for q in self._qualities.values():
-            if q.components == list(components):
+            if list(q.components) == components:
                 return copy.deepcopy(q)
         return None
diff --git a/pychord/utils.py b/pychord/utils.py
index 869bea7..3b8cc02 100644
--- a/pychord/utils.py
+++ b/pychord/utils.py
@@ -1,60 +1,52 @@
-# -*- coding: utf-8 -*-
+from typing import List
 
 from .constants import NOTE_VAL_DICT, SCALE_VAL_DICT
 
 
-def note_to_val(note):
-    """ Convert note to int
+def note_to_val(note: str) -> int:
+    """ Get index value of a note
 
     >>> note_to_val("C")
     0
     >>> note_to_val("B")
     11
-
-    :type note: str
-    :rtype: int
     """
     if note not in NOTE_VAL_DICT:
-        raise ValueError("Unknown note {}".format(note))
+        raise ValueError(f"Unknown note {note}")
     return NOTE_VAL_DICT[note]
 
 
-def val_to_note(val, scale="C"):
-    """ Convert int to note
+def val_to_note(val: int, scale: str = "C") -> str:
+    """ Return note by index in a scale
 
     >>> val_to_note(0)
     "C"
     >>> val_to_note(11, "D")
     "D#"
-
-    :type val: int
-    :param str scale: key scale
-    :rtype: str
     """
     val %= 12
     return SCALE_VAL_DICT[scale][val]
 
 
-def transpose_note(note, transpose, scale="C"):
+def transpose_note(note: str, transpose: int, scale: str = "C") -> str:
     """ Transpose a note
 
-    :param str note: note to transpose
-    :type transpose: int
-    :param str scale: key scale
-    :rtype: str
-    :return: transposed note
+    >>> transpose_note("C", 1)
+    "Db"
+    >>> transpose_note("D", 4, "A")
+    "F#"
     """
     val = note_to_val(note)
     val += transpose
     return val_to_note(val, scale)
 
 
-def display_appended(appended):
+def display_appended(appended: List[str]) -> str:
     # TODO: Implement this
     return ""
 
 
-def display_on(on_note):
+def display_on(on_note: str) -> str:
     if on_note:
-        return "/{}".format(on_note)
+        return f"/{on_note}"
     return ""
diff --git a/setup.py b/setup.py
index 1dcd2b3..7908120 100644
--- a/setup.py
+++ b/setup.py
@@ -1,16 +1,26 @@
+import sys
+
 from setuptools import setup, find_packages
 version = '0.6.3'
 
+CURRENT_PYTHON = sys.version_info[:2]
+REQUIRED_PYTHON = (3, 6)
+
+if CURRENT_PYTHON < REQUIRED_PYTHON:
+    sys.stderr.write("""PyChords only supports Python 3.6 and above.""")
+    sys.exit(1)
+
 setup(
     name='pychord',
     version=version,
     description="A library to handle musical chords in python.",
     classifiers=[
-        "Development Status :: 4 - Beta",
+        "Development Status :: 5 - Production/Stable"
         "Environment :: Console",
         "Intended Audience :: Developers",
         "Natural Language :: English",
         "Operating System :: OS Independent",
+        "Programming Language :: Python :: 3 :: Only",
         "Topic :: Multimedia :: Sound/Audio",
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Topic :: Utilities",
@@ -18,7 +28,7 @@ setup(
         ],
     keywords='music chord',
     author='Yuma Mihira',
-    author_email='info@yurax2.com',
+    author_email='info@yuma.cloud',
     url='https://github.com/yuma-m/pychord',
     license='MIT',
     packages=find_packages(exclude=['test']),
@@ -27,4 +37,5 @@ setup(
     long_description=open('README.md').read(),
     long_description_content_type='text/markdown',
     test_suite='test',
+    python_requires=">=3.6",
 )


Test patch:
diff --git a/test/test_analyzer.py b/test/test_analyzer.py
index 3c7123e..a3e16cc 100644
--- a/test/test_analyzer.py
+++ b/test/test_analyzer.py
@@ -1,9 +1,7 @@
-# -*- coding: utf-8 -*-
-
 import unittest
 
 from pychord import Chord
-from pychord.analyzer import get_all_rotated_notes, note_to_chord, notes_to_positions
+from pychord.analyzer import get_all_rotated_notes, find_chords_from_notes, notes_to_positions
 
 
 class TestNotesToPositions(unittest.TestCase):
@@ -27,7 +25,7 @@ class TestNotesToPositions(unittest.TestCase):
     def test_add9(self):
         pos = notes_to_positions(["Ab", "C", "Eb", "Bb"], "Ab")
         self.assertEqual(pos, [0, 4, 7, 14])
-    
+
     def test_major_add_9(self):
         # major add 9 is the same as add9
         self.test_add9()
@@ -56,73 +54,75 @@ class TestGetAllRotatedNotes(unittest.TestCase):
         self.assertEqual(notes_list, [["C", "F", "G"], ["F", "G", "C"], ["G", "C", "F"]])
 
 
-class TestNoteToChord(unittest.TestCase):
+class TestFindChordsFromNotes(unittest.TestCase):
+
     def _assert_chords(self, notes, expected_chords):
-        """"Validates that the specified notes translated to the expected chords.
+        """ Validates that the specified notes translated to the expected chords.
+
         :param notes: The notes of the chord, either as a list of strings,
           e.g. ["G", "C", "D"] or a string, e.g. "G C D"
-        :param expected_chords: the chords that the notes could translate to, 
-            specified as a list of strings, e.g. [ "Gsus4", "Csus2/G" ], 
+        :param expected_chords: the chords that the notes could translate to,
+            specified as a list of strings, e.g. [ "Gsus4", "Csus2/G" ],
             or a single string if only one chord expected.
         """
         if isinstance(notes, str):
             notes = notes.split()
-        c0 = note_to_chord(notes)
+        c0 = find_chords_from_notes(notes)
         if isinstance(expected_chords, str):
             expected_chords = [expected_chords]
         self.assertEqual(c0, [Chord(c) for c in expected_chords])
-    
+
     def test_major(self):
-        chords = note_to_chord(["C", "E", "G"])
+        chords = find_chords_from_notes(["C", "E", "G"])
         self.assertEqual(chords, [Chord("C")])
 
     def test_major_on_third(self):
-        chords = note_to_chord(["F#", "A", "D"])
+        chords = find_chords_from_notes(["F#", "A", "D"])
         self.assertEqual(chords, [Chord("D/F#")])
 
     def test_major_on_fifth(self):
-        chords = note_to_chord(["B", "E", "G#"])
+        chords = find_chords_from_notes(["B", "E", "G#"])
         self.assertEqual(chords, [Chord("E/B")])
 
     def test_dim(self):
-        chords = note_to_chord(["Eb", "Gb", "A"])
+        chords = find_chords_from_notes(["Eb", "Gb", "A"])
         self.assertEqual(chords, [Chord("Ebdim")])
 
     def test_sus4(self):
-        chords = note_to_chord(["G", "C", "D"])
+        chords = find_chords_from_notes(["G", "C", "D"])
         self.assertEqual(chords, [Chord("Gsus4"), Chord("Csus2/G")])
 
     def test_dim6(self):
-        chords = note_to_chord(["Eb", "Gb", "A", "C"])
+        chords = find_chords_from_notes(["Eb", "Gb", "A", "C"])
         self.assertEqual(chords, [Chord("Ebdim7"), Chord("Gbdim7/Eb"), Chord("Adim7/Eb"), Chord("Cdim7/Eb")])
 
     def test_aug(self):
-        chords = note_to_chord(["F", "A", "Db"])
+        chords = find_chords_from_notes(["F", "A", "Db"])
         self.assertEqual(chords, [Chord("Faug"), Chord("Aaug/F"), Chord("Dbaug/F")])
 
     def test_add9(self):
-        chords = note_to_chord(["C", "E", "G", "D"])
+        chords = find_chords_from_notes(["C", "E", "G", "D"])
         self.assertEqual(chords, [Chord("Cadd9")])
 
     def test_m7b5(self):
-        chords = note_to_chord(["F#", "A", "C", "E"])
+        chords = find_chords_from_notes(["F#", "A", "C", "E"])
         self.assertEqual(chords, [Chord("F#m7-5"), Chord("Am6/F#")])
 
     def test_m7dim5(self):
-        chords = note_to_chord(["F#", "A", "C", "E"])
+        chords = find_chords_from_notes(["F#", "A", "C", "E"])
         self.assertEqual(chords, [Chord("F#m7-5"), Chord("Am6/F#")])
-    
+
     def test_add4(self):
-        chords = note_to_chord(["C", "E", "F", "G"])
+        chords = find_chords_from_notes(["C", "E", "F", "G"])
         self.assertEqual(chords, [Chord("Cadd4")])
-    
+
     def test_minor_add4(self):
-        chords = note_to_chord(["C", "Eb", "F", "G"])
+        chords = find_chords_from_notes(["C", "Eb", "F", "G"])
         self.assertEqual(chords, [Chord("Cmadd4")])
-    
+
     def test_minor7_add11(self):
         self._assert_chords("C Eb G Bb F", ["Cm7add11", "F11/C"])
-    
+
     def test_major7_add11(self):
         self._assert_chords("C E G B F", "CM7add11")
 
@@ -132,9 +132,8 @@ class TestNoteToChord(unittest.TestCase):
     def test_major7_add13(self):
         self._assert_chords("C E G A B D", "CM7add13")
 
-
-    def test_call_repeatedly(self):
+    def test_idempotence(self):
         for _ in range(2):
-            chords = note_to_chord(["Eb", "Gb", "A", "C"])
+            chords = find_chords_from_notes(["Eb", "Gb", "A", "C"])
             self.assertEqual(chords, [Chord("Ebdim7"), Chord("Gbdim7/Eb"), Chord("Adim7/Eb"), Chord("Cdim7/Eb")])
             self.assertEqual(chords[0].components(visible=True), ["Eb", "Gb", "A", "C"])
diff --git a/test/test_chord.py b/test/test_chord.py
index 0c2b52c..de4b001 100644
--- a/test/test_chord.py
+++ b/test/test_chord.py
@@ -1,9 +1,6 @@
-# -*- coding: utf-8 -*-
-
 import unittest
 
 from pychord import Chord
-from pychord.chord import as_chord
 
 
 class TestChordCreations(unittest.TestCase):
@@ -75,24 +72,6 @@ class TestChordCreations(unittest.TestCase):
             print(c == 0)
 
 
-class TestAsChord(unittest.TestCase):
-
-    def test_as_chord_chord_input(self):
-        c = Chord("C")
-        ac = as_chord(c)
-        self.assertEqual(c, ac)
-
-    def test_as_chord_str_input(self):
-        chord = "C"
-        c = Chord(chord)
-        ac = as_chord(chord)
-        self.assertEqual(c, ac)
-
-    def test_as_chord_invalid_input(self):
-        with self.assertRaises(TypeError):
-            as_chord(1)
-
-
 class TestChordFromNoteIndex(unittest.TestCase):
 
     def test_note_1(self):
diff --git a/test/test_component.py b/test/test_component.py
index e3baf84..ba21605 100644
--- a/test/test_component.py
+++ b/test/test_component.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import unittest
 
 from pychord import Chord
@@ -7,7 +5,8 @@ from pychord import Chord
 
 class TestChordComponent(unittest.TestCase):
     def _assert_components(self, chord, qualities, notes):
-        """Validates if a chord is made up of specified qualities and notes.
+        """ Validates if a chord is made up of specified qualities and notes.
+
         :param str chord: A chord, specified as a string, e.g. "C7"
         :param qualities: The expected qualities of the chord, as a list of numbers
         :param notes: The expected notes of the chord, either as a list of strings,
@@ -90,18 +89,18 @@ class TestChordComponent(unittest.TestCase):
         self.assertEqual(com0, [1, 4, 7, 11, 14])
         com1 = c.components(visible=True)
         self.assertEqual(com1, ["C#", "E", "G", "B", "D"])
-    
+
     def test_major_add9(self):
-        # mmajor add 9 is a major chord with a Major ninth
+        # major add 9 is a major chord with a Major ninth
         base = Chord("C")
         base0 = list(base.components(visible=False))
         base1 = list(base.components(visible=True))
         c = Chord("CMadd9")
         com0 = c.components(visible=False)
-        self.assertEqual(com0,  base0 + [14])
+        self.assertEqual(com0, base0 + [14])
         com1 = c.components(visible=True)
         self.assertEqual(com1, base1 + ["D"])
-    
+
     def test_add4(self):
         self._assert_components("Cadd4", [0, 4, 5, 7], "C E F G")
 
@@ -111,19 +110,20 @@ class TestChordComponent(unittest.TestCase):
 
     def test_minor_add4(self):
         self._assert_components("Cmadd4", [0, 3, 5, 7], "C Eb F G")
-    
+
     def test_minor7_add11(self):
         self._assert_components('Cm7add11', [0, 3, 7, 10, 17], "C Eb G Bb F")
-    
+
     def test_major7_add11(self):
         self._assert_components('CM7add11', [0, 4, 7, 11, 17], "C E G B F")
-    
+
     def test_minormajor7_add11(self):
         self._assert_components('CmM7add11', [0, 3, 7, 11, 17], "C Eb G B F")
-    
+
     def test_major7_add13(self):
         self._assert_components("CM7add13", [0, 4, 7, 9, 11, 14], "C E G A B D")
 
+
 class TestChordComponentWithPitch(unittest.TestCase):
 
     def test_normal_chord(self):
diff --git a/test/test_constants.py b/test/test_constants.py
index 6dc681d..070b6a4 100644
--- a/test/test_constants.py
+++ b/test/test_constants.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import unittest
 
 from pychord.constants import NOTE_VAL_DICT, VAL_NOTE_DICT
diff --git a/test/test_progression.py b/test/test_progression.py
index 4bbe89b..7c346de 100644
--- a/test/test_progression.py
+++ b/test/test_progression.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import unittest
 
 from pychord import Chord, ChordProgression
@@ -21,6 +19,11 @@ class TestChordProgressionCreations(unittest.TestCase):
         cp = ChordProgression(c)
         self.assertEqual(cp.chords, [Chord(c)])
 
+    def test_one_chord_invalid_type(self):
+        c = 1
+        with self.assertRaises(TypeError):
+            ChordProgression(c)
+
     def test_one_chord_list(self):
         c = Chord("C")
         cp = ChordProgression([c])
diff --git a/test/test_quality.py b/test/test_quality.py
index 5cb945e..3c06072 100644
--- a/test/test_quality.py
+++ b/test/test_quality.py
@@ -2,7 +2,7 @@
 
 import unittest
 
-from pychord import QualityManager, Chord, note_to_chord
+from pychord import QualityManager, Chord, find_chords_from_notes
 
 
 class TestQuality(unittest.TestCase):
@@ -59,7 +59,7 @@ class TestOverwriteQuality(unittest.TestCase):
 
     def test_find_from_components(self):
         self.quality_manager.set_quality("13", (0, 4, 7, 10, 14, 17, 21))
-        chords = note_to_chord(['C', 'E', 'G', 'Bb', 'D', 'F', 'A'])
+        chords = find_chords_from_notes(['C', 'E', 'G', 'Bb', 'D', 'F', 'A'])
         self.assertEqual(chords, [Chord("C13")])
 
     def test_keep_existing_chord(self):
diff --git a/test/test_transpose.py b/test/test_transpose.py
index 15034f6..34926e6 100644
--- a/test/test_transpose.py
+++ b/test/test_transpose.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import unittest
 
 from pychord import Chord