Biopython05/15
Starting sandbox…
Beginner·4 graded tests

Bio.Phylo

Bio.Phylo

Core Principle: Bio.Phylo provides comprehensive phylogenetic tree parsing, topological analysis, clade traversal, and visualization across Newick, Nexus, phyloXML, and NeXML formats.

Reading and writing

from Bio import Phylo

tree = Phylo.read("tree.nwk", "newick")            # ONE tree
trees = Phylo.parse("bootstrap.nwk", "newick")     # many
Phylo.write(tree, "out.xml", "phyloxml")
Phylo.convert("in.nwk", "newick", "out.xml", "phyloxml")

Phylo.draw_ascii(tree)                              # quick terminal view
print(tree)

Formats: "newick", "nexus", "phyloxml", "nexml", "cdao".

Newick is lossy. It stores topology, branch lengths and names: nothing else. Support values, dates, per-node annotations and colours need phyloXML or NeXML. If you are annotating a tree programmatically, convert to phyloXML.

Navigating

tree.root
tree.get_terminals()          # leaves (tips)
tree.get_nonterminals()       # internal nodes
tree.count_terminals()
tree.depths()                  # node → distance from root (dict)
tree.total_branch_length()

for clade in tree.find_clades():
    clade.name, clade.branch_length, clade.confidence

tree.find_any(name="Homo_sapiens")
tree.common_ancestor("Homo_sapiens", "Pan_troglodytes")
tree.distance("Homo_sapiens", "Pan_troglodytes")   # patristic distance
tree.get_path("Homo_sapiens")                       # root → tip
tree.is_monophyletic([c1, c2, c3])

tree.distance(a, b) is the patristic distance: the sum of branch lengths along the path. That is an evolutionary distance, not a sequence identity; they correlate but are not interchangeable.

Manipulating

tree.root_with_outgroup("Outgroup_species")
tree.root_at_midpoint()
tree.ladderize(reverse=False)          # sort clades by size: much easier to read
tree.prune("Some_taxon")
tree.collapse_all(lambda c: c.confidence is not None and c.confidence < 70)
tree.is_bifurcating()
clade.clades                            # children

ladderize() before drawing, always. It costs one line and makes the tree dramatically more readable without changing the topology.

Rooting is a real decision, not a display option. An unrooted tree (which is what most inference methods produce) has no direction of time. Midpoint rooting assumes a molecular clock; outgroup rooting requires you to know an outgroup. State which you used.

Drawing

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 10))
Phylo.draw(tree, axes=ax, do_show=False)

Phylo.draw(tree, axes=ax, do_show=False,
           label_func=lambda c: c.name.replace("_", " ") if c.name else "",
           branch_labels=lambda c: f"{c.confidence:.0f}" if c.confidence else "",
           label_colors=lambda name: COLORS.get(name, "black"))

ax.set_xlabel("Substitutions per site")
fig.savefig("tree.pdf", bbox_inches="tight")

do_show=False and axes=ax are what let you compose a tree into a larger matplotlib figure. See Figure and Axes.

Biopython's drawing is functional but plain. For publication-quality trees:

  • ETE3 / ETE4: much richer rendering, heatmaps and annotations alongside tips
  • ToyTree: clean, programmatic, good defaults
  • iTOL: web-based, the field standard for complex annotated trees
  • ggtree (R): the most flexible if you are willing to cross languages

Biopython is best for manipulating trees programmatically; export to one of these for the final figure.

Building trees

Biopython includes distance-based methods, which are fast and approximate:

[1]python
error1ms
Traceback (most recent call last):
  File "/Users/leviosman/Developer/python-for-bio/webapp/scripts/curriculum/generate_cell_outputs.py", line 132, in run_cell_capture
    exec(compile(code, "<cell>", "exec"), namespace)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<cell>", line 4, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/AlignIO/__init__.py", line 380, in read
    alignment = next(iterator)
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/AlignIO/__init__.py", line 319, in parse
    with as_handle(handle) as fp:
         ~~~~~~~~~^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py", line 141, in __enter__
    return next(self.gen)
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/File.py", line 72, in as_handle
    with open(handleish, mode, **kwargs) as fp:
         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'family.aln'

These are for quick exploration, not for publication. Distance methods discard information and produce no branch support. For real inference use a maximum-likelihood or Bayesian method:

import subprocess
subprocess.run(["iqtree2", "-s", "family.phy", "-m", "MFP",
                "-B", "1000", "-T", "AUTO"], check=True)     # ML + model selection + UFBoot
subprocess.run(["raxml-ng", "--all", "--msa", "family.fa",
                "--model", "LG+G", "--bs-trees", "100"], check=True)

Bio.Phylo.Applications (the PhyML/RAxML wrappers) went with Bio.Application in 1.86. Use subprocess. See Biopython Deprecations.

Bootstrap support

trees = list(Phylo.parse("bootstrap_trees.nwk", "newick"))

from Bio.Phylo.Consensus import majority_consensus, get_support
consensus = majority_consensus(trees, cutoff=0.5)
supported = get_support(target_tree, trees)

for clade in supported.find_clades():
    if clade.confidence is not None and clade.confidence < 70:
        clade.confidence = None                  # hide weak support

Bootstrap values below ~70 are not evidence. A tree drawn without support values is uninterpretable: a confidently-drawn wrong topology looks exactly like a confidently-drawn right one.

Bioinformatics examples

# gene tree vs species tree: a duplication/HGT check
gene_tree = Phylo.read("gene.nwk", "newick")
gene_tree.root_with_outgroup("Outgroup")
gene_tree.ladderize()
Phylo.draw_ascii(gene_tree)

# pairwise patristic distance matrix
tips = gene_tree.get_terminals()
names = [t.name for t in tips]
D = pd.DataFrame(
    [[gene_tree.distance(a, b) for b in tips] for a in tips],
    index=names, columns=names,
)
sns.clustermap(D, cmap="rocket")

# extract a clade of interest
mrca = tree.common_ancestor("Species_A", "Species_B")
members = [t.name for t in mrca.get_terminals()]

# colour tips by a metadata attribute
GROUP_COLORS = {"mammal": "crimson", "bird": "steelblue", "reptile": "seagreen"}
fig, ax = plt.subplots(figsize=(7, 12))
Phylo.draw(tree, axes=ax, do_show=False,
           label_colors=lambda n: GROUP_COLORS.get(meta.get(n, ""), "black"))

# long branches: possible paralogues, contamination or alignment errors
long_tips = [t.name for t in tree.get_terminals()
             if t.branch_length and t.branch_length > 3 * median_bl]

# tree from an expression correlation matrix (not phylogeny, but same machinery)
from Bio.Phylo.TreeConstruction import DistanceMatrix
dm = DistanceMatrix(names=samples, matrix=lower_triangle_of(1 - corr))
sample_tree = DistanceTreeConstructor().nj(dm)

That last example is worth noting: neighbour-joining on a 1 - correlation distance is a legitimate sample-clustering method, and the same code works whether the distance is evolutionary or not. (Though sns.clustermap gives you the same thing plus a heatmap in one line: see Applied - Heatmaps and Clustermaps.)

Common mistakes

  • Ignoring rooting. Most inferred trees are unrooted; the visual root is arbitrary.
  • Drawing without support values.
  • Trusting distance-based trees for a published result.
  • Newick losing annotations. Use phyloXML.
  • Forgetting ladderize().
  • Confusing patristic distance with sequence identity.
  • Bio.Phylo.Applications. Removed in 1.86.
  • Reading branch lengths as time without a calibrated clock: they are substitutions per site.
  • Interpreting a gene tree as a species tree. Duplication, loss and incomplete lineage sorting make them differ.

See also

AlignIO · Substitution Matrices · Applied - Multiple Sequence Alignment Analysis · Biopython Deprecations · Figure and Axes

Exercise

Biopython is not installed here, so write the Newick parser yourself. Newick is small enough to parse in one pass, and doing so makes clear what the lesson means by "Newick is lossy": the grammar carries names, branch lengths and nothing else.

Write leaf_depths(text), taking one Newick string: nested parentheses, comma-separated children, an optional label after the closing parenthesis (which is where a bootstrap value lives), and an optional :length after any node: and returning (leaves, total, depths). leaves is the number of tips, total is the sum of every branch length below the root, and depths is the list of (name, root_to_tip_distance) pairs. A node with no branch length contributes 0.0, which is normally the case for the root. Print leaves=<n>, then total=<x> rounded to four decimals, then one name<TAB>distance line per tip, sorted by name, with the distance rounded to four decimals. A tree that is a single tip such as A; has one leaf, a total of 0.0 and a distance of 0.0.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin(((seq1:0.012,seq2:0.014)0.98:0.031,seq3:0.045)0.76:0.022,outgroup:0.121); leaves=4 total=0.2450 outgroup 0.1210 seq1 0.0650 seq2 0.0670 seq3 0.0670
2stdin((human:0.1,chimp:0.1)node1:0.2,(mouse:0.3,rat:0.25)node2:0.15,lamprey:0.9); leaves=5 total=2.0000 chimp 0.3000 human 0.3000 lamprey 0.9000 mouse 0.4500 rat 0.4000
3stdin(a,b,(c,d)); leaves=4 total=0.0000 a 0.0000 b 0.0000 c 0.0000 d 0.0000
4stdinA; leaves=1 total=0.0000 A 0.0000

Hints · 3

01Hint

A recursive function over a shared position index is the shortest route; declare the index nonlocal.

02Hint

A label runs until the next character in "(),:;" and may be empty for an unnamed internal node.

03Hint

Accumulate the distance as you descend: a child's depth is the parent's depth plus its own branch length.