Repository: great-expectations/great_expectations

Issue:
Allow creating an empty expectation suite
Whenever I run the `great_expectations suite new` workflow, GE runs the profiler and creates a few expectations for a subset of columns. In a lot of cases I don't care about these expectations, so I have to go and delete them one by one in the `suite edit` notebook.

I would like an option to create a blank expectation suite for a batch using the cli, e.g. something like this:

```
>> great expectations suite new
>> (choose data asset)
>> "Would you like to create sample expectations Y/n?
>> (maybe open the jupyter notebook automatically instead of data docs since there's nothing to look at in data docs anyway)


Discussion:
Is the request here that you want an empty expectation suite, created by CLI only? I am just clarifying as there is an easy way to do this via a py script (I am currently making a generic script for my team to do this adhoc based off new input files)
@PSUlion16 Correct. This seems like a neat and pretty simple built-in feature in the CLI rather than users having to create their custom scripts (which seems straightforward, but still...). 

Would your team use that if it was available, or would your script be doing something else besides that?

Resolving patch:
diff --git a/great_expectations/cli/datasource.py b/great_expectations/cli/datasource.py
--- a/great_expectations/cli/datasource.py
+++ b/great_expectations/cli/datasource.py
@@ -787,6 +787,7 @@ def create_expectation_suite(
     batch_kwargs=None,
     expectation_suite_name=None,
     additional_batch_kwargs=None,
+    empty_suite=False,
     show_intro_message=False,
     open_docs=False
 ):
@@ -794,25 +795,12 @@ def create_expectation_suite(
     """
     Create a new expectation suite.
 
-    :param context:
-    :param datasource_name:
-    :param generator_name:
-    :param generator_asset:
-    :param batch_kwargs:
-    :param expectation_suite_name:
-    :param additional_batch_kwargs:
     :return: a tuple: (success, suite name)
     """
 
-    msg_intro = """
-<cyan>========== Create sample Expectations ==========</cyan>
-
-
-"""
-
+    msg_intro = "\n<cyan>========== Create sample Expectations ==========</cyan>\n\n"
     msg_some_data_assets_not_found = """Some of the data assets you specified were not found: {0:s}    
     """
-
     msg_prompt_what_will_profiler_do = """
 Great Expectations will choose a couple of columns and generate expectations about them
 to demonstrate some examples of assertions you can make about your data. 
@@ -828,7 +816,7 @@ def create_expectation_suite(
 
     msg_suite_already_exists = "<red>An expectation suite named `{}` already exists. If you intend to edit the suite please use `great_expectations suite edit {}`.</red>"
 
-    if show_intro_message:
+    if show_intro_message and not empty_suite:
         cli_message(msg_intro)
 
     data_source = select_datasource(context, datasource_name=datasource_name)
@@ -878,6 +866,12 @@ def create_expectation_suite(
             else:
                 break
 
+    if empty_suite:
+        suite = context.create_expectation_suite(expectation_suite_name, overwrite_existing=False)
+        suite.add_citation(comment="New suite added via CLI", batch_kwargs=batch_kwargs)
+        context.save_expectation_suite(suite, expectation_suite_name)
+        return True, expectation_suite_name
+
     profiler = SampleExpectationsDatasetProfiler
 
     click.prompt(msg_prompt_what_will_profiler_do, default=True, show_default=False)
diff --git a/great_expectations/cli/suite.py b/great_expectations/cli/suite.py
--- a/great_expectations/cli/suite.py
+++ b/great_expectations/cli/suite.py
@@ -79,6 +79,10 @@ def suite_edit(suite, datasource, directory, jupyter, batch_kwargs):
 
     Read more about specifying batches of data in the documentation: https://docs.greatexpectations.io/
     """
+    _suite_edit(suite, datasource, directory, jupyter, batch_kwargs)
+
+
+def _suite_edit(suite, datasource, directory, jupyter, batch_kwargs):
     batch_kwargs_json = batch_kwargs
     batch_kwargs = None
     try:
@@ -158,11 +162,9 @@ def suite_edit(suite, datasource, directory, jupyter, batch_kwargs):
     )
     NotebookRenderer().render_to_disk(suite, notebook_path, batch_kwargs)
 
-    cli_message(
-        "To continue editing this suite, run <green>jupyter notebook {}</green>".format(
-            notebook_path
-        )
-    )
+    if not jupyter:
+        cli_message("To continue editing this suite, run <green>jupyter "
+                    f"notebook {notebook_path}</green>")
 
     if jupyter:
         subprocess.call(["jupyter", "notebook", notebook_path])
@@ -186,12 +188,19 @@ def _load_suite(context, suite_name):
 
 @suite.command(name="new")
 @click.option("--suite", "-es", default=None, help="Expectation suite name.")
+@click.option("--empty", "empty", flag_value=True, help="Create an empty suite.")
 @click.option(
     "--directory",
     "-d",
     default=None,
     help="The project's great_expectations directory.",
 )
+@click.option(
+    "--jupyter/--no-jupyter",
+    is_flag=True,
+    help="By default launch jupyter notebooks unless you specify the --no-jupyter flag",
+    default=True,
+)
 @click.option(
     "--view/--no-view",
     help="By default open in browser unless you specify the --no-view flag",
@@ -202,12 +211,14 @@ def _load_suite(context, suite_name):
     default=None,
     help="Additional keyword arguments to be provided to get_batch when loading the data asset. Must be a valid JSON dictionary",
 )
-def suite_new(suite, directory, view, batch_kwargs):
+def suite_new(suite, directory, empty, jupyter, view, batch_kwargs):
     """
     Create a new expectation suite.
 
     Great Expectations will choose a couple of columns and generate expectations about them
     to demonstrate some examples of assertions you can make about your data.
+
+    If you wish to skip the examples, add the `--empty` flag.
     """
     try:
         context = DataContext(directory)
@@ -231,6 +242,7 @@ def suite_new(suite, directory, view, batch_kwargs):
             batch_kwargs=batch_kwargs,
             expectation_suite_name=suite,
             additional_batch_kwargs={"limit": 1000},
+            empty_suite=empty,
             show_intro_message=False,
             open_docs=view,
         )
@@ -240,6 +252,8 @@ def suite_new(suite, directory, view, batch_kwargs):
                     suite_name
                 )
             )
+            if jupyter:
+                cli_message("<green>Because you requested an empty suite, we'll open a notebook for you now to edit it!</green>\n\n")
     except (
         ge_exceptions.DataContextError,
         ge_exceptions.ProfilerError,
@@ -249,6 +263,8 @@ def suite_new(suite, directory, view, batch_kwargs):
         cli_message("<red>{}</red>".format(e))
         sys.exit(1)
 
+    _suite_edit(suite_name, datasource_name, directory, jupyter=jupyter, batch_kwargs=batch_kwargs)
+
 
 @suite.command(name="list")
 @click.option(
diff --git a/great_expectations/render/renderer/notebook_renderer.py b/great_expectations/render/renderer/notebook_renderer.py
--- a/great_expectations/render/renderer/notebook_renderer.py
+++ b/great_expectations/render/renderer/notebook_renderer.py
@@ -139,21 +139,19 @@ def add_markdown_cell(self, markdown):
     def add_expectation_cells_from_suite(self, expectations):
         expectations_by_column = self._get_expectations_by_column(expectations)
         self.add_markdown_cell("### Table Expectation(s)")
-        if expectations_by_column["table_expectations"]:
-            for exp in expectations_by_column["table_expectations"]:
-                kwargs_string = self._build_kwargs_string(exp)
-                code = "batch.{}({})".format(exp["expectation_type"], kwargs_string)
-                self.add_code_cell(code, lint=True)
-        else:
-            self.add_markdown_cell(
-                "No table level expectations are in this suite. Feel free to "
-                "add some here. The all begin with `batch.expect_table_...`."
-            )
-
+        self._add_table_level_expectations(expectations_by_column)
         # Remove the table expectations since they are dealt with
         expectations_by_column.pop("table_expectations")
-
         self.add_markdown_cell("### Column Expectation(s)")
+        self._add_column_level_expectations(expectations_by_column)
+
+    def _add_column_level_expectations(self, expectations_by_column):
+        if not expectations_by_column:
+            self.add_markdown_cell(
+                "No column level expectations are in this suite. Feel free to "
+                "add some here. They all begin with `batch.expect_column_...`."
+            )
+            return
 
         for column, expectations in expectations_by_column.items():
             self.add_markdown_cell("#### `{}`".format(column))
@@ -166,6 +164,19 @@ def add_expectation_cells_from_suite(self, expectations):
                 )
                 self.add_code_cell(code, lint=True)
 
+    def _add_table_level_expectations(self, expectations_by_column):
+        if not expectations_by_column["table_expectations"]:
+            self.add_markdown_cell(
+                "No table level expectations are in this suite. Feel free to "
+                "add some here. They all begin with `batch.expect_table_...`."
+            )
+            return
+
+        for exp in expectations_by_column["table_expectations"]:
+            kwargs_string = self._build_kwargs_string(exp)
+            code = "batch.{}({})".format(exp["expectation_type"], kwargs_string)
+            self.add_code_cell(code, lint=True)
+
     @staticmethod
     def _build_meta_arguments(meta):
         if not meta:


Test patch:
diff --git a/tests/cli/test_suite.py b/tests/cli/test_suite.py
--- a/tests/cli/test_suite.py
+++ b/tests/cli/test_suite.py
@@ -4,9 +4,8 @@
 import json
 import os
 
-import pytest
+import mock
 from click.testing import CliRunner
-from six import PY2
 
 from great_expectations import DataContext
 from great_expectations.cli import cli
@@ -28,9 +27,7 @@ def test_suite_help_output(caplog,):
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
-def test_suite_new_on_context_with_no_datasources(
-    caplog, empty_data_context
-):
+def test_suite_new_on_context_with_no_datasources(caplog, empty_data_context):
     """
     We call the "suite new" command on a data context that has no datasources
     configured.
@@ -46,9 +43,7 @@ def test_suite_new_on_context_with_no_datasources(
     context = DataContext(root_dir)
     runner = CliRunner(mix_stderr=False)
     result = runner.invoke(
-        cli,
-        ["suite", "new", "-d", root_dir, "--no-view"],
-        catch_exceptions=False,
+        cli, ["suite", "new", "-d", root_dir, "--no-view"], catch_exceptions=False,
     )
     stdout = result.stdout
 
@@ -57,8 +52,9 @@ def test_suite_new_on_context_with_no_datasources(
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_new_enter_existing_suite_name_as_arg(
-    caplog, data_context
+    mock_subprocess, caplog, data_context
 ):
     """
     We call the "suite new" command with the name of an existing expectation suite in the --suite argument
@@ -68,6 +64,7 @@ def test_suite_new_enter_existing_suite_name_as_arg(
 
     not_so_empty_data_context = data_context
     project_root_dir = not_so_empty_data_context.root_directory
+    os.mkdir(os.path.join(project_root_dir, "uncommitted"))
 
     root_dir = project_root_dir
     os.chdir(root_dir)
@@ -85,8 +82,9 @@ def test_suite_new_enter_existing_suite_name_as_arg(
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_new_answer_suite_name_prompts_with_name_of_existing_suite(
-    caplog, data_context, filesystem_csv_2
+    mock_subprocess, caplog, data_context, filesystem_csv_2
 ):
     """
     We call the "suite new" command without the suite name argument
@@ -103,6 +101,7 @@ def test_suite_new_answer_suite_name_prompts_with_name_of_existing_suite(
 
     not_so_empty_data_context = data_context
     project_root_dir = not_so_empty_data_context.root_directory
+    os.mkdir(os.path.join(project_root_dir, "uncommitted"))
 
     root_dir = project_root_dir
     os.chdir(root_dir)
@@ -138,8 +137,71 @@ def test_suite_new_answer_suite_name_prompts_with_name_of_existing_suite(
     assert os.path.isfile(expected_suite_path)
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
+
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
+def test_suite_new_empty_suite_creates_empty_suite(
+    mock_subprocess, caplog, data_context, filesystem_csv_2
+):
+    """
+    Running "suite new --empty" should make an empty suite
+    """
+    project_root_dir = data_context.root_directory
+    os.mkdir(os.path.join(project_root_dir, "uncommitted"))
+    root_dir = project_root_dir
+    os.chdir(root_dir)
+    runner = CliRunner(mix_stderr=False)
+    csv = os.path.join(filesystem_csv_2, "f1.csv")
+    result = runner.invoke(
+        cli,
+        ["suite", "new", "-d", root_dir, "--empty", "--suite", "foo", "--no-view"],
+        input=f"{csv}\n",
+        catch_exceptions=False,
+    )
+    stdout = result.stdout
+
+    assert result.exit_code == 0
+    assert "Enter the path" in stdout
+    assert "Name the new expectation suite" not in stdout
+    assert (
+        "Great Expectations will choose a couple of columns and generate expectations"
+        not in stdout
+    )
+    assert "Generating example Expectation Suite..." not in stdout
+    assert "The following Data Docs sites were built" not in stdout
+    assert "A new Expectation suite 'foo' was added to your project" in stdout
+    assert "Because you requested an empty suite, we'll open a notebook for you now to edit it!" in stdout
+
+    expected_suite_path = os.path.join(root_dir, "expectations", "foo.json")
+    assert os.path.isfile(expected_suite_path)
+
+    expected_notebook = os.path.join(root_dir, "uncommitted", "foo.ipynb")
+    assert os.path.isfile(expected_notebook)
+
+    assert mock_subprocess.call_count == 1
+    call_args = mock_subprocess.call_args[0][0]
+    assert call_args[0] == "jupyter"
+    assert call_args[1] == "notebook"
+    assert expected_notebook in call_args[2]
+
+    context = DataContext(root_dir)
+    assert "foo" in context.list_expectation_suite_names()
+    suite = context.get_expectation_suite("foo")
+    assert suite.expectations == []
+    citations = suite.get_citations()
+    citations[0].pop("citation_date")
+    assert citations[0] == {
+        "batch_kwargs": {"datasource": "mydatasource", "path": csv},
+        "batch_markers": None,
+        "batch_parameters": None,
+        "comment": "New suite added via CLI",
+    }
+
+    assert_no_logging_messages_or_tracebacks(caplog, result)
+
+
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_new_one_datasource_without_generator_without_suite_name_argument(
-    caplog, empty_data_context, filesystem_csv_2
+    mock_subprocess, caplog, empty_data_context, filesystem_csv_2
 ):
     """
     We call the "suite new" command without the suite name argument
@@ -206,8 +268,9 @@ def test_suite_new_one_datasource_without_generator_without_suite_name_argument(
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_new_multiple_datasources_with_generator_without_suite_name_argument(
-    caplog, site_builder_data_context_with_html_store_titanic_random,
+    mock_subprocess, caplog, site_builder_data_context_with_html_store_titanic_random,
 ):
     """
     We call the "suite new" command without the suite name argument
@@ -231,12 +294,18 @@ def test_suite_new_multiple_datasources_with_generator_without_suite_name_argume
     stdout = result.stdout
 
     assert result.exit_code == 0
-    assert """Select a datasource
+    assert (
+        """Select a datasource
     1. random
-    2. titanic""" in stdout
-    assert """Which data would you like to use?
+    2. titanic"""
+        in stdout
+    )
+    assert (
+        """Which data would you like to use?
     1. f1 (file)
-    2. f2 (file)""" in stdout
+    2. f2 (file)"""
+        in stdout
+    )
     assert "Name the new expectation suite [f1.warning]" in stdout
     assert (
         "Great Expectations will choose a couple of columns and generate expectations"
@@ -264,8 +333,9 @@ def test_suite_new_multiple_datasources_with_generator_without_suite_name_argume
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_new_multiple_datasources_with_generator_with_suite_name_argument(
-    caplog, site_builder_data_context_with_html_store_titanic_random,
+    mock_subprocess, caplog, site_builder_data_context_with_html_store_titanic_random,
 ):
     """
     We call the "suite new" command with the suite name argument
@@ -409,8 +479,9 @@ def test_suite_edit_with_non_existent_datasource_shows_helpful_error_message(
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_edit_multiple_datasources_with_generator_with_no_additional_args_with_suite_without_citations(
-    caplog, site_builder_data_context_with_html_store_titanic_random,
+    mock_subprocess, caplog, site_builder_data_context_with_html_store_titanic_random,
 ):
     """
     Here we verify that the "suite edit" command helps the user to specify the batch
@@ -466,8 +537,9 @@ def test_suite_edit_multiple_datasources_with_generator_with_no_additional_args_
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_edit_multiple_datasources_with_generator_with_no_additional_args_with_suite_containing_citations(
-    caplog, site_builder_data_context_with_html_store_titanic_random,
+    mock_subprocess, caplog, site_builder_data_context_with_html_store_titanic_random,
 ):
     """
     Here we verify that the "suite edit" command uses the batch kwargs found in
@@ -515,8 +587,9 @@ def test_suite_edit_multiple_datasources_with_generator_with_no_additional_args_
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_edit_multiple_datasources_with_generator_with_batch_kwargs_arg(
-    caplog, site_builder_data_context_with_html_store_titanic_random,
+    mock_subprocess, caplog, site_builder_data_context_with_html_store_titanic_random,
 ):
     """
     Here we verify that when the "suite edit" command is called with batch_kwargs arg
@@ -588,7 +661,6 @@ def test_suite_edit_multiple_datasources_with_generator_with_batch_kwargs_arg(
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
-@pytest.mark.xfail(condition=PY2, reason="different error messages in py2")
 def test_suite_edit_on_exsiting_suite_one_datasources_with_batch_kwargs_without_datasource_raises_helpful_error(
     caplog, titanic_data_context,
 ):
@@ -674,8 +746,9 @@ def test_suite_edit_on_exsiting_suite_one_datasources_with_datasource_arg_and_ba
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
+@mock.patch("subprocess.call", return_value=True, side_effect=None)
 def test_suite_edit_one_datasources_no_generator_with_no_additional_args_and_no_citations(
-    caplog, empty_data_context, filesystem_csv_2
+    mock_subprocess, caplog, empty_data_context, filesystem_csv_2
 ):
     """
     Here we verify that the "suite edit" command helps the user to specify the batch
@@ -744,66 +817,52 @@ def test_suite_edit_one_datasources_no_generator_with_no_additional_args_and_no_
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
-def test_suite_list_with_zero_suites(
-    caplog, empty_data_context
-):
+def test_suite_list_with_zero_suites(caplog, empty_data_context):
     project_dir = empty_data_context.root_directory
     context = DataContext(project_dir)
     runner = CliRunner(mix_stderr=False)
 
     result = runner.invoke(
-        cli,
-        "suite list -d {}".format(project_dir),
-        catch_exceptions=False,
+        cli, "suite list -d {}".format(project_dir), catch_exceptions=False,
     )
     assert result.exit_code == 0
-    assert ("No expectation suites available" in result.output)
+    assert "No expectation suites available" in result.output
 
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
-def test_suite_list_with_one_suite(
-    caplog, empty_data_context
-):
+
+def test_suite_list_with_one_suite(caplog, empty_data_context):
     project_dir = empty_data_context.root_directory
     context = DataContext(project_dir)
     context.create_expectation_suite("a.warning")
     runner = CliRunner(mix_stderr=False)
 
     result = runner.invoke(
-    cli,
-        "suite list -d {}".format(project_dir),
-        catch_exceptions=False,
+        cli, "suite list -d {}".format(project_dir), catch_exceptions=False,
     )
     assert result.exit_code == 0
-    assert ("1 expectation suite available" in result.output)
-    assert ("\ta.warning" in result.output)
+    assert "1 expectation suite available" in result.output
+    assert "\ta.warning" in result.output
     assert_no_logging_messages_or_tracebacks(caplog, result)
 
 
-def test_suite_list_with_multiple_suites(
-        caplog, empty_data_context
-):
+def test_suite_list_with_multiple_suites(caplog, empty_data_context):
     project_dir = empty_data_context.root_directory
     context = DataContext(project_dir)
     context.create_expectation_suite("a.warning")
     context.create_expectation_suite("b.warning")
     context.create_expectation_suite("c.warning")
 
-
     runner = CliRunner(mix_stderr=False)
 
     result = runner.invoke(
-    cli,
-        "suite list -d {}".format(project_dir),
-        catch_exceptions=False,
+        cli, "suite list -d {}".format(project_dir), catch_exceptions=False,
     )
     output = result.output
-    print(output)
     assert result.exit_code == 0
     assert "3 expectation suites available:" in output
     assert "\ta.warning" in output
     assert "\tb.warning" in output
     assert "\tc.warning" in output
 
-
     assert_no_logging_messages_or_tracebacks(caplog, result)
diff --git a/tests/render/renderer/test_notebook_renderer.py b/tests/render/renderer/test_notebook_renderer.py
--- a/tests/render/renderer/test_notebook_renderer.py
+++ b/tests/render/renderer/test_notebook_renderer.py
@@ -370,7 +370,6 @@ def warning_suite():
     return schema.loads(json.dumps(warning_suite)).data
 
 
-@pytest.mark.xfail(condition=PY2, reason="legacy python")
 def test_render_without_batch_kwargs_uses_batch_kwargs_in_citations(
     critical_suite_with_citations,
 ):
@@ -423,7 +422,7 @@ def test_render_without_batch_kwargs_uses_batch_kwargs_in_citations(
             },
             {
                 "cell_type": "markdown",
-                "source": "No table level expectations are in this suite. Feel free to add some here. The all begin with `batch.expect_table_...`.",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
                 "metadata": {},
             },
             {
@@ -482,6 +481,107 @@ def test_render_without_batch_kwargs_uses_batch_kwargs_in_citations(
     assert obs == expected
 
 
+def test_render_with_no_column_cells(
+    critical_suite_with_citations,
+):
+    critical_suite_with_citations.expectations = []
+    obs = NotebookRenderer().render(critical_suite_with_citations)
+    assert isinstance(obs, dict)
+    expected = {
+        "nbformat": 4,
+        "nbformat_minor": 4,
+        "metadata": {},
+        "cells": [
+            {
+                "cell_type": "markdown",
+                "source": "# Edit Your Expectation Suite\nUse this notebook to recreate and modify your expectation suite:\n\n**Expectation Suite Name**: `critical`\n\nWe'd love it if you **reach out to us on** the [**Great Expectations Slack Channel**](https://greatexpectations.io/slack)",
+                "metadata": {},
+            },
+            {
+                "cell_type": "code",
+                "metadata": {},
+                "execution_count": None,
+                "source": """\
+from datetime import datetime
+import great_expectations as ge
+import great_expectations.jupyter_ux
+from great_expectations.data_context.types.resource_identifiers import ValidationResultIdentifier
+
+context = ge.data_context.DataContext()
+
+# Feel free to change the name of your suite here. Renaming this will not
+# remove the other one.
+expectation_suite_name = "critical"
+suite = context.get_expectation_suite(expectation_suite_name)
+suite.expectations = []
+
+batch_kwargs = {
+    'path': '/home/foo/data/10k.csv',
+    'datasource': 'files_datasource'}
+batch = context.get_batch(batch_kwargs, suite.expectation_suite_name)
+batch.head()""",
+                "outputs": [],
+            },
+            {
+                "cell_type": "markdown",
+                "source": "## Create & Edit Expectations\n\nAdd expectations by calling specific expectation methods on the `batch` object. They all begin with `.expect_` which makes autocompleting easy using tab.\n\nYou can see all the available expectations in the **[expectation glossary](https://docs.greatexpectations.io/en/latest/expectation_glossary.html?utm_source=notebook&utm_medium=create_expectations)**.",
+                "metadata": {},
+            },
+            {
+                "cell_type": "markdown",
+                "source": "### Table Expectation(s)",
+                "metadata": {},
+            },
+            {
+                "cell_type": "markdown",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
+                "metadata": {},
+            },
+            {
+                "cell_type": "markdown",
+                "source": "### Column Expectation(s)",
+                "metadata": {},
+            },
+            {
+                "cell_type": "markdown",
+                "source": "No column level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_column_...`.",
+                "metadata": {},
+            },
+            {
+                "cell_type": "markdown",
+                "source": "## Save & Review Your Expectations\n\nLet's save the expectation suite as a JSON file in the `great_expectations/expectations` directory of your project.\nIf you decide not to save some expectations that you created, use [remove_expectation method](https://docs.greatexpectations.io/en/latest/module_docs/data_asset_module.html?highlight=remove_expectation&utm_source=notebook&utm_medium=edit_expectations#great_expectations.data_asset.data_asset.DataAsset.remove_expectation).\n\nLet's now rebuild your Data Docs, which helps you communicate about your data with both machines and humans.",
+                "metadata": {},
+            },
+            {
+                "cell_type": "code",
+                "metadata": {},
+                "execution_count": None,
+                "source": """\
+batch.save_expectation_suite(discard_failed_expectations=False)
+
+# Let's make a simple sortable timestamp. Note this could come from your pipeline runner.
+run_id = datetime.utcnow().strftime("%Y%m%dT%H%M%S.%fZ")
+
+results = context.run_validation_operator("action_list_operator", assets_to_validate=[batch], run_id=run_id)
+expectation_suite_identifier = list(results["details"].keys())[0]
+validation_result_identifier = ValidationResultIdentifier(
+    expectation_suite_identifier=expectation_suite_identifier,
+    batch_identifier=batch.batch_kwargs.to_id(),
+    run_id=run_id
+)
+context.build_data_docs()
+context.open_data_docs(validation_result_identifier)""",
+                "outputs": [],
+            },
+        ],
+    }
+    del expected["nbformat_minor"]
+    del obs["nbformat_minor"]
+    for obs_cell, expected_cell in zip(obs["cells"], expected["cells"]):
+        assert obs_cell == expected_cell
+    assert obs == expected
+
+
 def test_render_without_batch_kwargs_and_no_batch_kwargs_in_citations_uses_blank_batch_kwargs(
     critical_suite_with_citations,
 ):
@@ -534,7 +634,7 @@ def test_render_without_batch_kwargs_and_no_batch_kwargs_in_citations_uses_blank
             },
             {
                 "cell_type": "markdown",
-                "source": "No table level expectations are in this suite. Feel free to add some here. The all begin with `batch.expect_table_...`.",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
                 "metadata": {},
             },
             {
@@ -646,7 +746,7 @@ def test_render_with_batch_kwargs_and_no_batch_kwargs_in_citations(
             },
             {
                 "cell_type": "markdown",
-                "source": "No table level expectations are in this suite. Feel free to add some here. The all begin with `batch.expect_table_...`.",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
                 "metadata": {},
             },
             {
@@ -755,7 +855,7 @@ def test_render_with_no_batch_kwargs_and_no_citations(critical_suite_with_citati
             },
             {
                 "cell_type": "markdown",
-                "source": "No table level expectations are in this suite. Feel free to add some here. The all begin with `batch.expect_table_...`.",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
                 "metadata": {},
             },
             {
@@ -867,7 +967,7 @@ def test_render_with_batch_kwargs_overrides_batch_kwargs_in_citations(
             },
             {
                 "cell_type": "markdown",
-                "source": "No table level expectations are in this suite. Feel free to add some here. The all begin with `batch.expect_table_...`.",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
                 "metadata": {},
             },
             {
@@ -976,7 +1076,7 @@ def test_render_with_no_batch_kwargs_multiple_batch_kwarg_citations(
             },
             {
                 "cell_type": "markdown",
-                "source": "No table level expectations are in this suite. Feel free to add some here. The all begin with `batch.expect_table_...`.",
+                "source": "No table level expectations are in this suite. Feel free to add some here. They all begin with `batch.expect_table_...`.",
                 "metadata": {},
             },
             {