Skip to content

10.1 -- Style

Python linters

Learning objectives:

By the end of this tutorial you should: - Understand why using a style guide is a good idea - Be familiar with the 'black' style guide for Python. - Be able to implement 'black' on your code from a shell.

Install some linters

Let's start with installing the pycodestyle, pylint and black linters.

conda install pycodestyle pylint black -c conda-forge

Reading

Please read the following short article about why using a style guide is a good idea. - Why use a style guide (JS)

Then read the following articles which introduce black, and why you should use it as your preferred style. (I do not necessary promote this as the best style guide, personally I use pylint, but I think black is good to know about.) - Black code style

Implementing black

Unlike other linters that we will learn about, black is intended not to be modified, in other words, if you use black you are expected to accept all of its recommendations. For this reason, it doesn't really function as a recommender, but moreso as an implementer. It can make changes directly to your Python files to fix the style errors that it finds. Personally, I find this a little obtrusive, so I always first run it with the argument --diff, which will show you the changes it intends to make without actually making them. It's sort of a dry run.

Let's try this on a few example files from a public repository. Use the commands below to clone two repos into your ~/hacks directory. We will not bother forking the repos this time, since we do not intend to push the changes we will make to these files back to the origin repos.

cd ~/hacks/
git clone https://github.com/rajanil/fastStructure
git clone https://github.com/eaton-lab/toytree/

Let's start by running black on one of the main scripts in the fastStructure package. This is a tool that was published about 6 years ago, so its a bit outdated now, but still in common use. It is written in Python2.7, and impressively black is able to recognize this and propose changes to the code style. Here I run it using the --diff option.

The format of this output is called a diff, it is showing the differences between the original file and the file that black would have made if it had been allowed to overwrite the original file (i.e., if we hadn't used the --diff arg). On the left side of each line it shows a minus or plus sign. The minus lines are from the old file, and the plus lines are showing what would replace the old lines. In some cases it may look like no change was made, this usually means that the difference has to do with the whitespace, for example, by removing extra spaces at the ends of lines.

black --diff fastStructure/structure.py
--- fastStructure/structure.py  2021-02-03 17:02:15.106875 +0000
+++ fastStructure/structure.py  2021-02-03 21:20:31.833481 +0000
@@ -1,137 +1,157 @@
-
 import numpy as np
-import fastStructure 
+import fastStructure
 import parse_bed
 import parse_str
 import random
 import getopt
 import sys
 import pdb
 import warnings

 # ignore warnings with these expressions
-warnings.filterwarnings('ignore', '.*divide by zero.*',)
-warnings.filterwarnings('ignore', '.*invalid value.*',)
+warnings.filterwarnings(
+    "ignore",
+    ".*divide by zero.*",
+)
+warnings.filterwarnings(
+    "ignore",
+    ".*invalid value.*",
+)
+

 def parseopts(opts):

     """
     parses the command-line flags and options passed to the script
     """

-    params = {'mintol': 1e-6,
-            'prior': "simple",
-            'cv': 0,
-            'full': False,
-            'format': 'bed'
-            }
+    params = {
+        "mintol": 1e-6,
+        "prior": "simple",
+        "cv": 0,
+        "full": False,
+        "format": "bed",
+    }

     for opt, arg in opts:

         if opt in ["-K"]:
-            params['K'] = int(arg)
+            params["K"] = int(arg)

         elif opt in ["--input"]:
-            params['inputfile'] = arg
+            params["inputfile"] = arg

         elif opt in ["--output"]:
-            params['outputfile'] = arg
+            params["outputfile"] = arg

         elif opt in ["--prior"]:
-            params['prior'] = arg
-
-            if params['prior'] not in ['simple','logistic']:
+            params["prior"] = arg
+
+            if params["prior"] not in ["simple", "logistic"]:
                 print "%s prior is not currently implemented, defaulting to the simple prior"
-                params['prior'] = 'simple'
+                params["prior"] = "simple"

         elif opt in ["--format"]:
-            params['format'] = arg
+            params["format"] = arg

         elif opt in ["--cv"]:
-            params['cv'] = int(arg)
-        
+            params["cv"] = int(arg)
+
         elif opt in ["--tol"]:
-            params['mintol'] = float(arg)
+            params["mintol"] = float(arg)

         elif opt in ["--full"]:
-            params['full'] = True
+            params["full"] = True

         elif opt in ["--seed"]:
             np.random.seed(int(arg))
             random.seed(int(arg))

     return params

+
 def checkopts(params):

     """
     checks if some of the command-line options passed are valid.
     In the case of invalid options, an exception is always thrown.
     """

-    if params['mintol']<=0:
+    if params["mintol"] <= 0:
         print "a non-positive value was provided as convergence criterion"
         raise ValueError
-    
-    if params['cv']<0:
+
+    if params["cv"] < 0:
         print "a negative value was provided for the number of cross-validations folds"
         raise ValueError

-    if not params.has_key('K'):
+    if not params.has_key("K"):
         print "a positive integer should be provided for number of populations"
         raise KeyError

-    if params['format'] not in ['bed','str']:
+    if params["format"] not in ["bed", "str"]:
         print "%s data format is not currently implemented"
         raise ValueError

-    if params['K']<=0:
+    if params["K"] <= 0:
         print "a negative value was provided for the number of populations"
         raise ValueError
-    
-    if not params.has_key('inputfile'):
+
+    if not params.has_key("inputfile"):
         print "an input file needs to be provided"
-        raise KeyError 
-
-    if not params.has_key('outputfile'):
+        raise KeyError
+
+    if not params.has_key("outputfile"):
         print "an output file needs to be provided"
         raise KeyError
-    
+
+
 def write_output(Q, P, other, params):

     """
     write the posterior means and variational parameters
     to separate output files.
     """

-    handle = open('%s.%d.meanQ'%(params['outputfile'],params['K']),'w')
-    handle.write('\n'.join(['  '.join(['%.6f'%i for i in q]) for q in Q])+'\n')
+    handle = open("%s.%d.meanQ" % (params["outputfile"], params["K"]), "w")
+    handle.write("\n".join(["  ".join(["%.6f" % i for i in q]) for q in Q]) + "\n")
     handle.close()

-    handle = open('%s.%d.meanP'%(params['outputfile'],params['K']),'w')
-    handle.write('\n'.join(['  '.join(['%.6f'%i for i in p]) for p in P])+'\n')
+    handle = open("%s.%d.meanP" % (params["outputfile"], params["K"]), "w")
+    handle.write("\n".join(["  ".join(["%.6f" % i for i in p]) for p in P]) + "\n")
     handle.close()

-    if params['full']:
-        handle = open('%s.%d.varQ'%(params['outputfile'],params['K']),'w')
-        handle.write('\n'.join(['  '.join(['%.6f'%i for i in q]) for q in other['varQ']])+'\n')
+    if params["full"]:
+        handle = open("%s.%d.varQ" % (params["outputfile"], params["K"]), "w")
+        handle.write(
+            "\n".join(["  ".join(["%.6f" % i for i in q]) for q in other["varQ"]])
+            + "\n"
+        )
         handle.close()

-        handle = open('%s.%d.varP'%(params['outputfile'],params['K']),'w')
-        handle.write('\n'.join(['  '.join(['%.6f'%i for i in np.hstack((pb,pg))]) \
-            for pb,pg in zip(other['varPb'],other['varPg'])])+'\n')
+        handle = open("%s.%d.varP" % (params["outputfile"], params["K"]), "w")
+        handle.write(
+            "\n".join(
+                [
+                    "  ".join(["%.6f" % i for i in np.hstack((pb, pg))])
+                    for pb, pg in zip(other["varPb"], other["varPg"])
+                ]
+            )
+            + "\n"
+        )
         handle.close()

+
 def usage():
-    
+
     """
     brief description of various flags and options for this script
     """

     print "\nHere is how you can use this script\n"
-    print "Usage: python %s"%sys.argv[0]
+    print "Usage: python %s" % sys.argv[0]
     print "\t -K <int> (number of populations)"
     print "\t --input=<file> (/path/to/input/file)"
     print "\t --output=<file> (/path/to/output/file)"
     print "\t --tol=<float> (convergence criterion; default: 10e-6)"
     print "\t --prior={simple,logistic} (choice of prior; default: simple)"
@@ -139,16 +159,25 @@
     print "\t --format={bed,str} (format of input file; default: bed)"
     print "\t --full (to output all variational parameters; optional)"
     print "\t --seed=<int> (manually specify seed for random number generator; optional)"


-if __name__=="__main__":
+if __name__ == "__main__":

     # parse command-line options
     argv = sys.argv[1:]
     smallflags = "K:"
-    bigflags = ["prior=", "tol=", "input=", "output=", "cv=", "seed=", "format=", "full"] 
+    bigflags = [
+        "prior=",
+        "tol=",
+        "input=",
+        "output=",
+        "cv=",
+        "seed=",
+        "format=",
+        "full",
+    ]
     try:
         opts, args = getopt.getopt(argv, smallflags, bigflags)
         if not opts:
             usage()
             sys.exit(2)
@@ -160,22 +189,27 @@
     params = parseopts(opts)

     # check if command-line options are valid
     try:
         checkopts(params)
-    except (ValueError,KeyError):
+    except (ValueError, KeyError):
         sys.exit(2)

     # load data
-    if params['format']=='bed':
-        G = parse_bed.load(params['inputfile'])
-    elif params['format']=='str':
-        G = parse_str.load(params['inputfile'])
-    G = np.require(G, dtype=np.uint8, requirements='C')
+    if params["format"] == "bed":
+        G = parse_bed.load(params["inputfile"])
+    elif params["format"] == "str":
+        G = parse_str.load(params["inputfile"])
+    G = np.require(G, dtype=np.uint8, requirements="C")

     # run the variational algorithm
-    Q, P, other = fastStructure.infer_variational_parameters(G, params['K'], \
-                    params['outputfile'], params['mintol'], \
-                    params['prior'], params['cv'])
+    Q, P, other = fastStructure.infer_variational_parameters(
+        G,
+        params["K"],
+        params["outputfile"],
+        params["mintol"],
+        params["prior"],
+        params["cv"],
+    )

     # write out inferred parameters
     write_output(Q, P, other, params)
<b>would reformat fastStructure/structure.py</b>
<b>All done! ✨ 🍰 ✨</b>
<b>1 file would be reformatted</b>.

Pylint

Running this first example below will encounter an error and STOP when run on the structure.py script, since the script cannot successfully execute in its current mode. The terminal error in this case is caused by the fact that pylint uses your current Python version to test for errors, and this script is written in Python2, whereas we are testing in Python3. It raises an error saying that the print function is being called incorrectly. This is a common error-producing difference between py2/3. This is fine, good to know, we'll continue to the next example.

pylint fastStructure/structure.py
************* Module structure
fastStructure/structure.py:44:23: E0001: Missing parentheses in call to &apos;print&apos;. Did you mean print(&quot;%s prior is not currently implemented, defaulting to the simple prior&quot;)? (&lt;unknown&gt;, line 44) (syntax-error)

Now let's run pylint on some code that is written to work in either Python 2 or Python 3. Writing code in this way is kind of a pain, but is worth it for big general use packages. The Python package Toytree is an example. You can see in this example that pylint finishes running and reports a score of 7.26/10. Not bad. It has several recommendations having to do with mundane style (extra whitespace), but also some useful suggestions for cleaner code, like "unnecessary else after break". If we were to edit the files at the suggested lines to implement these suggestions, and rerun pylint, it should return improved scores until it reaches 10/10.

pylint toytree/toytree/Rooter.py
************* Module toytree.Rooter
toytree/toytree/Rooter.py:52:26: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:90:42: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:106:27: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:114:65: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:138:31: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:139:33: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:158:60: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:182:73: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:183:66: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:196:69: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:197:70: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:216:88: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:244:70: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:247:23: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:248:32: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:250:9: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:262:73: C0303: Trailing whitespace (trailing-whitespace)
toytree/toytree/Rooter.py:1:0: C0103: Module name &quot;Rooter&quot; doesn&apos;t conform to snake_case naming style (invalid-name)
toytree/toytree/Rooter.py:11:0: C0115: Missing class docstring (missing-class-docstring)
toytree/toytree/Rooter.py:11:0: R0902: Too many instance attributes (11/7) (too-many-instance-attributes)
toytree/toytree/Rooter.py:50:8: C0103: Variable name &quot;x0&quot; doesn&apos;t conform to snake_case naming style (invalid-name)
toytree/toytree/Rooter.py:51:8: C0103: Variable name &quot;x1&quot; doesn&apos;t conform to snake_case naming style (invalid-name)
toytree/toytree/Rooter.py:70:4: C0116: Missing function or method docstring (missing-function-docstring)
toytree/toytree/Rooter.py:79:4: C0116: Missing function or method docstring (missing-function-docstring)
toytree/toytree/Rooter.py:83:8: W0212: Access to a protected member _coords of a client class (protected-access)
toytree/toytree/Rooter.py:131:12: R1723: Unnecessary &quot;else&quot; after &quot;break&quot; (no-else-break)
toytree/toytree/Rooter.py:104:4: R0912: Too many branches (13/12) (too-many-branches)
toytree/toytree/Rooter.py:228:4: C0112: Empty method docstring (empty-docstring)
toytree/toytree/Rooter.py:261:4: C0116: Missing function or method docstring (missing-function-docstring)
toytree/toytree/Rooter.py:281:8: C0103: Variable name &quot;x0&quot; doesn&apos;t conform to snake_case naming style (invalid-name)
toytree/toytree/Rooter.py:282:8: C0103: Variable name &quot;x1&quot; doesn&apos;t conform to snake_case naming style (invalid-name)

------------------------------------------------------------------
Your code has been rated at 7.26/10 (previous run: 7.26/10, +0.00)

You can see that pycodestyle makes similar recommendations as pylint, but fewer, mostly having to do with things of little consequence (extra whitespace or variable names). It does run quite a bit faster though, which is convenient for when the linter is running automatically in the background of your code editor (as we'll see later).

pycodestyle toytree/toytree/Rooter.py

toytree/toytree/Rooter.py:30:80: E501 line too long (82 &gt; 79 characters)
toytree/toytree/Rooter.py:52:27: W291 trailing whitespace
toytree/toytree/Rooter.py:70:5: E303 too many blank lines (3)
toytree/toytree/Rooter.py:79:5: E303 too many blank lines (2)
toytree/toytree/Rooter.py:87:5: E303 too many blank lines (3)
toytree/toytree/Rooter.py:90:43: W291 trailing whitespace
toytree/toytree/Rooter.py:104:5: E303 too many blank lines (3)
toytree/toytree/Rooter.py:106:28: W291 trailing whitespace
toytree/toytree/Rooter.py:114:66: W291 trailing whitespace
toytree/toytree/Rooter.py:138:32: W291 trailing whitespace
toytree/toytree/Rooter.py:139:34: W291 trailing whitespace
toytree/toytree/Rooter.py:145:80: E501 line too long (80 &gt; 79 characters)
toytree/toytree/Rooter.py:158:61: W291 trailing whitespace
toytree/toytree/Rooter.py:182:74: W291 trailing whitespace
toytree/toytree/Rooter.py:183:67: W291 trailing whitespace
toytree/toytree/Rooter.py:194:5: E303 too many blank lines (3)
toytree/toytree/Rooter.py:196:70: W291 trailing whitespace
toytree/toytree/Rooter.py:197:71: W291 trailing whitespace
toytree/toytree/Rooter.py:216:80: E501 line too long (88 &gt; 79 characters)
toytree/toytree/Rooter.py:216:89: W291 trailing whitespace
toytree/toytree/Rooter.py:220:17: E128 continuation line under-indented for visual indent
toytree/toytree/Rooter.py:220:80: E501 line too long (81 &gt; 79 characters)
toytree/toytree/Rooter.py:221:17: E128 continuation line under-indented for visual indent
toytree/toytree/Rooter.py:221:80: E501 line too long (81 &gt; 79 characters)
toytree/toytree/Rooter.py:222:17: E128 continuation line under-indented for visual indent
toytree/toytree/Rooter.py:223:17: E128 continuation line under-indented for visual indent
toytree/toytree/Rooter.py:228:5: E303 too many blank lines (3)
toytree/toytree/Rooter.py:244:71: W291 trailing whitespace
toytree/toytree/Rooter.py:247:24: W291 trailing whitespace
toytree/toytree/Rooter.py:248:33: W291 trailing whitespace
toytree/toytree/Rooter.py:250:10: W291 trailing whitespace
toytree/toytree/Rooter.py:261:5: E303 too many blank lines (3)
toytree/toytree/Rooter.py:262:74: W291 trailing whitespace
toytree/toytree/Rooter.py:270:5: E303 too many blank lines (3)

Finally, let's run black. Studying the output of the --diff command to black can be really insightful about what the actual solutions are to the many recommendations that were provided by pylint above.

black --diff toytree/toytree/Rooter.py
--- toytree/toytree/Rooter.py   2021-02-03 17:22:53.767184 +0000
+++ toytree/toytree/Rooter.py   2021-02-03 21:30:12.332704 +0000
@@ -26,11 +26,11 @@
         # the new node that will be inserted
         self.nnode = None

         # make a copy and ensure supports are either all int or float
         self.maxsup = max([int(i.support) for i in self.tree.treenode.traverse()])
-        self.maxsup = (1.0 if self.maxsup &lt;= 1.0 else 100)
+        self.maxsup = 1.0 if self.maxsup &lt;= 1.0 else 100
         self.get_features()

         # parse node selecting arguments (nastuple) with NodeAssist
         self.get_match(*nastuple)

@@ -45,13 +45,13 @@
             if len(self.node2.children) == 1:
                 self.node2 = self.node2.up
                 self.node1 = self.node1.up

         # if rooting where root already exists then return current tree
-        x0 = (self.node1.is_root())
-        x1 = (self.node2.is_root() and self.tree.is_rooted())
-        if not (x0 or x1):           
+        x0 = self.node1.is_root()
+        x1 = self.node2.is_root() and self.tree.is_rooted()
+        if not (x0 or x1):

             # create new root node on an existing edge to split it.
             self.insert_new_node()

             # update edge lengths given new node insertion
@@ -62,58 +62,51 @@
             self.restructure_tree()

             # update coodrds on tree
             self.update_tree_from_tdict()
             self.update()
-
-

     def update_tree_from_tdict(self):
         # update tree structure and node labels
         for node in self.tdict:
             node.up = self.tdict[node][0]
             node.children = self.tdict[node][1]
             for key, val in self.tdict[node][2].items():
                 setattr(node, key, val)

-
     def update(self):
         # update coordinates which updates idx and adds it to any new nodes.
         self.tree.treenode = self.nnode
         self.tree.treenode.ladderize()
         self.tree._coords.update()

-
-
     def redirect_edge_features(self):
         &quot;&quot;&quot;
         Set support values to maximum for new node since the user forced
-        rooting, i.e, it is not uncertain. 
+        rooting, i.e, it is not uncertain.
         &quot;&quot;&quot;
         # mark new split with zero...
         for feature in set(self.edge_features) - set([&quot;support&quot;, &quot;dist&quot;]):
             self.tdict[self.node2][2][feature] = 0.0

         # unless support value, then mark with full.
         if &quot;support&quot; in self.edge_features:
-            self.tdict[self.node2][2][&apos;support&apos;] = self.maxsup
+            self.tdict[self.node2][2][&quot;support&quot;] = self.maxsup
         else:
-            self.tdict[self.node2][2][&apos;support&apos;] = self.node2.support
-
-
+            self.tdict[self.node2][2][&quot;support&quot;] = self.node2.support

     def restructure_tree(self):
         &quot;&quot;&quot;
-        At this point tdict 
+        At this point tdict
            (node): (parent) (children), features
         {
             nnode: [None, [node1, node2], {}]
             node1: [nnode, node1.children, {&apos;dist&apos;}]
             node2: [nnode, node2.children, {&apos;dist&apos;: 0.0}]
         }
         &quot;&quot;&quot;
-        # start with the node leading from new root child2 to the 
+        # start with the node leading from new root child2 to the
         # rest of the tree structure and move up until old root.
         tnode = self.node2.up

         # label all remaining nodes by moving up from tnode to old root.
         while 1:
@@ -133,12 +126,12 @@
                 # need a root add feature here if unrooted...
                 if len(children) &gt; 1:

                     # update dist from new parent
                     self.tdict[tnode] = [
-                        parent, 
-                        children, 
+                        parent,
+                        children,
                         {&quot;dist&quot;: parent.dist},
                     ]

                     # update edge features from new parent
                     for feature in self.edge_features:
@@ -153,11 +146,11 @@

                 # get children that are not in tdict yet
                 else:
                     for child in children:

-                        # record whose children they are now 
+                        # record whose children they are now
                         # (node2 already did this)
                         if parent is self.node2:
                             self.tdict[self.node2][1].append(child)
                         else:
                             self.tdict[parent][1].append(child)
@@ -170,33 +163,31 @@
                 break

             # normal nodes
             else:
                 # update tnode.features [dist will be inherited from child]
-                features = {&apos;dist&apos;: tnode.dist, &apos;support&apos;: tnode.support}
+                features = {&quot;dist&quot;: tnode.dist, &quot;support&quot;: tnode.support}

                 # keep connecting swap parent-child up to root
                 if not tnode.up.is_root():
                     children += [tnode.up]

                 # pass support values down (up in new tree struct)
-                child = [i for i in tnode.children if i in self.tdict][0]                
-                for feature in {&apos;dist&apos;}.union(self.edge_features):                   
+                child = [i for i in tnode.children if i in self.tdict][0]
+                for feature in {&quot;dist&quot;}.union(self.edge_features):
                     features[feature] = getattr(child, feature)

                 # store node update vals
                 self.tdict[tnode] = [parent, children, features]

             # move towards root
             tnode = tnode.up

-
-
     def config_root_dist(self):
         &quot;&quot;&quot;
-        Now that the new root node is inserted .dist features must be 
-        set for the two descendant nodes. Midpoint rooting is a common 
+        Now that the new root node is inserted .dist features must be
+        set for the two descendant nodes. Midpoint rooting is a common
         option, but users can toggle &apos;resolve_root_dist&apos; to change this.
         &quot;&quot;&quot;
         # if not already at root polytomy, then connect node2 to parent
         if self.node2.up:
             if not self.node2.up.is_root():
@@ -206,31 +197,29 @@
         if self.resolve_root_dist is False:
             self.resolve_root_dist = 0.0

         # if True then use midpoint rooting
         elif self.resolve_root_dist is True:
-            self.tdict[self.node1][2][&quot;dist&quot;] = self.node1.dist / 2.
-            self.tdict[self.node2][2][&quot;dist&quot;] = self.node1.dist / 2.
+            self.tdict[self.node1][2][&quot;dist&quot;] = self.node1.dist / 2.0
+            self.tdict[self.node2][2][&quot;dist&quot;] = self.node1.dist / 2.0

         # split the edge on 0 or a float
         if isinstance(self.resolve_root_dist, float):
-            self.tdict[self.node1][2][&quot;dist&quot;] = self.node1.dist - self.resolve_root_dist            
+            self.tdict[self.node1][2][&quot;dist&quot;] = self.node1.dist - self.resolve_root_dist
             self.tdict[self.node2][2][&quot;dist&quot;] = self.resolve_root_dist
             if self.resolve_root_dist &gt; self.node1.dist:
-                raise ToytreeError(&quot;\n&quot;
-                &quot;To preserve existing edge lengths the &apos;resolve_root_dist&apos; arg\n&quot;
-                &quot;must be smaller than the edge being split (it is selecting a \n&quot;
-                &quot;a point along the edge.) The edge above node idx {} is {}.&quot;
-                .format(self.node1.idx, self.node1.dist)
+                raise ToytreeError(
+                    &quot;\n&quot;
+                    &quot;To preserve existing edge lengths the &apos;resolve_root_dist&apos; arg\n&quot;
+                    &quot;must be smaller than the edge being split (it is selecting a \n&quot;
+                    &quot;a point along the edge.) The edge above node idx {} is {}.&quot;.format(
+                        self.node1.idx, self.node1.dist
+                    )
                 )

-
-
     def insert_new_node(self):
-        &quot;&quot;&quot;
-
-        &quot;&quot;&quot;
+        &quot;&quot;&quot;&quot;&quot;&quot;
         # the new root node to be placed on the split
         self.nnode = self.tree.treenode.__class__()
         self.nnode.name = &quot;root&quot;
         self.nnode.add_feature(&quot;idx&quot;, self.tree.treenode.idx)
         self.nnode.support = self.maxsup
@@ -239,36 +228,32 @@
         self.node2.children.remove(self.node1)

         # new node has no parent and 1,2 as children and default features
         self.tdict[self.nnode] = [None, [self.node1, self.node2], {}]

-        # node1 has new root parent, same children, and dist preserved 
+        # node1 has new root parent, same children, and dist preserved
         # (or split?), or should: node1.dist / 2.
         self.tdict[self.node1] = [
-            self.nnode, 
-            self.node1.children, 
-            {&quot;dist&quot;: self.node1.dist}
-        ]  
+            self.nnode,
+            self.node1.children,
+            {&quot;dist&quot;: self.node1.dist},
+        ]

         # node2 has new root parent, same children + mods, and dist/supp mods
         self.tdict[self.node2] = [
             self.nnode,
             self.node2.children,
             {&quot;dist&quot;: 0.0},
         ]

-
-
     def get_features(self):
-        # define which features to use/keep on nodes and which are &quot;edge&quot; 
+        # define which features to use/keep on nodes and which are &quot;edge&quot;
         # features which must be redirected on rooting.
         testnode = self.tree.treenode.get_leaves()[0]
         extrafeat = {i for i in testnode.features if i not in self.features}
         self.features.update(extrafeat)

-
-
     def get_match(self, names, wildcard, regex):
         &quot;&quot;&quot;
         tries to get monophyletic clade from selection, then tests
         the reciprocal set, then reports error.
         &quot;&quot;&quot;
@@ -276,20 +261,18 @@
         self.nas = NodeAssist(self.tree, names, wildcard, regex)
         # self.nas.match_query()
         self.tipnames = self.nas.get_tipnames()

         # check for reciprocal match
-        x0 = (not self.nas.is_query_monophyletic())
-        x1 = (self.nas.get_mrca().is_root())
+        x0 = not self.nas.is_query_monophyletic()
+        x1 = self.nas.get_mrca().is_root()
         if x0 or x1:
             clade1 = self.nas.tipnames
             self.nas.match_reciprocal()

             # check reciprocal match
             if not self.nas.is_query_monophyletic():
                 # clade2 = self.nas.tipnames

                 # reports the smaller sized clade
-                raise ToytreeError(
-                    &quot;Matched query is paraphyletic: {}&quot;.format(clade1)
-                )
+                raise ToytreeError(&quot;Matched query is paraphyletic: {}&quot;.format(clade1))
                 # .format(sorted([clade1, clade2], key=len)[0]))
<b>would reformat toytree/toytree/Rooter.py</b>
<b>All done! ✨ 🍰 ✨</b>
<b>1 file would be reformatted</b>.

Assessment

None. Continue to the next tutorials where we will continue to learn about style.

Cleanup

Now that you've finished the exercise you can cleanup by removing the repos that we had cloned only for this example. Remember, to remove a git repo you will need to use the arguments -rf; this is because the .git/ subfolder in the repo wants you to be sure you really want to remove it before you do so. Here we are quite sure, so go ahead and run the commands below to remove both folders.

# move into your hacks directory
cd ~/hacks

# rm the two repos
rm -rf ./toytree
rm -rf ./fastStructure