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