pythonforbio.
[WASM idle]
Biopython05/15

Bio.Phylo

Starting sandbox…
Beginnerlesson

Bio.Phylo

In one line: read, manipulate and draw phylogenetic trees — Newick, Nexus, phyloXML and NeXML.

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:

from Bio.Phylo.TreeConstruction import DistanceCalculator, DistanceTreeConstructor
from Bio import AlignIO

aln = AlignIO.read("family.aln", "clustal")
calc = DistanceCalculator("blosum62")           # or "identity", "blastn"
dm = calc.get_distance(aln)

constructor = DistanceTreeConstructor()
nj_tree = constructor.nj(dm)                     # neighbour-joining
upgma_tree = constructor.upgma(dm)               # assumes a molecular clock

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

lesson example

No output yet — run the code to populate this drawer.