diff --git a/README.md b/README.md
index c74f30f..996ea0a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
-# python-switch
+# switchlang
+[![](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/) 
+[![](https://img.shields.io/pypi/l/markdown-subtemplate.svg)](https://github.com/mikeckennedy/python-switch/blob/master/LICENSE)
+[![](https://img.shields.io/pypi/dm/switchlang.svg)](https://pypi.org/project/switchlang/)
 
-Adds switch blocks to Python.
+
+Adds switch blocks to the Python language.
 
 This module adds explicit switch functionality to Python 
 without changing the language. It builds upon a standard
@@ -11,13 +15,14 @@ way to define execution blocks: the `with` statement.
 ```python
 from switchlang import switch
 
-num = 7
-val = input("Enter a key. a, b, c or any other: ")
+def main():
+    num = 7
+    val = input("Enter a character, a, b, c or any other: ")
 
-with switch(val) as s:
-    s.case('a', process_a)
-    s.case('b', lambda: process_with_data(val, num, 'other values still'))
-    s.default(process_any)
+    with switch(val) as s:
+        s.case('a', process_a)
+        s.case('b', lambda: process_with_data(val, num, 'other values still'))
+        s.default(process_any)
     
 def process_a():
     print("Found A!")
@@ -27,8 +32,18 @@ def process_any():
     
 def process_with_data(*value):
     print("Found with data: {}".format(value))
+
+main()
 ``` 
 
+## Installation
+
+Simply install via pip:
+
+```bash
+pip install switchlang
+```
+
 ## Features
 
 * More explicit than using dictionaries with functions as values.
@@ -84,7 +99,7 @@ switch = {
     3: method_three
 }
 
-result = switch.get(value, defult_method_to_run)()
+result = switch.get(value, default_method_to_run)()
 ```
 
 This works but is very low on the functionality level. We have a better solution here 
@@ -107,10 +122,10 @@ while True:
         s.case(range(1,6), lambda: set_level(action))
         s.default(unknown_command)
     
-    result = s.result
+    print('Result is {}'.format(s.result))
 ```
 
-Now compare that to they espoused *pythonic* way:
+Now compare that to the espoused *pythonic* way:
 
 ```python
 # with raw dicts
@@ -135,7 +150,70 @@ while True:
         '': lambda: None,
     }
     result = switch.get(action, unknown_command)()
+    print('Result is {}'.format(result))
 ```
 
 Personally, I much prefer to read and write the one above. That's why I wrote this module.
 It seems to convey the intent of switch way more than the dict. But either are options.
+
+## Why not just `if / elif / else`?
+
+The another push back on this idea is that we already have this problem solved.
+Switch statements are really if / elif / else blocks. So you write the following code.
+
+```python
+# with if / elif / else
+
+while True:
+    action = get_action(action)
+
+    if action == 'c' or action == 'a':
+        result = create_account()
+    elif action == 'l':
+        result = log_into_account()
+    elif action == 'r':
+        result = register_cage()
+    elif action == 'a':
+        result = update_availability()
+    elif action == 'v' or action == 'b':
+        result = view_bookings()
+    elif action == 'x':
+        result = exit_app()
+    elif action in {1, 2, 3, 4, 5}:
+        result = set_level(action)
+    else:
+        unknown_command()
+        
+    print('Result is {}'.format(result))
+```
+
+I actually believe this is a little better than the 
+[raw dict option](https://github.com/mikeckennedy/python-switch#why-not-just-raw-dict).
+But there are still things that are harder. 
+
+* How would you deal with fall-through cleanly?
+* Did you notice the bug? We forgot to set result in default case (`else`) and will result in a runtime error (but only if that case hits).
+* There is another bug. Update `update_availability` will never run because it's command (`a`) is bound to two cases. 
+This is guarded against in switch and you would receive a duplicate case error the first time it runs at all.
+* While it's pretty clear, it's much more verbose and less declarative than the switch version. 
+
+Again, compare the if / elif / else to what you have with switch. This code is identical except 
+doesn't have the default case bug.
+
+```python
+while True:
+    action = get_action(action)
+
+    with switch(action) as s:
+        s.case(['c', 'a'], create_account)
+        s.case('l', log_into_account)
+        s.case('r', register_cage)
+        s.case('u', update_availability)
+        s.case(['v', 'b'], view_bookings)
+        s.case('x', exit_app)
+        s.case('', lambda: None)
+        s.case(range(1,6), lambda: set_level(action))
+        s.default(unknown_command)
+    
+    print('Result is {}'.format(s.result))
+```
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..0aa42e0
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,2 @@
+pytest
+# twine
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..bf5b7de
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,54 @@
+import io
+import os
+import re
+
+from setuptools import find_packages
+from setuptools import setup
+
+
+def read(filename):
+    filename = os.path.join(os.path.dirname(__file__), filename)
+    text_type = type(u"")
+    with io.open(filename, mode="r", encoding='utf-8') as fd:
+        return re.sub(text_type(r':[a-z]+:`~?(.*?)`'), text_type(r'``\1``'), fd.read())
+
+
+def read_version():
+    filename = os.path.join(os.path.dirname(__file__), 'switchlang', '__init__.py')
+    with open(filename, mode="r", encoding='utf-8') as fin:
+        for line in fin:
+            if line and line.strip() and line.startswith('__version__'):
+                return line.split('=')[1].strip().strip("'")
+
+    return "0.0.0.0"
+
+
+requires = []
+
+setup(
+    name="switchlang",
+    version=read_version(),
+    url="https://github.com/mikeckennedy/python-switch",
+    license='MIT',
+
+    author="Michael Kennedy",
+    author_email="michael@talkpython.fm",
+
+    description="Adds switch blocks to the Python language.",
+    long_description=read("README.md"),
+    long_description_content_type='text/markdown',
+
+    packages=find_packages(exclude=('tests',)),
+
+    install_requires=requires,
+
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'License :: OSI Approved :: MIT License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+    ],
+)
diff --git a/switchlang.py b/switchlang.py
deleted file mode 100644
index 39568a2..0000000
--- a/switchlang.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Here is a first pass implementation at adding switch
-import uuid
-from typing import Callable, Any, List
-
-
-class switch:
-    __no_result = uuid.uuid4()
-
-    def __init__(self, value):
-        self.value = value
-        self.cases = {}
-        self.__result = switch.__no_result
-
-    def default(self, func: Callable[[], Any]):
-        self.case('__default__', func)
-
-    def case(self, key, func: Callable[[], Any]):
-        if isinstance(key, range):
-            for n in key:
-                self.case(n, func)
-            return
-
-        if isinstance(key, list):
-            for i in key:
-                self.case(i, func)
-            return
-
-        if key in self.cases:
-            raise ValueError("Duplicate case: {}".format(key))
-        if not func:
-            raise ValueError("Action for case cannot be None.")
-        if not callable(func):
-            raise ValueError("Func must be callable.")
-
-        self.cases[key] = func
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        if exc_val:
-            raise exc_val
-
-        func = self.cases.get(self.value)
-        if not func:
-            func = self.cases.get('__default__')
-
-        if not func:
-            raise Exception("Value does not match any case and there "
-                            "is no default case: value {}".format(self.value))
-
-        # noinspection PyCallingNonCallable
-        self.__result = func()
-
-    @property
-    def result(self):
-        if self.__result == switch.__no_result:
-            raise Exception("No result has been computed (did you access switch.result inside the with block?)")
-
-        return self.__result
-
-
-def closed_range(start: int, stop: int, step=1) -> range:
-    if start >= stop:
-        raise ValueError("Start must be less than stop.")
-
-    return range(start, stop+step, step)
diff --git a/switchlang/__init__.py b/switchlang/__init__.py
new file mode 100644
index 0000000..b14ca62
--- /dev/null
+++ b/switchlang/__init__.py
@@ -0,0 +1,14 @@
+"""
+switchlang - Adds switch blocks to Python
+
+See https://github.com/mikeckennedy/python-switch for full details.
+Copyright Michael Kennedy (https://twitter.com/mkennedy)
+License: MIT
+"""
+
+__version__ = '0.1.0'
+__author__ = 'Michael Kennedy <michael@talkpython.fm>'
+__all__ = ['switch', 'closed_range']
+
+from .__switchlang_impl import switch
+from .__switchlang_impl import closed_range
diff --git a/switchlang/__switchlang_impl.py b/switchlang/__switchlang_impl.py
new file mode 100644
index 0000000..81bfcef
--- /dev/null
+++ b/switchlang/__switchlang_impl.py
@@ -0,0 +1,150 @@
+import uuid
+from typing import Callable, Any, Optional
+
+
+class switch:
+    """
+        switch is a module-level implementation of the switch statement for Python.
+        See https://github.com/mikeckennedy/python-switch for full details.
+        Copyright Michael Kennedy (https://twitter.com/mkennedy)
+        License: MIT
+    """
+    __no_result = uuid.uuid4()
+    __default = uuid.uuid4()
+
+    def __init__(self, value):
+        self.value = value
+        self.cases = set()
+        self._found = False
+        self.__result = switch.__no_result
+        self._falling_through = False
+        self._func_stack = []
+
+    def default(self, func: Callable[[], Any]):
+        """
+        Use as option final statement in switch block.
+
+        ```
+            with switch(val) as s:
+               s.case(...)
+               s.case(...)
+               s.default(function)
+        ```
+
+        :param func: Any callable taking no parameters to be executed if this (default) case matches.
+        :return: None
+        """
+        self.case(switch.__default, func)
+
+    def case(self, key, func: Callable[[], Any], fallthrough: Optional[bool] = False):
+        """
+        Specify a case for the switch block:
+
+        ```
+            with switch(val) as s:
+               s.case('a', function)
+               s.case('b', function, fallthrough=True)
+               s.default(function)
+        ```
+
+        :param key: Key for the case test (if this is a list or range, the items will each be added as a case)
+        :param func: Any callable taking no parameters to be executed if this case matches.
+        :param fallthrough: Optionally fall through to the subsequent case (defaults to False)
+        :return:
+        """
+        if fallthrough is not None:
+            if self._falling_through:
+                self._func_stack.append(func)
+                if not fallthrough:
+                    self._falling_through = False
+
+        if isinstance(key, range):
+            key = list(key)
+
+        if isinstance(key, list):
+            if not key:
+                raise ValueError("You cannot pass an empty collection as the case. It will never match.")
+
+            found = False
+            for i in key:
+                if self.case(i, func, fallthrough=None):
+                    found = True
+                    if fallthrough is not None:
+                        self._falling_through = fallthrough
+
+            return found
+
+        if key in self.cases:
+            raise ValueError(f"Duplicate case: {key}")
+        if not func:
+            raise ValueError("Action for case cannot be None.")
+        if not callable(func):
+            raise ValueError("Func must be callable.")
+
+        self.cases.add(key)
+        if key == self.value or not self._found and key == self.__default:
+            self._func_stack.append(func)
+            self._found = True
+            if fallthrough is not None:
+                self._falling_through = fallthrough
+            return True
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if exc_val:
+            raise exc_val
+
+        if not self._func_stack:
+            raise Exception("Value does not match any case and there "
+                            "is no default case: value {}".format(self.value))
+
+        for func in self._func_stack:
+            # noinspection PyCallingNonCallable
+            self.__result = func()
+
+    @property
+    def result(self):
+        """
+        The value captured from the method called for a given case.
+
+        ```
+            value = 4
+            with switch(value) as s:
+                s.case(closed_range(1, 5), lambda: "1-to-5")
+                # ...
+
+            res = s.result  # res == '1-to-5'
+        ```
+
+        :return: The value captured from the method called for a given case.
+        """
+        if self.__result == switch.__no_result:
+            raise Exception("No result has been computed (did you access "
+                            "switch.result inside the with block?)")
+
+        return self.__result
+
+
+def closed_range(start: int, stop: int, step=1) -> range:
+    """
+    Creates a closed range that allows you to specify a case
+    from [start, stop] inclusively.
+
+    ```
+        with switch(value) as s:
+            s.case(closed_range(1, 5), lambda: "1-to-5")
+            s.case(closed_range(6, 7), lambda: "6")
+            s.default(lambda: 'default')
+    ```
+
+    :param start: The inclusive lower bound of the range [start, stop].
+    :param stop: The inclusive upper bound of the range [start, stop].
+    :param step: The step size between elements (defaults to 1).
+    :return: A range() generator that has a closed upper bound.
+    """
+    if start >= stop:
+        raise ValueError("Start must be less than stop.")
+
+    return range(start, stop + step, step)
diff --git a/tests/coretests.py b/tests/test_core.py
similarity index 50%
rename from tests/coretests.py
rename to tests/test_core.py
index 679acd2..f343adc 100644
--- a/tests/coretests.py
+++ b/tests/test_core.py
@@ -129,3 +129,104 @@ def test_closed_range(self):
             s.default(lambda: 'default')
 
         self.assertEqual(s.result, "6")
+
+    def test_fallthrough_simple(self):
+        visited = []
+        value = 2
+        with switch(value) as s:
+            s.case(1, lambda: visited.append(1) or 1)
+            s.case(2, lambda: visited.append(2) or 2, fallthrough=True)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, "default")
+        self.assertEqual(visited, [2, 'default'])
+
+    def test_fallthrough_list(self):
+        visited = []
+        value = 5
+        with switch(value) as s:
+            s.case([1, 2, 3], lambda: visited.append(1) or 1)
+            s.case([4, 5, 6], lambda: visited.append(4) or 4, fallthrough=True)
+            s.case([7, 8, 9], lambda: visited.append(7) or 7, fallthrough=True)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, "default")
+        self.assertEqual(visited, [4, 7, 'default'])
+
+    def test_fallthrough_some_list(self):
+        visited = []
+        value = 5
+        with switch(value) as s:
+            s.case([1, 2, 3], lambda: visited.append(1) or 1)
+            s.case([4, 5, 6], lambda: visited.append(4) or 4, fallthrough=True)
+            s.case([7, 8, 9], lambda: visited.append(7) or 7)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, 7)
+        self.assertEqual(visited, [4, 7])
+
+    def test_fallthrough_then_stop(self):
+        visited = []
+        value = 2
+        with switch(value) as s:
+            s.case(1, lambda: visited.append(1) or 1)
+            s.case(2, lambda: visited.append(2) or 2, fallthrough=True)
+            s.case(3, lambda: visited.append(3) or 3, fallthrough=True)
+            s.case(4, lambda: visited.append(4) or 4)
+            s.case(5, lambda: visited.append(5) or 5)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, 4)
+        self.assertEqual(visited, [2, 3, 4])
+
+    def test_fallthrough_middle_then_stop(self):
+        visited = []
+        value = 3
+        with switch(value) as s:
+            s.case(1, lambda: visited.append(1) or 1)
+            s.case(2, lambda: visited.append(2) or 2, fallthrough=True)
+            s.case(3, lambda: visited.append(3) or 3, fallthrough=True)
+            s.case(4, lambda: visited.append(4) or 4)
+            s.case(5, lambda: visited.append(5) or 5)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, 4)
+        self.assertEqual(visited, [3, 4])
+
+    def test_fallthrough_available_but_not_hit(self):
+        visited = []
+        value = 5
+        with switch(value) as s:
+            s.case(1, lambda: visited.append(1) or 1)
+            s.case(2, lambda: visited.append(2) or 2, fallthrough=True)
+            s.case(3, lambda: visited.append(3) or 3, fallthrough=True)
+            s.case(4, lambda: visited.append(4) or 4)
+            s.case(5, lambda: visited.append(5) or 5)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, 5)
+        self.assertEqual(visited, [5])
+
+    def test_fallthrough__no_match_but_not_hit(self):
+        visited = []
+        value = 'gone'
+        with switch(value) as s:
+            s.case(1, lambda: visited.append(1) or 1)
+            s.case(2, lambda: visited.append(2) or 2, fallthrough=True)
+            s.case(3, lambda: visited.append(3) or 3, fallthrough=True)
+            s.case(4, lambda: visited.append(4) or 4)
+            s.case(5, lambda: visited.append(5) or 5)
+            s.default(lambda: visited.append('default') or 'default')
+
+        self.assertEqual(s.result, 'default')
+        self.assertEqual(visited, ['default'])
+
+    def test_empty_collection_clause_is_error(self):
+        with self.assertRaises(ValueError):
+            with switch('val') as s:
+                s.case([], lambda: None)
+                s.default(lambda: 'default')
+
+
+if __name__ == '__main__':
+    unittest.main()
