Replace Path.join() with Path.joinpath or /

Replace config_types.Path.join() with config_types.Path.joinpath() or
the / operator. This makes config_types.Path look more like
pathlib.Path.

Bug: 329113288
Change-Id: I7e9fdb50502e9e71cb857f0ee8db9517ffa13a17
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/recipes-py/+/5450771
Auto-Submit: Rob Mohr <mohrr@google.com>
Commit-Queue: Yiwei Zhang <yiwzhang@google.com>
Reviewed-by: Yiwei Zhang <yiwzhang@google.com>
diff --git a/README.recipes.md b/README.recipes.md
index f9bf52f..5daebc3 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -1867,7 +1867,7 @@
 
 Raises: file.Error
 
-&mdash; **def [ensure\_directory](/recipe_modules/file/api.py#564)(self, name, dest, mode=511):**
+&mdash; **def [ensure\_directory](/recipe_modules/file/api.py#558)(self, name, dest, mode=511):**
 
 Ensures that `dest` exists and is a directory.
 
@@ -1896,7 +1896,7 @@
 Raises:
   file.Error and ValueError if passed paths input is not str or Path.
 
-&mdash; **def [filesizes](/recipe_modules/file/api.py#580)(self, name, files, test_data=None):**
+&mdash; **def [filesizes](/recipe_modules/file/api.py#574)(self, name, files, test_data=None):**
 
 Returns list of filesizes for the given files.
 
@@ -1906,7 +1906,7 @@
 
 Returns list[int], size of each file in bytes.
 
-&mdash; **def [flatten\_single\_directories](/recipe_modules/file/api.py#720)(self, name, path):**
+&mdash; **def [flatten\_single\_directories](/recipe_modules/file/api.py#714)(self, name, path):**
 
 Flattens singular directories, starting at path.
 
@@ -1965,7 +1965,7 @@
 
 Raises: file.Error.
 
-&mdash; **def [listdir](/recipe_modules/file/api.py#524)(self, name, source, recursive=False, test_data=(), include_log=True):**
+&mdash; **def [listdir](/recipe_modules/file/api.py#521)(self, name, source, recursive=False, test_data=(), include_log=True):**
 
 Lists all files inside a directory.
 
@@ -2059,7 +2059,7 @@
 
 Raises: file.Error
 
-&mdash; **def [remove](/recipe_modules/file/api.py#509)(self, name, source):**
+&mdash; **def [remove](/recipe_modules/file/api.py#506)(self, name, source):**
 
 Removes a file.
 
@@ -2071,7 +2071,7 @@
 
 Raises: file.Error.
 
-&mdash; **def [rmcontents](/recipe_modules/file/api.py#619)(self, name, source):**
+&mdash; **def [rmcontents](/recipe_modules/file/api.py#613)(self, name, source):**
 
 Similar to rmtree, but removes only contents not the directory.
 
@@ -2086,7 +2086,7 @@
 
 Raises: file.Error.
 
-&mdash; **def [rmglob](/recipe_modules/file/api.py#637)(self, name, source, pattern, recursive=True, include_hidden=True):**
+&mdash; **def [rmglob](/recipe_modules/file/api.py#631)(self, name, source, pattern, recursive=True, include_hidden=True):**
 
 Removes all entries in `source` matching the glob `pattern`.
 
@@ -2116,7 +2116,7 @@
 
 Raises: file.Error.
 
-&mdash; **def [rmtree](/recipe_modules/file/api.py#602)(self, name, source):**
+&mdash; **def [rmtree](/recipe_modules/file/api.py#596)(self, name, source):**
 
 Recursively removes a directory.
 
@@ -2130,7 +2130,7 @@
 
 Raises: file.Error.
 
-&mdash; **def [symlink](/recipe_modules/file/api.py#682)(self, name, source, linkname):**
+&mdash; **def [symlink](/recipe_modules/file/api.py#676)(self, name, source, linkname):**
 
 Creates a symlink on the local filesystem.
 
@@ -2143,14 +2143,14 @@
 
 Raises: file.Error
 
-&mdash; **def [symlink\_tree](/recipe_modules/file/api.py#699)(self, root):**
+&mdash; **def [symlink\_tree](/recipe_modules/file/api.py#693)(self, root):**
 
 Creates a SymlinkTree, given a root directory.
 
 Args:
   * root (Path): root of a tree of symlinks.
 
-&mdash; **def [truncate](/recipe_modules/file/api.py#707)(self, name, path, size_mb=100):**
+&mdash; **def [truncate](/recipe_modules/file/api.py#701)(self, name, path, size_mb=100):**
 
 Creates an empty file with path and size_mb on the local filesystem.
 
@@ -2873,7 +2873,7 @@
 
 #### **class [PathApi](/recipe_modules/path/api.py#328)([RecipeApi](/recipe_engine/recipe_api.py#471)):**
 
-&mdash; **def [\_\_contains\_\_](/recipe_modules/path/api.py#589)(self, pathname: NamedBasePathsType):**
+&mdash; **def [\_\_contains\_\_](/recipe_modules/path/api.py#588)(self, pathname: NamedBasePathsType):**
 
 This method is DEPRECATED.
 
@@ -2888,7 +2888,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#688)(self, name: NamedBasePathsType):**
+&mdash; **def [\_\_getitem\_\_](/recipe_modules/path/api.py#687)(self, name: NamedBasePathsType):**
 
 Gets the base path named `name`. See module docstring for more info.
 
@@ -2903,7 +2903,7 @@
   pass the Paths around instead of using this global variable).
 ***
 
-&mdash; **def [\_\_setitem\_\_](/recipe_modules/path/api.py#607)(self, pathname: CheckoutPathNameType, path: config_types.Path):**
+&mdash; **def [\_\_setitem\_\_](/recipe_modules/path/api.py#606)(self, pathname: CheckoutPathNameType, path: config_types.Path):**
 
 Sets the checkout path.
 
@@ -2913,7 +2913,7 @@
 
 The only valid value of `pathname` is the literal string CheckoutPathName.
 
-&mdash; **def [abs\_to\_path](/recipe_modules/path/api.py#526)(self, abs_string_path: str):**
+&mdash; **def [abs\_to\_path](/recipe_modules/path/api.py#525)(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.
@@ -2942,7 +2942,7 @@
 Raises an ValueError if the preconditions are not met, otherwise returns the
 Path object.
 
-&mdash; **def [abspath](/recipe_modules/path/api.py#798)(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.
 
@@ -2953,11 +2953,11 @@
 Args:
   * path - The path to check.
 
-&mdash; **def [basename](/recipe_modules/path/api.py#802)(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#732)(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.
 
@@ -2980,7 +2980,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#766)(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.
@@ -2993,20 +2993,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#627)(self, path: config_types.Path):**
+&emsp; **@checkout_dir.setter**<br>&mdash; **def [checkout\_dir](/recipe_modules/path/api.py#626)(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#757)(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#806)(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".
 
@@ -3019,7 +3019,7 @@
 
 Returns dirname of path
 
-&mdash; **def [eq](/recipe_modules/path/api.py#970)(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.
 
@@ -3027,20 +3027,20 @@
 **DEPRECATED**: Just directly compare path1 and path2 with `==`.
 ***
 
-&mdash; **def [exists](/recipe_modules/path/api.py#910)(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#901)(self, path):**
+&mdash; **def [expanduser](/recipe_modules/path/api.py#900)(self, path):**
 
 Do not use this, use `api.path.home_dir` instead.
 
 This ONLY handles `path` == "~", and returns `str(api.path.home_dir)`.
 
-&mdash; **def [get](/recipe_modules/path/api.py#656)(self, name: NamedBasePathsType):**
+&mdash; **def [get](/recipe_modules/path/api.py#655)(self, name: NamedBasePathsType):**
 
 Gets the base path named `name`. See module docstring for more info.
 
@@ -3055,7 +3055,7 @@
   pass the Paths around instead of using this global variable).
 ***
 
-&emsp; **@property**<br>&mdash; **def [home\_dir](/recipe_modules/path/api.py#714)(self):**
+&emsp; **@property**<br>&mdash; **def [home\_dir](/recipe_modules/path/api.py#713)(self):**
 
 This is the path to the current $HOME directory.
 
@@ -3067,7 +3067,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#977)(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.
 
@@ -3075,29 +3075,29 @@
 **DEPRECATED**: Just use `parent.is_parent_of(child)`.
 ***
 
-&mdash; **def [isdir](/recipe_modules/path/api.py#918)(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#926)(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#825)(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.
-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.
+new_path = p.joinpath('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
@@ -3112,7 +3112,7 @@
 
 Returns a Path to the new directory.
 
-&mdash; **def [mkstemp](/recipe_modules/path/api.py#498)(self, prefix: str=tempfile.template):**
+&mdash; **def [mkstemp](/recipe_modules/path/api.py#497)(self, prefix: str=tempfile.template):**
 
 Makes a new temporary file, returns Path to it.
 
@@ -3127,23 +3127,23 @@
 either a resource script of your recipe module or recipe.
 ***
 
-&mdash; **def [mock\_add\_directory](/recipe_modules/path/api.py#945)(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#941)(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#934)(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#949)(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#956)(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.
 
@@ -3152,34 +3152,34 @@
   should_remove: Called for every candidate path. Return True to remove this
     path.
 
-&mdash; **def [normpath](/recipe_modules/path/api.py#897)(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#783)(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#793)(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#885)(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#889)(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#788)(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#840)(self, path):**
+&mdash; **def [split](/recipe_modules/path/api.py#839)(self, path):**
 
 For "foo/bar/baz", return ("foo/bar", "baz").
 
@@ -3193,7 +3193,7 @@
 
 Returns (dirname(path), basename(path)).
 
-&mdash; **def [splitext](/recipe_modules/path/api.py#861)(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").
 
@@ -3208,7 +3208,7 @@
 Returns:
   (name, extension_including_dot).
 
-&emsp; **@property**<br>&mdash; **def [start\_dir](/recipe_modules/path/api.py#703)(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.
@@ -3217,7 +3217,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#723)(self):**
+&emsp; **@property**<br>&mdash; **def [tmp\_base\_dir](/recipe_modules/path/api.py#722)(self):**
 
 This directory is the system-configured temp dir.
 
diff --git a/recipe_engine/recipe_api.py b/recipe_engine/recipe_api.py
index 67fe28d..f6214da 100644
--- a/recipe_engine/recipe_api.py
+++ b/recipe_engine/recipe_api.py
@@ -490,7 +490,7 @@
     self._module = module
     self._resource_directory = config_types.Path(
         config_types.ResolvedBasePath.for_recipe_module(
-            test_data.enabled, module)).join('resources')
+            test_data.enabled, module)) / 'resources'
     self._repo_root = config_types.Path(
         config_types.ResolvedBasePath.for_bundled_repo(test_data.enabled,
                                                        module.repo))
@@ -603,13 +603,13 @@
     """
     # TODO(vadimsh): Verify that file exists. Including a case like:
     #  module.resource('dir').join('subdir', 'file.py')
-    return self._resource_directory.join(*path)
+    return self._resource_directory.joinpath(*path)
 
   def repo_resource(self, *path):
     """Returns a resource path, where path is relative to the root of
     the recipe repo where this module is defined.
     """
-    return self._repo_root.join(*path)
+    return self._repo_root.joinpath(*path)
 
 
 @dataclass
@@ -650,13 +650,13 @@
     """
     # TODO(vadimsh): Verify that file exists. Including a case like:
     #  module.resource('dir').join('subdir', 'file.py')
-    return self._resource_path.join(*path)
+    return self._resource_path.joinpath(*path)
 
   def repo_resource(self, *path):
     """Returns a resource path, where path is relative to the root of
     the recipe repo where this module is defined.
     """
-    return self._repo_path.join(*path)
+    return self._repo_path.joinpath(*path)
 
   def __getattr__(self, key):
     raise ModuleInjectionError(
diff --git a/recipe_modules/archive/examples/full.py b/recipe_modules/archive/examples/full.py
index c9d4e51..f9ac3eb 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.joinpath('output')
   api.file.rmtree('cleanup', out)
   api.file.ensure_directory('mkdirs out', out)
 
@@ -26,57 +26,57 @@
 
   # Make a bunch of files
   api.step('touch a', ['echo', 'hello a'],
-           stdout=api.raw_io.output(leak_to=temp.join('a')))
+           stdout=api.raw_io.output(leak_to=temp.joinpath('a')))
   api.step('touch b', ['echo', 'hello b'],
-           stdout=api.raw_io.output(leak_to=temp.join('b')))
-  api.file.ensure_directory('mkdirs sub/dir', temp.join('sub', 'dir'))
+           stdout=api.raw_io.output(leak_to=temp.joinpath('b')))
+  api.file.ensure_directory('mkdirs sub/dir', temp.joinpath('sub', 'dir'))
   api.step('touch c', ['echo', 'hello c'],
-           stdout=api.raw_io.output(leak_to=temp.join('sub', 'dir', 'c')))
+           stdout=api.raw_io.output(leak_to=temp.joinpath('sub', 'dir', 'c')))
 
   # Build a tar of the whole `temp` directory.
   out_tar = (api.archive.package(temp).
              with_dir(temp).
-             archive('archiving', out.join('output.tar.bz2')))
+             archive('archiving', out / 'output.tar.bz2'))
 
   # Build a zip for a subset.
   pkg = api.archive.package(temp)
   pkg = (pkg.
-         with_file(pkg.root.join('a')).
-         with_file(pkg.root.join('b')).
-         with_dir(pkg.root.join('sub')))
-  out_zip = pkg.archive('archiving more', out.join('more.zip'))
+         with_file(pkg.root.joinpath('a')).
+         with_file(pkg.root.joinpath('b')).
+         with_dir(pkg.root.joinpath('sub')))
+  out_zip = pkg.archive('archiving more', out / 'more.zip')
 
   # Zip the whole root
   all_zip = api.archive.package(temp).archive(
     'archiving all_zip',
-    out.join('all_zip.zip')
+    out / 'all_zip.zip'
   )
 
   # Build a tar.zst of the whole root as well.
   all_tzst = api.archive.package(temp).archive('archiving all_tzst',
-                                               out.join('all_tzst.tzst'))
+                                               out / 'all_tzst.tzst')
 
   # Extract the packages.
-  api.archive.extract('extract tar', out_tar, temp.join('output1'))
-  api.archive.extract('extract zip', out_zip, temp.join('output2'))
-  api.archive.extract('extract all_zip zip', all_zip, temp.join('output3'))
-  api.archive.extract('extract all_zip as zip', all_zip, temp.join('output4'),
-                      archive_type='zip')
-  api.archive.extract('extract all_tzst', all_tzst, temp.join('output5'))
+  api.archive.extract('extract tar', out_tar, temp.joinpath('output1'))
+  api.archive.extract('extract zip', out_zip, temp.joinpath('output2'))
+  api.archive.extract('extract all_zip zip', all_zip, temp.joinpath('output3'))
+  api.archive.extract('extract all_zip as zip', all_zip,
+                      temp.joinpath('output4'), archive_type='zip')
+  api.archive.extract('extract all_tzst', all_tzst, temp.joinpath('output5'))
 
   try:
-    api.archive.extract('extract failure', out_zip, temp.join('output3'))
+    api.archive.extract('extract failure', out_zip, temp.joinpath('output3'))
   except api.step.StepFailure:
     pass
 
   # List extracted content.
-  api.step('listing output1', ['find', temp.join('output1')])
-  api.step('listing output2', ['find', temp.join('output2')])
+  api.step('listing output1', ['find', temp.joinpath('output1')])
+  api.step('listing output2', ['find', temp.joinpath('output2')])
 
   # Extract only a subset
-  api.archive.extract('extract tar subset', out_tar, temp.join('output_sub'),
-                      include_files=['*/dir/*'])
-  api.step('listing output_sub', ['find', temp.join('output_sub')])
+  api.archive.extract('extract tar subset', out_tar,
+                      temp.joinpath('output_sub'), include_files=['*/dir/*'])
+  api.step('listing output_sub', ['find', temp.joinpath('output_sub')])
 
 
 def GenTests(api):
diff --git a/recipe_modules/bcid_reporter/api.py b/recipe_modules/bcid_reporter/api.py
index 1a51c71..87dbd23 100644
--- a/recipe_modules/bcid_reporter/api.py
+++ b/recipe_modules/bcid_reporter/api.py
@@ -31,12 +31,12 @@
     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 / 'reporter'
       ensure_file = self.m.cipd.EnsureFile().add_package(
           'infra/tools/security/provenance_broker/${platform}',
           _LATEST_STABLE_VERSION)
       self.m.cipd.ensure(reporter_dir, ensure_file)
-      self._broker_bin = reporter_dir.join('snoopy_broker')
+      self._broker_bin = reporter_dir / 'snoopy_broker'
     return self._broker_bin
 
   def report_stage(self, stage, server_url=None):
diff --git a/recipe_modules/buildbucket/api.py b/recipe_modules/buildbucket/api.py
index 3487e82..28e99b8 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_dir.join('builder')
+    return self.m.path.cache_dir / 'builder'
 
   # RPCs.
 
diff --git a/recipe_modules/cas/examples/full.py b/recipe_modules/cas/examples/full.py
index 3559d8a..c33c544 100644
--- a/recipe_modules/cas/examples/full.py
+++ b/recipe_modules/cas/examples/full.py
@@ -17,13 +17,13 @@
 
   # Prepare files.
   temp = api.path.mkdtemp('cas-example')
-  api.step('touch a', ['touch', temp.join('a')])
-  api.step('touch b', ['touch', temp.join('b')])
-  api.file.ensure_directory('mkdirs', temp.join('sub', 'dir'))
-  api.step('touch d', ['touch', temp.join('sub', 'dir', 'd')])
+  api.step('touch a', ['touch', temp / 'a'])
+  api.step('touch b', ['touch', temp / 'b'])
+  api.file.ensure_directory('mkdirs', temp / 'sub' / 'dir')
+  api.step('touch d', ['touch', temp / 'sub' / 'dir' / 'd'])
 
   digest = api.cas.archive('archive', temp,
-                           *[temp.join(p) for p in ('a', 'b', 'sub')])
+                           *[temp / p for p in ('a', 'b', 'sub')])
   # You can also archive the entire directory.
   with api.cas.with_instance('projects/other-cas-server/instances/instance'):
     api.cas.archive('archive directory', temp, log_level='debug', timeout=60)
diff --git a/recipe_modules/cas_input/api.py b/recipe_modules/cas_input/api.py
index d42356a..28def26 100644
--- a/recipe_modules/cas_input/api.py
+++ b/recipe_modules/cas_input/api.py
@@ -48,7 +48,7 @@
     for cache in caches:
       cache_out = output_dir
       if cache.local_relpath:
-        cache_out = cache_out.join(cache.local_relpath)
+        cache_out = cache_out / cache.local_relpath
       self.m.cas.download("download cache", cache.digest, cache_out)
 
     return output_dir
diff --git a/recipe_modules/cipd/api.py b/recipe_modules/cipd/api.py
index 2371b00..dc7a369 100644
--- a/recipe_modules/cipd/api.py
+++ b/recipe_modules/cipd/api.py
@@ -1017,10 +1017,10 @@
     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.joinpath('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(
+    package_dir = package_dir.joinpath(
         hashlib.sha256(version.encode('utf-8')).hexdigest())
     basename = package_parts[-1]
 
@@ -1044,4 +1044,4 @@
     if executable_path is None:
       executable_path = basename
 
-    return package_dir.join(*executable_path.split('/'))
+    return package_dir / executable_path
diff --git a/recipe_modules/cipd/examples/full.py b/recipe_modules/cipd/examples/full.py
index 7c24c8a..076edad 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 / '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 / '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 / '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 / '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 / 'val1.json',
       ),
       api.cipd.Metadata(
           key='key2',
-          value_from_file=api.path.start_dir.join('val2.json'),
+          value_from_file=api.path.start_dir / '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 / '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 / 'raw_root',
+    api.path.start_dir / 'fetched_pkg')
 
   api.cipd.ensure(
       cipd_root,
-      api.path.start_dir.join('cipd.ensure'),
+      api.path.start_dir / 'cipd.ensure',
       name='ensure with existing file')
   api.cipd.ensure_file_resolve(
-      api.path.start_dir.join('cipd.ensure'),
+      api.path.start_dir / '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/examples/full.py b/recipe_modules/context/examples/full.py
index 315a6be..89e2e77 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 / '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 / 'pants'
+  shirt = api.path.start_dir / '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 91f2a4a..523aadd 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 / '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 dcca227..211a99e 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 / 'pants'
+  shirt = api.path.start_dir / 'shirt'
+  good_hat = api.path.start_dir / 'good_hat'
+  bad_hat = api.path.start_dir / 'bad_hat'
   with api.context(env={_KEY: 'bar'}):
     expect_step('env step', 'bar')
 
@@ -94,4 +94,3 @@
 
 def GenTests(api):
   yield api.test('basic')
-
diff --git a/recipe_modules/file/api.py b/recipe_modules/file/api.py
index 6e6173e..b28fd1e 100644
--- a/recipe_modules/file/api.py
+++ b/recipe_modules/file/api.py
@@ -499,10 +499,7 @@
       cmd.append('--hidden')
     result = self._run(name, cmd, lambda: self.test_api.glob_paths(test_data),
                        self.m.raw_io.output_text())
-    ret = [
-        source.join(*x.split(self.m.path.sep))
-        for x in result.stdout.splitlines()
-    ]
+    ret = [source / x for x in result.stdout.splitlines()]
     result.presentation.logs["glob"] = [str(x) for x in ret]
     return ret
 
@@ -553,10 +550,7 @@
                        (['--recursive'] if recursive else
                         []), lambda: self.test_api.listdir(test_data),
                        self.m.raw_io.output_text())
-    ret = [
-        source.join(*x.split(self.m.path.sep))
-        for x in result.stdout.splitlines()
-    ]
+    ret = [source / x for x in result.stdout.splitlines()]
     if include_log:
       result.presentation.logs['listdir'] = [str(x) for x in ret]
     return ret
diff --git a/recipe_modules/file/examples/compute_hash.py b/recipe_modules/file/examples/compute_hash.py
index 9a7faa7..692c29a 100644
--- a/recipe_modules/file/examples/compute_hash.py
+++ b/recipe_modules/file/examples/compute_hash.py
@@ -10,12 +10,12 @@
 
 def RunSteps(api):
   base_path = api.path.start_dir
-  some_dir = api.path.start_dir.join('some_dir')
+  some_dir = api.path.start_dir / 'some_dir'
   api.file.ensure_directory('ensure some_dir', some_dir)
 
-  some_file = some_dir.join('some file')
-  sub_dir = some_dir.join('sub')
-  in_subdir = sub_dir.join('f')
+  some_file = some_dir / 'some file'
+  sub_dir = some_dir / 'sub'
+  in_subdir = sub_dir / 'f'
 
   api.file.write_text('write some file', some_file, 'some data')
   api.file.ensure_directory('ensure sub_dir', sub_dir)
@@ -26,10 +26,10 @@
   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 / 'some_other_dir'
   api.file.ensure_directory('ensure some_other_dir', some_other_dir)
 
-  some_other_file = some_other_dir.join('new_f')
+  some_other_file = some_other_dir / 'new_f'
   api.file.write_text('write new_f file', some_other_file, 'some data')
 
   result = api.file.compute_hash('compute_hash of list of dir',
@@ -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 / '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 f76fcbc..019e92b 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 / '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 / '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 / '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 / 'new path',
+                api.path.start_dir / '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 / '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 db4df9e..85939a1 100644
--- a/recipe_modules/file/examples/copytree.py
+++ b/recipe_modules/file/examples/copytree.py
@@ -11,32 +11,32 @@
 def RunSteps(api):
   file_names = ['a', 'aa', 'b', 'bb', 'c', 'cc']
 
-  dest = api.path.start_dir.join('some dir')
+  dest = api.path.start_dir / '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])
+    api.file.write_text('write %s' % fname, dest / fname, fname)
+  api.file.filesizes('check filesizes', [dest / f for f in file_names])
 
-  dest2 = api.path.start_dir.join('some other dir')
+  dest2 = api.path.start_dir / 'some other dir'
   api.file.rmtree('make sure dest is gone', dest2)
   api.file.copytree('copy it', dest, dest2)
 
   paths = api.file.listdir('list new dir', dest2, test_data=file_names)
-  assert paths == [dest2.join(n) for n in file_names], paths
+  assert paths == [dest2 / n for n in file_names], paths
 
   paths = api.file.glob_paths('glob *a', dest2, '*a', test_data=['a', 'aa'])
-  assert paths == [dest2.join('a'), dest2.join('aa')], paths
+  assert paths == [dest2 / 'a', dest2 / 'aa'], paths
 
   for pth in paths:
     assert api.file.read_text('read %s' % pth, pth, pth.name)
 
-  api.file.remove('rm a', dest2.join('a'))
+  api.file.remove('rm a', dest2 / 'a')
   paths = api.file.glob_paths('glob *a', dest2, '*a', test_data=['aa'])
-  assert paths == [dest2.join('aa')], paths
+  assert paths == [dest2 / 'aa'], paths
 
   api.file.rmglob('rm b*', dest2, 'b*')
   paths = api.file.listdir('list new dir', dest2, test_data=['aa', 'c', 'cc'])
-  assert paths == [dest2.join(p) for p in ['aa', 'c', 'cc']], paths
+  assert paths == [dest2 / p for p in ['aa', 'c', 'cc']], paths
 
   api.file.rmcontents('remove "some other dir/*"', dest2)
   assert api.path.exists(dest2), dest2
diff --git a/recipe_modules/file/examples/error.py b/recipe_modules/file/examples/error.py
index 2a41c90..cba7a90 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 / '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 5a19d01..2fd61da 100644
--- a/recipe_modules/file/examples/file_hash.py
+++ b/recipe_modules/file/examples/file_hash.py
@@ -9,10 +9,10 @@
 ]
 
 def RunSteps(api):
-  some_dir = api.path.start_dir.join('some_dir')
+  some_dir = api.path.start_dir / 'some_dir'
   api.file.ensure_directory('ensure some_dir', some_dir)
 
-  some_file = some_dir.join('some file')
+  some_file = some_dir / 'some file'
 
   api.file.write_text('write some file', some_file, 'some data')
 
@@ -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 / '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 46a1d24..2ce7f9f 100644
--- a/recipe_modules/file/examples/flatten_single_directories.py
+++ b/recipe_modules/file/examples/flatten_single_directories.py
@@ -9,23 +9,23 @@
 
 
 def RunSteps(api):
-  base = api.path.start_dir.join('dir')
-  long_dir = base.join('which_has', 'some', 'singular', 'subdirs')
+  base = api.path.start_dir / 'dir'
+  long_dir = base / 'which_has' / 'some' / 'singular' / 'subdirs'
 
   api.file.ensure_directory('make chain of single dirs', long_dir)
 
   filenames = ['bunch', 'of', 'files']
   for n in filenames:
-    api.file.truncate('touch %s' % n, long_dir.join(n), 1)
+    api.file.truncate('touch %s' % n, long_dir / n, 1)
 
   api.file.flatten_single_directories('remove single dirs', base)
   # To satisfy simulation; run this example for real to get the useful
   # assertions below.
   for n in filenames:
-    api.path.mock_add_paths(base.join(n))
+    api.path.mock_add_paths(base / n)
 
   for n in filenames:
-    path = base.join(n)
+    path = base / n
     assert api.path.exists(path), path
 
 
diff --git a/recipe_modules/file/examples/glob.py b/recipe_modules/file/examples/glob.py
index 9a7f94c..a498d84 100644
--- a/recipe_modules/file/examples/glob.py
+++ b/recipe_modules/file/examples/glob.py
@@ -11,43 +11,42 @@
 def RunSteps(api):
   sd = api.path.start_dir
 
-  api.file.ensure_directory('mkdir a', sd.join('a'))
-  api.file.ensure_directory('mkdir b', sd.join('b'))
+  api.file.ensure_directory('mkdir a', sd / 'a')
+  api.file.ensure_directory('mkdir b', sd / 'b')
 
   for fname in ['thing.pat', 'other.pat', 'something', 'file', '.hidden.pat']:
-    api.file.write_text("write %s" % fname, sd.join(fname), 'data')
-    api.file.write_text("write a/%s" % fname, sd.join('a', fname), 'data')
-    api.file.write_text("write b/%s" % fname, sd.join('b', fname), 'data')
+    api.file.write_text("write %s" % fname, sd / fname, 'data')
+    api.file.write_text("write a/%s" % fname, sd / 'a' / fname, 'data')
+    api.file.write_text("write b/%s" % fname, sd / 'b' / fname, 'data')
 
   hits = api.file.glob_paths("pat", sd, '*.pat', test_data=['thing.pat', 'other.pat'])
-  assert hits == [sd.join('other.pat'), sd.join('thing.pat')], hits
+  assert hits == [sd / 'other.pat', sd / 'thing.pat'], hits
 
   hits = api.file.glob_paths("noop", sd, '*.nop', test_data=[])
   assert hits == [], hits
 
   hits = api.file.glob_paths("thing", sd, '*thing*', test_data=['thing.pat', 'something'])
-  assert hits == [sd.join('something'), sd.join('thing.pat')], hits
+  assert hits == [sd / 'something', sd / 'thing.pat'], hits
 
   hits = api.file.glob_paths("nest", sd, '*/*.pat', test_data=[
     'a/other.pat', 'b/thing.pat', 'b/other.pat', 'a/thing.pat',
   ])
-  assert hits == [sd.join('a', 'other.pat'), sd.join('a', 'thing.pat'),
-                  sd.join('b', 'other.pat'), sd.join('b', 'thing.pat')], hits
+  assert hits == [sd / 'a' / 'other.pat', sd / 'a' / 'thing.pat',
+                  sd / 'b' / 'other.pat', sd / 'b' / 'thing.pat'], hits
 
   hits = api.file.glob_paths("recursive", sd, '**/*.pat', test_data=[
     'thing.pat', 'other.pat', 'a/other.pat', 'b/thing.pat',
     'b/other.pat', 'a/thing.pat',
   ])
-  assert hits == [sd.join('a', 'other.pat'), sd.join('a', 'thing.pat'),
-                  sd.join('b', 'other.pat'), sd.join('b', 'thing.pat'),
-                  sd.join('other.pat'), sd.join('thing.pat')], hits
+  assert hits == [sd / 'a' / 'other.pat', sd / 'a' / 'thing.pat',
+                  sd / 'b' / 'other.pat', sd / 'b' / 'thing.pat',
+                  sd / 'other.pat', sd / 'thing.pat'], hits
 
   hits = api.file.glob_paths("hidden", sd, '*.pat', include_hidden=True, test_data=[
     '.hidden.pat', 'thing.pat', 'other.pat'])
-  assert hits == [sd.join('.hidden.pat'), sd.join('other.pat'),
-                  sd.join('thing.pat')], hits
+  assert hits == [sd / '.hidden.pat', sd / 'other.pat',
+                  sd / 'thing.pat'], hits
 
 
 def GenTests(api):
   yield api.test('basic')
-
diff --git a/recipe_modules/file/examples/handle_json_file.py b/recipe_modules/file/examples/handle_json_file.py
index fa5c44c..3cbba6f 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 / '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 4610ad2..2ca225b 100644
--- a/recipe_modules/file/examples/listdir.py
+++ b/recipe_modules/file/examples/listdir.py
@@ -9,15 +9,15 @@
 
 
 def RunSteps(api):
-  root_dir = api.path.start_dir.join('root_dir')
+  root_dir = api.path.start_dir / 'root_dir'
   api.file.ensure_directory('ensure root_dir', root_dir)
 
   listdir_result = api.file.listdir('listdir root_dir', root_dir, test_data=[])
   assert listdir_result == [], (listdir_result, [])
 
-  some_file = root_dir.join('some file')
-  sub_dir = root_dir.join('sub')
-  in_subdir = sub_dir.join('f')
+  some_file = root_dir / 'some file'
+  sub_dir = root_dir / 'sub'
+  in_subdir = sub_dir / 'f'
 
   api.file.write_text('write some file', some_file, 'some data')
   api.file.ensure_directory('mkdir', sub_dir)
diff --git a/recipe_modules/file/examples/raw_copy.py b/recipe_modules/file/examples/raw_copy.py
index 2596c5e..3aaebbd 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 / '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 / '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 / '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 / 'new path',
+                api.path.start_dir / '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 / '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 c8b707b..5a9d44b 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 / '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 e87b239..77e30b1 100644
--- a/recipe_modules/file/examples/symlink.py
+++ b/recipe_modules/file/examples/symlink.py
@@ -10,26 +10,26 @@
 
 
 def RunSteps(api):
-  src = api.path.start_dir.join('some file')
+  src = api.path.start_dir / '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 / '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 / 'new path', test_data=data)
 
   assert read_data == data, (read_data, data)
 
 
   # Also create a tree of symlinks.
-  root = api.path.cleanup_dir.join('root')
+  root = api.path.cleanup_dir / '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')
-  tree.register_link(src2, root.join('yet', 'another', 'symlink'))
+  tree.register_link(src, root / 'another' / 'symlink')
+  tree.register_link(src, root / 'another' / 'symlink')
+  src2 = api.path.start_dir / 'a-second-file'
+  tree.register_link(src2, root / 'yet' / 'another' / 'symlink')
   tree.create_links('create a tree of symlinks')
 
 def GenTests(api):
diff --git a/recipe_modules/file/examples/truncate.py b/recipe_modules/file/examples/truncate.py
index 14c573d..761bb14 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 / '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 5446970..5cf26fc 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_dir.join('pid_file')
+    pid_file = api.path.cleanup_dir / '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 d91f724..ac52b53 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 / 'deep'):
       work.append(api.futures.spawn(
           api.step, 'cool step', cmd=['echo', 'cool']))
 
diff --git a/recipe_modules/golang/api.py b/recipe_modules/golang/api.py
index 31e3bb4..c14cb89 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_dir.join('golang')
-    cache = cache or self.m.path.cache_dir.join('gocache')
+    path = path or self.m.path.cache_dir / 'golang'
+    cache = cache or self.m.path.cache_dir / '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):
@@ -67,9 +67,9 @@
 
         # Caches and GOBIN can be shared across Go versions: defaults are shared
         # hardcoded paths under '~'.
-        'GOBIN': cache.join('bin'),
-        'GOCACHE': cache.join('cache'),
-        'GOMODCACHE': cache.join('modcache'),
+        'GOBIN': cache / 'bin',
+        'GOCACHE': cache / 'cache',
+        'GOMODCACHE': cache / 'modcache',
     }
 
     # Disable cgo on Windows by default since it lacks a C compiler by default.
@@ -77,7 +77,7 @@
       env['CGO_ENABLED'] = '0'
 
     env_prefixes = {
-        'PATH': [path.join('bin')],
+        'PATH': [path / 'bin'],
     }
 
     env_suffixes = {
diff --git a/recipe_modules/json/examples/full.py b/recipe_modules/json/examples/full.py
index a18fc97..ad00ade 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_dir.join('temp.json')
+  leak_path = api.path.tmp_base_dir / '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_dir.join('leak.json')),
+      api.json.output(leak_to=api.path.tmp_base_dir / 'leak.json'),
   ])
   assert step_result.json.output == example_dict
 
diff --git a/recipe_modules/nodejs/api.py b/recipe_modules/nodejs/api.py
index 9888c6a..ffa273e 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_dir.join('nodejs')
-    cache = cache or self.m.path.cache_dir.join('npmcache')
+    path = path or self.m.path.cache_dir / 'nodejs'
+    cache = cache or self.m.path.cache_dir / '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):
@@ -57,9 +57,9 @@
 
     env = {
         # npm's content-addressed cache.
-        'npm_config_cache': cache.join('npm'),
+        'npm_config_cache': cache / 'npm',
         # Where packages are installed when using 'npm -g ...'.
-        'npm_config_prefix': cache.join('pfx'),
+        'npm_config_prefix': cache / 'pfx',
     }
 
     env_prefixes = {
@@ -67,8 +67,8 @@
             # Putting this in front of PATH (before `bin` from the CIPD package)
             # allows doing stuff like `npm install -g npm@8.1.4` and picking up
             # the updated `npm` binary from `<npm_config_prefix>/bin`.
-            env['npm_config_prefix'].join('bin'),
-            path.join('bin'),
+            env['npm_config_prefix'] / 'bin',
+            path / 'bin',
         ],
     }
 
diff --git a/recipe_modules/path/api.py b/recipe_modules/path/api.py
index 3f36307..1846437 100644
--- a/recipe_modules/path/api.py
+++ b/recipe_modules/path/api.py
@@ -135,7 +135,7 @@
     for filepath in test_data.get('files_exist', ()):
       if isinstance(filepath, test_api.UnvalidatedPath):
         filepath = _cast_to_path_impl(path_mod,
-                                      filepath.base).join(*filepath.pieces)
+                                      filepath.base).joinpath(*filepath.pieces)
       assert isinstance(filepath, config_types.Path), (
           f'path.files_exist module test data contains non-Path {type(filepath)}'
       )
@@ -144,7 +144,7 @@
     for dirpath in test_data.get('dirs_exist', ()):
       if isinstance(dirpath, test_api.UnvalidatedPath):
         dirpath = _cast_to_path_impl(path_mod,
-                                     dirpath.base).join(*dirpath.pieces)
+                                     dirpath.base).joinpath(*dirpath.pieces)
       assert isinstance(dirpath, config_types.Path), (
           f'path.files_exist module test data contains non-Path {type(dirpath)}'
       )
@@ -486,11 +486,10 @@
       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_dir.join(new_path[len(cleanup_dir):])
+      temp_dir = self.cleanup_dir / new_path[len(cleanup_dir):]
     else:
       self._test_counter[prefix] += 1
-      temp_dir = self.cleanup_dir.join(
-          f'{prefix}_tmp_{self._test_counter[prefix]}')
+      temp_dir = self.cleanup_dir / f'{prefix}_tmp_{self._test_counter[prefix]}'
 
     self.mock_add_paths(temp_dir, FileType.DIRECTORY)
     return temp_dir
@@ -514,11 +513,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_dir.join(new_path[len(cleanup_dir):])
+      temp_file = self.cleanup_dir / new_path[len(cleanup_dir):]
       os.close(fd)
     else:
       self._test_counter[prefix] += 1
-      temp_file = self.cleanup_dir.join(
+      temp_file = self.cleanup_dir.joinpath(
           f'{prefix}_tmp_{self._test_counter[prefix]}')
     self.mock_add_paths(temp_file, FileType.FILE)
     return temp_file
@@ -584,7 +583,7 @@
                        abs_string_path)
 
     sub_path = abs_string_path[len(sPath):].strip(self.sep)
-    return path.join(*sub_path.split(self.sep))
+    return path.joinpath(*sub_path.split(self.sep))
 
   def __contains__(self, pathname: NamedBasePathsType) -> bool:
     """This method is DEPRECATED.
@@ -827,9 +826,9 @@
 
     Note that Path objects returned from this module (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.
+    new_path = p.joinpath('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
diff --git a/recipe_modules/path/examples/full.py b/recipe_modules/path/examples/full.py
index 312b4d8..1b8ac3e 100644
--- a/recipe_modules/path/examples/full.py
+++ b/recipe_modules/path/examples/full.py
@@ -17,7 +17,7 @@
 
 @recipe_api.ignore_warnings('recipe_engine/CHECKOUT_DIR_DEPRECATED')
 def RunSteps(api):
-  api.step('step1', ['/bin/echo', str(api.path.tmp_base_dir.join('foo'))])
+  api.step('step1', ['/bin/echo', str(api.path.tmp_base_dir / 'foo')])
 
   # module.resource(...) demo.
   api.step('print resource',
@@ -28,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 / '.' == api.path.start_dir
 
   assert 'checkout' not in api.path
-  api.path.checkout_dir = api.path.tmp_base_dir.join('checkout')
+  api.path.checkout_dir = api.path.tmp_base_dir / 'checkout'
   assert 'checkout' in api.path
 
   # Test missing/default value.
@@ -43,7 +43,7 @@
     assert 'unknown base path' in str(ex), str(ex)
 
   # Global dynamic paths (see config.py example for declaration):
-  dynamic_path = api.path.checkout_dir.join('jerky')
+  dynamic_path = api.path.checkout_dir / 'jerky'
   api.step('checkout path', ['/bin/echo', dynamic_path])
 
   # Methods from python os.path are available via api.path. For testing, we
@@ -56,7 +56,7 @@
   temp_file = api.path.mkstemp('kawaac')
   assert api.path.exists(temp_file)
 
-  file_path = api.path.tmp_base_dir.join('new_file')
+  file_path = api.path.tmp_base_dir / 'new_file'
   abspath = api.path.abspath(file_path)
   api.path.assert_absolute(abspath)
   try:
@@ -79,8 +79,8 @@
   assert file_path.parent == api.path.tmp_base_dir
   assert api.path.split(file_path) == (api.path.tmp_base_dir, 'new_file')
 
-  thing_bat = api.path.tmp_base_dir.join('thing.bat')
-  thing_bat_mkv = api.path.tmp_base_dir.join('thing.bat.mkv')
+  thing_bat = api.path.tmp_base_dir / 'thing.bat'
+  thing_bat_mkv = api.path.tmp_base_dir / 'thing.bat.mkv'
   assert api.path.splitext(thing_bat_mkv) == (thing_bat, '.mkv')
 
   assert api.path.abs_to_path(api.path.tmp_base_dir) == api.path.tmp_base_dir
@@ -112,8 +112,8 @@
   normpath = api.path.normpath(file_path)
   assert api.path.exists(normpath)
 
-  directory = api.path.start_dir.join('directory')
-  filepath = directory.join('filepath')
+  directory = api.path.start_dir / 'directory'
+  filepath = directory / 'filepath'
   api.step('rm directory (initial)', ['rm', '-rf', directory])
   assert not api.path.exists(directory)
   assert not api.path.isdir(directory)
@@ -144,27 +144,27 @@
   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 / 'copy1'
+  copy10 = api.path.start_dir / 'copy10'
+  copy2 = api.path.start_dir / 'copy2'
+  copy20 = api.path.start_dir / 'copy20'
   api.step('rm copy2 (initial)', ['rm', '-rf', copy2])
   api.step('rm copy20 (initial)', ['rm', '-rf', copy20])
 
-  api.step('mkdirs', ['mkdir', '-p', copy1.join('foo', 'bar')])
-  api.path.mock_add_paths(copy1.join('foo', 'bar'))
+  api.step('mkdirs', ['mkdir', '-p', copy1 / 'foo' / 'bar'])
+  api.path.mock_add_paths(copy1.joinpath('foo', 'bar'))
   api.step('touch copy10', ['touch', copy10])
   api.path.mock_add_paths(copy10)
   api.step('cp copy1 copy2', ['cp', '-a', copy1, copy2])
   api.path.mock_copy_paths(copy1, copy2)
-  assert api.path.exists(copy2.join('foo', 'bar'))
+  assert api.path.exists(copy2 / 'foo' / 'bar')
   assert not api.path.exists(copy20)
 
   # We can mock remove paths. See the file module to do this for real.
-  api.step('rm copy2/foo', ['rm', '-rf', copy2.join('foo')])
+  api.step('rm copy2/foo', ['rm', '-rf', copy2 / 'foo'])
   api.path.mock_remove_paths(str(copy2)+api.path.sep)
-  assert not api.path.exists(copy2.join('foo', 'bar'))
-  assert not api.path.exists(copy2.join('foo'))
+  assert not api.path.exists(copy2 / 'foo' / 'bar')
+  assert not api.path.exists(copy2 / 'foo')
   assert api.path.exists(copy2)
 
   api.step('touch copy20', ['touch', copy20])
@@ -177,7 +177,7 @@
   # Convert strings to Paths.
   def _mk_paths():
     return [
-        api.path.start_dir.join('some', 'thing'),
+        api.path.start_dir / 'some' / 'thing',
         api.path.start_dir,
         api.path.cache_dir / 'a file',
         api.path.home_dir / 'another file',
@@ -227,15 +227,15 @@
   assert not (b < a)
 
   # there is also a join method on the path module
-  assert start_dir.join('a') == api.path.join(start_dir, 'a')
+  assert start_dir / '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 / f'foo{api.path.sep}bar'
+  separated_path = api.path.start_dir / '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.joinpath(
       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/tests/dynamic_paths.py b/recipe_modules/path/tests/dynamic_paths.py
index 701c097..2fe2721 100644
--- a/recipe_modules/path/tests/dynamic_paths.py
+++ b/recipe_modules/path/tests/dynamic_paths.py
@@ -17,26 +17,26 @@
   try:
     # Note - legacy api.path.get('checkout') is the only way to get a dynamic
     # checkout path before assignment to checkout_dir.
-    api.path.checkout_dir = api.path.get('checkout').join('something')
+    api.path.checkout_dir = api.path.get('checkout') / 'something'
     assert False, 'able to assign string to path?'  # pragma: no cover
   except ValueError as ex:
     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 / '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_dir = api.path.start_dir.join('coolstuff')
+  api.path.checkout_dir = api.path.start_dir / 'coolstuff'
 
   # Can re-set to the same thing
-  api.path.checkout_dir = api.path.start_dir.join('coolstuff')
+  api.path.checkout_dir = api.path.start_dir / 'coolstuff'
 
   try:
     # Setting a new value is not allowed
-    api.path.checkout_dir = api.path.start_dir.join('neatstuff')
+    api.path.checkout_dir = api.path.start_dir / '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/test_api_legacy.py b/recipe_modules/path/tests/test_api_legacy.py
index b72fe4f..9199b7e 100644
--- a/recipe_modules/path/tests/test_api_legacy.py
+++ b/recipe_modules/path/tests/test_api_legacy.py
@@ -31,8 +31,8 @@
       name += '_dir'
     return getattr(api.path, name)
 
-  paths = [base_path(name).join('file') for name in GETITEM_NAMES]
-  paths.append(api.path.checkout_dir.join('file'))
+  paths = [base_path(name) / 'file' for name in GETITEM_NAMES]
+  paths.append(api.path.checkout_dir / 'file')
 
   yield api.test(
       'basic',
diff --git a/recipe_modules/proto/tests/placeholders.py b/recipe_modules/proto/tests/placeholders.py
index dce3bae..d2ffa5e 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 / '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 / '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 2ac863d..776072f 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_dir.join('out.txt')),
+          leak_to=api.path.tmp_base_dir / '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_dir.join('out'))])
+      leak_to=api.path.tmp_base_dir / '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_dir.join('missing.txt'))])
+       api.raw_io.output(leak_to=api.path.tmp_base_dir / '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 a2c691f..c7c9c1c 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 / 'key_name.json'),
   )
 
   yield api.test(
diff --git a/recipe_modules/step/api.py b/recipe_modules/step/api.py
index 0941c7c..ee8119a 100644
--- a/recipe_modules/step/api.py
+++ b/recipe_modules/step/api.py
@@ -411,7 +411,7 @@
       raise ValueError('expected None, str or Path; got %r' % (output_path,))
     ext = '.pb'
     if output_path is None:
-      output_path = self.m.path.mkdtemp().join('sub_build' + ext)
+      output_path = self.m.path.mkdtemp() / f'sub_build{ext}'
     else:
       if self.m.path.exists(output_path):
         raise ValueError('expected non-existent output path; '
diff --git a/recipe_modules/step/examples/full.py b/recipe_modules/step/examples/full.py
index c71fb5c..f264794 100644
--- a/recipe_modules/step/examples/full.py
+++ b/recipe_modules/step/examples/full.py
@@ -42,7 +42,7 @@
 
   # 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 / '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
diff --git a/recipe_modules/step/tests/sub_build.py b/recipe_modules/step/tests/sub_build.py
index 8e273a3..c4cb20f 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 = (
-      getattr(api.path, props.output_path.base).join(props.output_path.file))
+      getattr(api.path, props.output_path.base) / 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 / 'sub_build.json'),
       api.expect_exception('ValueError'),
       api.post_process(post_process.StatusException),
       api.post_process(
diff --git a/recipe_modules/swarming/api.py b/recipe_modules/swarming/api.py
index 224ede7..724d675 100644
--- a/recipe_modules/swarming/api.py
+++ b/recipe_modules/swarming/api.py
@@ -964,7 +964,7 @@
       self._output = raw_results.get('output')
       if self._output_dir and raw_results.get('outputs'):
         self._outputs = {
-            output: api.path.join(self._output_dir, output)
+            output: self._output_dir / output
             for output in raw_results['outputs']
         }
 
@@ -1438,8 +1438,8 @@
       task_request = self._task_requests.get((task_id, self._server), [None])[0]
       parsed_results.append(
           TaskResult(self.m, task_request, task_id, task,
-                     output_dir.join(task_id) if output_dir else None,
-                     text_output_dir.join('%s.txt' % task_id)
+                     output_dir / task_id if output_dir else None,
+                     text_output_dir / f'{task_id}.txt'
                         if text_output_dir else None))
 
     parsed_results.sort(key=lambda result: result.name or '')
diff --git a/recipe_modules/tricium/api.py b/recipe_modules/tricium/api.py
index 57122fa..7a8b71b 100644
--- a/recipe_modules/tricium/api.py
+++ b/recipe_modules/tricium/api.py
@@ -171,9 +171,9 @@
         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_dir.join(analyzer.name)
-          output_base = analyzer_dir.join('out')
-          package_dir = analyzer_dir.join('package')
+          analyzer_dir = self.m.path.cleanup_dir / analyzer.name
+          output_base = analyzer_dir / 'out'
+          package_dir = analyzer_dir / 'package'
           self._fetch_legacy_analyzer(package_dir, analyzer)
           results = self._run_legacy_analyzer(
               package_dir,
@@ -192,7 +192,7 @@
           presentation.step_text = 'failed'
     # The tricium data dir with files.json is written in the checkout cache
     # directory and should be cleaned up.
-    self.m.file.rmtree('clean up tricium data dir', input_base.join('tricium'))
+    self.m.file.rmtree('clean up tricium data dir', input_base / 'tricium')
 
     if emit:
       self.write_comments()
@@ -216,7 +216,7 @@
     data_dir = self._ensure_data_dir(base_dir)
     self.m.file.write_proto(
         'write files.json',
-        data_dir.join('files.json'),
+        data_dir / 'files.json',
         files,
         'JSONPB',
         # Tricium analyzers expect camelCase field names.
@@ -234,7 +234,7 @@
     data_dir = self._ensure_data_dir(base_dir)
     results_json = self.m.file.read_text(
         'read results',
-        data_dir.join('results.json'),
+        data_dir / 'results.json',
         test_data='{"comments":[]}')
     return json_format.Parse(results_json, Data.Results())
 
@@ -250,7 +250,7 @@
 
     Returns: Tricium data file directory inside base_dir.
     """
-    data_dir = base_dir.join('tricium', 'data')
+    data_dir = base_dir / 'tricium' / 'data'
     self.m.file.ensure_directory('ensure tricium data dir', data_dir)
     return data_dir
 
@@ -280,7 +280,7 @@
     # expected to be the directory with the analyzer.
     with self.m.context(cwd=package_dir):
       cmd = [
-          package_dir.join(analyzer.executable), '-input', input_dir, '-output',
+          package_dir / analyzer.executable, '-input', input_dir, '-output',
           output_dir
       ] + analyzer.extra_args
       self.m.step('run analyzer',
diff --git a/recipe_modules/tricium/examples/wrapper.py b/recipe_modules/tricium/examples/wrapper.py
index 224d1a0..f50f718 100644
--- a/recipe_modules/tricium/examples/wrapper.py
+++ b/recipe_modules/tricium/examples/wrapper.py
@@ -17,9 +17,9 @@
 
 
 def RunSteps(api):
-  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')
+  checkout_base = api.path.cleanup_dir / 'checkout'
+  api.file.write_text('one', checkout_base / 'one.txt', 'one')
+  api.file.write_text('two', checkout_base / 'foo' / 'two.txt', 'two')
 
   analyzers = [
       api.tricium.analyzers.SPACEY,
diff --git a/recipe_modules/url/examples/full.py b/recipe_modules/url/examples/full.py
index b7499cf..9f1c1b6 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 / 'download.bin'
     v = api.url.get_file(TEST_HTTPS_URL, dest,
                          headers={'Authorization': 'thing'})
     assert str(v.output) == str(dest)
diff --git a/recipes/engine_tests/early_termination.py b/recipes/engine_tests/early_termination.py
index 7764e68..a49c54d 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_dir.join('output_touchfile')
+    output_touchfile = api.path.cleanup_dir.joinpath('output_touchfile')
   running_touchfile = props.running_touchfile
   if not running_touchfile:
-    running_touchfile = api.path.cleanup_dir.join('running_touchfile')
+    running_touchfile = api.path.cleanup_dir.joinpath('running_touchfile')
   # make sure touchfile is there
   api.file.write_text("ensure output_touchfile", output_touchfile,
                       "meep".encode('utf-8'))