diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index f26b2e0..0000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-environment:
-  matrix:
-    - TOXENV: py36
-    - TOXENV: py35
-    - TOXENV: py34
-    - TOXENV: py27
-
-matrix:
-  fast_finish: true
-
-build: false
-
-install: C:\Python36\python -m pip install -U tox
-
-test_script: C:\Python36\scripts\tox
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..aa472e2
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,83 @@
+name: packages
+on:
+  push:
+    tags:
+    - 'v[0-9]+.[0-9]+.[0-9]+'
+    - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+'
+    - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+'
+    - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+'
+
+jobs:
+  conda_build:
+    name: Build Conda Packages
+    runs-on: 'ubuntu-latest'
+    defaults:
+      run:
+        shell: bash -l {0}
+    env:
+      CHANS_DEV: "-c pyviz/label/dev"
+      PKG_TEST_PYTHON: "--test-python=py37"
+      PYTHON_VERSION: "3.7"
+      CHANS: "-c pyviz"
+      CONDA_UPLOAD_TOKEN: ${{ secrets.CONDA_UPLOAD_TOKEN }}
+    steps:
+      - uses: actions/checkout@v2
+      - name: Fetch unshallow
+        run: git fetch --prune --tags --unshallow -f
+      - uses: actions/setup-python@v2
+        with:
+          python-version: "3.7"
+      - uses: conda-incubator/setup-miniconda@v2
+        with:
+          miniconda-version: "latest"
+      - name: Set output
+        id: vars
+        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
+      - name: conda setup
+        run: |
+          eval "$(conda shell.bash hook)"
+          conda config --set always_yes yes --set changeps1 no
+          conda update conda
+          conda install anaconda-client conda-build
+      - name: conda build
+        run: |
+          eval "$(conda shell.bash hook)"
+          conda build conda.recipe/
+      - name: conda dev upload
+        if: (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))
+        run: |
+          eval "$(conda shell.bash hook)"
+          anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev $(conda build --output conda.recipe)
+      - name: conda main upload
+        if: (!(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc')))
+        run: |
+          eval "$(conda shell.bash hook)"
+          anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev --label=main $(conda build --output conda.recipe)
+  pip_build:
+    name: Build PyPI Packages
+    runs-on: 'ubuntu-latest'
+    defaults:
+      run:
+        shell: bash -l {0}
+    env:
+      TOX_ENV: "py3.7"
+    steps:
+      - uses: actions/checkout@v2
+      - name: Fetch unshallow
+        run: git fetch --prune --tags --unshallow -f
+      - uses: actions/setup-python@v2
+        with:
+          python-version: "3.7"
+      - name: env setup
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install setuptools wheel twine tox
+      - name: pip build
+        run: |
+          python setup.py sdist bdist_wheel
+      - name: Publish package to PyPI
+        uses: pypa/gh-action-pypi-publish@master
+        with:
+          user: ${{ secrets.PPU }}
+          password: ${{ secrets.PPP }}
+          packages_dir: dist/
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..98ddfc9
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,56 @@
+name: docs
+on:
+  push:
+    tags:
+      - 'v[0-9]+.[0-9]+.[0-9]+'
+      - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+'
+      - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+'
+      - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+'
+
+jobs:
+  build_docs:
+    name: Documentation
+    runs-on: 'ubuntu-latest'
+    strategy:
+      fail-fast: false
+    timeout-minutes: 120
+    defaults:
+      run:
+        shell: bash -l {0}
+    env:
+      DESC: "Documentation build"
+    steps:
+      - uses: actions/checkout@v2
+      - name: Fetch unshallow
+        run: git fetch --prune --tags --unshallow -f
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+      - name: Set output
+        id: vars
+        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
+      - name: env setup
+        run: |
+          python -m pip install graphviz nbsite sphinx_holoviz_theme "nbconvert <=5.3.1"
+          python -m pip install -e .
+      - name: build docs
+        run: |
+          python -m nbsite generate-rst --org pyviz --repo param --project-name param
+          mkdir doc/Reference_Manual && nbsite_generate_modules.py param -d ./doc/Reference_Manual -n param -e tests
+          python -m nbsite build --examples-assets=''
+      - name: Deploy dev
+        uses: peaceiris/actions-gh-pages@v3
+        if: (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))
+        with:
+          personal_token: ${{ secrets.ACCESS_TOKEN }}
+          external_repository: pyviz-dev/param
+          publish_dir: ./builtdocs
+          force_orphan: true
+      - name: Deploy main
+        if: (!(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc')))
+        uses: peaceiris/actions-gh-pages@v3
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          publish_dir: ./builtdocs
+          cname: param.holoviz.org
+          force_orphan: true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..2ec6c24
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,73 @@
+# things not included
+# language
+# notifications - no email notifications set up
+
+name: pytest
+on:
+  pull_request:
+    branches:
+    - '*'
+
+jobs:
+  test_suite:
+    name: Tox on ${{ matrix.python-version }}, ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: ['ubuntu-latest', 'windows-latest']
+        python-version: [2.7, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
+    timeout-minutes: 30
+    defaults:
+      run:
+        shell: bash -l {0} 
+    env:
+      PYTHON_VERSION: ${{ matrix.python-version }}
+      CHANS_DEV: "-c pyviz/label/dev"
+      CHANS: "-c pyviz"
+      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: "100"
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Fetch
+        run: git fetch --prune --tags
+      - name: env setup
+        run: |
+          set -xe
+          python -VV
+          python -m site
+          python -m pip install --upgrade pip
+          python -m pip install tox flake8
+      - name: lint
+        run: |
+          flake8
+      - name: unit python
+        if: (!startsWith(matrix.python-version, 'py'))
+        run: |
+          pyver="${{ matrix.python-version }}"
+          tox_env="py${pyver//.}"
+          tox -e $tox_env
+      - name: unit pypy
+        if: startsWith(matrix.python-version, 'py')
+        run: |
+          pyver="${{ matrix.python-version }}"
+          tox_env="${pyver//.}"
+          tox -e $tox_env
+      - name: unit with_ipython
+        run: tox -e with_ipython
+      - name: unit with_numpy
+        run: tox -e with_numpy
+      - name: unit with_pandas
+        run: tox -e with_pandas
+      - name: unit with_jsonschema
+        run: tox -e with_jsonschema
+      - name: unit with_gmpy
+        if: (contains(matrix.os, 'ubuntu') && !startsWith(matrix.python-version, 'py'))
+        run: tox -e with_gmpy
+      - name: unit all_deps
+        if: (contains(matrix.os, 'ubuntu') && !startsWith(matrix.python-version, 'py'))
+        run: tox -e with_all
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index baf980d..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,158 +0,0 @@
-sudo: false
-
-language: python
-
-# quick hack to determine what tag is (improvements welcomed)
-#     release: ^v(\d+|\.)+[^a-z]\d+$
-# dev release: ^v(\d+|\.)+[a-z]\d+$
-
-stages:
-  - lint
-  - test
-  - name: pip_dev_package
-    if: tag =~ ^v(\d+|\.)+[a-z]\d+$
-  - name: pip_package
-    if: tag =~ ^v(\d+|\.)+[^a-z]\d+$
-  - name: conda_dev_package
-    if: tag =~ ^v(\d+|\.)+[a-z]\d+$
-  - name: conda_package
-    if: tag =~ ^v(\d+|\.)+[^a-z]\d+$
-  - name: website_dev
-    if: tag =~ ^v(\d+|\.)+[a-z]\d+$ OR tag = website_dev
-  - name: website_release
-    if: tag =~ ^v(\d+|\.)+[^a-z]\d+$ OR tag = website
-
-
-jobs:
-  fast_finish: true
-  include:
-    - &default
-      stage: test
-      python: 3.6
-      env: TOX_ENV=py36
-      install:
-        - pip install tox
-      script:
-        - tox -e $TOX_ENV
-
-    - <<: *default
-      python: 3.7-dev
-      env: TOX_ENV=py37
-
-    - <<: *default
-      python: 3.5
-      env: TOX_ENV=py35
-
-    - <<: *default
-      python: 3.4
-      env: TOX_ENV=py34
-
-    - <<: *default
-      python: 2.7
-      env: TOX_ENV=py27
-
-    - <<: *default
-      python: pypy
-      env: TOX_ENV=pypy
-
-    # could consider running with_ipython,numpy over py36 and 27
-
-    - <<: *default
-      env: TOX_ENV=with_ipython
-
-    - <<: *default
-      env: TOX_ENV=with_numpy
-
-    - <<: *default
-      env: TOX_ENV=with_pandas
-
-    - <<: *default
-      env: TOX_ENV=coverage
-
-    - <<: *default
-      stage: lint
-      env: TOX_ENV=flakes
-
-    # TODO: the below packaging sections will be simplified with
-    # doit/pyct (and note that using after_success means no alert to
-    # failure uploading)
-
-    - &conda_default
-      env: LABELS="--label dev"
-      stage: conda_dev_package
-      install:
-        - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
-        - bash miniconda.sh -b -p $HOME/miniconda
-        - export PATH="$HOME/miniconda/bin:$PATH"
-        - conda config --set always_yes yes --set changeps1 no
-        - conda update conda
-        - conda install anaconda-client conda-build
-      script:
-        - conda build conda.recipe/
-      after_success:
-        - anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz $LABELS $(conda build --output conda.recipe)
-
-    - <<: *conda_default
-      env: LABELS="--label dev --label main"
-      stage: conda_package
-
-    - <<: *default
-      stage: pip_dev_package
-      deploy:
-        provider: pypi
-        server: https://test.pypi.org/legacy/
-        distributions: "sdist bdist_wheel"
-        on:
-          tags: true
-        user: $TESTPYPI_USER
-        password: $TESTPYPI_PWD
-
-    - <<: *default
-      stage: pip_package
-      deploy:
-        provider: pypi
-        distributions: "sdist bdist_wheel"
-        on:
-          tags: true
-        user: $PYPI_USER
-        password: $PYPI_PWD
-
-    - &website
-      <<: *default
-      addons:
-        apt:
-          packages:
-          - graphviz
-      stage: website_release
-      before_install:
-        - pip install graphviz
-      install:
-        - pip install nbsite sphinx_ioam_theme "tornado<6"
-        - pip install -e .
-      script:
-        # TODO: nbsite commands will be simplified eventually...
-        - nbsite generate-rst --org pyviz --repo param --project-name param
-        - mkdir doc/Reference_Manual && nbsite_generate_modules.py param -d ./doc/Reference_Manual -n param -e tests
-        - nbsite build --examples-assets=''
-      deploy:
-        - provider: pages
-          skip_cleanup: true
-          github_token: $GITHUB_TOKEN
-          local_dir: ./builtdocs
-          fqdn: param.pyviz.org
-          on:
-            tags: true
-            all_branches: true
-
-    - <<: *website
-      stage: website_dev
-      env: DESC="pyviz-dev.github.io/param"
-      deploy:
-        - provider: pages
-          skip_cleanup: true
-          github_token: $GITHUB_TOKEN
-          local_dir: ./builtdocs
-          repo: pyviz-dev/param
-          on:
-            tags: true
-            all_branches: true
diff --git a/LICENSE.txt b/LICENSE.txt
index a4d7a35..6362367 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2018, IOAM (ioam.github.com)
+Copyright (c) 2005-2020, HoloViz team.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
@@ -13,9 +13,9 @@ met:
    documentation and/or other materials provided with the
    distribution.
 
- * Neither the name of IOAM nor the names of its contributors
-   may be used to endorse or promote products derived from this
-   software without specific prior written permission.
+ * Neither the name of the copyright holder nor the names of any
+   contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
diff --git a/README.rst b/README.rst
index c5d4d14..704046d 100644
--- a/README.rst
+++ b/README.rst
@@ -13,17 +13,17 @@ dependencies, and is provided freely for both non-commercial and
 commercial use under a BSD license, so that it can easily be included
 as part of other projects.
 
-Please see `param's website <http://param.pyviz.org>`_ for
+Please see `param's website <http://param.holoviz.org>`_ for
 official releases, installation instructions, documentation, and examples.
 
-.. |LinuxTests| image:: https://travis-ci.org/pyviz/param.svg?branch=master
-.. _LinuxTests: https://travis-ci.org/pyviz/param
+.. |LinuxTests| image:: https://travis-ci.org/holoviz/param.svg?branch=master
+.. _LinuxTests: https://travis-ci.org/holoviz/param
 
-.. |WinTests| image:: https://ci.appveyor.com/api/projects/status/1p5aom8o0tfgok1r?svg=true
-.. _WinTests: https://ci.appveyor.com/project/pyviz/param/branch/master
+.. |WinTests| image:: https://ci.appveyor.com/api/projects/status/jv1j2036g0a5t5qx/branch/master?svg=true
+.. _WinTests: https://ci.appveyor.com/project/holoviz-developers/param/branch/master
 
-.. |Coverage| image:: https://img.shields.io/coveralls/pyviz/param.svg
-.. _Coverage: https://coveralls.io/r/pyviz/param?branch=master
+.. |Coverage| image:: https://coveralls.io/repos/github/holoviz/param/badge.svg?branch=master
+.. _Coverage: https://coveralls.io/github/holoviz/param?branch=master
 
 .. |PyPIVersion| image:: http://img.shields.io/pypi/v/param.svg
 .. _PyPIVersion: https://pypi.python.org/pypi/param
diff --git a/debian/changelog b/debian/changelog
index 39062b7..7f7a536 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,6 @@
+python-param (1.10.1-1) UNRELEASED; urgency=low
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 29 Mar 2021 04:00:52 -0000
+
 python-param (1.9.3-2) unstable; urgency=medium
 
   * Source only upload for migration to testing
diff --git a/doc/conf.py b/doc/conf.py
index 8144838..7811bf4 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -3,7 +3,7 @@
 from nbsite.shared_conf import *
 
 project = u'Param'
-authors = u'PyViz authors'
+authors = u'HoloViz authors'
 copyright = u'\u00a9 2005-2018, ' + authors
 description = 'Declarative Python programming using Parameters.'
 
@@ -11,7 +11,7 @@ import param
 version = release = param.__version__
 
 html_static_path += ['_static']
-html_theme = 'sphinx_ioam_theme'
+html_theme = 'sphinx_holoviz_theme'
 html_theme_options = {
     'logo':'logo.png',
     'favicon':'favicon.ico',
@@ -28,8 +28,9 @@ html_context.update({
     'DESCRIPTION': description,
     'AUTHOR': authors,
     # canonical URL (for search engines); can ignore for local builds
-    'WEBSITE_SERVER': 'https://param.pyviz.org',
+    'WEBSITE_SERVER': 'https://param.holoviz.org',
     'VERSION': version,
+    'GOOGLE_ANALYTICS_UA': 'UA-154795830-6',
     'NAV': _NAV,
     'LINKS': _NAV,
     'SOCIAL': (
diff --git a/examples/About.ipynb b/examples/About.ipynb
index 5906209..8340ecb 100644
--- a/examples/About.ipynb
+++ b/examples/About.ipynb
@@ -4,7 +4,22 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Param is part of [PyViz](http://pyviz.org), a collaborative project to produce a coherent solution to a wide range of Python visualization problems."
+    "Param is part of the [HoloViz](https://holoviz.org) family of tools. The\n",
+    "[holoviz.org](https://holoviz.org) website shows how to use Param\n",
+    "together with other libraries to solve complex problems, with detailed\n",
+    "tutorials and examples.\n",
+    "\n",
+    "Param is completely open source, available under a [BSD license](https://github.com/holoviz/param/blob/master/LICENSE.txt) freely for both commercial and non-commercial use. Param was originally developed by the Neural Networks Research Group at the University of Texas at Austin and the Institute for Adaptive and Neural Computation at the University of Edinburgh with support from the United States National Institutes of Health (grant 1R01-MH66991), and is now maintained by developers at [Anaconda Inc.](https://anaconda.com) and community contributors.\n",
+    "\n",
+    "If you have any questions or usage issues visit the [Param Discourse](https://discourse.holoviz.org/c/param/) site. If you are interested in contributing to Param development to help address some of the [open issues](https://github.com/holoviz/param/issues), see our [developer instructions](https://param.org/getting_started/#developer-instructions) to set up your development environment.\n",
+    "\n",
+    "Param is part of [HoloViz](http://holoviz.org), a collaborative project\n",
+    "to produce a coherent solution to a wide range of Python visualization\n",
+    "problems.\n",
+    "\n",
+    "If you like Param and have built something you want to share, tweet a link or screenshot of your latest creation at @HoloViz_org. Thanks!\n",
+    "\n",
+    "\n"
    ]
   }
  ],
diff --git a/examples/index.ipynb b/examples/index.ipynb
index 152175f..9f2c622 100644
--- a/examples/index.ipynb
+++ b/examples/index.ipynb
@@ -280,12 +280,7 @@
     "\n",
     "Recent release notes are available on [GitHub](https://github.com/ioam/param/releases).\n",
     "\n",
-    "For older releases, see our [historical release notes](historical_release_notes.html).\n",
-    "\n",
-    "\n",
-    "# Support\n",
-    "\n",
-    "Questions and comments are welcome at https://github.com/ioam/param/issues."
+    "For older releases, see our [historical release notes](historical_release_notes.html).\n"
    ]
   }
  ],
diff --git a/numbergen/__init__.py b/numbergen/__init__.py
index a1c5435..ffb6c01 100644
--- a/numbergen/__init__.py
+++ b/numbergen/__init__.py
@@ -202,7 +202,7 @@ class Hash(object):
 
         I32 = 4294967296 # Maximum 32 bit unsigned int (i.e. 'I') value
         if isinstance(val, int):
-             numer, denom = val, 1
+            numer, denom = val, 1
         elif isinstance(val, fractions.Fraction):
             numer, denom = val.numerator, val.denominator
         elif hasattr(val, 'numer'):
diff --git a/param/__init__.py b/param/__init__.py
index 8c91d1d..4aa7469 100644
--- a/param/__init__.py
+++ b/param/__init__.py
@@ -29,7 +29,7 @@ from .parameterized import (
     descendents, get_logger, instance_descriptor, basestring)
 
 from .parameterized import (batch_watch, depends, output, # noqa: api import
-                            discard_events, edit_constant)
+                            discard_events, edit_constant, instance_descriptor)
 from .parameterized import logging_level     # noqa: api import
 from .parameterized import shared_parameters # noqa: api import
 
@@ -41,7 +41,7 @@ from numbers import Real
 # only two required files.
 try:
     from .version import Version
-    __version__ = str(Version(fpath=__file__, archive_commit="9123ba0", reponame="param"))
+    __version__ = str(Version(fpath=__file__, archive_commit="3ffe0750", reponame="param"))
 except:
     __version__ = "0.0.0+unknown"
 
@@ -221,7 +221,7 @@ def parameterized_class(name, params, bases=Parameterized):
     supplied parameters, inheriting from the specified base(s).
     """
     if not (isinstance(bases, list) or isinstance(bases, tuple)):
-          bases=[bases]
+        bases=[bases]
     return type(name, tuple(bases), params)
 
 
@@ -924,7 +924,7 @@ class Integer(Number):
     """Numeric Parameter required to be an Integer"""
 
     def __init__(self,default=0,**params):
-       Number.__init__(self,default=default,**params)
+        Number.__init__(self,default=default,**params)
 
     def _validate(self, val):
         if callable(val): return
@@ -1014,6 +1014,14 @@ class Tuple(Parameter):
                              (self.name,len(val),self.length))
 
 
+    @classmethod
+    def serialize(cls, value):
+        return list(value) # As JSON has no tuple representation
+
+    @classmethod
+    def deserialize(cls, value):
+        return tuple(value) # As JSON has no tuple representation
+
 
 class NumericTuple(Tuple):
     """A numeric tuple Parameter (e.g. (4.5,7.6,3)) with a fixed tuple length."""
@@ -1068,7 +1076,6 @@ def _is_abstract(class_):
         return False
 
 
-
 # CEBALERT: this should be a method of ClassSelector.
 def concrete_descendents(parentclass):
     """
@@ -1084,7 +1091,6 @@ def concrete_descendents(parentclass):
                 if not _is_abstract(c))
 
 
-
 class Composite(Parameter):
     """
     A Parameter that is a composite of a set of other attributes of the class.
@@ -1248,7 +1254,7 @@ class ObjectSelector(SelectorBase):
         to check each item instead.
         """
         if not (val in self.objects):
-           self.objects.append(val)
+            self.objects.append(val)
 
     def get_range(self):
         """
@@ -1290,6 +1296,7 @@ class Selector(ObjectSelector):
                                       check_on_set=check_on_set,
                                       allow_None=allow_None, **params)
 
+
 class ClassSelector(SelectorBase):
     """
     Parameter allowing selection of either a subclass or an instance of a given set of classes.
@@ -1306,7 +1313,6 @@ class ClassSelector(SelectorBase):
         super(ClassSelector,self).__init__(default=default,instantiate=instantiate,**params)
         self._validate(default)
 
-
     def _validate(self,val):
         """val must be None, an instance of self.class_ if self.is_instance=True or a subclass of self_class if self.is_instance=False"""
         if isinstance(self.class_, tuple):
@@ -1324,7 +1330,6 @@ class ClassSelector(SelectorBase):
                     "Parameter '%s' must be a subclass of %s, not '%s'" %
                     (val.__name__, class_name, val.__class__.__name__))
 
-
     def get_range(self):
         """
         Return the possible types for this parameter's value.
@@ -1396,7 +1401,6 @@ class List(Parameter):
                 assert isinstance(v,self.class_),repr(self.name)+": "+repr(v)+" is not an instance of " + repr(self.class_) + "."
 
 
-
 class HookList(List):
     """
     Parameter whose value is a list of callable objects.
@@ -1412,7 +1416,6 @@ class HookList(List):
             assert callable(v),repr(self.name)+": "+repr(v)+" is not callable."
 
 
-
 class Dict(ClassSelector):
     """
     Parameter whose value is a dictionary.
@@ -1431,6 +1434,15 @@ class Array(ClassSelector):
         from numpy import ndarray
         super(Array,self).__init__(ndarray, allow_None=True, default=default, **params)
 
+    @classmethod
+    def serialize(cls, value):
+        return value.tolist()
+
+    @classmethod
+    def deserialize(cls, value):
+        from numpy import asarray
+        return asarray(value)
+
 
 class DataFrame(ClassSelector):
     """
@@ -1457,7 +1469,7 @@ class DataFrame(ClassSelector):
         self.rows = rows
         self.columns = columns
         self.ordered = ordered
-        super(DataFrame,self).__init__(pdDFrame, default=default, allow_None=True, **params)
+        super(DataFrame,self).__init__(pdDFrame, default=default, **params)
         self._validate(self.default)
 
 
@@ -1502,6 +1514,15 @@ class DataFrame(ClassSelector):
         if self.rows is not None:
             self._length_bounds_check(self.rows, len(val), 'Row')
 
+    @classmethod
+    def serialize(cls, value):
+        return value.to_dict('records')
+
+    @classmethod
+    def deserialize(cls, value):
+        from pandas import DataFrame as pdDFrame
+        return pdDFrame(value)
+
 
 class Series(ClassSelector):
     """
@@ -1843,6 +1864,16 @@ class Date(Number):
 
         self._checkBounds(val)
 
+    @classmethod
+    def serialize(cls, value):
+        if not isinstance(value, (dt.datetime, dt.date)): # i.e np.datetime64
+            value = value.astype(dt.datetime)
+        return value.strftime("%Y-%m-%dT%H:%M:%S.%f")
+
+    @classmethod
+    def deserialize(cls, value):
+        return dt.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
+
 
 class CalendarDate(Number):
     """
@@ -1868,6 +1899,14 @@ class CalendarDate(Number):
 
         self._checkBounds(val)
 
+    @classmethod
+    def serialize(cls, value):
+        return value.strftime("%Y-%m-%d")
+
+    @classmethod
+    def deserialize(cls, value):
+        return dt.datetime.strptime(value, "%Y-%m-%d").date()
+
 
 class Color(Parameter):
     """
@@ -1875,7 +1914,7 @@ class Color(Parameter):
     prefix.
     """
 
-    def __init__(self, default=None, allow_None=False, **kwargs):
+    def __init__(self, default=None, **kwargs):
         super(Color, self).__init__(default=default, **kwargs)
         self._validate(default)
 
@@ -1977,7 +2016,7 @@ class DateRange(Range):
 
         start, end = val
         if not end >= start:
-           raise ValueError("DateRange '%s': end datetime %s is before start datetime %s."%(self.name,val[1],val[0]))
+            raise ValueError("DateRange '%s': end datetime %s is before start datetime %s."%(self.name,val[1],val[0]))
 
         # Calling super(DateRange, self)._check(val) would also check
         # values are numeric, which is redundant, so just call
@@ -1999,9 +2038,67 @@ class CalendarDateRange(Range):
 
         start, end = val
         if not end >= start:
-           raise ValueError("CalendarDateRange '%s': end date %s is before start date %s."%(self.name,val[1],val[0]))
+            raise ValueError("CalendarDateRange '%s': end date %s is before start date %s."%(self.name,val[1],val[0]))
 
         # Calling super(CalendarDateRange, self)._check(val) would also check
         # values are numeric, which is redundant, so just call
         # _checkBounds().
         self._checkBounds(val)
+
+
+class Event(Boolean):
+    """
+    An Event Parameter is one whose value is intimately linked to the
+    triggering of events for watchers to consume. Event has a Boolean
+    value, which when set to True triggers the associated watchers (as
+    any Parameter does) and then is automatically set back to
+    False. Conversely, if events are triggered directly via `.trigger`,
+    the value is transiently set to True (so that it's clear which of
+    many parameters being watched may have changed), then restored to
+    False when the triggering completes. An Event parameter is thus like
+    a momentary switch or pushbutton with a transient True value that
+    serves only to launch some other action (e.g. via a param.depends
+    decorator), rather than encapsulating the action itself as
+    param.Action does.
+    """
+
+    # _autotrigger_value specifies the value used to set the parameter
+    # to when the parameter is supplied to the trigger method. This
+    # value change is then what triggers the watcher callbacks.
+    __slots__ = ['_autotrigger_value', '_mode', '_autotrigger_reset_value']
+
+    def __init__(self,default=False,bounds=(0,1),**params):
+        self._autotrigger_value = True
+        self._autotrigger_reset_value = False
+        self._mode = 'set-reset'
+        # Mode can be one of 'set', 'set-reset' or 'reset'
+
+        # 'set' is normal Boolean parameter behavior when set with a value.
+        # 'set-reset' temporarily sets the parameter (which triggers
+        # watching callbacks) but immediately resets the value back to
+        # False.
+        # 'reset' applies the reset from True to False without
+        # triggering watched callbacks
+
+        # This _mode attribute is one of the few places where a specific
+        # parameter has a special behavior that is relied upon by the
+        # core functionality implemented in
+        # parameterized.py. Specifically, the set_param method
+        # temporarily sets this attribute in order to disable resetting
+        # back to False while triggered callbacks are executing
+        super(Event, self).__init__(default=default,**params)
+
+    def _reset_event(self, obj, val):
+        val = False
+        if obj is None:
+            self.default = val
+        else:
+            obj.__dict__[self._internal_name] = val
+        self._post_setter(obj, val)
+
+    @instance_descriptor
+    def __set__(self, obj, val):
+        if self._mode in ['set-reset', 'set']:
+            super(Event, self).__set__(obj, val)
+        if self._mode in ['set-reset', 'reset']:
+            self._reset_event(obj, val)
diff --git a/param/ipython.py b/param/ipython.py
index cab822d..ec40b73 100644
--- a/param/ipython.py
+++ b/param/ipython.py
@@ -279,7 +279,7 @@ class ParamPager(object):
         return "%s\n\n%s\n\n%s\n\n%s" % (top_heading, table, docstring_heading, docstrings)
 
 
-message = """Welcome to the param IPython extension! (http://ioam.github.io/param/)"""
+message = """Welcome to the param IPython extension! (https://param.holoviz.org/)"""
 message += '\nAvailable magics: %params'
 
 _loaded = False
diff --git a/param/parameterized.py b/param/parameterized.py
index a7a292d..eeac91a 100644
--- a/param/parameterized.py
+++ b/param/parameterized.py
@@ -11,10 +11,17 @@ import random
 import numbers
 import operator
 
-from collections import namedtuple, OrderedDict
+# Allow this file to be used standalone if desired, albeit without JSON serialization
+try:
+    from . import serializer
+except ImportError:
+    serializer = None
+
+
+from collections import defaultdict, namedtuple, OrderedDict
+from functools import partial, wraps, reduce
 from operator import itemgetter,attrgetter
 from types import FunctionType
-from functools import partial, wraps, reduce
 
 import logging
 from contextlib import contextmanager
@@ -133,7 +140,8 @@ def discard_events(parameterized):
     """
     batch_watch = parameterized.param._BATCH_WATCH
     parameterized.param._BATCH_WATCH = True
-    watchers, events = parameterized.param._watchers, parameterized.param._events
+    watchers, events = (list(parameterized.param._watchers),
+                        list(parameterized.param._events))
     try:
         yield
     except:
@@ -144,6 +152,11 @@ def discard_events(parameterized):
         parameterized.param._events = events
 
 
+# External components can register an async executor which will run
+# async functions
+async_executor = None
+
+
 def classlist(class_):
     """
     Return a list of the class hierarchy above (and including) the given class.
@@ -286,6 +299,15 @@ def no_instance_params(cls):
     return cls
 
 
+def iscoroutinefunction(function):
+    """
+    Whether the function is an asynchronous coroutine function.
+    """
+    if not hasattr(inspect, 'iscoroutinefunction'):
+        return False
+    return inspect.iscoroutinefunction(function)
+
+
 def instance_descriptor(f):
     # If parameter has an instance Parameter delegate setting
     def _f(self, obj, val):
@@ -297,6 +319,17 @@ def instance_descriptor(f):
     return _f
 
 
+def get_method_owner(method):
+    """
+    Gets the instance that owns the supplied method
+    """
+    if not inspect.ismethod(method):
+        return None
+    if isinstance(method, partial):
+        method = method.func
+    return method.__self__ if sys.version_info.major >= 3 else method.im_self
+
+
 @accept_arguments
 def depends(func, *dependencies, **kw):
     """
@@ -346,14 +379,17 @@ def depends(func, *dependencies, **kw):
                              'or function is not supported when referencing '
                              'parameters by name.')
 
-    if not string_specs and watch:
-        def cb(event):
+    if not string_specs and watch: # string_specs case handled elsewhere (later), in Parameterized.__init__
+        def cb(*events):
             args = (getattr(dep.owner, dep.name) for dep in dependencies)
             dep_kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw.items()}
             return func(*args, **dep_kwargs)
 
+        grouped = defaultdict(list)
         for dep in deps:
-            dep.owner.param.watch(cb, dep.name)
+            grouped[id(dep.owner)].append(dep)
+        for group in grouped.values():
+            group[0].owner.param.watch(cb, [dep.name for dep in group])
 
     _dinfo = getattr(func, '_dinfo', {})
     _dinfo.update({'dependencies': dependencies,
@@ -472,8 +508,18 @@ def _params_depended_on(minfo):
     return params
 
 
-def _m_caller(self,n):
-    return lambda event: getattr(self,n)()
+def _m_caller(self, n):
+    function = getattr(self, n)
+    if iscoroutinefunction(function):
+        import asyncio
+        @asyncio.coroutine
+        def caller(*events):
+            yield function()
+    else:
+        def caller(*events):
+            return function()
+    caller._watcher_name = n
+    return caller
 
 
 PInfo = namedtuple("PInfo","inst cls name pobj what")
@@ -671,6 +717,8 @@ class Parameter(object):
     # class is created, owner, name, and _internal_name are
     # set.
 
+    _serializers = {'json':serializer.JSONSerialization}
+
     def __init__(self,default=None,doc=None,label=None,precedence=None,  # pylint: disable-msg=R0913
                  instantiate=False,constant=False,readonly=False,
                  pickle_default_value=True, allow_None=False,
@@ -714,6 +762,24 @@ class Parameter(object):
         self.watchers = {}
         self.per_instance = per_instance
 
+    @classmethod
+    def serialize(cls, value):
+        "Given the parameter value, return a Python value suitable for serialization"
+        return value
+
+    @classmethod
+    def deserialize(cls, value):
+        "Given a serializable Python value, return a value that the parameter can be set to"
+        return value
+
+    def schema(self, safe=False, subset=None, mode='json'):
+        if serializer is None:
+            raise ImportError('Cannot import serializer.py needed to generate schema')
+        if mode not in  self._serializers:
+            raise KeyError('Mode %r not in available serialization formats %r'
+                           % (mode, list(self._serializers.keys())))
+        return self._serializers[mode].parameter_schema(self.__class__.__name__, self,
+                                                        safe=safe, subset=subset)
 
     @property
     def label(self):
@@ -757,12 +823,15 @@ class Parameter(object):
 
         super(Parameter, self).__setattr__(attribute, value)
 
-        if old is not NotImplemented:
-            event = Event(what=attribute,name=self.name,obj=None,cls=self.owner,old=old,new=value, type=None)
-            for watcher in self.watchers[attribute]:
-                self.owner.param._call_watcher(watcher, event)
-            if not self.owner.param._BATCH_WATCH:
-                self.owner.param._batch_call_watchers()
+        if old is NotImplemented:
+            return
+
+        event = Event(what=attribute,name=self.name, obj=None, cls=self.owner,
+                      old=old, new=value, type=None)
+        for watcher in self.watchers[attribute]:
+            self.owner.param._call_watcher(watcher, event)
+        if not self.owner.param._BATCH_WATCH:
+            self.owner.param._batch_call_watchers()
 
 
     def __get__(self,obj,objtype): # pylint: disable-msg=W0613
@@ -827,16 +896,17 @@ class Parameter(object):
         # Parameterized class)
         if self.constant or self.readonly:
             if self.readonly:
-                raise TypeError("Read-only parameter '%s' cannot be modified"%self.name)
-            elif obj is None:  #not obj
+                raise TypeError("Read-only parameter '%s' cannot be modified" % self.name)
+            elif obj is None:
                 _old = self.default
                 self.default = val
             elif not obj.initialized:
-                _old = obj.__dict__.get(self._internal_name,self.default)
+                _old = obj.__dict__.get(self._internal_name, self.default)
                 obj.__dict__[self._internal_name] = val
             else:
-                raise TypeError("Constant parameter '%s' cannot be modified"%self.name)
-
+                _old = obj.__dict__.get(self._internal_name, self.default)
+                if val is not _old:
+                    raise TypeError("Constant parameter '%s' cannot be modified"%self.name)
         else:
             if obj is None:
                 _old = self.default
@@ -848,15 +918,20 @@ class Parameter(object):
         self._post_setter(obj, val)
 
         if obj is None:
-            watchers = self.watchers.get("value",[])
+            watchers = self.watchers.get("value")
+        elif hasattr(obj, '_param_watchers') and self.name in obj._param_watchers:
+            watchers = obj._param_watchers[self.name].get('value')
+            if watchers is None:
+                watchers = self.watchers.get("value")
         else:
-            watchers = getattr(obj,"_param_watchers",{}).get(self.name,{}).get('value',self.watchers.get("value",[]))
+            watchers = None
 
-        event = Event(what='value',name=self.name,obj=obj,cls=self.owner,old=_old,new=val, type=None)
         obj = self.owner if obj is None else obj
-        if obj is None:
+        if obj is None or not watchers:
             return
 
+        event = Event(what='value',name=self.name, obj=obj, cls=self.owner,
+                      old=_old, new=val, type=None)
         for watcher in watchers:
             obj.param._call_watcher(watcher, event)
         if not obj.param._BATCH_WATCH:
@@ -886,8 +961,7 @@ class Parameter(object):
                                  % (type(self).__name__, self.name,
                                     self.owner.name, attrib_name))
         self.name = attrib_name
-
-        self._internal_name = "_%s_param_value"%attrib_name
+        self._internal_name = "_%s_param_value" % attrib_name
 
 
     def __getstate__(self):
@@ -986,10 +1060,10 @@ def as_uninitialized(fn):
     @wraps(fn)
     def override_initialization(self_,*args,**kw):
         parameterized_instance = self_.self
-        original_initialized=parameterized_instance.initialized
-        parameterized_instance.initialized=False
-        fn(parameterized_instance,*args,**kw)
-        parameterized_instance.initialized=original_initialized
+        original_initialized = parameterized_instance.initialized
+        parameterized_instance.initialized = False
+        fn(parameterized_instance, *args, **kw)
+        parameterized_instance.initialized = original_initialized
     return override_initialization
 
 
@@ -1062,7 +1136,7 @@ class Parameters(object):
     class or the instance as necessary.
     """
 
-    _disable_stubs = None # Flag used to disable stubs in the API1 tests
+    _disable_stubs = False # Flag used to disable stubs in the API1 tests
                           # None for no action, True to raise and False to warn.
 
     def __init__(self_, cls, self=None):
@@ -1072,15 +1146,52 @@ class Parameters(object):
         """
         self_.cls = cls
         self_.self = self
-        self_._BATCH_WATCH = False  # If true, Event and watcher objects are queued.
-        self_._TRIGGER = False
-        self_._events = []         # Queue of batched eventd
-        self_._watchers = []         # Queue of batched watchers
+
+    @property
+    def _BATCH_WATCH(self_):
+        return self_.self_or_cls._parameters_state['BATCH_WATCH']
+
+    @_BATCH_WATCH.setter
+    def _BATCH_WATCH(self_, value):
+        self_.self_or_cls._parameters_state['BATCH_WATCH'] = value
+
+    @property
+    def _TRIGGER(self_):
+        return self_.self_or_cls._parameters_state['TRIGGER']
+
+    @_TRIGGER.setter
+    def _TRIGGER(self_, value):
+        self_.self_or_cls._parameters_state['TRIGGER'] = value
+
+    @property
+    def _events(self_):
+        return self_.self_or_cls._parameters_state['events']
+
+    @_events.setter
+    def _events(self_, value):
+        self_.self_or_cls._parameters_state['events'] = value
+
+    @property
+    def _watchers(self_):
+        return self_.self_or_cls._parameters_state['watchers']
+
+    @_watchers.setter
+    def _watchers(self_, value):
+        self_.self_or_cls._parameters_state['watchers'] = value
 
     @property
     def self_or_cls(self_):
         return self_.cls if self_.self is None else self_.self
 
+    def __setstate__(self, state):
+        # Set old parameters state on Parameterized._parameters_state
+        self_or_cls = state.get('self', state.get('cls'))
+        for k in self_or_cls._parameters_state:
+            key = '_'+k
+            if key in state:
+                self_or_cls._parameters_state[k] = state.pop(key)
+        for k, v in state.items():
+            setattr(self, k, v)
 
     def __getitem__(self_, key):
         """
@@ -1089,7 +1200,7 @@ class Parameters(object):
         inst = self_.self
         parameters = self_.objects(False) if inst is None else inst.param.objects(False)
         p = parameters[key]
-        if (inst is not None and p.per_instance and
+        if (inst is not None and getattr(inst, 'initialized', False) and p.per_instance and
             not getattr(inst, '_disable_instance__params', False)):
             if key not in inst._instance__params:
                 try:
@@ -1100,7 +1211,7 @@ class Parameters(object):
                 except:
                     raise
                 finally:
-                    p.watchers = watchers
+                    p.watchers = {k: list(v) for k, v in watchers.items()}
                 p.owner = inst
                 inst._instance__params[key] = p
             else:
@@ -1171,7 +1282,7 @@ class Parameters(object):
         First, ensures that all Parameters with 'instantiate=True'
         (typically used for mutable Parameters) are copied directly
         into each object, to ensure that there is an independent copy
-        (to avoid suprising aliasing errors).  Then sets each of the
+        (to avoid surprising aliasing errors).  Then sets each of the
         keyword arguments, warning when any of them are not defined as
         parameters.
 
@@ -1185,21 +1296,21 @@ class Parameters(object):
         for class_ in classlist(type(self)):
             if not issubclass(class_, Parameterized):
                 continue
-            for (k,v) in class_.__dict__.items():
+            for (k, v) in class_.param._parameters.items():
                 # (avoid replacing name with the default of None)
-                if isinstance(v,Parameter) and v.instantiate and k!="name":
-                    params_to_instantiate[k]=v
+                if v.instantiate and k != "name":
+                    params_to_instantiate[k] = v
 
         for p in params_to_instantiate.values():
             self.param._instantiate_param(p)
 
         ## keyword arg setting
-        for name,val in params.items():
+        for name, val in params.items():
             desc = self.__class__.get_param_descriptor(name)[0] # pylint: disable-msg=E1101
             if not desc:
-                self.param.warning("Setting non-parameter attribute %s=%s using a mechanism intended only for parameters",name,val)
+                self.param.warning("Setting non-parameter attribute %s=%s using a mechanism intended only for parameters", name, val)
             # i.e. if not desc it's setting an attribute in __dict__, not a Parameter
-            setattr(self,name,val)
+            setattr(self, name, val)
 
     @classmethod
     def deprecate(cls, fn):
@@ -1232,14 +1343,14 @@ class Parameters(object):
 
 
     # CEBALERT: this is a bit ugly
-    def _instantiate_param(self_,param_obj,dict_=None,key=None):
+    def _instantiate_param(self_, param_obj, dict_=None, key=None):
         # deepcopy param_obj.default into self.__dict__ (or dict_ if supplied)
         # under the parameter's _internal_name (or key if supplied)
         self = self_.self
         dict_ = dict_ or self.__dict__
         key = key or param_obj._internal_name
-        param_key = (str(type(self)), param_obj.name)
         if shared_parameters._share:
+            param_key = (str(type(self)), param_obj.name)
             if param_key in shared_parameters._shared_cache:
                 new_object = shared_parameters._shared_cache[param_key]
             else:
@@ -1247,11 +1358,12 @@ class Parameters(object):
                 shared_parameters._shared_cache[param_key] = new_object
         else:
             new_object = copy.deepcopy(param_obj.default)
-        dict_[key]=new_object
 
-        if isinstance(new_object,Parameterized):
+        dict_[key] = new_object
+
+        if isinstance(new_object, Parameterized):
             global object_count
-            object_count+=1
+            object_count += 1
             # CB: writes over name given to the original object;
             # should it instead keep the same name?
             new_object.param._generate_name()
@@ -1309,18 +1421,6 @@ class Parameters(object):
         Includes Parameters from this class and its
         superclasses.
         """
-        if self_.self is not None and self_.self._instance__params:
-            self_.warning('The Parameterized instance has instance '
-                          'parameters created using new-style param '
-                          'APIs, which are incompatible with .params. '
-                          'Use the new more explicit APIs on the '
-                          '.param accessor to query parameter instances.'
-                          'To query all parameter instances use '
-                          '.param.objects with the option to return '
-                          'either class or instance parameter objects. '
-                          'Alternatively use .param[name] indexing to '
-                          'access a specific parameter object by name.')
-
         pdict = self_.objects(instance='existing')
         if parameter_name is None:
             return pdict
@@ -1350,6 +1450,13 @@ class Parameters(object):
                 raise ValueError("Invalid positional arguments for %s.set_param" %
                                  (self_or_cls.name))
 
+        trigger_params = [k for k in kwargs
+                          if ((k in self_.self_or_cls.param) and
+                              hasattr(self_.self_or_cls.param[k], '_autotrigger_value'))]
+
+        for tp in trigger_params:
+            self_.self_or_cls.param[tp]._mode = 'set'
+
         for (k, v) in kwargs.items():
             if k not in self_or_cls.param:
                 self_.self_or_cls.param._BATCH_WATCH = False
@@ -1364,6 +1471,11 @@ class Parameters(object):
         if not BATCH_WATCH:
             self_._batch_call_watchers()
 
+        for tp in trigger_params:
+            p = self_.self_or_cls.param[tp]
+            p._mode = 'reset'
+            setattr(self_or_cls, tp, p._autotrigger_reset_value)
+            p._mode = 'set-reset'
 
     def objects(self_, instance=True):
         """
@@ -1398,7 +1510,7 @@ class Parameters(object):
 
         if instance and self_.self is not None:
             if instance == 'existing':
-                if self_.self._instance__params:
+                if getattr(self_.self, 'initialized', False) and self_.self._instance__params:
                     return dict(pdict, **self_.self._instance__params)
                 return pdict
             else:
@@ -1410,8 +1522,15 @@ class Parameters(object):
         """
         Trigger watchers for the given set of parameter names. Watchers
         will be triggered whether or not the parameter values have
-        actually changed.
+        actually changed. As a special case, the value will actually be
+        changed for a Parameter of type Event, setting it to True so
+        that it is clear which Event parameter has been triggered.
         """
+        trigger_params = [p for p in self_.self_or_cls.param
+                          if hasattr(self_.self_or_cls.param[p], '_autotrigger_value')]
+        triggers = {p:self_.self_or_cls.param[p]._autotrigger_value
+                    for p in trigger_params if p in param_names}
+
         events = self_.self_or_cls.param._events
         watchers = self_.self_or_cls.param._watchers
         self_.self_or_cls.param._events  = []
@@ -1419,7 +1538,7 @@ class Parameters(object):
         param_values = dict(self_.get_param_values())
         params = {name: param_values[name] for name in param_names}
         self_.self_or_cls.param._TRIGGER = True
-        self_.set_param(**params)
+        self_.set_param(**dict(params, **triggers))
         self_.self_or_cls.param._TRIGGER = False
         self_.self_or_cls.param._events += events
         self_.self_or_cls.param._watchers += watchers
@@ -1436,6 +1555,23 @@ class Parameters(object):
         return Event(what=event.what, name=event.name, obj=event.obj, cls=event.cls,
                      old=event.old, new=event.new, type=event_type)
 
+    def _execute_watcher(self, watcher, events):
+        if watcher.mode == 'args':
+            args, kwargs = events, {}
+        else:
+            args, kwargs = (), {event.name: event.new for event in events}
+
+        if iscoroutinefunction(watcher.fn):
+            if async_executor is None:
+                raise RuntimeError("Could not execute %s coroutine function. "
+                                   "Please register a asynchronous executor on "
+                                   "param.parameterized.async_executor, which "
+                                   "schedules the function on an event loop." %
+                                   watcher.fn)
+            async_executor(partial(watcher.fn, *args, **kwargs))
+        else:
+            watcher.fn(*args, **kwargs)
+
     def _call_watcher(self_, watcher, event):
         """
         Invoke the given the watcher appropriately given a Event object.
@@ -1452,11 +1588,7 @@ class Parameters(object):
         else:
             event = self_._update_event_type(watcher, event, self_.self_or_cls.param._TRIGGER)
             with batch_watch(self_.self_or_cls, enable=watcher.queued, run=False):
-                if watcher.mode == 'args':
-                    watcher.fn(event)
-                else:
-                    watcher.fn(**{event.name: event.new})
-
+                self_._execute_watcher(watcher, (event,))
 
     def _batch_call_watchers(self_):
         """
@@ -1476,11 +1608,7 @@ class Parameters(object):
                           for name in watcher.parameter_names
                           if (name, watcher.what) in event_dict]
                 with batch_watch(self_.self_or_cls, enable=watcher.queued, run=False):
-                    if watcher.mode == 'args':
-                        watcher.fn(*events)
-                    else:
-                        watcher.fn(**{c.name:c.new for c in events})
-
+                    self_._execute_watcher(watcher, events)
 
     def set_dynamic_time_fn(self_,time_fn,sublistattr=None):
         """
@@ -1523,7 +1651,47 @@ class Parameters(object):
             for obj in sublist:
                 obj.param.set_dynamic_time_fn(time_fn,sublistattr)
 
-    def get_param_values(self_,onlychanged=False):
+    def serialize_parameters(self_, subset=None, mode='json'):
+        self_or_cls = self_.self_or_cls
+        if mode not in Parameter._serializers:
+            raise ValueError('Mode %r not in available serialization formats %r'
+                             % (mode, list(Parameter._serializers.keys())))
+        serializer = Parameter._serializers[mode]
+        return serializer.serialize_parameters(self_or_cls, subset=subset)
+
+    def serialize_value(self_, pname, mode='json'):
+        self_or_cls = self_.self_or_cls
+        if mode not in Parameter._serializers:
+            raise ValueError('Mode %r not in available serialization formats %r'
+                             % (mode, list(Parameter._serializers.keys())))
+        serializer = Parameter._serializers[mode]
+        return serializer.serialize_parameter_value(self_or_cls, pname)
+
+    def deserialize_parameters(self_, serialization, subset=None, mode='json'):
+        self_or_cls = self_.self_or_cls
+        serializer = Parameter._serializers[mode]
+        return serializer.deserialize_parameters(self_or_cls, serialization, subset=subset)
+
+    def deserialize_value(self_, pname, value, mode='json'):
+        self_or_cls = self_.self_or_cls
+        if mode not in Parameter._serializers:
+            raise ValueError('Mode %r not in available serialization formats %r'
+                             % (mode, list(Parameter._serializers.keys())))
+        serializer = Parameter._serializers[mode]
+        return serializer.deserialize_parameter_value(self_or_cls, pname, value)
+
+    def schema(self_, safe=False, subset=None, mode='json'):
+        """
+        Returns a schema for the parameters on this Parameterized object.
+        """
+        self_or_cls = self_.self_or_cls
+        if mode not in Parameter._serializers:
+            raise ValueError('Mode %r not in available serialization formats %r'
+                             % (mode, list(Parameter._serializers.keys())))
+        serializer = Parameter._serializers[mode]
+        return serializer.schema(self_or_cls, safe=safe, subset=subset)
+
+    def get_param_values(self_, onlychanged=False):
         """
         Return a list of name,value pairs for all Parameters of this
         object.
@@ -1538,11 +1706,11 @@ class Parameters(object):
         # (would need to distinguish instantiation of default from
         # user setting of value).
         vals = []
-        for name,val in self_or_cls.param.objects('existing').items():
+        for name, val in self_or_cls.param.objects('existing').items():
             value = self_or_cls.param.get_value_generator(name)
             # (this is pointless for cls)
-            if not onlychanged or not all_equal(value,val.default):
-                vals.append((name,value))
+            if not onlychanged or not all_equal(value, val.default):
+                vals.append((name, value))
 
         vals.sort(key=itemgetter(0))
         return vals
@@ -1884,12 +2052,20 @@ class ParameterizedMetaclass(type):
         # 'name' to '__name__'?)
         mcs.name = name
 
-        mcs.param = Parameters(mcs)
+        mcs._parameters_state = {
+            "BATCH_WATCH": False, # If true, Event and watcher objects are queued.
+            "TRIGGER": False,
+            "events": [], # Queue of batched events
+            "watchers": [] # Queue of batched watchers
+        }
+        mcs._param = Parameters(mcs)
 
         # All objects (with their names) of type Parameter that are
         # defined in this class
         parameters = [(n,o) for (n,o) in dict_.items()
-                      if isinstance(o,Parameter)]
+                      if isinstance(o, Parameter)]
+
+        mcs._param._parameters = dict(parameters)
 
         for param_name,param in parameters:
             mcs._initialize_parameter(param_name,param)
@@ -1961,15 +2137,18 @@ class ParameterizedMetaclass(type):
         # _ParameterizedMetaclass__abstract before running, but
         # the actual class object will have an attribute
         # _ClassName__abstract.  So, we have to mangle it ourselves at
-        # runtime.
+        # runtime. Mangling follows description in https://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references
         try:
-            return getattr(mcs,'_%s__abstract'%mcs.__name__)
+            return getattr(mcs,'_%s__abstract'%mcs.__name__.lstrip("_"))
         except AttributeError:
             return False
 
     abstract = property(__is_abstract)
 
+    def _get_param(mcs):
+        return mcs._param
 
+    param = property(_get_param)
 
     def __setattr__(mcs,attribute_name,value):
         """
@@ -2321,18 +2500,21 @@ class Parameterized(object):
     see documentation for the 'logging' module.
     """
 
-    name           = String(default=None,constant=True,doc="""
-    String identifier for this object.""")
-
+    name = String(default=None, constant=True, doc="""
+        String identifier for this object.""")
 
-    def __init__(self,**params):
+    def __init__(self, **params):
         global object_count
 
         # Flag that can be tested to see if e.g. constant Parameters
         # can still be set
-        self.initialized=False
-        # Override class level param namespace with instance namespace
-        self.param = Parameters(self.__class__, self=self)
+        self.initialized = False
+        self._parameters_state = {
+            "BATCH_WATCH": False, # If true, Event and watcher objects are queued.
+            "TRIGGER": False,
+            "events": [], # Queue of batched events
+            "watchers": [] # Queue of batched watchers
+        }
         self._instance__params = {}
         self._param_watchers = {}
 
@@ -2349,11 +2531,19 @@ class Parameterized(object):
                 # instantiation of Parameterized with watched deps. Will
                 # probably store expanded deps on class - see metaclass
                 # 'dependers'.
-                for p in self.param.params_depended_on(n):
+                grouped = defaultdict(list)
+                for dep in self.param.params_depended_on(n):
+                    grouped[(id(dep.inst),id(dep.cls),dep.what)].append(dep)
+                for group in grouped.values():
                     # TODO: can't remember why not just pass m (rather than _m_caller) here
-                    (p.inst or p.cls).param.watch(_m_caller(self, n), p.name, p.what, queued=queued)
+                    gdep = group[0] # Need to grab representative dep from this group
+                    (gdep.inst or gdep.cls).param.watch(_m_caller(self, n), [d.name for d in group], gdep.what, queued=queued)
 
-        self.initialized=True
+        self.initialized = True
+
+    @property
+    def param(self):
+        return Parameters(self.__class__, self=self)
 
     # 'Special' methods
 
@@ -2365,7 +2555,6 @@ class Parameterized(object):
         """
         # remind me, why is it a copy? why not just state.update(self.__dict__)?
         state = self.__dict__.copy()
-
         for slot in get_occupied_slots(self):
             state[slot] = getattr(self,slot)
 
@@ -2386,10 +2575,30 @@ class Parameterized(object):
         """
         self.initialized=False
 
+        # When making a copy the internal watchers have to be
+        # recreated and point to the new instance
+        if '_param_watchers' in state:
+            param_watchers = state['_param_watchers']
+            for p, attrs in param_watchers.items():
+                for attr, watchers in attrs.items():
+                    new_watchers = []
+                    for watcher in watchers:
+                        watcher_args = list(watcher)
+                        if watcher.inst is not None:
+                            watcher_args[0] = self
+                        fn = watcher.fn
+                        if hasattr(fn, '_watcher_name'):
+                            watcher_args[2] = _m_caller(self, fn._watcher_name)
+                        elif get_method_owner(fn) is watcher.inst:
+                            watcher_args[2] = getattr(self, fn.__name__)
+                        new_watchers.append(Watcher(*watcher_args))
+                    param_watchers[p][attr] = new_watchers
+
         if '_instance__params' not in state:
             state['_instance__params'] = {}
         if '_param_watchers' not in state:
             state['_param_watchers'] = {}
+        state.pop('param', None)
 
         for name,value in state.items():
             setattr(self,name,value)
@@ -2794,7 +3003,7 @@ class ParameterizedFunction(Parameterized):
             cls = self_or_cls
         else:
             p = params
-            params = dict(self_or_cls.get_param_values())
+            params = dict(self_or_cls.param.get_param_values())
             params.update(p)
             params.pop('name')
             cls = self_or_cls.__class__
diff --git a/param/serializer.py b/param/serializer.py
new file mode 100644
index 0000000..f9d3395
--- /dev/null
+++ b/param/serializer.py
@@ -0,0 +1,299 @@
+"""
+Classes used to support string serialization of Parameters and
+Parameterized objects.
+"""
+
+import json
+
+class UnserializableException(Exception):
+    pass
+
+class UnsafeserializableException(Exception):
+    pass
+
+def JSONNullable(json_type):
+    "Express a JSON schema type as nullable to easily support Parameters that allow_None"
+    return { "anyOf": [ json_type, { "type": "null"}] }
+
+
+
+class Serialization(object):
+    """
+    Base class used to implement different types of serialization.
+    """
+
+    @classmethod
+    def schema(cls, pobj, subset=None):
+        raise NotImplementedError        # noqa: unimplemented method
+
+    @classmethod
+    def serialize_parameters(cls, pobj, subset=None):
+        """
+        Serialize the parameters on a Parameterized object into a
+        single serialized object, e.g. a JSON string.
+        """
+        raise NotImplementedError        # noqa: unimplemented method
+
+    @classmethod
+    def deserialize_parameters(cls, pobj, serialized, subset=None):
+        """
+        Deserialize a serialized object representing one or
+        more Parameters into a dictionary of parameter values.
+        """
+        raise NotImplementedError        # noqa: unimplemented method
+
+    @classmethod
+    def serialize_parameter_value(cls, pobj, pname):
+        """
+        Serialize a single parameter value.
+        """
+        raise NotImplementedError        # noqa: unimplemented method
+
+    @classmethod
+    def deserialize_parameter_value(cls, pobj, pname, value):
+        """
+        Deserialize a single parameter value.
+        """
+        raise NotImplementedError        # noqa: unimplemented method
+
+
+class JSONSerialization(Serialization):
+    """
+    Class responsible for specifying JSON serialization, deserialization
+    and JSON schemas for Parameters and Parameterized classes and
+    objects.
+    """
+
+    unserializable_parameter_types = ['Callable']
+
+    json_schema_literal_types = {int:'integer', float:'number', str:'string',
+                                 type(None):'null'}
+
+    @classmethod
+    def loads(cls, serialized):
+        return json.loads(serialized)
+
+    @classmethod
+    def dumps(cls, obj):
+        return json.dumps(obj)
+
+    @classmethod
+    def schema(cls, pobj, safe=False, subset=None):
+        schema = {}
+        for name, p in pobj.param.objects('existing').items():
+            if subset is not None and name not in subset:
+                continue
+            schema[name] = p.schema(safe=safe)
+            if p.doc:
+                schema[name]["description"] = p.doc.strip()
+            if p.label:
+                schema[name]["title"] = p.label
+        return schema
+
+    @classmethod
+    def serialize_parameters(cls, pobj, subset=None):
+        components = {}
+        for name, p in pobj.param.objects('existing').items():
+            if subset is not None and name not in subset:
+                continue
+            value = pobj.param.get_value_generator(name)
+            components[name] = p.serialize(value)
+        return cls.dumps(components)
+
+    @classmethod
+    def deserialize_parameters(cls, pobj, serialization, subset=None):
+        deserialized = cls.loads(serialization)
+        components = {}
+        for name, value in deserialized.items():
+            if subset is not None and name not in subset:
+                continue
+            deserialized = pobj.param[name].deserialize(value)
+            components[name] = deserialized
+        return components
+
+    # Parameter level methods
+
+    @classmethod
+    def _get_method(cls, ptype, suffix):
+        "Returns specialized method if available, otherwise None"
+        method_name = ptype.lower()+'_' + suffix
+        return getattr(cls, method_name, None)
+
+    @classmethod
+    def parameter_schema(cls, ptype, p, safe=False, subset=None):
+        if ptype in cls.unserializable_parameter_types:
+            raise UnserializableException
+        dispatch_method = cls._get_method(ptype, 'schema')
+        if dispatch_method:
+            schema = dispatch_method(p, safe=safe)
+        else:
+            schema = { "type": ptype.lower()}
+
+        return JSONNullable(schema) if p.allow_None else schema
+
+    @classmethod
+    def serialize_parameter_value(cls, pobj, pname):
+        value = pobj.param.get_value_generator(pname)
+        return cls.dumps(pobj.param[pname].serialize(value))
+
+    @classmethod
+    def deserialize_parameter_value(cls, pobj, pname, value):
+        value = cls.loads(value)
+        return pobj.param[pname].deserialize(value)
+
+    # Custom Schemas
+
+    @classmethod
+    def array_schema(cls, p, safe=False):
+        if safe is True:
+            msg = ('Array is not guaranteed to be safe for '
+                   'serialization as the dtype is unknown')
+            raise UnsafeserializableException(msg)
+        return { "type": "array"}
+
+    @classmethod
+    def dict_schema(cls, p, safe=False):
+        if safe is True:
+            msg = ('Dict is not guaranteed to be safe for '
+                   'serialization as the key and value types are unknown')
+            raise UnsafeserializableException(msg)
+        return { "type": "object"}
+
+    @classmethod
+    def date_schema(cls, p, safe=False):
+        return { "type": "string", "format": "date-time"}
+
+    @classmethod
+    def calendardate_schema(cls, p, safe=False):
+        return { "type": "string", "format": "date"}
+
+    @classmethod
+    def tuple_schema(cls, p, safe=False):
+        schema = { "type": "array"}
+        if p.length is not None:
+            schema['minItems'] =  p.length
+            schema['maxItems'] =  p.length
+        return schema
+
+    @classmethod
+    def number_schema(cls, p, safe=False):
+        schema = { "type": p.__class__.__name__.lower() }
+        return cls.declare_numeric_bounds(schema, p.bounds, p.inclusive_bounds)
+
+    @classmethod
+    def declare_numeric_bounds(cls, schema, bounds, inclusive_bounds):
+        "Given an applicable numeric schema, augment with bounds information"
+        if bounds is not None:
+            (low, high) = bounds
+            if low is not None:
+                key = 'minimum' if inclusive_bounds[0] else 'exclusiveMinimum'
+                schema[key] = low
+            if high is not None:
+                key = 'maximum' if inclusive_bounds[1] else 'exclusiveMaximum'
+                schema[key] = high
+        return schema
+
+    @classmethod
+    def integer_schema(cls, p, safe=False):
+        return cls.number_schema(p)
+
+    @classmethod
+    def numerictuple_schema(cls, p, safe=False):
+        schema = cls.tuple_schema(p, safe=safe)
+        schema["additionalItems"] = { "type": "number" }
+        return schema
+
+    @classmethod
+    def xycoordinates_schema(cls, p, safe=False):
+        return cls.numerictuple_schema(p, safe=safe)
+
+    @classmethod
+    def range_schema(cls, p, safe=False):
+        schema =  cls.tuple_schema(p, safe=safe)
+        bounded_number = cls.declare_numeric_bounds({ "type": "number" },
+                                                    p.bounds, p.inclusive_bounds)
+        schema["additionalItems"] = bounded_number
+        return schema
+
+    @classmethod
+    def list_schema(cls, p, safe=False):
+        schema =  { "type": "array"}
+        if safe is True and p.class_ is None:
+            msg = ('List without a class specified cannot be guaranteed '
+                   'to be safe for serialization')
+            raise UnsafeserializableException(msg)
+        if p.class_ is not None and p.class_ in cls.json_schema_literal_types:
+            schema['items'] = {"type": cls.json_schema_literal_types[p.class_]}
+        return schema
+
+    @classmethod
+    def objectselector_schema(cls, p, safe=False):
+        try:
+            allowed_types = [{'type': cls.json_schema_literal_types[type(obj)]}
+                             for obj in p.objects]
+            schema =  { "anyOf": allowed_types}
+            schema['enum'] = p.objects
+            return schema
+        except:
+            if safe is True:
+                msg = ('ObjectSelector cannot be guaranteed to be safe for '
+                       'serialization due to unserializable type in objects')
+                raise UnsafeserializableException(msg)
+            return {}
+
+    @classmethod
+    def listselector_schema(cls, p, safe=False):
+        if p.objects is None:
+            if safe is True:
+                msg = ('ListSelector cannot be guaranteed to be safe for '
+                       'serialization as allowed objects unspecified')
+            return {'type': 'array'}
+        for obj in p.objects:
+            if type(obj) not in cls.json_schema_literal_types:
+                msg = "ListSelector cannot serialize type %s" % type(obj)
+                raise UnserializableException(msg)
+        return {'type': 'array', 'items':{'enum':p.objects}}
+
+
+    @classmethod
+    def dataframe_schema(cls, p, safe=False):
+        schema = {'type': 'array'}
+        if safe is True:
+            msg = ('DataFrame is not guaranteed to be safe for '
+                   'serialization as the column dtypes are unknown')
+            raise UnsafeserializableException(msg)
+        if p.columns is None:
+            schema['items'] = {'type': 'object'}
+            return schema
+
+        mincols, maxcols = None, None
+        if isinstance(p.columns, int):
+            mincols, maxcols = p.columns, p.columns
+        elif isinstance(p.columns, tuple):
+            mincols, maxcols = p.columns
+
+        if isinstance(p.columns, int) or isinstance(p.columns, tuple):
+            schema['items'] =  {'type': 'object',
+                                'minItems': mincols,
+                                'maxItems': maxcols}
+
+        if isinstance(p.columns, list) or isinstance(p.columns, set):
+            literal_types = [{'type':el} for el in cls.json_schema_literal_types.values()]
+            allowable_types = {"anyOf": literal_types}
+            properties = {name: allowable_types for name in p.columns}
+            schema['items'] =  {'type': 'object',
+                                'properties' : properties }
+
+
+        minrows, maxrows = None, None
+        if isinstance(p.rows, int):
+            minrows, maxrows = p.rows, p.rows
+        elif isinstance(p.rows, tuple):
+            minrows, maxrows = p.rows
+
+        if minrows is not None:
+            schema['minItems'] = minrows
+        if maxrows is not None:
+            schema['maxItems'] = maxrows
+
+        return schema
diff --git a/param/version.py b/param/version.py
index 756d7dd..f81f491 100644
--- a/param/version.py
+++ b/param/version.py
@@ -2,7 +2,7 @@
 Provide consistent and up-to-date ``__version__`` strings for
 Python packages.
 
-See https://github.com/pyviz/autover for more information.
+See https://github.com/holoviz/autover for more information.
 """
 
 # The Version class is a copy of autover.version.Version v0.2.5,
@@ -26,7 +26,7 @@ def run_cmd(args, cwd=None):
                             cwd=cwd)
     output, error = (str(s.decode()).strip() for s in proc.communicate())
 
-    if proc.returncode != 0:
+    if proc.returncode != 0 or len(error) > 0:
         raise Exception(proc.returncode, error)
     return output
 
diff --git a/setup.cfg b/setup.cfg
index 04c3502..18e650b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,23 +2,30 @@
 universal = 1
 
 [flake8]
-# TODO tests (one day)
+# TODO tests should not be excluded (one day...)
 include = setup.py param numbergen
-exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,tests
-ignore = E1,
+exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,tests,.ipynb_checkpoints,run_test.py
+ignore = E114,
+         E116,
+         E126,
+         E128,
+         E129,
          E2,
          E3,
          E4,
          E5,
+         E731,
          E701,
          E702,
          E703,
-         E704,   
+         E704,
          E722,
          E741,
          E742,
          E743,
-         W503
+         W503,
+         W504,
+         W605
 
 [nosetests]
 verbosity = 2
diff --git a/setup.py b/setup.py
index 021dbce..26ea37e 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ extras_require = {
     # (https://github.com/pypa/pip/issues/1197)
     'tests': [
         'nose',
-        'flake8'
+        'flake8',
     ]
 }
 
@@ -32,15 +32,15 @@ extras_require['all'] = sorted(set(sum(extras_require.values(), [])))
 setup_args = dict(
     name='param',
     version=get_setup_version("param"),
-    description='Declarative Python programming using Parameters.',
+    description='Make your Python code clearer and more reliable by declaring Parameters.',
     long_description=open('README.rst').read() if os.path.isfile('README.rst') else 'Consult README.rst',
-    author="IOAM",
-    author_email="developers@topographica.org",
-    maintainer="IOAM",
-    maintainer_email="developers@topographica.org",
+    author="HoloViz",
+    author_email="developers@holoviz.org",
+    maintainer="HoloViz",
+    maintainer_email="developers@holoviz.org",
     platforms=['Windows', 'Mac OS X', 'Linux'],
     license='BSD',
-    url='http://ioam.github.com/param/',
+    url='http://param.holoviz.org/',
     packages=["param","numbergen"],
     provides=["param","numbergen"],
     include_package_data = True,
@@ -48,6 +48,13 @@ setup_args = dict(
     install_requires=[],
     extras_require=extras_require,
     tests_require=extras_require['tests'],
+    project_urls={
+        "Documentation": "https://param.holoviz.org/",
+        "Releases": "https://github.com/holoviz/param/releases",
+        "Bug Tracker": "https://github.com/holoviz/param/issues",
+        "Source Code": "https://github.com/holoviz/param",
+        "Panel Examples": "https://panel.holoviz.org/user_guide/Param.html",
+    },
     classifiers=[
         "License :: OSI Approved :: BSD License",
         "Development Status :: 5 - Production/Stable",
@@ -57,6 +64,8 @@ setup_args = dict(
         "Programming Language :: Python :: 3.4",
         "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
         "Operating System :: OS Independent",
         "Intended Audience :: Science/Research",
         "Intended Audience :: Developers",
diff --git a/tests/API0/testparameterizedobject.py b/tests/API0/testparameterizedobject.py
index d045b07..18bb493 100644
--- a/tests/API0/testparameterizedobject.py
+++ b/tests/API0/testparameterizedobject.py
@@ -39,6 +39,10 @@ class AnotherTestPO(param.Parameterized):
 class TestAbstractPO(param.Parameterized):
     __abstract = True
 
+class _AnotherAbstractPO(param.Parameterized):
+    __abstract = True
+
+
 @nottest
 class TestParamInstantiation(AnotherTestPO):
     instPO = param.Parameter(default=AnotherTestPO(),instantiate=False)
@@ -118,6 +122,7 @@ class TestParameterized(unittest.TestCase):
     def test_abstract_class(self):
         """Check that a class declared abstract actually shows up as abstract."""
         self.assertEqual(TestAbstractPO.abstract,True)
+        self.assertEqual(_AnotherAbstractPO.abstract,True)
         self.assertEqual(TestPO.abstract,False)
 
 
diff --git a/tests/API0/testtimedependent.py b/tests/API0/testtimedependent.py
index 6a7f707..9e79fe9 100644
--- a/tests/API0/testtimedependent.py
+++ b/tests/API0/testtimedependent.py
@@ -12,9 +12,12 @@ import fractions
 
 try:
     import gmpy
-except:
-    gmpy = None
-
+except ImportError:
+    import os
+    if os.getenv('PARAM_TEST_GMPY','0') == '1':
+        raise ImportError("PARAM_TEST_GMPY=1 but gmpy not available.")
+    else:
+        gmpy = None
 
 
 class TestTimeClass(unittest.TestCase):
diff --git a/tests/API1/testjsonserialization.py b/tests/API1/testjsonserialization.py
new file mode 100644
index 0000000..22636fe
--- /dev/null
+++ b/tests/API1/testjsonserialization.py
@@ -0,0 +1,266 @@
+"""
+Testing JSON serialization of parameters and the corresponding schemas.
+"""
+
+import datetime
+import json
+
+import param
+
+from unittest import SkipTest, skipIf
+from . import API1TestCase
+
+try:
+    from jsonschema import validate, ValidationError
+except ImportError:
+    import os
+    if os.getenv('PARAM_TEST_JSONSCHEMA','0') == '1':
+        raise ImportError("PARAM_TEST_JSONSCHEMA=1 but jsonschema not available.")
+    validate = None
+
+try:
+    import numpy as np
+    ndarray = np.array([[1,2,3],[4,5,6]])
+except:
+    np, ndarray = None, None
+
+np_skip = skipIf(np is None, "NumPy is not available")
+
+try:
+    import pandas as pd
+    df1 = pd.DataFrame({'A':[1,2,3], 'B':[1.1,2.2,3.3]})
+    df2 = pd.DataFrame({'A':[1.1,2.2,3.3], 'B':[1.1,2.2,3.3]})
+except:
+    pd, df1, df2 = None, None, None
+
+pd_skip = skipIf(pd is None, "pandas is not available")
+
+simple_list = [1]
+
+class TestSet(param.Parameterized):
+
+    numpy_params = ['r']
+    pandas_params = ['s','t','u']
+    conditionally_unsafe = ['f', 'o']
+
+    a = param.Integer(default=5, doc='Example doc', bounds=(2,30), inclusive_bounds=(True, False))
+    b = param.Number(default=4.3, allow_None=True)
+    c = param.String(default='foo')
+    d = param.Boolean(default=False)
+    e = param.List([1,2,3], class_=int)
+    f = param.List([1,2,3])
+    g = param.Date(default=datetime.datetime.now())
+    h = param.Tuple(default=(1,2,3), length=3)
+    i = param.NumericTuple(default=(1,2,3,4))
+    j = param.XYCoordinates(default=(32.1, 51.5))
+    k = param.Integer(default=1)
+    l = param.Range(default=(1.1,2.3), bounds=(1,3))
+    m = param.String(default='baz', allow_None=True)
+    n = param.ObjectSelector(default=3, objects=[3,'foo'], allow_None=False)
+    o = param.ObjectSelector(default=simple_list, objects=[simple_list], allow_None=False)
+    p = param.ListSelector(default=[1,4,5], objects=[1,2,3,4,5,6])
+    q = param.CalendarDate(default=datetime.date.today())
+    r = None if np is None else param.Array(default=ndarray)
+    s = None if pd is None else param.DataFrame(default=df1, columns=2)
+    t = None if pd is None else param.DataFrame(default=pd.DataFrame(
+        {'A':[1,2,3], 'B':[1.1,2.2,3.3]}), columns=(1,4), rows=(2,5))
+    u = None if pd is None else param.DataFrame(default=df2, columns=['A', 'B'])
+    v = param.Dict({'1':2})
+
+
+test = TestSet(a=29)
+
+
+class TestSerialization(API1TestCase):
+    """
+    Base class for testing serialization of Parameter values
+    """
+
+    mode = None
+
+    __test__ = False
+
+    def _test_serialize(self, obj, pname):
+        serialized = obj.param.serialize_value(pname, mode=self.mode)
+        deserialized = obj.param.deserialize_value(pname, serialized, mode=self.mode)
+        self.assertEqual(deserialized, getattr(obj, pname))
+
+    def test_serialize_integer_class(self):
+        self._test_serialize(TestSet, 'a')
+
+    def test_serialize_integer_instance(self):
+        self._test_serialize(test, 'a')
+
+    def test_serialize_number_class(self):
+        self._test_serialize(TestSet, 'b')
+
+    def test_serialize_number_instance(self):
+        self._test_serialize(test, 'b')
+
+    def test_serialize_string_class(self):
+        self._test_serialize(TestSet, 'c')
+
+    def test_serialize_string_instance(self):
+        self._test_serialize(test, 'c')
+
+    def test_serialize_boolean_class(self):
+        self._test_serialize(TestSet, 'd')
+
+    def test_serialize_boolean_instance(self):
+        self._test_serialize(test, 'd')
+
+    def test_serialize_list_class(self):
+        self._test_serialize(TestSet, 'e')
+
+    def test_serialize_list_instance(self):
+        self._test_serialize(test, 'e')
+
+    def test_serialize_date_class(self):
+        self._test_serialize(TestSet, 'g')
+
+    def test_serialize_date_instance(self):
+        self._test_serialize(test, 'g')
+
+    def test_serialize_tuple_class(self):
+        self._test_serialize(TestSet, 'h')
+
+    def test_serialize_tuple_instance(self):
+        self._test_serialize(test, 'h')
+
+    def test_serialize_calendar_date_class(self):
+        self._test_serialize(TestSet, 'q')
+
+    def test_serialize_calendar_date_instance(self):
+        self._test_serialize(test, 'q')
+
+    @np_skip
+    def test_serialize_array_class(self):
+        serialized = TestSet.param.serialize_value('r', mode=self.mode)
+        deserialized = TestSet.param.deserialize_value('r', serialized, mode=self.mode)
+        self.assertTrue(np.array_equal(deserialized, getattr(TestSet, 'r')))
+
+    @np_skip
+    def test_serialize_array_instance(self):
+        serialized = test.param.serialize_value('r', mode=self.mode)
+        deserialized = test.param.deserialize_value('r', serialized, mode=self.mode)
+        self.assertTrue(np.array_equal(deserialized, getattr(test, 'r')))
+
+    @pd_skip
+    def test_serialize_dataframe_class(self):
+        serialized = TestSet.param.serialize_value('s', mode=self.mode)
+        deserialized = TestSet.param.deserialize_value('s', serialized, mode=self.mode)
+        self.assertTrue(getattr(TestSet, 's').equals(deserialized))
+
+    @pd_skip
+    def test_serialize_dataframe_instance(self):
+        serialized = test.param.serialize_value('s', mode=self.mode)
+        deserialized = test.param.deserialize_value('s', serialized, mode=self.mode)
+        self.assertTrue(getattr(test, 's').equals(deserialized))
+
+    def test_serialize_dict_class(self):
+        self._test_serialize(TestSet, 'v')
+
+    def test_serialize_dict_instance(self):
+        self._test_serialize(test, 'v')
+
+    def test_instance_serialization(self):
+        parameters = [p for p in test.param if p not in test.numpy_params + test.pandas_params]
+        serialized = test.param.serialize_parameters(subset=parameters, mode=self.mode)
+        deserialized = TestSet.param.deserialize_parameters(serialized, mode=self.mode)
+        for pname in parameters:
+            self.assertEqual(deserialized[pname], getattr(test, pname))
+
+    @np_skip
+    def test_numpy_instance_serialization(self):
+        serialized = test.param.serialize_parameters(subset=test.numpy_params, mode=self.mode)
+        deserialized = TestSet.param.deserialize_parameters(serialized, mode=self.mode)
+        for pname in test.numpy_params:
+            self.assertTrue(np.array_equal(deserialized[pname], getattr(test, pname)))
+
+    @pd_skip
+    def test_pandas_instance_serialization(self):
+        serialized = test.param.serialize_parameters(subset=test.pandas_params, mode=self.mode)
+        deserialized = TestSet.param.deserialize_parameters(serialized, mode=self.mode)
+        for pname in test.pandas_params:
+            self.assertTrue(getattr(test, pname).equals(deserialized[pname]))
+
+
+
+class TestJSONSerialization(TestSerialization):
+
+    mode = 'json'
+
+    __test__ = True
+
+
+class TestJSONSchema(API1TestCase):
+
+    def test_serialize_integer_schema_class(self):
+        if validate is None:
+            raise SkipTest('jsonschema needed for schema validation testing')
+        param_schema = TestSet.param.schema(safe=True, subset=['a'], mode='json')
+        schema = {"type" : "object", "properties" : param_schema}
+        serialized = json.loads(TestSet.param.serialize_parameters(subset=['a']))
+        self.assertEqual({'a':
+                          {'type': 'integer', 'minimum': 2, 'exclusiveMaximum': 30,
+                           'description': 'Example doc', 'title': 'A'}},
+                         param_schema)
+        validate(instance=serialized, schema=schema)
+
+    def test_serialize_integer_schema_class_invalid(self):
+        if validate is None:
+            raise SkipTest('jsonschema needed for schema validation testing')
+        param_schema = TestSet.param.schema(safe=True, subset=['a'], mode='json')
+        schema = {"type" : "object", "properties" : param_schema}
+        self.assertEqual({'a':
+                          {'type': 'integer', 'minimum': 2, 'exclusiveMaximum': 30,
+                           'description': 'Example doc', 'title': 'A'}},
+                         param_schema)
+
+        exception = "1 is not of type 'object'"
+        with self.assertRaisesRegexp(ValidationError, exception):
+            validate(instance=1, schema=schema)
+
+    def test_serialize_integer_schema_instance(self):
+        if validate is None:
+            raise SkipTest('jsonschema needed for schema validation testing')
+        param_schema = test.param.schema(safe=True, subset=['a'], mode='json')
+        schema = {"type" : "object", "properties" : param_schema}
+        serialized = json.loads(test.param.serialize_parameters(subset=['a']))
+        self.assertEqual({'a':
+                          {'type': 'integer', 'minimum': 2, 'exclusiveMaximum': 30,
+                           'description': 'Example doc', 'title': 'A'}},
+                         param_schema)
+        validate(instance=serialized, schema=schema)
+
+    @np_skip
+    def test_numpy_schemas_always_unsafe(self):
+        for param_name in test.numpy_params:
+            with self.assertRaisesRegexp(param.serializer.UnsafeserializableException,''):
+                test.param.schema(safe=True, subset=[param_name], mode='json')
+
+    @pd_skip
+    def test_pandas_schemas_always_unsafe(self):
+        for param_name in test.pandas_params:
+            with self.assertRaisesRegexp(param.serializer.UnsafeserializableException,''):
+                test.param.schema(safe=True, subset=[param_name], mode='json')
+
+    def test_class_instance_schemas_match_and_validate_unsafe(self):
+        if validate is None:
+            raise SkipTest('jsonschema needed for schema validation testing')
+
+        for param_name in list(test.param):
+            class_schema = TestSet.param.schema(safe=False, subset=[param_name], mode='json')
+            instance_schema = test.param.schema(safe=False, subset=[param_name], mode='json')
+            self.assertEqual(class_schema, instance_schema)
+
+            instance_serialization_val = test.param.serialize_parameters(subset=[param_name])
+            validate(instance=instance_serialization_val, schema=class_schema)
+
+            class_serialization_val = TestSet.param.serialize_parameters(subset=[param_name])
+            validate(instance=class_serialization_val, schema=class_schema)
+
+    def test_conditionally_unsafe(self):
+        for param_name in test.conditionally_unsafe:
+            with self.assertRaisesRegexp(param.serializer.UnsafeserializableException,''):
+                test.param.schema(safe=True, subset=[param_name], mode='json')
diff --git a/tests/API1/testparameterizedobject.py b/tests/API1/testparameterizedobject.py
index 2362f7e..a08524d 100644
--- a/tests/API1/testparameterizedobject.py
+++ b/tests/API1/testparameterizedobject.py
@@ -53,6 +53,9 @@ class AnotherTestPO(param.Parameterized):
 class TestAbstractPO(param.Parameterized):
     __abstract = True
 
+class _AnotherAbstractPO(param.Parameterized):
+    __abstract = True
+
 @nottest
 class TestParamInstantiation(AnotherTestPO):
     instPO = param.Parameter(default=AnotherTestPO(),instantiate=False)
@@ -140,6 +143,7 @@ class TestParameterized(API1TestCase):
     def test_abstract_class(self):
         """Check that a class declared abstract actually shows up as abstract."""
         self.assertEqual(TestAbstractPO.abstract,True)
+        self.assertEqual(_AnotherAbstractPO.abstract,True)
         self.assertEqual(TestPO.abstract,False)
 
 
@@ -233,8 +237,9 @@ class TestParameterized(API1TestCase):
         inst.param['inst']
 
         inst.param.params()
-        self.log_handler.assertContains(
-            'WARNING', 'The Parameterized instance has instance parameters')
+        if param.parameterized.Parameters._disable_stubs is None:
+            self.log_handler.assertContains(
+                'WARNING', 'The Parameterized instance has instance parameters')
 
 
     def test_instance_param_getitem(self):
diff --git a/tests/API1/testtimedependent.py b/tests/API1/testtimedependent.py
index f1b8612..210a924 100644
--- a/tests/API1/testtimedependent.py
+++ b/tests/API1/testtimedependent.py
@@ -12,9 +12,12 @@ import fractions
 
 try:
     import gmpy
-except:
-    gmpy = None
-
+except ImportError:
+    import os
+    if os.getenv('PARAM_TEST_GMPY','0') == '1':
+        raise ImportError("PARAM_TEST_GMPY=1 but gmpy not available.")
+    else:
+        gmpy = None
 
 
 class TestTimeClass(API1TestCase):
diff --git a/tests/API1/testwatch.py b/tests/API1/testwatch.py
index 09ec8a1..107aaf6 100644
--- a/tests/API1/testwatch.py
+++ b/tests/API1/testwatch.py
@@ -1,14 +1,15 @@
 """
 Unit test for watch mechanism
 """
-from . import API1TestCase
-
-from .utils import MockLoggingHandler
+import copy
 
 import param
 
 from param.parameterized import discard_events
 
+from . import API1TestCase
+from .utils import MockLoggingHandler
+
 
 class Accumulator(object):
 
@@ -36,6 +37,11 @@ class SimpleWatchExample(param.Parameterized):
     b = param.Parameter(default=0)
     c = param.Parameter(default=0)
     d = param.Integer(default=0)
+    e = param.Event()
+    f = param.Event()
+
+    def method(self, event):
+        self.b = self.a * 2
 
 
 class SimpleWatchSubclass(SimpleWatchExample):
@@ -62,13 +68,21 @@ class WatchMethodExample(SimpleWatchSubclass):
     def _set_d_bounds(self):
         self.param.d.bounds = (self.c, self.c*2)
 
+    @param.depends('e', watch=True)
+    def _e_event_triggered(self):
+        assert self.e is True
+        self.d = 30
+
+    @param.depends('f', watch=True)
+    def _f_event_triggered(self):
+        assert self.f is True
+        self.b = 420
 
 class WatchSubclassExample(WatchMethodExample):
 
     pass
 
 
-
 class TestWatch(API1TestCase):
 
     @classmethod
@@ -78,7 +92,6 @@ class TestWatch(API1TestCase):
         cls.log_handler = MockLoggingHandler(level='DEBUG')
         log.addHandler(cls.log_handler)
 
-
     def setUp(self):
         super(TestWatch, self).setUp()
         self.accumulator = 0
@@ -94,7 +107,6 @@ class TestWatch(API1TestCase):
         obj.a = 2
         self.assertEqual(self.accumulator, 3)
 
-
     def test_discard_events_decorator(self):
         def accumulator(change):
             self.accumulator += change.new
@@ -105,8 +117,7 @@ class TestWatch(API1TestCase):
             obj.a = 1
         self.assertEqual(self.accumulator, 0)
         obj.a = 2
-        self.assertEqual(self.accumulator, 3)
-
+        self.assertEqual(self.accumulator, 2)
 
     def test_triggered_when_changed_iterator_type(self):
         def accumulator(change):
@@ -119,7 +130,6 @@ class TestWatch(API1TestCase):
         obj.a = tuple()
         self.assertEqual(self.accumulator, tuple())
 
-
     def test_triggered_when_changed_mapping_type(self):
         def accumulator(change):
             self.accumulator = change.new
@@ -131,7 +141,6 @@ class TestWatch(API1TestCase):
         obj.a = {}
         self.assertEqual(self.accumulator, {})
 
-
     def test_untriggered_when_unchanged(self):
         def accumulator(change):
             self.accumulator += change.new
@@ -143,7 +152,6 @@ class TestWatch(API1TestCase):
         obj.a = 1
         self.assertEqual(self.accumulator, 1)
 
-
     def test_triggered_when_unchanged_complex_type(self):
         def accumulator(change):
             self.accumulator += 1
@@ -156,7 +164,6 @@ class TestWatch(API1TestCase):
         obj.a = subobj
         self.assertEqual(self.accumulator, 2)
 
-
     def test_triggered_when_unchanged_if_not_onlychanged(self):
         accumulator = Accumulator()
         obj = SimpleWatchExample()
@@ -167,6 +174,7 @@ class TestWatch(API1TestCase):
         args = accumulator.args_for_call(0)
         self.assertEqual(len(args), 1)
         self.assertEqual(args[0].name, 'a')
+        self.assertEqual(args[0].what, 'value')
         self.assertEqual(args[0].old, 0)
         self.assertEqual(args[0].new, 1)
         self.assertEqual(args[0].type, 'set')
@@ -175,12 +183,11 @@ class TestWatch(API1TestCase):
         args = accumulator.args_for_call(1)
         self.assertEqual(len(args), 1)
         self.assertEqual(args[0].name, 'a')
+        self.assertEqual(args[0].what, 'value')
         self.assertEqual(args[0].old, 1)
         self.assertEqual(args[0].new, 1)
         self.assertEqual(args[0].type, 'set')
 
-
-
     def test_untriggered_when_unwatched(self):
         def accumulator(change):
             self.accumulator += change.new
@@ -193,7 +200,6 @@ class TestWatch(API1TestCase):
         obj.a = 2
         self.assertEqual(self.accumulator, 1)
 
-
     def test_warning_unwatching_when_unwatched(self):
         def accumulator(change):
             self.accumulator += change.new
@@ -210,7 +216,7 @@ class TestWatch(API1TestCase):
         accumulator = Accumulator()
 
         obj = SimpleWatchExample()
-        obj.param.watch(accumulator, ['a','b'])
+        obj.param.watch(accumulator, ['a', 'b'])
 
         obj.a = 2
         self.assertEqual(accumulator.call_count(), 1)
@@ -275,7 +281,6 @@ class TestWatch(API1TestCase):
         # second call to accumulator
         self.assertEqual(accumulator.call_count(), 2)
 
-
     def test_simple_batched_watch(self):
 
         accumulator = Accumulator()
@@ -298,7 +303,6 @@ class TestWatch(API1TestCase):
         self.assertEqual(args[1].new, 42)
         self.assertEqual(args[1].type, 'changed')
 
-
     def test_simple_class_batched_watch(self):
 
         accumulator = Accumulator()
@@ -324,7 +328,6 @@ class TestWatch(API1TestCase):
         SimpleWatchExample.param.unwatch(watcher)
         obj.param.set_param(a=0, b=0)
 
-
     def test_simple_batched_watch_callback_reuse(self):
 
         accumulator = Accumulator()
@@ -357,6 +360,40 @@ class TestWatch(API1TestCase):
             else:
                 raise Exception('Invalid number of arguments')
 
+    def test_context_manager_batched_watch_reuse(self):
+
+        accumulator = Accumulator()
+
+        obj = SimpleWatchExample()
+        obj.param.watch(accumulator, ['a','b'])
+        obj.param.watch(accumulator, ['c'])
+
+        with param.batch_watch(obj):
+            obj.a = 23
+            obj.b = 42
+            obj.c = 99
+
+        self.assertEqual(accumulator.call_count(), 2)
+        # Order may be undefined for Python <3.6
+        for args in [accumulator.args_for_call(i) for i in [0, 1]]:
+            if len(args) == 1:  # ['c']
+                self.assertEqual(args[0].name, 'c')
+                self.assertEqual(args[0].old, 0)
+                self.assertEqual(args[0].new, 99)
+                self.assertEqual(args[0].type, 'changed')
+
+            elif len(args) == 2:  # ['a', 'b']
+                self.assertEqual(args[0].name, 'a')
+                self.assertEqual(args[0].old, 0)
+                self.assertEqual(args[0].new, 23)
+                self.assertEqual(args[0].type, 'changed')
+
+                self.assertEqual(args[1].name, 'b')
+                self.assertEqual(args[1].old, 0)
+                self.assertEqual(args[1].new, 42)
+                self.assertEqual(args[0].type, 'changed')
+            else:
+                raise Exception('Invalid number of arguments')
 
     def test_subclass_batched_watch(self):
 
@@ -381,7 +418,6 @@ class TestWatch(API1TestCase):
         self.assertEqual(args[1].new, 42)
         self.assertEqual(args[1].type, 'changed')
 
-
     def test_nested_batched_watch(self):
 
         accumulator = Accumulator()
@@ -391,7 +427,7 @@ class TestWatch(API1TestCase):
         def set_param(*changes):
             obj.param.set_param(a=10, d=12)
 
-        obj.param.watch(accumulator, ['a', 'b','c', 'd'])
+        obj.param.watch(accumulator, ['a', 'b', 'c', 'd'])
         obj.param.watch(set_param, ['b', 'c'])
         obj.param.set_param(b=23, c=42)
 
@@ -422,7 +458,6 @@ class TestWatch(API1TestCase):
         self.assertEqual(args[1].new, 12)
         self.assertEqual(args[1].type, 'changed')
 
-
     def test_nested_batched_watch_not_onlychanged(self):
         accumulator = Accumulator()
 
@@ -446,7 +481,37 @@ class TestWatch(API1TestCase):
         self.assertEqual(args[1].new, 0)
         self.assertEqual(args[1].type, 'set')
 
+    def test_watch_deepcopy(self):
+        obj = SimpleWatchExample()
+
+        obj.param.watch(obj.method, ['a'])
+
+        copied = copy.deepcopy(obj)
+
+        copied.a = 2
 
+        self.assertEqual(copied.b, 4)
+        self.assertEqual(obj.b, 0)
+
+    def test_watch_event_value_trigger(self):
+        obj = WatchMethodExample()
+        obj.e = True
+        self.assertEqual(obj.d, 30)
+        self.assertEqual(obj.e, False)
+
+    def test_watch_event_trigger_method(self):
+        obj = WatchMethodExample()
+        obj.param.trigger('e')
+        self.assertEqual(obj.d, 30)
+        self.assertEqual(obj.e, False)
+
+    def test_watch_event_batched_trigger_method(self):
+        obj = WatchMethodExample()
+        obj.param.trigger('e', 'f')
+        self.assertEqual(obj.d, 30)
+        self.b = 420
+        self.assertEqual(obj.e, False)
+        self.assertEqual(obj.f, False)
 
 class TestWatchMethod(API1TestCase):
 
@@ -489,13 +554,29 @@ class TestWatchMethod(API1TestCase):
         self.assertEqual(obj.param.d.bounds, (2, 4))
         self.assertEqual(accumulator.call_count(), 1)
 
+        args = accumulator.args_for_call(0)
+        self.assertEqual(len(args), 1)
+
+        self.assertEqual(args[0].name, 'd')
+        self.assertEqual(args[0].what, 'bounds')
+        self.assertEqual(args[0].old, None)
+        self.assertEqual(args[0].new, (2, 4))
+        self.assertEqual(args[0].type, 'changed')
+
     def test_depends_with_watch_on_subclass(self):
         obj = WatchSubclassExample()
 
         obj.b = 3
         self.assertEqual(obj.c, 6)
 
+    def test_watcher_method_deepcopy(self):
+        obj = WatchMethodExample(b=5)
+
+        copied = copy.deepcopy(obj)
 
+        copied.b = 11
+        self.assertEqual(copied.b, 10)
+        self.assertEqual(obj.b, 5)
 
 
 class TestWatchValues(API1TestCase):
@@ -515,7 +596,6 @@ class TestWatchValues(API1TestCase):
         obj.a = 2
         self.assertEqual(self.accumulator, 3)
 
-
     def test_untriggered_when_values_unchanged(self):
         def accumulator(a):
             self.accumulator += a
@@ -527,7 +607,6 @@ class TestWatchValues(API1TestCase):
         obj.a = 1
         self.assertEqual(self.accumulator, 1)
 
-
     def test_untriggered_when_values_unwatched(self):
         def accumulator(a):
             self.accumulator += a
@@ -540,7 +619,6 @@ class TestWatchValues(API1TestCase):
         obj.a = 2
         self.assertEqual(self.accumulator, 1)
 
-
     def test_simple_batched_watch_values_setattr(self):
 
         accumulator = Accumulator()
@@ -560,7 +638,6 @@ class TestWatchValues(API1TestCase):
         kwargs = accumulator.kwargs_for_call(1)
         self.assertEqual(kwargs, {'b':3})
 
-
     def test_simple_batched_watch_values(self):
 
         accumulator = Accumulator()
@@ -573,7 +650,6 @@ class TestWatchValues(API1TestCase):
         kwargs = accumulator.kwargs_for_call(0)
         self.assertEqual(kwargs, {'a':23, 'b':42})
 
-
     def test_simple_batched_watch_values_callback_reuse(self):
 
         accumulator = Accumulator()
@@ -595,9 +671,6 @@ class TestWatchValues(API1TestCase):
                 raise Exception('Invalid number of arguments')
 
 
-
-
-
 class TestWatchAttributes(API1TestCase):
 
     def setUp(self):
@@ -630,7 +703,6 @@ class TestWatchAttributes(API1TestCase):
         assert self.accumulator == [(0, 3)]
 
 
-
 class TestTrigger(API1TestCase):
 
     def setUp(self):
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..06bc7cd
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,13 @@
+In 2018, we moved most of `Parameterized` onto a `param` namespace
+object, expecting this to be the public API of param 2.0, and cleaning
+up the namespace of user classes.
+
+The new API has been in use for a while within holoviz projects, but
+we're still changing it. Meanwhile, the previous API remains
+available.
+
+The original API's tests were copied into an `API0` subdirectory,
+while tests in `API1` use the new API.
+
+(Probably not ideal to just copy everything, and cleaning up would be
+great, but this explains the two directories you see here.)
diff --git a/tests/__init__.py b/tests/__init__.py
index df36c05..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,6 +0,0 @@
-import sys
-import unittest # noqa
-
-if sys.version_info[0]==2 and sys.version_info[1]<7:
-    del sys.modules['unittest']
-    sys.modules['unittest'] = __import__('unittest2')
diff --git a/tox.ini b/tox.ini
index cf48434..d7879e6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,26 +1,21 @@
 [tox]
 envlist =
-    py37,py36,py35,py34,py27,pypy,
-    {py27,py36}-flakes,
-    {py27,py36}-with_numpy,
-    {py27,py36}-with_ipython
-    {py27,py35,py36,py37}-with_pandas
+    py39,py38,py37,py36,py35,py27,pypy2,pypy3
+    # below tests only really need to run for one python version;
+    # migrate to py38-x, py39-x etc as they become default/typical
+    py37-flakes,
+    py37-coverage,
+    py37-with_numpy,
+    py37-with_ipython,
+    py37-with_pandas,
+    py37-with_jsonsschema,
+    py37-with_gmpy,
+    py37-with_all
 
 [testenv]
 deps = .[tests]
 commands = nosetests
 
-[testenv:coverage]
-# remove develop install if https://github.com/ioam/param/issues/219
-# implemented
-setdevelop = True
-passenv = TRAVIS TRAVIS_*
-deps = {[testenv]deps}
-       coveralls
-commands = nosetests --with-coverage --cover-package=param
-           coveralls
-# TODO missing numbergen
-
 [testenv:with_numpy]
 deps = {[testenv]deps}
        numpy
@@ -31,17 +26,44 @@ deps = {[testenv]deps}
        pandas
 setenv = PARAM_TEST_PANDAS = 1
 
-
 [testenv:with_ipython]
 deps = {[testenv]deps}
        ipython
 setenv = PARAM_TEST_IPYTHON = 1
 
+[testenv:with_jsonschema]
+deps = {[testenv]deps}
+       jsonschema
+setenv = PARAM_TEST_JSONSCHEMA = 1
+
+[testenv:with_gmpy]
+deps = {[testenv]deps}
+       gmpy
+setenv = PARAM_TEST_GMPY = 1
+
+[testenv:with_all]
+deps = {[testenv:with_numpy]deps}
+       {[testenv:with_pandas]deps}
+       {[testenv:with_ipython]deps}
+       {[testenv:with_jsonschema]deps}
+       {[testenv:with_gmpy]deps}
+setenv = {[testenv:with_numpy]setenv}
+         {[testenv:with_pandas]setenv}
+         {[testenv:with_ipython]setenv}
+         {[testenv:with_jsonschema]setenv}
+         {[testenv:with_gmpy]setenv}
+
+[testenv:coverage]
+# remove develop install if https://github.com/ioam/param/issues/219
+# implemented
+setdevelop = True
+passenv = TRAVIS TRAVIS_*
+deps = {[testenv:with_all]deps}
+       coveralls
+setenv = {[testenv:with_all]setenv}
+commands = nosetests --with-coverage --cover-package=param --cover-package=numbergen
+           coveralls
+
 [testenv:flakes]
 skip_install = true
 commands = flake8
-
-[flake8]
-ignore = E,W,W605
-include = *.py
-exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,.ipynb_checkpoints,run_test.py