Replace api.path['foo'] with api.path.foo

Also minor other changes to keep coverage.

Bug: 329113288
Change-Id: I5c1828ce6fb8d5c08bd41c4727071b0c91fa7fcf
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/recipes-py/+/5444879
Commit-Queue: Robbie Iannucci <iannucci@chromium.org>
Reviewed-by: Robbie Iannucci <iannucci@chromium.org>
Auto-Submit: Rob Mohr <mohrr@google.com>
diff --git a/README.recipes.md b/README.recipes.md
index b8beb07..9ef5a48 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -173,6 +173,7 @@
   * [nodejs:examples/full](#recipes-nodejs_examples_full)
   * [path:examples/full](#recipes-path_examples_full)
   * [path:tests/cast_to_path](#recipes-path_tests_cast_to_path)
+  * [path:tests/deprecated](#recipes-path_tests_deprecated)
   * [path:tests/dynamic_paths](#recipes-path_tests_dynamic_paths)
   * [path:tests/exists](#recipes-path_tests_exists)
   * [path:tests/test_api_legacy](#recipes-path_tests_test_api_legacy) &mdash; Test to cover legacy aspects of PathTestApi.
@@ -291,7 +292,7 @@
     # Archive root/**
     zip_path = (
       api.archive.package(root).
-      archive('archive step', api.path['start_dir'].join('output.zip'))
+      archive('archive step', api.path.start_dir.join('output.zip'))
     )
 
 Args:
@@ -991,7 +992,7 @@
 
 Args:
   output_dir: The output directory to download the caches to. If you're
-    unsure of what directory to use, self.m.path['start_dir'] is a directory
+    unsure of what directory to use, self.m.path.start_dir is a directory
     the recipe engine sets up for you that you can use.
   caches: A CasCache proto message containing the caches which should be
     downloaded. See properties.proto for the message definition.
@@ -1381,7 +1382,7 @@
 
 Example:
 ```python
-with api.context(cwd=api.path['start_dir'].join('subdir')):
+with api.context(cwd=api.path.start_dir.join('subdir')):
   # this step is run inside of the subdir directory.
   api.step("cat subdir/foo", ['cat', './foo'])
 ```
@@ -1395,7 +1396,7 @@
 Args:
   * cwd (Path) - the current working directory to use for all steps.
     To 'reset' to the original cwd at the time recipes started, pass
-    `api.path['start_dir']`.
+    `api.path.start_dir`.
   * env_prefixes (dict) - Environmental variable prefix augmentations. See
       below for more info.
   * env_suffixes (dict) - Environmental variable suffix augmentations. See
@@ -1444,7 +1445,7 @@
 Returns the current working directory that steps will run in.
 
 **Returns (Path|None)** - The current working directory. A value of None is
-equivalent to api.path['start_dir'], though only occurs if no cwd has been
+equivalent to api.path.start_dir, though only occurs if no cwd has been
 set (e.g. in the outermost context of RunSteps).
 
 &emsp; **@property**<br>&mdash; **def [deadline](/recipe_modules/context/api.py#345)(self):**
@@ -2381,7 +2382,7 @@
 
 #### **class [GeneratorScriptApi](/recipe_modules/generator_script/api.py#12)([RecipeApi](/recipe_engine/recipe_api.py#471)):**
 
-&mdash; **def [\_\_call\_\_](/recipe_modules/generator_script/api.py#71)(self, path_to_script, \*args, \*\*_):**
+&mdash; **def [\_\_call\_\_](/recipe_modules/generator_script/api.py#71)(self, path_to_script, \*args, checkout_dir=None, \*\*_):**
 
 Run a script and generate the steps emitted by that script.
 
@@ -2835,19 +2836,19 @@
 
 In this way, all paths in Recipes are absolute, and are constructed from a small
 collection of anchor points. The built-in anchor points are:
-  * `api.path['start_dir']` - This is the directory that the recipe started in.
+  * `api.path.start_dir` - This is the directory that the recipe started in.
     it's similar to `cwd`, except that it's constant.
-  * `api.path['cache']` - This directory is provided by whatever's running the
+  * `api.path.cache_dir` - This directory is provided by whatever's running the
     recipe. Files and directories created under here /may/ be evicted in between
     runs of the recipe (i.e. to relieve disk pressure).
-  * `api.path['cleanup']` - This directory is provided by whatever's running the
+  * `api.path.cleanup_dir` - This directory is provided by whatever's running the
     recipe. Files and directories created under here /are guaranteed/ to be
     evicted in between runs of the recipe. Additionally, this directory is
     guaranteed to be empty when the recipe starts.
-  * `api.path['tmp_base']` - This directory is the system-configured temp dir.
+  * `api.path.tmp_base_dir` - This directory is the system-configured temp dir.
     This is a weaker form of 'cleanup', and its use should be avoided. This may
     be removed in the future (or converted to an alias of 'cleanup').
-  * `api.path['checkout']` - This directory is set by various checkout modules
+  * `api.path.checkout_dir` - This directory is set by various checkout modules
     in recipes. It was originally intended to make recipes easier to read and
     make code somewhat generic or homogeneous, but this was a mistake. New code
     should avoid 'checkout', and instead just explicitly pass paths around. This
@@ -2855,7 +2856,7 @@
 
 #### **class [PathApi](/recipe_modules/path/api.py#328)([RecipeApi](/recipe_engine/recipe_api.py#471)):**
 
-&mdash; **def [\_\_contains\_\_](/recipe_modules/path/api.py#588)(self, pathname: NamedBasePathsType):**
+&mdash; **def [\_\_contains\_\_](/recipe_modules/path/api.py#589)(self, pathname: NamedBasePathsType):**
 
 This method is DEPRECATED.
 
@@ -2870,7 +2871,7 @@
 a very complicated 'config' system. All of that has been removed, but this
 method remains for now.
 
-&mdash; **def [\_\_getitem\_\_](/recipe_modules/path/api.py#687)(self, name: NamedBasePathsType):**
+&mdash; **def [\_\_getitem\_\_](/recipe_modules/path/api.py#688)(self, name: NamedBasePathsType):**
 
 Gets the base path named `name`. See module docstring for more info.
 
@@ -2885,7 +2886,7 @@
   pass the Paths around instead of using this global variable).
 ***
 
-&mdash; **def [\_\_setitem\_\_](/recipe_modules/path/api.py#606)(self, pathname: CheckoutPathNameType, path: config_types.Path):**
+&mdash; **def [\_\_setitem\_\_](/recipe_modules/path/api.py#607)(self, pathname: CheckoutPathNameType, path: config_types.Path):**
 
 Sets the checkout path.
 
@@ -2895,7 +2896,7 @@
 
 The only valid value of `pathname` is the literal string CheckoutPathName.
 
-&mdash; **def [abs\_to\_path](/recipe_modules/path/api.py#525)(self, abs_string_path: str):**
+&mdash; **def [abs\_to\_path](/recipe_modules/path/api.py#526)(self, abs_string_path: str):**
 
 Converts an absolute path string `abs_string_path` to a real Path
 object, using the most appropriate known base path.
@@ -2924,7 +2925,7 @@
 Raises an ValueError if the preconditions are not met, otherwise returns the
 Path object.
 
-&mdash; **def [abspath](/recipe_modules/path/api.py#796)(self, path: (config_types.Path | str)):**
+&mdash; **def [abspath](/recipe_modules/path/api.py#797)(self, path: (config_types.Path | str)):**
 
 Equivalent to os.abspath.
 
@@ -2935,11 +2936,11 @@
 Args:
   * path - The path to check.
 
-&mdash; **def [basename](/recipe_modules/path/api.py#800)(self, path: (config_types.Path | str)):**
+&mdash; **def [basename](/recipe_modules/path/api.py#801)(self, path: (config_types.Path | str)):**
 
 Equivalent to os.path.basename.
 
-&emsp; **@property**<br>&mdash; **def [cache\_dir](/recipe_modules/path/api.py#730)(self):**
+&emsp; **@property**<br>&mdash; **def [cache\_dir](/recipe_modules/path/api.py#731)(self):**
 
 This directory is provided by whatever's running the recipe.
 
@@ -2962,7 +2963,7 @@
 Note that directories created under here /may/ be evicted in between runs of
 the recipe (i.e. to relieve disk pressure).
 
-&mdash; **def [cast\_to\_path](/recipe_modules/path/api.py#764)(self, strpath: str):**
+&mdash; **def [cast\_to\_path](/recipe_modules/path/api.py#765)(self, strpath: str):**
 
 This returns a Path for strpath which can be used anywhere a Path is
 required.
@@ -2975,20 +2976,20 @@
 cache_dir), the returned Path will be based on that known path. This is
 important for test compatibility.
 
-&emsp; **@checkout_dir.setter**<br>&mdash; **def [checkout\_dir](/recipe_modules/path/api.py#626)(self, path: config_types.Path):**
+&emsp; **@checkout_dir.setter**<br>&mdash; **def [checkout\_dir](/recipe_modules/path/api.py#627)(self, path: config_types.Path):**
 
 Sets the global variable `api.path.checkout_dir` to the given path.
 
     
 
-&emsp; **@property**<br>&mdash; **def [cleanup\_dir](/recipe_modules/path/api.py#755)(self):**
+&emsp; **@property**<br>&mdash; **def [cleanup\_dir](/recipe_modules/path/api.py#756)(self):**
 
 This directory is guaranteed to be cleaned up (eventually) after the
 execution of this recipe.
 
 This directory is guaranteed to be empty when the recipe starts.
 
-&mdash; **def [dirname](/recipe_modules/path/api.py#804)(self, path: (config_types.Path | str)):**
+&mdash; **def [dirname](/recipe_modules/path/api.py#805)(self, path: (config_types.Path | str)):**
 
 For "foo/bar/baz", return "foo/bar".
 
@@ -3001,7 +3002,7 @@
 
 Returns dirname of path
 
-&mdash; **def [eq](/recipe_modules/path/api.py#968)(self, path1: config_types.Path, path2: config_types.Path):**
+&mdash; **def [eq](/recipe_modules/path/api.py#969)(self, path1: config_types.Path, path2: config_types.Path):**
 
 Check whether path1 points to the same path as path2.
 
@@ -3009,20 +3010,20 @@
 **DEPRECATED**: Just directly compare path1 and path2 with `==`.
 ***
 
-&mdash; **def [exists](/recipe_modules/path/api.py#908)(self, path):**
+&mdash; **def [exists](/recipe_modules/path/api.py#909)(self, path):**
 
 Equivalent to os.path.exists.
 
 The presence or absence of paths can be mocked during the execution of the
 recipe by using the mock_* methods.
 
-&mdash; **def [expanduser](/recipe_modules/path/api.py#899)(self, path):**
+&mdash; **def [expanduser](/recipe_modules/path/api.py#900)(self, path):**
 
-Do not use this, use `api.path['home']` instead.
+Do not use this, use `api.path.home_dir` instead.
 
-This ONLY handles `path` == "~", and returns `str(api.path['home'])`.
+This ONLY handles `path` == "~", and returns `str(api.path.home_dir)`.
 
-&mdash; **def [get](/recipe_modules/path/api.py#655)(self, name: NamedBasePathsType):**
+&mdash; **def [get](/recipe_modules/path/api.py#656)(self, name: NamedBasePathsType):**
 
 Gets the base path named `name`. See module docstring for more info.
 
@@ -3037,7 +3038,7 @@
   pass the Paths around instead of using this global variable).
 ***
 
-&emsp; **@property**<br>&mdash; **def [home\_dir](/recipe_modules/path/api.py#712)(self):**
+&emsp; **@property**<br>&mdash; **def [home\_dir](/recipe_modules/path/api.py#713)(self):**
 
 This is the path to the current $HOME directory.
 
@@ -3049,7 +3050,7 @@
 This is called by the recipe engine immediately after __init__(), but
 with `self._paths_client` initialized.
 
-&mdash; **def [is\_parent\_of](/recipe_modules/path/api.py#975)(self, parent: config_types.Path, child: config_types.Path):**
+&mdash; **def [is\_parent\_of](/recipe_modules/path/api.py#976)(self, parent: config_types.Path, child: config_types.Path):**
 
 Check whether child is contained within parent.
 
@@ -3057,32 +3058,32 @@
 **DEPRECATED**: Just use `parent.is_parent_of(child)`.
 ***
 
-&mdash; **def [isdir](/recipe_modules/path/api.py#916)(self, path):**
+&mdash; **def [isdir](/recipe_modules/path/api.py#917)(self, path):**
 
 Equivalent to os.path.isdir.
 
 The presence or absence of paths can be mocked during the execution of the
 recipe by using the mock_* methods.
 
-&mdash; **def [isfile](/recipe_modules/path/api.py#924)(self, path):**
+&mdash; **def [isfile](/recipe_modules/path/api.py#925)(self, path):**
 
 Equivalent to os.path.isfile.
 
 The presence or absence of paths can be mocked during the execution of the
 recipe by using the mock_* methods.
 
-&mdash; **def [join](/recipe_modules/path/api.py#823)(self, path, \*paths):**
+&mdash; **def [join](/recipe_modules/path/api.py#824)(self, path, \*paths):**
 
 Equivalent to os.path.join.
 
 Note that Path objects returned from this module (e.g.
-api.path['start_dir']) have a built-in join method (e.g.
+api.path.start_dir) have a built-in join method (e.g.
 new_path = p.join('some', 'name')). Many recipe modules expect Path objects
 rather than strings. Using this `join` method gives you raw path joining
 functionality and returns a string.
 
 If your path is rooted in one of the path module's root paths (i.e. those
-retrieved with api.path[something]), then you can convert from a string path
+retrieved with api.path.something), then you can convert from a string path
 back to a Path with the `abs_to_path` method.
 
 &mdash; **def [mkdtemp](/recipe_modules/path/api.py#474)(self, prefix: str=tempfile.template):**
@@ -3094,7 +3095,7 @@
 
 Returns a Path to the new directory.
 
-&mdash; **def [mkstemp](/recipe_modules/path/api.py#497)(self, prefix: str=tempfile.template):**
+&mdash; **def [mkstemp](/recipe_modules/path/api.py#498)(self, prefix: str=tempfile.template):**
 
 Makes a new temporary file, returns Path to it.
 
@@ -3109,23 +3110,23 @@
 either a resource script of your recipe module or recipe.
 ***
 
-&mdash; **def [mock\_add\_directory](/recipe_modules/path/api.py#943)(self, path: config_types.Path):**
+&mdash; **def [mock\_add\_directory](/recipe_modules/path/api.py#944)(self, path: config_types.Path):**
 
 For testing purposes, mark that file |path| exists.
 
-&mdash; **def [mock\_add\_file](/recipe_modules/path/api.py#939)(self, path: config_types.Path):**
+&mdash; **def [mock\_add\_file](/recipe_modules/path/api.py#940)(self, path: config_types.Path):**
 
 For testing purposes, mark that file |path| exists.
 
-&mdash; **def [mock\_add\_paths](/recipe_modules/path/api.py#932)(self, path: config_types.Path, kind: FileType=FileType.FILE):**
+&mdash; **def [mock\_add\_paths](/recipe_modules/path/api.py#933)(self, path: config_types.Path, kind: FileType=FileType.FILE):**
 
 For testing purposes, mark that |path| exists.
 
-&mdash; **def [mock\_copy\_paths](/recipe_modules/path/api.py#947)(self, source: config_types.Path, dest: config_types.Path):**
+&mdash; **def [mock\_copy\_paths](/recipe_modules/path/api.py#948)(self, source: config_types.Path, dest: config_types.Path):**
 
 For testing purposes, copy |source| to |dest|.
 
-&mdash; **def [mock\_remove\_paths](/recipe_modules/path/api.py#954)(self, path: config_types.Path, should_remove: Callable[([str], bool)]=(lambda p: True)):**
+&mdash; **def [mock\_remove\_paths](/recipe_modules/path/api.py#955)(self, path: config_types.Path, should_remove: Callable[([str], bool)]=(lambda p: True)):**
 
 For testing purposes, mark that |path| doesn't exist.
 
@@ -3134,34 +3135,34 @@
   should_remove: Called for every candidate path. Return True to remove this
     path.
 
-&mdash; **def [normpath](/recipe_modules/path/api.py#895)(self, path):**
+&mdash; **def [normpath](/recipe_modules/path/api.py#896)(self, path):**
 
 Equivalent to os.path.normpath.
 
-&emsp; **@property**<br>&mdash; **def [pardir](/recipe_modules/path/api.py#781)(self):**
+&emsp; **@property**<br>&mdash; **def [pardir](/recipe_modules/path/api.py#782)(self):**
 
 Equivalent to os.pardir.
 
-&emsp; **@property**<br>&mdash; **def [pathsep](/recipe_modules/path/api.py#791)(self):**
+&emsp; **@property**<br>&mdash; **def [pathsep](/recipe_modules/path/api.py#792)(self):**
 
 Equivalent to os.pathsep.
 
-&mdash; **def [realpath](/recipe_modules/path/api.py#883)(self, path: (config_types.Path | str)):**
+&mdash; **def [realpath](/recipe_modules/path/api.py#884)(self, path: (config_types.Path | str)):**
 
 Equivalent to os.path.realpath.
 
-&mdash; **def [relpath](/recipe_modules/path/api.py#887)(self, path, start):**
+&mdash; **def [relpath](/recipe_modules/path/api.py#888)(self, path, start):**
 
 Roughly equivalent to os.path.relpath.
 
 Unlike os.path.relpath, `start` is _required_. If you want the 'current
 directory', use the `recipe_engine/context` module's `cwd` property.
 
-&emsp; **@property**<br>&mdash; **def [sep](/recipe_modules/path/api.py#786)(self):**
+&emsp; **@property**<br>&mdash; **def [sep](/recipe_modules/path/api.py#787)(self):**
 
 Equivalent to os.sep.
 
-&mdash; **def [split](/recipe_modules/path/api.py#838)(self, path):**
+&mdash; **def [split](/recipe_modules/path/api.py#839)(self, path):**
 
 For "foo/bar/baz", return ("foo/bar", "baz").
 
@@ -3175,7 +3176,7 @@
 
 Returns (dirname(path), basename(path)).
 
-&mdash; **def [splitext](/recipe_modules/path/api.py#859)(self, path: (config_types.Path | str)):**
+&mdash; **def [splitext](/recipe_modules/path/api.py#860)(self, path: (config_types.Path | str)):**
 
 For "foo/bar.baz", return ("foo/bar", ".baz").
 
@@ -3190,7 +3191,7 @@
 Returns:
   (name, extension_including_dot).
 
-&emsp; **@property**<br>&mdash; **def [start\_dir](/recipe_modules/path/api.py#701)(self):**
+&emsp; **@property**<br>&mdash; **def [start\_dir](/recipe_modules/path/api.py#702)(self):**
 
 This is the directory that the recipe started in. it's similar to `cwd`,
 except that it's constant for the duration of the entire program.
@@ -3199,7 +3200,7 @@
 See the 'recipe_engine/context' module which allows modifying the cwd safely
 via a context manager.
 
-&emsp; **@property**<br>&mdash; **def [tmp\_base\_dir](/recipe_modules/path/api.py#721)(self):**
+&emsp; **@property**<br>&mdash; **def [tmp\_base\_dir](/recipe_modules/path/api.py#722)(self):**
 
 This directory is the system-configured temp dir.
 
@@ -4322,17 +4323,17 @@
 # output path, cwd and cache directory.
 with api.context(
     # Change the cwd of the launched LUCI executable
-    cwd=api.path['start_dir'].join('subdir'),
+    cwd=api.path.start_dir.join('subdir'),
     # Change the cache_dir of the launched LUCI executable. Defaults to
-    # api.path['cache'] if unchanged.
-    luciexe=sections_pb2.LUCIExe(cache_dir=api.path['cache'].join('sub')),
+    # api.path.cache_dir if unchanged.
+    luciexe=sections_pb2.LUCIExe(cache_dir=api.path.cache_dir.join('sub')),
   ):
   # Command executed:
   #   `/path/to/run_exe --output [CLEANUP]/build.json --foo bar baz`
   ret = api.sub_build("launch sub build",
                       [run_exe, '--foo', 'bar', 'baz'],
                       api.buildbucket.build,
-                      output_path=api.path['cleanup'].join('build.json'))
+                      output_path=api.path.cleanup_dir.join('build.json'))
 sub_build = ret.step.sub_build  # access final build proto result
 ```
 
@@ -5720,16 +5721,22 @@
 &mdash; **def [RunSteps](/recipe_modules/nodejs/examples/full.py#12)(api):**
 ### *recipes* / [path:examples/full](/recipe_modules/path/examples/full.py)
 
-[DEPS](/recipe_modules/path/examples/full.py#5): [json](#recipe_modules-json), [path](#recipe_modules-path), [platform](#recipe_modules-platform), [properties](#recipe_modules-properties), [step](#recipe_modules-step)
+[DEPS](/recipe_modules/path/examples/full.py#7): [json](#recipe_modules-json), [path](#recipe_modules-path), [platform](#recipe_modules-platform), [properties](#recipe_modules-properties), [step](#recipe_modules-step)
 
 
-&mdash; **def [RunSteps](/recipe_modules/path/examples/full.py#16)(api):**
+&emsp; **@recipe_api.ignore_warnings('recipe_engine/CHECKOUT_DIR_DEPRECATED')**<br>&mdash; **def [RunSteps](/recipe_modules/path/examples/full.py#18)(api):**
 ### *recipes* / [path:tests/cast\_to\_path](/recipe_modules/path/tests/cast_to_path.py)
 
 [DEPS](/recipe_modules/path/tests/cast_to_path.py#8): [path](#recipe_modules-path), [platform](#recipe_modules-platform)
 
 
 &mdash; **def [RunSteps](/recipe_modules/path/tests/cast_to_path.py#14)(api):**
+### *recipes* / [path:tests/deprecated](/recipe_modules/path/tests/deprecated.py)
+
+[DEPS](/recipe_modules/path/tests/deprecated.py#7): [path](#recipe_modules-path), [step](#recipe_modules-step)
+
+
+&emsp; **@recipe_api.ignore_warnings('recipe_engine/CHECKOUT_DIR_DEPRECATED', 'recipe_engine/PATH_GETITEM_DEPRECATED')**<br>&mdash; **def [RunSteps](/recipe_modules/path/tests/deprecated.py#13)(api):**
 ### *recipes* / [path:tests/dynamic\_paths](/recipe_modules/path/tests/dynamic_paths.py)
 
 [DEPS](/recipe_modules/path/tests/dynamic_paths.py#7): [path](#recipe_modules-path)
diff --git a/doc/walkthrough.md b/doc/walkthrough.md
index 5826872..1ebbacd 100644
--- a/doc/walkthrough.md
+++ b/doc/walkthrough.md
@@ -692,7 +692,7 @@
 
   def greet(self, default_verb=None):
     self.m.step('Greet Admired Individual', [
-        self.m.path['start_dir'].join(self.c.tool),
+        self.m.path.start_dir.join(self.c.tool),
         self.c.verb % self.c.TARGET])
 ```
 
@@ -836,15 +836,15 @@
 def RunSteps(api):
   step_result = api.step(
       'Determine blue moon',
-      [api.path['start_dir'].join('is_blue_moon.sh')],
+      [api.path.start_dir.join('is_blue_moon.sh')],
       ok_ret='any')
 
   if step_result.retcode == 0:
     api.step('HARLEM SHAKE!',
-             [api.path['start_dir'].join('do_the_harlem_shake.sh')])
+             [api.path.start_dir.join('do_the_harlem_shake.sh')])
   else:
     api.step('Boring',
-             [api.path['start_dir'].join('its_a_small_world.sh')])
+             [api.path.start_dir.join('its_a_small_world.sh')])
 
 def GenTests(api):
   yield api.test(
@@ -897,14 +897,14 @@
 def RunSteps(api):
   step_result = api.step(
       'run tests',
-      [api.path['start_dir'].join('do_test_things.sh'), api.json.output()])
+      [api.path.start_dir.join('do_test_things.sh'), api.json.output()])
   num_passed = step_result.json.output['num_passed']
   if num_passed > 500:
-    api.step('victory', [api.path['start_dir'].join('do_a_dance.sh')])
+    api.step('victory', [api.path.start_dir.join('do_a_dance.sh')])
   elif num_passed > 200:
-    api.step('not defeated', [api.path['start_dir'].join('woohoo.sh')])
+    api.step('not defeated', [api.path.start_dir.join('woohoo.sh')])
   else:
-    api.step('deads!', [api.path['start_dir'].join('you_r_deads.sh')])
+    api.step('deads!', [api.path.start_dir.join('you_r_deads.sh')])
 
 def GenTests(api):
   yield api.test(
diff --git a/misc/fake_bbagent.sh b/misc/fake_bbagent.sh
index 9ac56b7..e1c5c3d 100755
--- a/misc/fake_bbagent.sh
+++ b/misc/fake_bbagent.sh
@@ -14,9 +14,9 @@
 #
 # This puts all the outputs from the executed recipe in the current git repo's
 # //workdir directory:
-#   * //workdir/tmp   - api.path['tmp_base']
-#   * //workdir/cache - api.path['cache']
-#   * //workdir/wd    - api.path['start_dir']
+#   * //workdir/tmp   - api.path.tmp_base_dir
+#   * //workdir/cache - api.path.cache_dir
+#   * //workdir/wd    - api.path.start_dir
 #   * //workdir/logs  - Dumps of all the logdog streams emitted by the recipe
 #     engine (and any child processes).
 #
diff --git a/recipe_modules/archive/api.py b/recipe_modules/archive/api.py
index cff331a..0a801d6 100644
--- a/recipe_modules/archive/api.py
+++ b/recipe_modules/archive/api.py
@@ -27,7 +27,7 @@
         # Archive root/**
         zip_path = (
           api.archive.package(root).
-          archive('archive step', api.path['start_dir'].join('output.zip'))
+          archive('archive step', api.path.start_dir.join('output.zip'))
         )
 
     Args:
diff --git a/recipe_modules/archive/examples/full.py b/recipe_modules/archive/examples/full.py
index 22454d0..c9d4e51 100644
--- a/recipe_modules/archive/examples/full.py
+++ b/recipe_modules/archive/examples/full.py
@@ -18,7 +18,7 @@
 
 def RunSteps(api):
   # Prepare directories.
-  out = api.path['start_dir'].join('output')
+  out = api.path.start_dir.join('output')
   api.file.rmtree('cleanup', out)
   api.file.ensure_directory('mkdirs out', out)
 
diff --git a/recipe_modules/bcid_reporter/api.py b/recipe_modules/bcid_reporter/api.py
index 46d9f90..1a51c71 100644
--- a/recipe_modules/bcid_reporter/api.py
+++ b/recipe_modules/bcid_reporter/api.py
@@ -31,7 +31,7 @@
     broker will be installed using cipd.
     """
     if self._broker_bin is None:
-      reporter_dir = self.m.path['start_dir'].join('reporter')
+      reporter_dir = self.m.path.start_dir.join('reporter')
       ensure_file = self.m.cipd.EnsureFile().add_package(
           'infra/tools/security/provenance_broker/${platform}',
           _LATEST_STABLE_VERSION)
diff --git a/recipe_modules/buildbucket/api.py b/recipe_modules/buildbucket/api.py
index e9f4af8..2214cc4 100644
--- a/recipe_modules/buildbucket/api.py
+++ b/recipe_modules/buildbucket/api.py
@@ -283,7 +283,7 @@
     See "Builder cache" in
     https://chromium.googlesource.com/infra/luci/luci-go/+/main/buildbucket/proto/project_config.proto
     """
-    return self.m.path['cache'].join('builder')
+    return self.m.path.cache_dir.join('builder')
 
   # RPCs.
 
diff --git a/recipe_modules/cas_input/api.py b/recipe_modules/cas_input/api.py
index 0efd691..d42356a 100644
--- a/recipe_modules/cas_input/api.py
+++ b/recipe_modules/cas_input/api.py
@@ -34,7 +34,7 @@
 
     Args:
       output_dir: The output directory to download the caches to. If you're
-        unsure of what directory to use, self.m.path['start_dir'] is a directory
+        unsure of what directory to use, self.m.path.start_dir is a directory
         the recipe engine sets up for you that you can use.
       caches: A CasCache proto message containing the caches which should be
         downloaded. See properties.proto for the message definition.
diff --git a/recipe_modules/cas_input/examples/full.py b/recipe_modules/cas_input/examples/full.py
index 657e0ee..a1ce3d3 100644
--- a/recipe_modules/cas_input/examples/full.py
+++ b/recipe_modules/cas_input/examples/full.py
@@ -14,7 +14,10 @@
 
 
 def RunSteps(api):
-  download_dir = api.path[api.properties.get('download_dir', 'start_dir')]
+  if dd := api.properties.get('download_dir'):
+    download_dir = api.path.abs_to_path(dd)
+  else:
+    download_dir = api.path.start_dir
   api.cas_input.download_caches(download_dir)
 
 
@@ -35,7 +38,7 @@
       'download_to_directory',
       cas_props(
           InputProperties(caches=[CasCache(digest='deadbeef')]),
-          download_dir='tmp_base'),
+          download_dir='[TMP_BASE]'),
       api.post_process(StepSuccess, 'download cache'),
       api.post_process(StepCommandContains, 'download cache', '[TMP_BASE]'),
       api.post_process(DropExpectation))
diff --git a/recipe_modules/cipd/api.py b/recipe_modules/cipd/api.py
index d594c07..2371b00 100644
--- a/recipe_modules/cipd/api.py
+++ b/recipe_modules/cipd/api.py
@@ -1017,7 +1017,7 @@
     cache_key = (package, version)
 
     package_parts = [p for p in package.split('/') if '${' not in p]
-    package_dir = self.m.path['start_dir'].join('cipd_tool', *package_parts)
+    package_dir = self.m.path.start_dir.join('cipd_tool', *package_parts)
     # Hashing the version is the easiest way to produce a string with no special
     # characters e.g. removing colons which don't work on Windows.
     package_dir = package_dir.join(
diff --git a/recipe_modules/cipd/examples/full.py b/recipe_modules/cipd/examples/full.py
index 1942fae..7c24c8a 100644
--- a/recipe_modules/cipd/examples/full.py
+++ b/recipe_modules/cipd/examples/full.py
@@ -46,7 +46,7 @@
       for i, v in enumerate(metadata)
   ]
 
-  cipd_root = api.path['start_dir'].join('packages')
+  cipd_root = api.path.start_dir.join('packages')
   # Some packages don't require credentials to be installed or queried.
   api.cipd.ensure(cipd_root, ensure_file)
   api.cipd.ensure_file_resolve(ensure_file)
@@ -95,7 +95,7 @@
 
   # Create (build & register).
   if use_pkg:
-    root = api.path['start_dir'].join('some_subdir')
+    root = api.path.start_dir.join('some_subdir')
     pkg = api.cipd.PackageDefinition(
         'infra/fake-package',
         root,
@@ -118,13 +118,13 @@
 
     api.cipd.create_from_pkg(pkg, refs=refs, tags=tags, metadata=md)
   else:
-    api.cipd.build_from_yaml(api.path['start_dir'].join('fake-package.yaml'),
+    api.cipd.build_from_yaml(api.path.start_dir.join('fake-package.yaml'),
                              'fake-package-path', pkg_vars=pkg_vars,
                              compression_level=9)
     api.cipd.register('infra/fake-package', 'fake-package-path',
                       refs=refs, tags=tags, metadata=md)
 
-    api.cipd.create_from_yaml(api.path['start_dir'].join('fake-package.yaml'),
+    api.cipd.create_from_yaml(api.path.start_dir.join('fake-package.yaml'),
                               refs=refs, tags=tags, metadata=md,
                               pkg_vars=pkg_vars, compression_level=9,
                               verification_timeout='20m')
@@ -145,30 +145,30 @@
       api.cipd.Metadata(key='key1', value='val2', content_type='text/plain'),
       api.cipd.Metadata(
           key='key2',
-          value_from_file=api.path['start_dir'].join('val1.json'),
+          value_from_file=api.path.start_dir.join('val1.json'),
       ),
       api.cipd.Metadata(
           key='key2',
-          value_from_file=api.path['start_dir'].join('val2.json'),
+          value_from_file=api.path.start_dir.join('val2.json'),
           content_type='application/json',
       ),
   ])
 
   # Fetch a raw package
-  api.cipd.pkg_fetch(api.path['start_dir'].join('fetched_pkg'),
+  api.cipd.pkg_fetch(api.path.start_dir.join('fetched_pkg'),
                      'fake-package/${platform}', 'some:tag')
 
   # Deploy a raw package
   api.cipd.pkg_deploy(
-    api.path['start_dir'].join('raw_root'),
-    api.path['start_dir'].join('fetched_pkg'))
+    api.path.start_dir.join('raw_root'),
+    api.path.start_dir.join('fetched_pkg'))
 
   api.cipd.ensure(
       cipd_root,
-      api.path['start_dir'].join('cipd.ensure'),
+      api.path.start_dir.join('cipd.ensure'),
       name='ensure with existing file')
   api.cipd.ensure_file_resolve(
-      api.path['start_dir'].join('cipd.ensure'),
+      api.path.start_dir.join('cipd.ensure'),
       name='ensure-file-resolve with existing file')
 
   # Install a tool using the high-level helper function. This operation should
diff --git a/recipe_modules/context/api.py b/recipe_modules/context/api.py
index 694c3ac..d0a1007 100644
--- a/recipe_modules/context/api.py
+++ b/recipe_modules/context/api.py
@@ -20,7 +20,7 @@
 
 Example:
 ```python
-with api.context(cwd=api.path['start_dir'].join('subdir')):
+with api.context(cwd=api.path.start_dir.join('subdir')):
   # this step is run inside of the subdir directory.
   api.step("cat subdir/foo", ['cat', './foo'])
 ```
@@ -114,7 +114,7 @@
     Args:
       * cwd (Path) - the current working directory to use for all steps.
         To 'reset' to the original cwd at the time recipes started, pass
-        `api.path['start_dir']`.
+        `api.path.start_dir`.
       * env_prefixes (dict) - Environmental variable prefix augmentations. See
           below for more info.
       * env_suffixes (dict) - Environmental variable suffix augmentations. See
@@ -254,7 +254,7 @@
     """Returns the current working directory that steps will run in.
 
     **Returns (Path|None)** - The current working directory. A value of None is
-    equivalent to api.path['start_dir'], though only occurs if no cwd has been
+    equivalent to api.path.start_dir, though only occurs if no cwd has been
     set (e.g. in the outermost context of RunSteps).
     """
     return self._state.cwd
diff --git a/recipe_modules/context/examples/full.py b/recipe_modules/context/examples/full.py
index 9f16869..315a6be 100644
--- a/recipe_modules/context/examples/full.py
+++ b/recipe_modules/context/examples/full.py
@@ -26,13 +26,13 @@
 
   # can change cwd
   api.step('mk subdir', ['mkdir', '-p', 'subdir'])
-  with api.context(cwd=api.path['start_dir'].join('subdir')):
+  with api.context(cwd=api.path.start_dir.join('subdir')):
     api.step('subdir step', ['bash', '-c', 'pwd'])
     api.step('other subdir step', ['bash', '-c', 'echo hi again!'])
 
   # can set envvars, and path prefix.
-  pants = api.path['start_dir'].join('pants')
-  shirt = api.path['start_dir'].join('shirt')
+  pants = api.path.start_dir.join('pants')
+  shirt = api.path.start_dir.join('shirt')
   with api.context(env={'FOO': 'bar'}):
     api.step('env step', ['bash', '-c', 'echo $FOO'])
 
diff --git a/recipe_modules/context/tests/cwd.py b/recipe_modules/context/tests/cwd.py
index 2e408d9..91f2a4a 100644
--- a/recipe_modules/context/tests/cwd.py
+++ b/recipe_modules/context/tests/cwd.py
@@ -11,7 +11,7 @@
 def RunSteps(api):
   api.step('no cwd', ['echo', 'hello'])
 
-  with api.context(cwd=api.path['start_dir'].join('subdir')):
+  with api.context(cwd=api.path.start_dir.join('subdir')):
     api.step('with cwd', ['echo', 'hello', 'subdir'])
 
   with api.context(cwd=None):
diff --git a/recipe_modules/context/tests/env.py b/recipe_modules/context/tests/env.py
index 136a83b..dcca227 100644
--- a/recipe_modules/context/tests/env.py
+++ b/recipe_modules/context/tests/env.py
@@ -45,10 +45,10 @@
   with api.context(env={_KEY: None}):
     expect_step('drop', '')
 
-  pants = api.path['start_dir'].join('pants')
-  shirt = api.path['start_dir'].join('shirt')
-  good_hat = api.path['start_dir'].join('good_hat')
-  bad_hat = api.path['start_dir'].join('bad_hat')
+  pants = api.path.start_dir.join('pants')
+  shirt = api.path.start_dir.join('shirt')
+  good_hat = api.path.start_dir.join('good_hat')
+  bad_hat = api.path.start_dir.join('bad_hat')
   with api.context(env={_KEY: 'bar'}):
     expect_step('env step', 'bar')
 
diff --git a/recipe_modules/file/examples/compute_hash.py b/recipe_modules/file/examples/compute_hash.py
index e0e1fc5..9a7faa7 100644
--- a/recipe_modules/file/examples/compute_hash.py
+++ b/recipe_modules/file/examples/compute_hash.py
@@ -9,8 +9,8 @@
 ]
 
 def RunSteps(api):
-  base_path = api.path['start_dir']
-  some_dir = api.path['start_dir'].join('some_dir')
+  base_path = api.path.start_dir
+  some_dir = api.path.start_dir.join('some_dir')
   api.file.ensure_directory('ensure some_dir', some_dir)
 
   some_file = some_dir.join('some file')
@@ -26,7 +26,7 @@
   expected = 'deadbeef'
   api.assertions.assertEqual(result, expected)
 
-  some_other_dir = api.path['start_dir'].join('some_other_dir')
+  some_other_dir = api.path.start_dir.join('some_other_dir')
   api.file.ensure_directory('ensure some_other_dir', some_other_dir)
 
   some_other_file = some_other_dir.join('new_f')
@@ -39,7 +39,7 @@
   expected = 'abcdefab'
   api.assertions.assertEqual(result, expected)
 
-  another_file = api.path['start_dir'].join('another_file')
+  another_file = api.path.start_dir.join('another_file')
   api.file.write_text('write another file', another_file, 'some data')
 
   result = api.file.compute_hash('compute_hash of list of dirs and file',
diff --git a/recipe_modules/file/examples/copy.py b/recipe_modules/file/examples/copy.py
index df0ee62..f76fcbc 100644
--- a/recipe_modules/file/examples/copy.py
+++ b/recipe_modules/file/examples/copy.py
@@ -10,21 +10,21 @@
 
 
 def RunSteps(api):
-  dest = api.path['start_dir'].join('some file')
+  dest = api.path.start_dir.join('some file')
   data = 'Here is some text data'
 
   api.file.write_text('write a file', dest, data)
-  api.file.copy('copy it', dest, api.path['start_dir'].join('new path'))
+  api.file.copy('copy it', dest, api.path.start_dir.join('new path'))
   read_data = api.file.read_text(
-    'read it', api.path['start_dir'].join('new path'), test_data=data)
+    'read it', api.path.start_dir.join('new path'), test_data=data)
 
   assert read_data == data, (read_data, data)
 
-  api.file.move('move it', api.path['start_dir'].join('new path'),
-                api.path['start_dir'].join('new new path'))
+  api.file.move('move it', api.path.start_dir.join('new path'),
+                api.path.start_dir.join('new new path'))
 
   read_data = api.file.read_text(
-    'read it', api.path['start_dir'].join('new new path'), test_data=data)
+    'read it', api.path.start_dir.join('new new path'), test_data=data)
 
   assert read_data == data, (read_data, data)
 
diff --git a/recipe_modules/file/examples/copytree.py b/recipe_modules/file/examples/copytree.py
index 8834444..d80b945 100644
--- a/recipe_modules/file/examples/copytree.py
+++ b/recipe_modules/file/examples/copytree.py
@@ -11,13 +11,13 @@
 def RunSteps(api):
   file_names = ['a', 'aa', 'b', 'bb', 'c', 'cc']
 
-  dest = api.path['start_dir'].join('some dir')
+  dest = api.path.start_dir.join('some dir')
   api.file.ensure_directory('ensure "some dir"', dest)
   for fname in file_names:
     api.file.write_text('write %s' % fname, dest.join(fname), fname)
   api.file.filesizes('check filesizes', [dest.join(f) for f in file_names])
 
-  dest2 = api.path['start_dir'].join('some other dir')
+  dest2 = api.path.start_dir.join('some other dir')
   api.file.rmtree('make sure dest is gone', dest2)
   api.file.copytree('copy it', dest, dest2)
 
diff --git a/recipe_modules/file/examples/error.py b/recipe_modules/file/examples/error.py
index e625fe7..2a41c90 100644
--- a/recipe_modules/file/examples/error.py
+++ b/recipe_modules/file/examples/error.py
@@ -11,7 +11,7 @@
 def RunSteps(api):
   try:
     api.file.read_text(
-      'does not exist', api.path['start_dir'].join('not_there'))
+      'does not exist', api.path.start_dir.join('not_there'))
     assert False, "never reached"  # pragma: no cover
   except api.file.Error as e:
     assert e.errno_name == 'ENOENT'
diff --git a/recipe_modules/file/examples/file_hash.py b/recipe_modules/file/examples/file_hash.py
index d06aa11..5a19d01 100644
--- a/recipe_modules/file/examples/file_hash.py
+++ b/recipe_modules/file/examples/file_hash.py
@@ -9,7 +9,7 @@
 ]
 
 def RunSteps(api):
-  some_dir = api.path['start_dir'].join('some_dir')
+  some_dir = api.path.start_dir.join('some_dir')
   api.file.ensure_directory('ensure some_dir', some_dir)
 
   some_file = some_dir.join('some file')
@@ -21,7 +21,7 @@
   expected = 'deadbeef'
   api.assertions.assertEqual(result, expected)
 
-  another_file = api.path['start_dir'].join('another_file')
+  another_file = api.path.start_dir.join('another_file')
   api.file.write_text('write another file', another_file, 'some data')
 
   result = api.file.file_hash(another_file,
diff --git a/recipe_modules/file/examples/flatten_single_directories.py b/recipe_modules/file/examples/flatten_single_directories.py
index 733562a..46a1d24 100644
--- a/recipe_modules/file/examples/flatten_single_directories.py
+++ b/recipe_modules/file/examples/flatten_single_directories.py
@@ -9,7 +9,7 @@
 
 
 def RunSteps(api):
-  base = api.path['start_dir'].join('dir')
+  base = api.path.start_dir.join('dir')
   long_dir = base.join('which_has', 'some', 'singular', 'subdirs')
 
   api.file.ensure_directory('make chain of single dirs', long_dir)
diff --git a/recipe_modules/file/examples/glob.py b/recipe_modules/file/examples/glob.py
index 8bc5c2b..9a7f94c 100644
--- a/recipe_modules/file/examples/glob.py
+++ b/recipe_modules/file/examples/glob.py
@@ -9,7 +9,7 @@
 ]
 
 def RunSteps(api):
-  sd = api.path['start_dir']
+  sd = api.path.start_dir
 
   api.file.ensure_directory('mkdir a', sd.join('a'))
   api.file.ensure_directory('mkdir b', sd.join('b'))
diff --git a/recipe_modules/file/examples/handle_json_file.py b/recipe_modules/file/examples/handle_json_file.py
index b1d0c56..fa5c44c 100644
--- a/recipe_modules/file/examples/handle_json_file.py
+++ b/recipe_modules/file/examples/handle_json_file.py
@@ -9,7 +9,7 @@
 
 
 def RunSteps(api):
-  dest = api.path['start_dir'].join('some_file.json')
+  dest = api.path.start_dir.join('some_file.json')
   # Test a non-trivial number of keys in a dict.  This tests that the keys
   # are sorted in the output.
   data = {str('key%d' % i): True for i in range(10)}
diff --git a/recipe_modules/file/examples/listdir.py b/recipe_modules/file/examples/listdir.py
index dfc66b8..4610ad2 100644
--- a/recipe_modules/file/examples/listdir.py
+++ b/recipe_modules/file/examples/listdir.py
@@ -9,7 +9,7 @@
 
 
 def RunSteps(api):
-  root_dir = api.path['start_dir'].join('root_dir')
+  root_dir = api.path.start_dir.join('root_dir')
   api.file.ensure_directory('ensure root_dir', root_dir)
 
   listdir_result = api.file.listdir('listdir root_dir', root_dir, test_data=[])
diff --git a/recipe_modules/file/examples/raw_copy.py b/recipe_modules/file/examples/raw_copy.py
index f9eb69d..2596c5e 100644
--- a/recipe_modules/file/examples/raw_copy.py
+++ b/recipe_modules/file/examples/raw_copy.py
@@ -10,21 +10,21 @@
 
 
 def RunSteps(api):
-  dest = api.path['start_dir'].join('some file')
+  dest = api.path.start_dir.join('some file')
   data = b'\xef\xbb\xbft'
 
   api.file.write_raw('write a file', dest, data)
-  api.file.copy('copy it', dest, api.path['start_dir'].join('new path'))
+  api.file.copy('copy it', dest, api.path.start_dir.join('new path'))
   read_data = api.file.read_raw(
-    'read it', api.path['start_dir'].join('new path'), test_data=data)
+    'read it', api.path.start_dir.join('new path'), test_data=data)
 
   assert read_data == data, (read_data, data)
 
-  api.file.move('move it', api.path['start_dir'].join('new path'),
-                api.path['start_dir'].join('new new path'))
+  api.file.move('move it', api.path.start_dir.join('new path'),
+                api.path.start_dir.join('new new path'))
 
   read_data = api.file.read_raw(
-    'read it', api.path['start_dir'].join('new new path'), test_data=data)
+    'read it', api.path.start_dir.join('new new path'), test_data=data)
 
   assert read_data == data, (read_data, data)
 
diff --git a/recipe_modules/file/examples/read_write_proto.py b/recipe_modules/file/examples/read_write_proto.py
index 44c89da..c8b707b 100644
--- a/recipe_modules/file/examples/read_write_proto.py
+++ b/recipe_modules/file/examples/read_write_proto.py
@@ -14,7 +14,7 @@
 def RunSteps(api):
   msg = SomeMessage(fields=['abc', 'def'])
 
-  dest = api.path['start_dir'].join('message.textproto')
+  dest = api.path.start_dir.join('message.textproto')
   api.file.write_proto('write_proto', dest, msg, 'TEXTPB')
 
   read_msg = api.file.read_proto(
diff --git a/recipe_modules/file/examples/symlink.py b/recipe_modules/file/examples/symlink.py
index 13dda8b..e87b239 100644
--- a/recipe_modules/file/examples/symlink.py
+++ b/recipe_modules/file/examples/symlink.py
@@ -10,25 +10,25 @@
 
 
 def RunSteps(api):
-  src = api.path['start_dir'].join('some file')
+  src = api.path.start_dir.join('some file')
   data = 'Here is some text data'
 
   api.file.write_text('write a file', src, data)
-  api.file.symlink('symlink it', src, api.path['start_dir'].join('new path'))
+  api.file.symlink('symlink it', src, api.path.start_dir.join('new path'))
   read_data = api.file.read_text(
-    'read it', api.path['start_dir'].join('new path'), test_data=data)
+    'read it', api.path.start_dir.join('new path'), test_data=data)
 
   assert read_data == data, (read_data, data)
 
 
   # Also create a tree of symlinks.
-  root = api.path['cleanup'].join('root')
+  root = api.path.cleanup_dir.join('root')
   tree = api.file.symlink_tree(root)
   assert root == tree.root
   # It is okay to register the same pair multiple times.
   tree.register_link(src, root.join('another', 'symlink'))
   tree.register_link(src, root.join('another', 'symlink'))
-  src2 = api.path['start_dir'].join('a-second-file')
+  src2 = api.path.start_dir.join('a-second-file')
   tree.register_link(src2, root.join('yet', 'another', 'symlink'))
   tree.create_links('create a tree of symlinks')
 
diff --git a/recipe_modules/file/examples/truncate.py b/recipe_modules/file/examples/truncate.py
index 4193995..14c573d 100644
--- a/recipe_modules/file/examples/truncate.py
+++ b/recipe_modules/file/examples/truncate.py
@@ -9,7 +9,7 @@
 
 
 def RunSteps(api):
-  filepath = api.path['start_dir'].join('some_file')
+  filepath = api.path.start_dir.join('some_file')
   size_mb = 300
 
   MBtoB = lambda x: x * 1024 * 1024
diff --git a/recipe_modules/futures/examples/background_helper.py b/recipe_modules/futures/examples/background_helper.py
index 713269c..5446970 100644
--- a/recipe_modules/futures/examples/background_helper.py
+++ b/recipe_modules/futures/examples/background_helper.py
@@ -18,7 +18,7 @@
 
 def manage_helper(api, chn):
   with api.step.nest('helper'):
-    pid_file = api.path['cleanup'].join('pid_file')
+    pid_file = api.path.cleanup_dir.join('pid_file')
     helper_future = api.futures.spawn_immediate(
         api.step, 'helper loop',
         ['python3', api.resource('helper.py'), pid_file],
diff --git a/recipe_modules/futures/examples/extreme_namespaces.py b/recipe_modules/futures/examples/extreme_namespaces.py
index 2e86dc9..d91f724 100644
--- a/recipe_modules/futures/examples/extreme_namespaces.py
+++ b/recipe_modules/futures/examples/extreme_namespaces.py
@@ -13,7 +13,7 @@
 def Level2(api, i):
   work = []
   with api.step.nest('Level2 [%d]' % i):
-    with api.context(cwd=api.path['start_dir'].join('deep')):
+    with api.context(cwd=api.path.start_dir.join('deep')):
       work.append(api.futures.spawn(
           api.step, 'cool step', cmd=['echo', 'cool']))
 
diff --git a/recipe_modules/generator_script/api.py b/recipe_modules/generator_script/api.py
index 6ba67c7..ca68ccd 100644
--- a/recipe_modules/generator_script/api.py
+++ b/recipe_modules/generator_script/api.py
@@ -68,7 +68,7 @@
         raise cls.MalformedCmd(generator_step_result.name)
 
 
-  def __call__(self, path_to_script, *args, **_):
+  def __call__(self, path_to_script, *args, checkout_dir=None, **_):
     """Run a script and generate the steps emitted by that script.
 
     The script will be invoked with --output-json /path/to/file.json. The script
@@ -109,7 +109,7 @@
     f = '--output-json'
     step_name = 'gen step(%s)' % self.m.path.basename(path_to_script)
 
-    with self.m.context(cwd=self.m.path['checkout']):
+    with self.m.context(cwd=checkout_dir or self.m.path.checkout_dir):
       if str(path_to_script).endswith('.py'):
         step_result = self.m.step(
             step_name,
diff --git a/recipe_modules/generator_script/examples/full.py b/recipe_modules/generator_script/examples/full.py
index be5f19a..c9f6a92 100644
--- a/recipe_modules/generator_script/examples/full.py
+++ b/recipe_modules/generator_script/examples/full.py
@@ -18,9 +18,9 @@
 }
 
 def RunSteps(api, script_name):
-  api.path['checkout'] = api.path['tmp_base']
-  script_name = api.properties['script_name']
-  api.generator_script(script_name)
+  api.generator_script(
+      path_to_script=api.properties['script_name'],
+      checkout_dir=api.path.tmp_base_dir)
 
 def GenTests(api):
   yield api.test(
diff --git a/recipe_modules/golang/api.py b/recipe_modules/golang/api.py
index 3ea7ec4..31e3bb4 100644
--- a/recipe_modules/golang/api.py
+++ b/recipe_modules/golang/api.py
@@ -42,8 +42,8 @@
       * path (Path) - a path to install Go into.
       * cache (Path) - a path to put Go caches under.
     """
-    path = path or self.m.path['cache'].join('golang')
-    cache = cache or self.m.path['cache'].join('gocache')
+    path = path or self.m.path.cache_dir.join('golang')
+    cache = cache or self.m.path.cache_dir.join('gocache')
     with self.m.context(infra_steps=True):
       env, env_pfx, env_sfx = self._ensure_installed(version, path, cache)
     with self.m.context(env=env, env_prefixes=env_pfx, env_suffixes=env_sfx):
diff --git a/recipe_modules/json/examples/full.py b/recipe_modules/json/examples/full.py
index 5d0bf8e..a18fc97 100644
--- a/recipe_modules/json/examples/full.py
+++ b/recipe_modules/json/examples/full.py
@@ -69,7 +69,7 @@
   assert step_result.stdout == example_dict
 
   # json.read reads a file containing JSON data.
-  leak_path = api.path['tmp_base'].join('temp.json')
+  leak_path = api.path.tmp_base_dir.join('temp.json')
   api.step('write json to file',
     ['cat', api.json.input(example_dict)],
     stdout=api.raw_io.output(leak_to=leak_path))
@@ -83,7 +83,7 @@
       'python3',
       api.resource('cool_script.py'),
       '{"x":1,"y":2}',
-      api.json.output(leak_to=api.path['tmp_base'].join('leak.json')),
+      api.json.output(leak_to=api.path.tmp_base_dir.join('leak.json')),
   ])
   assert step_result.json.output == example_dict
 
@@ -116,7 +116,7 @@
   assert step_result.json.output is None
 
   # Check that certain non-stdlib types are JSON serializable.
-  assert api.json.dumps(api.path['start_dir']) == '"%s"' % api.path['start_dir']
+  assert api.json.dumps(api.path.start_dir) == '"%s"' % api.path.start_dir
   assert api.json.dumps(engine_types.FrozenDict(foo='bar')) == '{"foo": "bar"}'
   foobar_struct = struct_pb2.Struct(
       fields={'foo': struct_pb2.Value(string_value='bar')})
diff --git a/recipe_modules/led/api.py b/recipe_modules/led/api.py
index 3bc73b8..ea66a43 100644
--- a/recipe_modules/led/api.py
+++ b/recipe_modules/led/api.py
@@ -439,7 +439,7 @@
           # trace a bit more obvious.
           build = self.test_api._transform_build(
               previous, cmd, self._mock_edits,
-              str(self.m.context.cwd or self.m.path['start_dir']))
+              str(self.m.context.cwd or self.m.path.start_dir))
           kwargs['step_test_data'] = (
             lambda: self.test_api.m.proto.output_stream(build))
 
diff --git a/recipe_modules/nodejs/api.py b/recipe_modules/nodejs/api.py
index c8e02d2..9888c6a 100644
--- a/recipe_modules/nodejs/api.py
+++ b/recipe_modules/nodejs/api.py
@@ -40,8 +40,8 @@
       * path (Path) - a path to install Node.js into.
       * cache (Path) - a path to put Node.js caches under.
     """
-    path = path or self.m.path['cache'].join('nodejs')
-    cache = cache or self.m.path['cache'].join('npmcache')
+    path = path or self.m.path.cache_dir.join('nodejs')
+    cache = cache or self.m.path.cache_dir.join('npmcache')
     with self.m.context(infra_steps=True):
       env, env_pfx = self._ensure_installed(version, path, cache)
     with self.m.context(env=env, env_prefixes=env_pfx):
diff --git a/recipe_modules/path/api.py b/recipe_modules/path/api.py
index e8a20b1..a1943fc 100644
--- a/recipe_modules/path/api.py
+++ b/recipe_modules/path/api.py
@@ -11,19 +11,19 @@
 
 In this way, all paths in Recipes are absolute, and are constructed from a small
 collection of anchor points. The built-in anchor points are:
-  * `api.path['start_dir']` - This is the directory that the recipe started in.
+  * `api.path.start_dir` - This is the directory that the recipe started in.
     it's similar to `cwd`, except that it's constant.
-  * `api.path['cache']` - This directory is provided by whatever's running the
+  * `api.path.cache_dir` - This directory is provided by whatever's running the
     recipe. Files and directories created under here /may/ be evicted in between
     runs of the recipe (i.e. to relieve disk pressure).
-  * `api.path['cleanup']` - This directory is provided by whatever's running the
+  * `api.path.cleanup_dir` - This directory is provided by whatever's running the
     recipe. Files and directories created under here /are guaranteed/ to be
     evicted in between runs of the recipe. Additionally, this directory is
     guaranteed to be empty when the recipe starts.
-  * `api.path['tmp_base']` - This directory is the system-configured temp dir.
+  * `api.path.tmp_base_dir` - This directory is the system-configured temp dir.
     This is a weaker form of 'cleanup', and its use should be avoided. This may
     be removed in the future (or converted to an alias of 'cleanup').
-  * `api.path['checkout']` - This directory is set by various checkout modules
+  * `api.path.checkout_dir` - This directory is set by various checkout modules
     in recipes. It was originally intended to make recipes easier to read and
     make code somewhat generic or homogeneous, but this was a mistake. New code
     should avoid 'checkout', and instead just explicitly pass paths around. This
@@ -486,11 +486,12 @@
       new_path = tempfile.mkdtemp(prefix=prefix, dir=cleanup_dir)
       assert new_path.startswith(cleanup_dir), (
           f'{new_path=!r} -- {cleanup_dir=!r}')
-      temp_dir = self['cleanup'].join(new_path[len(cleanup_dir):])
+      temp_dir = self.cleanup_dir.join(new_path[len(cleanup_dir):])
     else:
       self._test_counter[prefix] += 1
-      temp_dir = self['cleanup'].join(
+      temp_dir = self.cleanup_dir.join(
           f'{prefix}_tmp_{self._test_counter[prefix]}')
+
     self.mock_add_paths(temp_dir, FileType.DIRECTORY)
     return temp_dir
 
@@ -513,11 +514,11 @@
       fd, new_path = tempfile.mkstemp(prefix=prefix, dir=cleanup_dir)
       assert new_path.startswith(cleanup_dir), (
           f'{new_path=!r} -- {cleanup_dir=!r}')
-      temp_file = self['cleanup'].join(new_path[len(cleanup_dir):])
+      temp_file = self.cleanup_dir.join(new_path[len(cleanup_dir):])
       os.close(fd)
     else:
       self._test_counter[prefix] += 1
-      temp_file = self['cleanup'].join(
+      temp_file = self.cleanup_dir.join(
           f'{prefix}_tmp_{self._test_counter[prefix]}')
     self.mock_add_paths(temp_file, FileType.FILE)
     return temp_file
@@ -824,13 +825,13 @@
     """Equivalent to os.path.join.
 
     Note that Path objects returned from this module (e.g.
-    api.path['start_dir']) have a built-in join method (e.g.
+    api.path.start_dir) have a built-in join method (e.g.
     new_path = p.join('some', 'name')). Many recipe modules expect Path objects
     rather than strings. Using this `join` method gives you raw path joining
     functionality and returns a string.
 
     If your path is rooted in one of the path module's root paths (i.e. those
-    retrieved with api.path[something]), then you can convert from a string path
+    retrieved with api.path.something), then you can convert from a string path
     back to a Path with the `abs_to_path` method.
     """
     return self._path_mod.join(str(path), *[str(p) for p in paths])
@@ -897,12 +898,12 @@
     return self._path_mod.normpath(str(path))
 
   def expanduser(self, path):  # pragma: no cover
-    """Do not use this, use `api.path['home']` instead.
+    """Do not use this, use `api.path.home_dir` instead.
 
-    This ONLY handles `path` == "~", and returns `str(api.path['home'])`.
+    This ONLY handles `path` == "~", and returns `str(api.path.home_dir)`.
     """
     if path == "~":
-      return str(self['home'])
+      return str(self.home_dir)
     raise ValueError("expanduser only supports `~`.")
 
   def exists(self, path):
diff --git a/recipe_modules/path/examples/full.py b/recipe_modules/path/examples/full.py
index 9851278..c3d5179 100644
--- a/recipe_modules/path/examples/full.py
+++ b/recipe_modules/path/examples/full.py
@@ -2,6 +2,8 @@
 # Use of this source code is governed under the Apache License, Version 2.0
 # that can be found in the LICENSE file.
 
+from recipe_engine import recipe_api
+
 DEPS = [
   'json',
   'path',
@@ -13,8 +15,9 @@
 from builtins import range, zip
 
 
+@recipe_api.ignore_warnings('recipe_engine/CHECKOUT_DIR_DEPRECATED')
 def RunSteps(api):
-  api.step('step1', ['/bin/echo', str(api.path['tmp_base'].join('foo'))])
+  api.step('step1', ['/bin/echo', str(api.path.tmp_base_dir.join('foo'))])
 
   # module.resource(...) demo.
   api.step('print resource',
@@ -25,10 +28,10 @@
            ['echo', api.path.repo_resource('dir', 'file.py')])
 
   assert 'start_dir' in api.path
-  assert api.path['start_dir'].join('.') == api.path['start_dir']
+  assert api.path.start_dir.join('.') == api.path.start_dir
 
   assert 'checkout' not in api.path
-  api.path['checkout'] = api.path['tmp_base'].join('checkout')
+  api.path.checkout_dir = api.path.tmp_base_dir.join('checkout')
   assert 'checkout' in api.path
 
   # Test missing/default value.
@@ -39,18 +42,13 @@
   except ValueError as ex:
     assert 'unknown base path' in str(ex), str(ex)
 
-  try:
-    raise Exception('Should never raise: %s' % (api.path['nonexistent'],))
-  except ValueError:
-    pass
-
   # Global dynamic paths (see config.py example for declaration):
-  dynamic_path = api.path['checkout'].join('jerky')
+  dynamic_path = api.path.checkout_dir.join('jerky')
   api.step('checkout path', ['/bin/echo', dynamic_path])
 
   # Methods from python os.path are available via api.path. For testing, we
   # asserted that this file existed in the test description below.
-  assert api.path.exists(api.path['tmp_base'])
+  assert api.path.exists(api.path.tmp_base_dir)
 
   temp_dir = api.path.mkdtemp('kawaab')
   assert api.path.exists(temp_dir)
@@ -58,7 +56,7 @@
   temp_file = api.path.mkstemp('kawaac')
   assert api.path.exists(temp_file)
 
-  file_path = api.path['tmp_base'].join('new_file')
+  file_path = api.path.tmp_base_dir.join('new_file')
   abspath = api.path.abspath(file_path)
   api.path.assert_absolute(abspath)
   try:
@@ -76,16 +74,16 @@
     assert api.path.pathsep == ':'
 
   assert api.path.basename(file_path) == 'new_file'
-  assert api.path.dirname(file_path) == api.path['tmp_base']
-  assert api.path.split(file_path) == (api.path['tmp_base'], 'new_file')
+  assert api.path.dirname(file_path) == api.path.tmp_base_dir
+  assert api.path.split(file_path) == (api.path.tmp_base_dir, 'new_file')
 
-  thing_bat = api.path['tmp_base'].join('thing.bat')
-  thing_bat_mkv = api.path['tmp_base'].join('thing.bat.mkv')
+  thing_bat = api.path.tmp_base_dir.join('thing.bat')
+  thing_bat_mkv = api.path.tmp_base_dir.join('thing.bat.mkv')
   assert api.path.splitext(thing_bat_mkv) == (thing_bat, '.mkv')
 
-  assert api.path.abs_to_path(api.path['tmp_base']) == api.path['tmp_base']
+  assert api.path.abs_to_path(api.path.tmp_base_dir) == api.path.tmp_base_dir
 
-  assert api.path.relpath(file_path, api.path['tmp_base']) == 'new_file'
+  assert api.path.relpath(file_path, api.path.tmp_base_dir) == 'new_file'
 
   assert api.path.splitext('abc.xyz') == ('abc', '.xyz')
   assert api.path.split('abc/xyz') == ('abc', 'xyz')
@@ -107,7 +105,7 @@
   normpath = api.path.normpath(file_path)
   assert api.path.exists(normpath)
 
-  directory = api.path['start_dir'].join('directory')
+  directory = api.path.start_dir.join('directory')
   filepath = directory.join('filepath')
   api.step('rm directory (initial)', ['rm', '-rf', directory])
   assert not api.path.exists(directory)
@@ -139,10 +137,10 @@
   assert not api.path.isfile(directory)
 
   # We can mock copy paths. See the file module to do this for real.
-  copy1 = api.path['start_dir'].join('copy1')
-  copy10 = api.path['start_dir'].join('copy10')
-  copy2 = api.path['start_dir'].join('copy2')
-  copy20 = api.path['start_dir'].join('copy20')
+  copy1 = api.path.start_dir.join('copy1')
+  copy10 = api.path.start_dir.join('copy10')
+  copy2 = api.path.start_dir.join('copy2')
+  copy20 = api.path.start_dir.join('copy20')
   api.step('rm copy2 (initial)', ['rm', '-rf', copy2])
   api.step('rm copy20 (initial)', ['rm', '-rf', copy20])
 
@@ -172,10 +170,10 @@
   # Convert strings to Paths.
   def _mk_paths():
     return [
-        api.path['start_dir'].join('some', 'thing'),
-        api.path['start_dir'],
-        api.path['cache'] / 'a file',
-        api.path['home'] / 'another file',
+        api.path.start_dir.join('some', 'thing'),
+        api.path.start_dir,
+        api.path.cache_dir / 'a file',
+        api.path.home_dir / 'another file',
         api.path.resource("module_resource.py"),
         api.path.resource(),
         api.resource("recipe_resource.py"),
@@ -214,7 +212,7 @@
   except ValueError as ex:
     assert "could not figure out" in str(ex), ex
 
-  start_dir = api.path['start_dir']
+  start_dir = api.path.start_dir
   a = start_dir / 'a'
   b = start_dir / 'b'
   assert a < b
@@ -224,13 +222,13 @@
   # there is also a join method on the path module
   assert start_dir.join('a') == api.path.join(start_dir, 'a')
 
-  slashy_path = api.path['start_dir'].join(f'foo{api.path.sep}bar')
-  separated_path = api.path['start_dir'].join('foo', 'bar')
+  slashy_path = api.path.start_dir.join(f'foo{api.path.sep}bar')
+  separated_path = api.path.start_dir.join('foo', 'bar')
   assert str(slashy_path) == str(separated_path)
   assert slashy_path == separated_path
   assert api.path.eq(slashy_path, separated_path)
 
-  slashy_file = api.path['start_dir'].join(
+  slashy_file = api.path.start_dir.join(
       f'foo{api.path.sep}bar{api.path.sep}baz.txt')
   assert separated_path.is_parent_of(slashy_file)
   assert api.path.is_parent_of(separated_path, slashy_file)
diff --git a/recipe_modules/path/test_api.py b/recipe_modules/path/test_api.py
index 877495b..aac30ae 100644
--- a/recipe_modules/path/test_api.py
+++ b/recipe_modules/path/test_api.py
@@ -88,8 +88,8 @@
         return self.checkout_dir
 
     # Avoid circular import.
-    from .api import PathApi
-    if name not in PathApi.NamedBasePaths:
+    from .api import PathApi  # pragma: no cover
+    if name not in PathApi.NamedBasePaths:  # pragma: no cover
       raise ValueError(
           f'Unknown base path {name!r} - allowed names are {PathApi.NamedBasePaths!r}'
       )
diff --git a/recipe_modules/path/tests/deprecated.py b/recipe_modules/path/tests/deprecated.py
new file mode 100644
index 0000000..44828ff
--- /dev/null
+++ b/recipe_modules/path/tests/deprecated.py
@@ -0,0 +1,49 @@
+# Copyright 2024 The LUCI Authors. All rights reserved.
+# Use of this source code is governed under the Apache License, Version 2.0
+# that can be found in the LICENSE file.
+
+from recipe_engine import post_process, recipe_api
+
+DEPS = [
+    'path',
+    'step',
+]
+
+
+@recipe_api.ignore_warnings(
+    'recipe_engine/CHECKOUT_DIR_DEPRECATED',
+    'recipe_engine/PATH_GETITEM_DEPRECATED',
+)
+def RunSteps(api):
+  assert api.path['cache'] == api.path.cache_dir
+  assert api.path['cleanup'] == api.path.cleanup_dir
+  assert api.path['home'] == api.path.home_dir
+  assert api.path['start_dir'] == api.path.start_dir
+  assert api.path['tmp_base'] == api.path.tmp_base_dir
+
+  api.path['checkout'] = api.path.start_dir
+  assert api.path['checkout'] == api.path.checkout_dir
+
+  api.step.empty('cache', step_text=str(api.path.cache_dir))
+  api.step.empty('cleanup', step_text=str(api.path.cleanup_dir))
+  api.step.empty('home', step_text=str(api.path.home_dir))
+  api.step.empty('start_dir', step_text=str(api.path.start_dir))
+  api.step.empty('tmp_base', step_text=str(api.path.tmp_base_dir))
+
+  api.step.empty('checkout', step_text=str(api.path.checkout_dir))
+
+
+def GenTests(api):
+  def equals(name):
+    return api.post_process(post_process.StepTextEquals, name, api.path[name])
+
+  yield api.test(
+      'equal',
+      equals('cache'),
+      equals('cleanup'),
+      equals('home'),
+      equals('start_dir'),
+      equals('tmp_base'),
+      equals('checkout'),
+      api.post_process(post_process.DropExpectation),
+  )
diff --git a/recipe_modules/path/tests/dynamic_paths.py b/recipe_modules/path/tests/dynamic_paths.py
index 1ee69cc..701c097 100644
--- a/recipe_modules/path/tests/dynamic_paths.py
+++ b/recipe_modules/path/tests/dynamic_paths.py
@@ -23,20 +23,20 @@
     assert 'cannot be rooted in checkout_dir' in str(ex), str(ex)
 
   try:
-    api.path['something'] = api.path['start_dir'].join('coolstuff')
+    api.path['something'] = api.path.start_dir.join('coolstuff')
     assert False, 'able to assign path to non-dynamic path?'  # pragma: no cover
   except ValueError as ex:
     assert 'The only valid dynamic path value is `checkout`' in str(ex), str(ex)
 
   # OK!
-  api.path['checkout'] = api.path['start_dir'].join('coolstuff')
+  api.path.checkout_dir = api.path.start_dir.join('coolstuff')
 
   # Can re-set to the same thing
-  api.path['checkout'] = api.path['start_dir'].join('coolstuff')
+  api.path.checkout_dir = api.path.start_dir.join('coolstuff')
 
   try:
     # Setting a new value is not allowed
-    api.path['checkout'] = api.path['start_dir'].join('neatstuff')
+    api.path.checkout_dir = api.path.start_dir.join('neatstuff')
     assert False, 'able to set a dynamic path twice?'  # pragma: no cover
   except ValueError as ex:
     assert 'can only be set once' in str(ex), str(ex)
diff --git a/recipe_modules/path/tests/exists.py b/recipe_modules/path/tests/exists.py
index 76feb02..04d329e 100644
--- a/recipe_modules/path/tests/exists.py
+++ b/recipe_modules/path/tests/exists.py
@@ -24,7 +24,7 @@
   # directory. However, the checkout directory still must be set before this
   # check is done.
   api.path.checkout_dir = api.path.cache_dir / 'builder' / 'src'
-  assert api.path.exists(api.path['checkout'] / 'somefile')
+  assert api.path.exists(api.path.checkout_dir / 'somefile')
 
 
 def GenTests(api):
diff --git a/recipe_modules/path/tests/test_api_legacy.py b/recipe_modules/path/tests/test_api_legacy.py
index 6b076cb..b72fe4f 100644
--- a/recipe_modules/path/tests/test_api_legacy.py
+++ b/recipe_modules/path/tests/test_api_legacy.py
@@ -26,16 +26,13 @@
 
 
 def GenTests(api):
-  paths = [api.path[name].join('file') for name in GETITEM_NAMES]
-  paths.append(api.path['checkout'].join('file'))
+  def base_path(name):
+    if not name.endswith('_dir'):
+      name += '_dir'
+    return getattr(api.path, name)
 
-  # This is for coverage - we need to make sure that api.path[typo] raises an
-  # exception.
-  try:
-    api.path['chekout']  # note the typo
-    assert False, 'PathTestApi did not catch typo'  # pragma: no cover
-  except ValueError:
-    pass
+  paths = [base_path(name).join('file') for name in GETITEM_NAMES]
+  paths.append(api.path.checkout_dir.join('file'))
 
   yield api.test(
       'basic',
diff --git a/recipe_modules/proto/tests/placeholders.py b/recipe_modules/proto/tests/placeholders.py
index 263f9a2..dce3bae 100644
--- a/recipe_modules/proto/tests/placeholders.py
+++ b/recipe_modules/proto/tests/placeholders.py
@@ -30,13 +30,13 @@
   step = api.step('read missing output', [
     'python3', api.resource('dump.py'),
     api.proto.output(SomeMessage, 'JSONPB',
-                     leak_to=api.path['start_dir'].join('gone')),
+                     leak_to=api.path.start_dir.join('gone')),
   ])
 
   step = api.step('read invalid output', [
     'python3', api.resource('dump.py'),
     api.proto.output(SomeMessage, 'JSONPB',
-                     leak_to=api.path['start_dir'].join('gone')),
+                     leak_to=api.path.start_dir.join('gone')),
   ])
 
   api.step('write to script (jsonpb)', [
diff --git a/recipe_modules/raw_io/examples/full.py b/recipe_modules/raw_io/examples/full.py
index ea54eb1..2ac863d 100644
--- a/recipe_modules/raw_io/examples/full.py
+++ b/recipe_modules/raw_io/examples/full.py
@@ -67,14 +67,14 @@
   step_result = api.step(
       'leak stdout', ['echo', 'leaking'],
       stdout=api.raw_io.output_text(
-          leak_to=api.path['tmp_base'].join('out.txt')),
+          leak_to=api.path.tmp_base_dir.join('out.txt')),
       step_test_data=(
           lambda: api.raw_io.test_api.stream_output_text('leaking\n')))
   assert step_result.stdout == 'leaking\n'
 
   api.step('list temp dir', ['ls', api.raw_io.output_dir()])
   api.step('leak dir', ['ls', api.raw_io.output_dir(
-      leak_to=api.path['tmp_base'].join('out'))])
+      leak_to=api.path.tmp_base_dir.join('out'))])
 
   step_result = api.step(
       'dump output_dir',
@@ -95,7 +95,7 @@
   step_result = api.step(
       'nothing leaked to leak_to',
       ['echo',
-       api.raw_io.output(leak_to=api.path['tmp_base'].join('missing.txt'))])
+       api.raw_io.output(leak_to=api.path.tmp_base_dir.join('missing.txt'))])
 
   # Example of overriding default mocked output for a single named output.
   step_result = api.step(
diff --git a/recipe_modules/service_account/examples/full.py b/recipe_modules/service_account/examples/full.py
index fbba4c8..a2c691f 100644
--- a/recipe_modules/service_account/examples/full.py
+++ b/recipe_modules/service_account/examples/full.py
@@ -49,7 +49,7 @@
   yield api.test(
       'json_key',
       api.platform('linux', 64),
-      props(key_path=api.path['start_dir'].join('key_name.json')),
+      props(key_path=api.path.start_dir.join('key_name.json')),
   )
 
   yield api.test(
diff --git a/recipe_modules/step/api.py b/recipe_modules/step/api.py
index d5fed19..0941c7c 100644
--- a/recipe_modules/step/api.py
+++ b/recipe_modules/step/api.py
@@ -377,7 +377,7 @@
     return cost or _ResourceCost.zero()
 
   def _normalize_cwd(self, cwd):
-    if cwd and cwd == self.m.path['start_dir']:
+    if cwd and cwd == self.m.path.start_dir:
       cwd = None
     elif cwd is not None:
       cwd = str(cwd)
@@ -523,17 +523,17 @@
     # output path, cwd and cache directory.
     with api.context(
         # Change the cwd of the launched LUCI executable
-        cwd=api.path['start_dir'].join('subdir'),
+        cwd=api.path.start_dir.join('subdir'),
         # Change the cache_dir of the launched LUCI executable. Defaults to
-        # api.path['cache'] if unchanged.
-        luciexe=sections_pb2.LUCIExe(cache_dir=api.path['cache'].join('sub')),
+        # api.path.cache_dir if unchanged.
+        luciexe=sections_pb2.LUCIExe(cache_dir=api.path.cache_dir.join('sub')),
       ):
       # Command executed:
       #   `/path/to/run_exe --output [CLEANUP]/build.json --foo bar baz`
       ret = api.sub_build("launch sub build",
                           [run_exe, '--foo', 'bar', 'baz'],
                           api.buildbucket.build,
-                          output_path=api.path['cleanup'].join('build.json'))
+                          output_path=api.path.cleanup_dir.join('build.json'))
     sub_build = ret.step.sub_build  # access final build proto result
     ```
 
diff --git a/recipe_modules/step/examples/full.py b/recipe_modules/step/examples/full.py
index 2f9f706..c71fb5c 100644
--- a/recipe_modules/step/examples/full.py
+++ b/recipe_modules/step/examples/full.py
@@ -42,13 +42,13 @@
 
   # You can change the current working directory as well.
   api.step('mk subdir', ['mkdir', '-p', 'something'])
-  with api.context(cwd=api.path['start_dir'].join('something')):
+  with api.context(cwd=api.path.start_dir.join('something')):
     api.step('something', ['bash', '-c', 'echo Why hello, there, in a subdir.'])
 
   # By default, all steps run in 'start_dir', or the cwd of the recipe engine
   # when the recipe begins. Because of this, setting cwd to start_dir doesn't
   # show anything in particular in the expectations.
-  with api.context(cwd=api.path['start_dir']):
+  with api.context(cwd=api.path.start_dir):
     api.step('start_dir ignored', ['bash', '-c', 'echo what happen'])
 
   # You can also manipulate various aspects of the step, such as env.
diff --git a/recipe_modules/step/tests/sub_build.py b/recipe_modules/step/tests/sub_build.py
index 65c32b3..8e273a3 100644
--- a/recipe_modules/step/tests/sub_build.py
+++ b/recipe_modules/step/tests/sub_build.py
@@ -30,7 +30,7 @@
   output_path = None
   if props.HasField('output_path'):
     output_path = (
-      api.path[props.output_path.base].join(props.output_path.file))
+      getattr(api.path, props.output_path.base).join(props.output_path.file))
   with api.context(infra_steps=props.infra_step):
     input_build = props.input_build if props.HasField('input_build') else (
         build_pb2.Build(id=11111, status=common_pb2.SCHEDULED))
@@ -93,7 +93,7 @@
               base='start_dir',
               file='sub_build.json'),
       ),
-      api.path.exists(api.path['start_dir'].join('sub_build.json')),
+      api.path.exists(api.path.start_dir.join('sub_build.json')),
       api.expect_exception('ValueError'),
       api.post_process(post_process.StatusException),
       api.post_process(
diff --git a/recipe_modules/tricium/api.py b/recipe_modules/tricium/api.py
index e1d272c..57122fa 100644
--- a/recipe_modules/tricium/api.py
+++ b/recipe_modules/tricium/api.py
@@ -171,7 +171,7 @@
         if not _matches_path_filters(affected_files, analyzer.path_filters):
           presentation.step_text = 'skipped due to path filters'
         try:
-          analyzer_dir = self.m.path['cleanup'].join(analyzer.name)
+          analyzer_dir = self.m.path.cleanup_dir.join(analyzer.name)
           output_base = analyzer_dir.join('out')
           package_dir = analyzer_dir.join('package')
           self._fetch_legacy_analyzer(package_dir, analyzer)
diff --git a/recipe_modules/tricium/examples/wrapper.py b/recipe_modules/tricium/examples/wrapper.py
index 90ca014..224d1a0 100644
--- a/recipe_modules/tricium/examples/wrapper.py
+++ b/recipe_modules/tricium/examples/wrapper.py
@@ -17,7 +17,7 @@
 
 
 def RunSteps(api):
-  checkout_base = api.path['cleanup'].join('checkout')
+  checkout_base = api.path.cleanup_dir.join('checkout')
   api.file.write_text('one', checkout_base.join('one.txt'), 'one')
   api.file.write_text('two', checkout_base.join('foo', 'two.txt'), 'two')
 
diff --git a/recipe_modules/url/examples/full.py b/recipe_modules/url/examples/full.py
index 4fe9ab5..b7499cf 100644
--- a/recipe_modules/url/examples/full.py
+++ b/recipe_modules/url/examples/full.py
@@ -30,7 +30,7 @@
   assert api.url.urlencode({'foo': 'bar'}) == 'foo=bar'
 
   with api.step.nest('get_file'):
-    dest = api.path['start_dir'].join('download.bin')
+    dest = api.path.start_dir.join('download.bin')
     v = api.url.get_file(TEST_HTTPS_URL, dest,
                          headers={'Authorization': 'thing'})
     assert str(v.output) == str(dest)
diff --git a/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto b/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto
index 1c7b3fa..68bd53e 100644
--- a/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto
+++ b/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto
@@ -703,7 +703,7 @@
       // Must use POSIX format (forward slashes).
       // In most cases, it does not need slashes at all.
       //
-      // In recipes, use api.path['cache'].join(path) to get absolute path.
+      // In recipes, use api.path.cache_dir.join(path) to get absolute path.
       //
       // Must be unique in the build.
       string path = 2;
diff --git a/recipe_proto/go.chromium.org/luci/buildbucket/proto/common.proto b/recipe_proto/go.chromium.org/luci/buildbucket/proto/common.proto
index 5f1a582..79a5e85 100644
--- a/recipe_proto/go.chromium.org/luci/buildbucket/proto/common.proto
+++ b/recipe_proto/go.chromium.org/luci/buildbucket/proto/common.proto
@@ -308,7 +308,7 @@
   // Must use POSIX format (forward slashes).
   // In most cases, it does not need slashes at all.
   //
-  // In recipes, use api.path['cache'].join(path) to get absolute path.
+  // In recipes, use api.path.cache_dir.join(path) to get absolute path.
   //
   // Must be unique in the build.
   string path = 2;
diff --git a/recipes/engine_tests/early_termination.py b/recipes/engine_tests/early_termination.py
index 502bc6c..7764e68 100644
--- a/recipes/engine_tests/early_termination.py
+++ b/recipes/engine_tests/early_termination.py
@@ -23,10 +23,10 @@
 
   output_touchfile = props.output_touchfile
   if not output_touchfile:
-    output_touchfile = api.path['cleanup'].join('output_touchfile')
+    output_touchfile = api.path.cleanup_dir.join('output_touchfile')
   running_touchfile = props.running_touchfile
   if not running_touchfile:
-    running_touchfile = api.path['cleanup'].join('running_touchfile')
+    running_touchfile = api.path.cleanup_dir.join('running_touchfile')
   # make sure touchfile is there
   api.file.write_text("ensure output_touchfile", output_touchfile,
                       "meep".encode('utf-8'))
diff --git a/recipes/engine_tests/missing_start_dir.py b/recipes/engine_tests/missing_start_dir.py
index 328c318..718aa74 100644
--- a/recipes/engine_tests/missing_start_dir.py
+++ b/recipes/engine_tests/missing_start_dir.py
@@ -12,7 +12,7 @@
 
 def RunSteps(api):
   api.step('innocent step', ['bash', '-c', "echo some step"])
-  api.step('nuke it', ['rm', '-rf', api.path['start_dir']])
+  api.step('nuke it', ['rm', '-rf', api.path.start_dir])
 
   try:
     api.step('bash needs cwd', ['bash', '-c', "echo fail"])