Repository: pyvista/pyvista

Issue:
Strictly enforce keyword arguments
I see folks quite often forget the s in the `scalars` argument for the `BasePlotter.add_mesh()` method. We should allow `scalar` as an alias much like how we allow `rng` and `clim` for the colorbar range/limits


Discussion:
I would disagree. 2 arguments:

`There should be one-- and preferably only one --obvious way to do it.` as says https://www.python.org/dev/peps/pep-0020/

it makes documentation harder.
Raise an exception to tell the user to use `scalars` when `scalar` is used as a keyword arg. 
Okay, I'm convinced. 

We may want to do some keyword refactoring and not just allow an endless dict of `**kwargs` to be passed to `add_mesh`. For example, you can pass all types of typos and not have an error thrown:

```py
import pyvista as pv
p = pv.Plotter()
p.add_mesh(pv.Cube(), my_crazy_argument="foooooooo")
p.show()
```

There are several places in the API where this can happen that will need to be refactored 
What we could do is pop out the allowed aliases like `rng` and then raise an exception if the `kwargs` dict is not empty that might say something like:

> "[list of arguments still in the kwargs] are not valid keyword arguments."

Patch:
diff --git a/examples/01-filter/decimate.py b/examples/01-filter/decimate.py
--- a/examples/01-filter/decimate.py
+++ b/examples/01-filter/decimate.py
@@ -13,10 +13,10 @@
 
 # Define a camera potion the shows this mesh properly
 cpos = [(0.4, -0.07, -0.31), (0.05, -0.13, -0.06), (-0.1, 1, 0.08)]
-dargs = dict(cpos=cpos, show_edges=True, color=True)
+dargs = dict(show_edges=True, color=True)
 
 # Preview the mesh
-mesh.plot(**dargs)
+mesh.plot(cpos=cpos, **dargs)
 
 ###############################################################################
 #  Now let's define a target reduction and compare the
@@ -28,13 +28,13 @@
 ###############################################################################
 decimated = mesh.decimate(target_reduction)
 
-decimated.plot(**dargs)
+decimated.plot(cpos=cpos, **dargs)
 
 
 ###############################################################################
 pro_decimated = mesh.decimate_pro(target_reduction, preserve_topology=True)
 
-pro_decimated.plot(**dargs)
+pro_decimated.plot(cpos=cpos, **dargs)
 
 
 ###############################################################################
diff --git a/examples/01-filter/streamlines.py b/examples/01-filter/streamlines.py
--- a/examples/01-filter/streamlines.py
+++ b/examples/01-filter/streamlines.py
@@ -62,7 +62,7 @@
 boundary = mesh.decimate_boundary().wireframe()
 
 p = pv.Plotter()
-p.add_mesh(streamlines.tube(radius=0.2), ligthing=False)
+p.add_mesh(streamlines.tube(radius=0.2), lighting=False)
 p.add_mesh(src)
 p.add_mesh(boundary, color="grey", opacity=0.25)
 p.camera_position = [(10, 9.5, -43), (87.0, 73.5, 123.0), (-0.5, -0.7, 0.5)]
diff --git a/pyvista/core/filters.py b/pyvista/core/filters.py
--- a/pyvista/core/filters.py
+++ b/pyvista/core/filters.py
@@ -32,7 +32,8 @@
 
 import pyvista
 from pyvista.utilities import (CELL_DATA_FIELD, POINT_DATA_FIELD, NORMALS,
-                               generate_plane, get_array, wrap)
+                               assert_empty_kwargs, generate_plane, get_array,
+                               wrap)
 
 
 def _get_output(algorithm, iport=0, iconnection=0, oport=0, active_scalar=None,
@@ -1031,14 +1032,13 @@ def warp_by_scalar(dataset, scalars=None, factor=1.0, normal=None,
         inplace : bool
             If True, the points of the give dataset will be updated.
         """
+        factor = kwargs.pop('scale_factor', factor)
+        assert_empty_kwargs(**kwargs)
         if scalars is None:
             field, scalars = dataset.active_scalar_info
         arr, field = get_array(dataset, scalars, preference='point', info=True)
         if field != pyvista.POINT_DATA_FIELD:
             raise AssertionError('Dataset can only by warped by a point data array.')
-        scale_factor = kwargs.get('scale_factor', None)
-        if scale_factor is not None:
-            factor = scale_factor
         # Run the algorithm
         alg = vtk.vtkWarpScalar()
         alg.SetInputDataObject(dataset)
@@ -2374,7 +2374,7 @@ def plot_curvature(poly_data, curv_type='mean', **kwargs):
             - Minimum
 
         **kwargs : optional
-            See help(pyvista.plot)
+            See :func:`pyvista.plot`
 
         Returns
         -------
@@ -2973,6 +2973,7 @@ def clean(poly_data, point_merging=True, tolerance=None, lines_to_points=True,
         """
         if tolerance is None:
             tolerance = kwargs.pop('merge_tol', None)
+        assert_empty_kwargs(**kwargs)
         clean = vtk.vtkCleanPolyData()
         clean.SetPointMerging(point_merging)
         clean.SetConvertLinesToPoints(lines_to_points)
@@ -3133,14 +3134,26 @@ def ray_trace(poly_data, origin, end_point, first_point=False, plot=False,
         return intersection_points, intersection_cells
 
 
-    def plot_boundaries(poly_data, **kwargs):
-        """ Plots boundaries of a mesh """
+    def plot_boundaries(poly_data, edge_color="red", **kwargs):
+        """ Plots boundaries of a mesh
+
+        Parameters
+        ----------
+        edge_color : str, etc.
+            The color of the edges when they are added to the plotter.
+
+        kwargs : optional
+            All additional keyword arguments will be passed to
+            :func:`pyvista.BasePlotter.add_mesh`
+
+        """
         edges = DataSetFilters.extract_edges(poly_data)
 
         plotter = pyvista.Plotter(off_screen=kwargs.pop('off_screen', False),
                                   notebook=kwargs.pop('notebook', None))
-        plotter.add_mesh(edges, 'r', style='wireframe', legend='Edges')
-        plotter.add_mesh(poly_data, legend='Mesh', **kwargs)
+        plotter.add_mesh(edges, color=edge_color, style='wireframe', label='Edges')
+        plotter.add_mesh(poly_data, label='Mesh', **kwargs)
+        plotter.add_legend()
         return plotter.show()
 
 
diff --git a/pyvista/core/objects.py b/pyvista/core/objects.py
--- a/pyvista/core/objects.py
+++ b/pyvista/core/objects.py
@@ -5,8 +5,9 @@
 import vtk
 
 import pyvista
-from pyvista.utilities import (ROW_DATA_FIELD, convert_array, get_array,
-                               parse_field_choice, row_array, vtk_bit_array_to_char)
+from pyvista.utilities import (ROW_DATA_FIELD, assert_empty_kwargs,
+                               convert_array, get_array, parse_field_choice,
+                               row_array, vtk_bit_array_to_char)
 
 from .common import DataObject, _ScalarsDict
 
@@ -409,6 +410,8 @@ def adder(self, scalars, name, set_active=False, deep=True):
 class Texture(vtk.vtkTexture):
     """A helper class for vtkTextures"""
     def __init__(self, *args, **kwargs):
+        assert_empty_kwargs(**kwargs)
+
         if len(args) == 1:
             if isinstance(args[0], vtk.vtkTexture):
                 self._from_texture(args[0])
diff --git a/pyvista/plotting/helpers.py b/pyvista/plotting/helpers.py
--- a/pyvista/plotting/helpers.py
+++ b/pyvista/plotting/helpers.py
@@ -74,6 +74,11 @@ def plot(var_item, off_screen=None, full_screen=False, screenshot=None,
     if notebook is None:
         notebook = scooby.in_ipykernel()
 
+    eye_dome_lighting = kwargs.pop("edl", eye_dome_lighting)
+    show_grid = kwargs.pop('show_grid', False)
+    height = kwargs.get('height', 400)
+    auto_close = kwargs.get('auto_close', rcParams['auto_close'])
+
     if notebook:
         off_screen = notebook
     plotter = Plotter(off_screen=off_screen, notebook=notebook)
@@ -109,11 +114,10 @@ def plot(var_item, off_screen=None, full_screen=False, screenshot=None,
     if text:
         plotter.add_text(text)
 
-    if show_bounds or kwargs.get('show_grid', False):
-        if kwargs.get('show_grid', False):
-            plotter.show_grid()
-        else:
-            plotter.show_bounds()
+    if show_grid:
+        plotter.show_grid()
+    elif show_bounds:
+        plotter.show_bounds()
 
     if cpos is None:
         cpos = plotter.get_default_cam_pos()
@@ -122,7 +126,6 @@ def plot(var_item, off_screen=None, full_screen=False, screenshot=None,
     else:
         plotter.camera_position = cpos
 
-    eye_dome_lighting = kwargs.pop("edl", eye_dome_lighting)
     if eye_dome_lighting:
         plotter.enable_eye_dome_lighting()
 
@@ -136,10 +139,10 @@ def plot(var_item, off_screen=None, full_screen=False, screenshot=None,
                           screenshot=screenshot,
                           return_img=return_img,
                           use_panel=use_panel,
-                          height=kwargs.get('height', 400))
+                          height=height)
 
     # close and return camera position and maybe image
-    if kwargs.get('auto_close', rcParams['auto_close']):
+    if auto_close:
         plotter.close()
 
     # Result will be handled by plotter.show(): cpos or [cpos, img]
diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py
--- a/pyvista/plotting/plotting.py
+++ b/pyvista/plotting/plotting.py
@@ -15,8 +15,9 @@
 import warnings
 
 import pyvista
-from pyvista.utilities import (convert_array, convert_string_array,
-                               get_array, is_pyvista_dataset, numpy_to_texture,
+from pyvista.utilities import (assert_empty_kwargs, convert_array,
+                               convert_string_array, get_array,
+                               is_pyvista_dataset, numpy_to_texture,
                                raise_not_matching, try_callback, wrap)
 
 from .colors import get_cmap_safe
@@ -580,7 +581,7 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
                  specular_power=100.0, nan_color=None, nan_opacity=1.0,
                  loc=None, culling=None, rgb=False, categories=False,
                  use_transparency=False, below_color=None, above_color=None,
-                 annotations=None, pickable=True, **kwargs):
+                 annotations=None, pickable=True, preference="point", **kwargs):
         """
         Adds any PyVista/VTK mesh or dataset that PyVista can wrap to the
         scene. This method using a mesh representation to view the surfaces
@@ -804,8 +805,10 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
         if lighting is None:
             lighting = rcParams['lighting']
 
-        if clim is None:
-            clim = kwargs.get('rng', None)
+        # supported aliases
+        clim = kwargs.pop('rng', clim)
+        cmap = kwargs.pop('colormap', cmap)
+        culling = kwargs.pop("backface_culling", culling)
 
         if render_points_as_spheres is None:
             render_points_as_spheres = rcParams['render_points_as_spheres']
@@ -815,18 +818,22 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
 
         if nan_color is None:
             nan_color = rcParams['nan_color']
-        nanr, nanb, nang = parse_color(nan_color)
-        nan_color = nanr, nanb, nang, nan_opacity
+        nan_color = list(parse_color(nan_color))
+        nan_color.append(nan_opacity)
         if color is True:
             color = rcParams['color']
 
         if texture is False:
             texture = None
 
-        if culling is None:
-            culling = kwargs.get("backface_culling", False)
-            if culling is True:
-                culling = 'backface'
+        if culling is True:
+            culling = 'backface'
+
+        rgb = kwargs.pop('rgba', rgb)
+
+        if "scalar" in kwargs:
+            raise TypeError("`scalar` is an invalid keyword argument for `add_mesh`. Perhaps you mean `scalars` with an s?")
+        assert_empty_kwargs(**kwargs)
 
         ##### Handle composite datasets #####
 
@@ -846,9 +853,9 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
                     raise RuntimeError('Scalar array must be given as a string name for multiblock datasets.')
 
             the_arguments = locals()
-            the_arguments.update(kwargs)
             the_arguments.pop('self')
             the_arguments.pop('mesh')
+            the_arguments.pop('kwargs')
 
             if multi_colors:
                 # Compute unique colors for each index of the block
@@ -965,7 +972,7 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
             self.mapper.SetArrayName(scalars)
             original_scalar_name = scalars
             scalars = get_array(mesh, scalars,
-                                preference=kwargs.get('preference', 'cell'), err=True)
+                                preference=preference, err=True)
             if stitle is None:
                 stitle = original_scalar_name
 
@@ -995,7 +1002,7 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
             try:
                 # Get array from mesh
                 opacity = get_array(mesh, opacity,
-                                    preference=kwargs.get('preference', 'cell'), err=True)
+                                    preference=preference, err=True)
                 opacity = normalize(opacity)
                 _custom_opac = True
             except:
@@ -1018,8 +1025,6 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
             opacity = 255 - opacity
 
         # Scalar formatting ===================================================
-        if cmap is None: # grab alias for cmaps: colormap
-            cmap = kwargs.get('colormap', None)
         if cmap is None: # Set default map if matplotlib is avaialble
             if has_matplotlib:
                 cmap = rcParams['cmap']
@@ -1049,8 +1054,6 @@ def add_mesh(self, mesh, color=None, style=None, scalars=None,
                 scalar_bar_args.setdefault('n_labels', 0)
                 _using_labels = True
 
-            if rgb is False or rgb is None:
-                rgb = kwargs.get('rgba', False)
             if rgb:
                 if scalars.ndim != 2 or scalars.shape[1] < 3 or scalars.shape[1] > 4:
                     raise ValueError('RGB array must be n_points/n_cells by 3/4 in shape.')
@@ -1229,7 +1232,7 @@ def add_volume(self, volume, scalars=None, clim=None, resolution=None,
                    loc=None, culling=False, multi_colors=False,
                    blending='composite', mapper='fixed_point',
                    stitle=None, scalar_bar_args=None, show_scalar_bar=None,
-                   annotations=None, pickable=True, **kwargs):
+                   annotations=None, pickable=True, preference="point", **kwargs):
         """
         Adds a volume, rendered using a fixed point ray cast mapper by default.
 
@@ -1355,8 +1358,14 @@ def add_volume(self, volume, scalars=None, clim=None, resolution=None,
         if name is None:
             name = '{}({})'.format(type(volume).__name__, str(hex(id(volume))))
 
-        if clim is None:
-            clim = kwargs.get('rng', None)
+        # Supported aliases
+        clim = kwargs.pop('rng', clim)
+        cmap = kwargs.pop('colormap', cmap)
+        culling = kwargs.pop("backface_culling", culling)
+
+        if "scalar" in kwargs:
+            raise TypeError("`scalar` is an invalid keyword argument for `add_mesh`. Perhaps you mean `scalars` with an s?")
+        assert_empty_kwargs(**kwargs)
 
         if scalar_bar_args is None:
             scalar_bar_args = {}
@@ -1364,10 +1373,8 @@ def add_volume(self, volume, scalars=None, clim=None, resolution=None,
         if show_scalar_bar is None:
             show_scalar_bar = rcParams['show_scalar_bar']
 
-        if culling is None:
-            culling = kwargs.get("backface_culling", False)
-            if culling is True:
-                culling = 'backface'
+        if culling is True:
+            culling = 'backface'
 
         # Convert the VTK data object to a pyvista wrapped object if neccessary
         if not is_pyvista_dataset(volume):
@@ -1416,7 +1423,7 @@ def add_volume(self, volume, scalars=None, clim=None, resolution=None,
                                     reset_camera=reset_camera, name=next_name,
                                     ambient=ambient, categories=categories, loc=loc,
                                     culling=culling, clim=clim,
-                                    mapper=mapper, pickable=pickable, **kwargs)
+                                    mapper=mapper, pickable=pickable)
 
                 actors.append(a)
             return actors
@@ -1444,7 +1451,7 @@ def add_volume(self, volume, scalars=None, clim=None, resolution=None,
         if isinstance(scalars, str):
             title = scalars
             scalars = get_array(volume, scalars,
-                                preference=kwargs.get('preference', 'point'), err=True)
+                                preference=preference, err=True)
             if stitle is None:
                 stitle = title
         else:
@@ -1512,11 +1519,9 @@ def add_volume(self, volume, scalars=None, clim=None, resolution=None,
             for val, anno in annotations.items():
                 table.SetAnnotation(float(val), str(anno))
 
-        if cmap is None: # grab alias for cmaps: colormap
-            cmap = kwargs.get('colormap', None)
-            if cmap is None: # Set default map if matplotlib is avaialble
-                if has_matplotlib:
-                    cmap = rcParams['cmap']
+        if cmap is None: # Set default map if matplotlib is avaialble
+            if has_matplotlib:
+                cmap = rcParams['cmap']
 
         if cmap is not None:
             if not has_matplotlib:
@@ -2971,7 +2976,7 @@ def add_point_labels(self, points, labels, italic=False, bold=True,
                          name=None, shape_color='grey', shape='rounded_rect',
                          fill_shape=True, margin=3, shape_opacity=1.0,
                          pickable=False, render_points_as_spheres=False,
-                         tolerance=0.001, **kwargs):
+                         tolerance=0.001):
         """
         Creates a point actor with one label from list labels assigned to
         each point.
@@ -3060,9 +3065,6 @@ def add_point_labels(self, points, labels, italic=False, bold=True,
             font_family = rcParams['font']['family']
         if font_size is None:
             font_size = rcParams['font']['size']
-        if point_color is None and text_color is None and kwargs.get('color', None) is not None:
-            point_color = kwargs.get('color', None)
-            text_color = kwargs.get('color', None)
         if point_color is None:
             point_color = rcParams['color']
         if text_color is None:
diff --git a/pyvista/plotting/qt_plotting.py b/pyvista/plotting/qt_plotting.py
--- a/pyvista/plotting/qt_plotting.py
+++ b/pyvista/plotting/qt_plotting.py
@@ -311,7 +311,7 @@ def __init__(self, parent=None, title=None, shape=(1, 1), off_screen=None,
                  border=None, border_color='k', border_width=2.0,
                  multi_samples=None, line_smoothing=False,
                  point_smoothing=False, polygon_smoothing=False,
-                 splitting_position=None, **kwargs):
+                 splitting_position=None):
         """ Initialize Qt interactor """
         if not has_pyqt:
             raise AssertionError('Requires PyQt5')
diff --git a/pyvista/plotting/widgets.py b/pyvista/plotting/widgets.py
--- a/pyvista/plotting/widgets.py
+++ b/pyvista/plotting/widgets.py
@@ -13,7 +13,7 @@ class WidgetHelper(object):
 
     def add_box_widget(self, callback, bounds=None, factor=1.25,
                        rotation_enabled=True, color=None, use_planes=False,
-                       outline_translation=True, pass_widget=False, **kwargs):
+                       outline_translation=True, pass_widget=False):
         """Add a box widget to the scene. This is useless without a callback
         function. You can pass a callable function that takes a single
         argument, the PolyData box output from this widget, and performs a
@@ -174,7 +174,7 @@ def add_plane_widget(self, callback, normal='x', origin=None,
                          assign_to_axis=None, tubing=False,
                          outline_translation=False,
                          origin_translation=True, implicit=True,
-                         pass_widget=False, test_callback=True, **kwargs):
+                         pass_widget=False, test_callback=True):
         """Add a plane widget to the scene. This is useless without a callback
         function. You can pass a callable function that takes two
         arguments, the normal and origin of the plane in that order output
@@ -492,7 +492,7 @@ def add_mesh_slice_orthogonal(self, mesh, generate_triangles=False,
 
     def add_line_widget(self, callback, bounds=None, factor=1.25,
                         resolution=100, color=None, use_vertices=False,
-                        pass_widget=False, **kwargs):
+                        pass_widget=False):
         """Add a line widget to the scene. This is useless without a callback
         function. You can pass a callable function that takes a single
         argument, the PolyData line output from this widget, and performs a
@@ -825,7 +825,7 @@ def callback(value):
     def add_spline_widget(self, callback, bounds=None, factor=1.25,
                           n_hanldes=5, resolution=25, color="yellow",
                           show_ribbon=False, ribbon_color="pink",
-                          ribbon_opacity=0.5, pass_widget=False, **kwargs):
+                          ribbon_opacity=0.5, pass_widget=False):
         """Create and add a spline widget to the scene. Use the bounds
         argument to place this widget. Several "handles" are used to control a
         parametric function for building this spline. Click directly on the
diff --git a/pyvista/utilities/__init__.py b/pyvista/utilities/__init__.py
--- a/pyvista/utilities/__init__.py
+++ b/pyvista/utilities/__init__.py
@@ -1,8 +1,8 @@
-from .errors import (Observer, Report, send_errors_to_logging,
-                     set_error_output_file)
+from .errors import (Observer, Report, assert_empty_kwargs,
+                     send_errors_to_logging, set_error_output_file)
 from .features import *
 from .fileio import *
 from .geometric_objects import *
 from .parametric_objects import *
 from .sphinx_gallery import Scraper, _get_sg_image_scraper
-from .utilities import *
+from .helpers import *
diff --git a/pyvista/utilities/errors.py b/pyvista/utilities/errors.py
--- a/pyvista/utilities/errors.py
+++ b/pyvista/utilities/errors.py
@@ -2,6 +2,7 @@
 import os
 import re
 import scooby
+import sys
 
 import vtk
 
@@ -125,3 +126,21 @@ def __init__(self, additional=None, ncol=3, text_width=80, sort=False):
         scooby.Report.__init__(self, additional=additional, core=core,
                                optional=optional, ncol=ncol,
                                text_width=text_width, sort=sort)
+
+
+def assert_empty_kwargs(**kwargs):
+    """An internal helper to assert that all keyword arguments have been used.
+    If any keyword arguments are passed, a ``TypeError`` is raised.
+    """
+    n = len(kwargs)
+    if n == 0:
+        return True
+    caller = sys._getframe(1).f_code.co_name
+    keys = list(kwargs.keys())
+    bad_arguments = "[" + ("{}, " * (n - 1) + "{}").format(*keys) + "]"
+    if n == 1:
+        grammar = "is an invalid keyword argument"
+    else:
+        grammar = "are invalid keyword arguments"
+    message = "{} {} for `{}`".format(bad_arguments, grammar, caller)
+    raise TypeError(message)
diff --git a/pyvista/utilities/geometric_objects.py b/pyvista/utilities/geometric_objects.py
--- a/pyvista/utilities/geometric_objects.py
+++ b/pyvista/utilities/geometric_objects.py
@@ -17,6 +17,7 @@
 import vtk
 
 import pyvista
+from pyvista.utilities import assert_empty_kwargs
 
 
 NORMALS = {
@@ -51,7 +52,7 @@ def translate(surf, center=[0., 0., 0.], direction=[1., 0., 0.]):
 
 
 def Cylinder(center=(0.,0.,0.), direction=(1.,0.,0.), radius=0.5, height=1.0,
-             resolution=100, **kwargs):
+             resolution=100, capping=True, **kwargs):
 
     """
     Create the surface of a cylinder.
@@ -91,7 +92,8 @@ def Cylinder(center=(0.,0.,0.), direction=(1.,0.,0.), radius=0.5, height=1.0,
     >>> cylinder.plot() # doctest:+SKIP
 
     """
-    capping = kwargs.get('capping', kwargs.get('cap_ends', True))
+    capping = kwargs.pop('cap_ends', capping)
+    assert_empty_kwargs(**kwargs)
     cylinderSource = vtk.vtkCylinderSource()
     cylinderSource.SetRadius(radius)
     cylinderSource.SetHeight(height)
@@ -528,12 +530,12 @@ def Text3D(string, depth=0.5):
     return pyvista.wrap(tri_filter.GetOutput())
 
 
-def SuperToroid(**kwargs):
+def SuperToroid(*args, **kwargs):
     """DEPRECATED: use :func:`pyvista.ParametricSuperToroid`"""
     raise RuntimeError('use `pyvista.ParametricSuperToroid` instead')
 
 
-def Ellipsoid(**kwargs):
+def Ellipsoid(*args, **kwargs):
     """DEPRECATED: use :func:`pyvista.ParametricEllipsoid`"""
     raise RuntimeError('use `pyvista.ParametricEllipsoid` instead')
 
diff --git a/pyvista/utilities/utilities.py b/pyvista/utilities/helpers.py
similarity index 100%
rename from pyvista/utilities/utilities.py
rename to pyvista/utilities/helpers.py
diff --git a/pyvista/utilities/parametric_objects.py b/pyvista/utilities/parametric_objects.py
--- a/pyvista/utilities/parametric_objects.py
+++ b/pyvista/utilities/parametric_objects.py
@@ -3,6 +3,7 @@
 import vtk
 
 import pyvista
+from pyvista.utilities import assert_empty_kwargs
 
 from .geometric_objects import translate
 
@@ -361,7 +362,15 @@ def ParametricEllipsoid(xradius=None, yradius=None, zradius=None,
     >>> mesh.plot(color='w', smooth_shading=True)  # doctest:+SKIP
     """
     parametric_function = vtk.vtkParametricEllipsoid()
-    parametric_keywords(parametric_function, **kwargs)
+    parametric_keywords(parametric_function, min_u=kwargs.pop("min_u", 0),
+                        max_u=kwargs.pop("max_u", 2*pi),
+                        min_v=kwargs.pop("min_v", 0.0),
+                        max_v=kwargs.pop("max_v", 2*pi),
+                        join_u=kwargs.pop("join_u", False),
+                        join_v=kwargs.pop("join_v", False),
+                        twist_u=kwargs.pop("twist_u", False),
+                        twist_v=kwargs.pop("twist_v", False),
+                        clockwise=kwargs.pop("clockwise", True),)
 
     if xradius is not None:
         parametric_function.SetXRadius(xradius)
@@ -964,7 +973,7 @@ def ParametricTorus(ringradius=None, crosssectionradius=None, **kwargs):
 
 def parametric_keywords(parametric_function, min_u=0, max_u=2*pi,
                         min_v=0.0, max_v=2*pi, join_u=False, join_v=False,
-                        twist_u=False, twist_v=False, clockwise=True, **kwargs):
+                        twist_u=False, twist_v=False, clockwise=True):
     """Applys keyword arguments to a parametric function.
 
     Parameters
@@ -1013,7 +1022,7 @@ def parametric_keywords(parametric_function, min_u=0, max_u=2*pi,
 
 
 def surface_from_para(parametric_function, u_res=100, v_res=100,
-                      w_res=100, **kwargs):
+                      w_res=100):
     """Construct a mesh from a parametric function.
 
     Parameters


Test patch:
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -837,3 +837,21 @@ def test_fail_plot_table():
     with pytest.raises(TypeError):
         plotter = pyvista.Plotter(off_screen=OFF_SCREEN)
         plotter.add_mesh(table)
+
+
+@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting")
+def test_bad_keyword_arguments():
+    """Make sure bad keyword arguments raise an error"""
+    mesh = examples.load_uniform()
+    with pytest.raises(TypeError):
+        pyvista.plot(mesh, foo=5, off_screen=OFF_SCREEN)
+    with pytest.raises(TypeError):
+        pyvista.plot(mesh, scalar=mesh.active_scalar_name, off_screen=OFF_SCREEN)
+    with pytest.raises(TypeError):
+        plotter = pyvista.Plotter(off_screen=OFF_SCREEN)
+        plotter.add_mesh(mesh, scalar=mesh.active_scalar_name)
+        plotter.show()
+    with pytest.raises(TypeError):
+        plotter = pyvista.Plotter(off_screen=OFF_SCREEN)
+        plotter.add_mesh(mesh, foo="bad")
+        plotter.show()
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -6,8 +6,9 @@
 
 import pyvista
 from pyvista import examples as ex
-from pyvista import utilities
-from pyvista import fileio
+from pyvista.utilities import helpers
+from pyvista.utilities import fileio
+from pyvista.utilities import errors
 
 # Only set this here just the once.
 pyvista.set_error_output_file(os.path.join(os.path.dirname(__file__), 'ERROR_OUTPUT.txt'))
@@ -17,13 +18,13 @@ def test_createvectorpolydata_error():
     orig = np.random.random((3, 1))
     vec = np.random.random((3, 1))
     with pytest.raises(Exception):
-        utilities.vector_poly_data(orig, vec)
+        helpers.vector_poly_data(orig, vec)
 
 
 def test_createvectorpolydata_1D():
     orig = np.random.random(3)
     vec = np.random.random(3)
-    vdata = utilities.vector_poly_data(orig, vec)
+    vdata = helpers.vector_poly_data(orig, vec)
     assert np.any(vdata.points)
     assert np.any(vdata.point_arrays['vectors'])
 
@@ -31,7 +32,7 @@ def test_createvectorpolydata_1D():
 def test_createvectorpolydata():
     orig = np.random.random((100, 3))
     vec = np.random.random((100, 3))
-    vdata = utilities.vector_poly_data(orig, vec)
+    vdata = helpers.vector_poly_data(orig, vec)
     assert np.any(vdata.points)
     assert np.any(vdata.point_arrays['vectors'])
 
@@ -72,12 +73,12 @@ def test_get_array():
     grid._add_point_array(oarr, 'other')
     farr = np.random.rand(grid.n_points * grid.n_cells)
     grid._add_field_array(farr, 'field_data')
-    assert np.allclose(carr, utilities.get_array(grid, 'test_data', preference='cell'))
-    assert np.allclose(parr, utilities.get_array(grid, 'test_data', preference='point'))
-    assert np.allclose(oarr, utilities.get_array(grid, 'other'))
-    assert utilities.get_array(grid, 'foo') is None
-    assert utilities.get_array(grid, 'test_data', preference='field') is None
-    assert np.allclose(farr, utilities.get_array(grid, 'field_data', preference='field'))
+    assert np.allclose(carr, helpers.get_array(grid, 'test_data', preference='cell'))
+    assert np.allclose(parr, helpers.get_array(grid, 'test_data', preference='point'))
+    assert np.allclose(oarr, helpers.get_array(grid, 'other'))
+    assert helpers.get_array(grid, 'foo') is None
+    assert helpers.get_array(grid, 'test_data', preference='field') is None
+    assert np.allclose(farr, helpers.get_array(grid, 'field_data', preference='field'))
 
 
 
@@ -85,11 +86,11 @@ def test_get_array():
 def test_is_inside_bounds():
     data = ex.load_uniform()
     bnds = data.bounds
-    assert utilities.is_inside_bounds((0.5, 0.5, 0.5), bnds)
-    assert not utilities.is_inside_bounds((12, 5, 5), bnds)
-    assert not utilities.is_inside_bounds((5, 12, 5), bnds)
-    assert not utilities.is_inside_bounds((5, 5, 12), bnds)
-    assert not utilities.is_inside_bounds((12, 12, 12), bnds)
+    assert helpers.is_inside_bounds((0.5, 0.5, 0.5), bnds)
+    assert not helpers.is_inside_bounds((12, 5, 5), bnds)
+    assert not helpers.is_inside_bounds((5, 12, 5), bnds)
+    assert not helpers.is_inside_bounds((5, 5, 12), bnds)
+    assert not helpers.is_inside_bounds((12, 12, 12), bnds)
 
 
 def test_get_sg_image_scraper():
@@ -166,3 +167,13 @@ def test_transform_vectors_sph_to_cart():
         [uu[-1, -1], vv[-1, -1], ww[-1, -1]],
         [67.80403533828323, 360.8359915416445, -70000.0],
     )
+
+def test_assert_empty_kwargs():
+    kwargs = {}
+    assert errors.assert_empty_kwargs(**kwargs)
+    with pytest.raises(TypeError):
+        kwargs = {"foo":6}
+        errors.assert_empty_kwargs(**kwargs)
+    with pytest.raises(TypeError):
+        kwargs = {"foo":6, "goo":"bad"}
+        errors.assert_empty_kwargs(**kwargs)