Skill Tracer
Skills › Scientific Agent Skills › etetoolkit › workflows.md

workflows.md

550 lines · skills/etetoolkit/references/workflows.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1---
2title: ETE 4 Workflows
3description: Markdown guide to validated tree-analysis patterns with ETE 4.4.0.
4---
5
6# ETE 4 Workflows
7
8Complete patterns for common ETE 4.4.0 tasks. Adapt parsers and biological
9assumptions to the actual data source.

ETE 4 Workflows

Complete patterns for common ETE 4.4.0 tasks. Adapt parsers and biological assumptions to the actual data source.

Raw lines 1 to 10
11## Guide Map
13- Validate names, branch lengths, and support before analysis
14- Preprocess and root an existing tree reproducibly
15- Compare rooted or unrooted topologies
16- Join metadata without silently dropping records
17- Detect evolutionary events or reconcile against a species tree
18- Split duplicated gene families
19- Query NCBI or GTDB taxonomy
20- Batch-process and cache large trees
21- Hand off clustering and tree inference to the right upstream tools
22- Match trusted topology patterns
24The fenced Python snippets are examples within this Markdown guide, not a
25single executable program. Run only the section needed for the current task.

Guide Map

  • Validate names, branch lengths, and support before analysis
  • Preprocess and root an existing tree reproducibly
  • Compare rooted or unrooted topologies
  • Join metadata without silently dropping records
  • Detect evolutionary events or reconcile against a species tree
  • Split duplicated gene families
  • Query NCBI or GTDB taxonomy
  • Batch-process and cache large trees
  • Hand off clustering and tree inference to the right upstream tools
  • Match trusted topology patterns

The fenced Python snippets are examples within this Markdown guide, not a single executable program. Run only the section needed for the current task.

Raw lines 11 to 26
27## 1. Validate Before Analysis
29Name-based operations and tree comparisons become unreliable when labels are
30empty or duplicated.
32```python
33from collections import Counter
34from pathlib import Path
36from ete4 import Tree
39def load_and_validate(path: Path, parser=1) -> Tree:
40 with path.open(encoding="utf-8") as handle:
41 tree = Tree(handle, parser=parser)
43 names = list(tree.leaf_names())
44 empty = [leaf.id for leaf in tree.leaves() if not leaf.name]
45 duplicate_counts = {
46 name: count
47 for name, count in Counter(names).items()
48 if name and count > 1
49 }
51 if empty:
52 raise ValueError(f"{path}: unnamed leaves at positions {empty[:10]}")
53 if duplicate_counts:
54 raise ValueError(f"{path}: duplicate leaf names {duplicate_counts}")
55 if len(names) < 2:
56 raise ValueError(f"{path}: expected at least two leaves")
58 return tree
61tree = load_and_validate(Path("tree.nw"), parser=1)
62```
64Also verify that the parsed support and branch-length ranges are plausible for
65the program that produced the tree:
67```python
68supports = [
69 node.support
70 for node in tree.traverse()
71 if not node.is_leaf and node.support is not None
73distances = [
74 node.dist
75 for node in tree.traverse()
76 if not node.is_root and node.dist is not None
79if any(distance < 0 for distance in distances):
80 raise ValueError("Negative branch length detected")
82print("support range:", (min(supports), max(supports)) if supports else None)
83print("distance range:", (min(distances), max(distances)) if distances else None)
84```
86Support may be represented as fractions or percentages. Do not apply a
87threshold before checking its scale.

1. Validate Before Analysis

Name-based operations and tree comparisons become unreliable when labels are empty or duplicated.

from collections import Counter
from pathlib import Path

from ete4 import Tree


def load_and_validate(path: Path, parser=1) -> Tree:
    with path.open(encoding="utf-8") as handle:
        tree = Tree(handle, parser=parser)

    names = list(tree.leaf_names())
    empty = [leaf.id for leaf in tree.leaves() if not leaf.name]
    duplicate_counts = {
        name: count
        for name, count in Counter(names).items()
        if name and count > 1
    }

    if empty:
        raise ValueError(f"{path}: unnamed leaves at positions {empty[:10]}")
    if duplicate_counts:
        raise ValueError(f"{path}: duplicate leaf names {duplicate_counts}")
    if len(names) < 2:
        raise ValueError(f"{path}: expected at least two leaves")

    return tree


tree = load_and_validate(Path("tree.nw"), parser=1)

Also verify that the parsed support and branch-length ranges are plausible for the program that produced the tree:

supports = [
    node.support
    for node in tree.traverse()
    if not node.is_leaf and node.support is not None
]
distances = [
    node.dist
    for node in tree.traverse()
    if not node.is_root and node.dist is not None
]

if any(distance < 0 for distance in distances):
    raise ValueError("Negative branch length detected")

print("support range:", (min(supports), max(supports)) if supports else None)
print("distance range:", (min(distances), max(distances)) if distances else None)

Support may be represented as fractions or percentages. Do not apply a threshold before checking its scale.

Raw lines 27 to 88
89## 2. Reproducible Tree Preprocessing
91```python
92from collections import Counter
93from pathlib import Path
95from ete4 import Tree
97input_path = Path("inferred_tree.nw")
98output_path = Path("processed_tree.nw")
100with input_path.open(encoding="utf-8") as handle:
101 tree = Tree(handle, parser="support")
103# Keep a known sample set and preserve retained pairwise distances.
104keep = ["sample_A", "sample_B", "sample_C", "outgroup"]
105counts = Counter(tree.leaf_names())
106invalid = {name: counts[name] for name in keep if counts[name] != 1}
107if invalid:
108 raise ValueError(f"Requested tips must exist exactly once: {invalid}")
109tips = {leaf.name: leaf for leaf in tree.leaves()}
110tree.prune([tips[name] for name in keep], preserve_branch_length=True)
112# Prefer a biological outgroup when justified.
113tree.set_outgroup(tips["outgroup"])
115# Stable presentation order only; this does not alter clade membership.
116tree.ladderize()
118tree.write(
119 outfile=str(output_path),
120 parser="support",
121 props=[],
123```
125Record the original input checksum, ETE version, parser, retained sample set,
126rooting rule, and output parser in an analysis manifest.

2. Reproducible Tree Preprocessing

from collections import Counter
from pathlib import Path

from ete4 import Tree

input_path = Path("inferred_tree.nw")
output_path = Path("processed_tree.nw")

with input_path.open(encoding="utf-8") as handle:
    tree = Tree(handle, parser="support")

# Keep a known sample set and preserve retained pairwise distances.
keep = ["sample_A", "sample_B", "sample_C", "outgroup"]
counts = Counter(tree.leaf_names())
invalid = {name: counts[name] for name in keep if counts[name] != 1}
if invalid:
    raise ValueError(f"Requested tips must exist exactly once: {invalid}")
tips = {leaf.name: leaf for leaf in tree.leaves()}
tree.prune([tips[name] for name in keep], preserve_branch_length=True)

# Prefer a biological outgroup when justified.
tree.set_outgroup(tips["outgroup"])

# Stable presentation order only; this does not alter clade membership.
tree.ladderize()

tree.write(
    outfile=str(output_path),
    parser="support",
    props=[],
)

Record the original input checksum, ETE version, parser, retained sample set, rooting rule, and output parser in an analysis manifest.

Raw lines 89 to 127
128## 3. Midpoint Rooting
130Midpoint rooting is useful when a defensible biological outgroup is not
131available, but it assumes the longest leaf-to-leaf path can approximate a
132clock-like split.
134```python
135from ete4 import Tree
137tree = Tree("((A:1,B:1):1,(C:2,D:2):1);")
138tree.set_midpoint_outgroup()
139print(tree.write(props=[]))
140```
142For auditability:
144```python
145tree = Tree("((A:1,B:1):1,(C:2,D:2):1);")
146candidate = tree.get_midpoint_outgroup()
147candidate_id = candidate.id
148tree.set_midpoint_outgroup()
149print("midpoint candidate:", candidate_id)
150```
152The candidate identifies an edge. Calling `set_outgroup(candidate)` instead
153uses the default cut along that edge and can miss the exact diameter midpoint.
155Do not describe midpoint rooting as evidence for the direction of evolution.

3. Midpoint Rooting

Midpoint rooting is useful when a defensible biological outgroup is not available, but it assumes the longest leaf-to-leaf path can approximate a clock-like split.

from ete4 import Tree

tree = Tree("((A:1,B:1):1,(C:2,D:2):1);")
tree.set_midpoint_outgroup()
print(tree.write(props=[]))

For auditability:

tree = Tree("((A:1,B:1):1,(C:2,D:2):1);")
candidate = tree.get_midpoint_outgroup()
candidate_id = candidate.id
tree.set_midpoint_outgroup()
print("midpoint candidate:", candidate_id)

The candidate identifies an edge. Calling set_outgroup(candidate) instead uses the default cut along that edge and can miss the exact diameter midpoint.

Do not describe midpoint rooting as evidence for the direction of evolution.

Raw lines 128 to 156
157## 4. Compare Two Trees
159```python
160from collections import Counter
161from pathlib import Path
163from ete4 import Tree
166def load(path: Path, parser=1) -> Tree:
167 with path.open(encoding="utf-8") as handle:
168 return Tree(handle, parser=parser)
171def assert_unique_leaf_names(tree: Tree, label: str) -> None:
172 counts = Counter(tree.leaf_names())
173 if any(not name for name in counts):
174 raise ValueError(f"{label} has unnamed leaves")
175 duplicates = sorted(name for name, count in counts.items() if count > 1)
176 if duplicates:
177 raise ValueError(f"{label} has duplicate leaves: {duplicates}")
180tree_a = load(Path("method_a.nw"))
181tree_b = load(Path("method_b.nw"))
182assert_unique_leaf_names(tree_a, "method_a")
183assert_unique_leaf_names(tree_b, "method_b")
186 rf,
187 max_rf,
188 common,
189 edges_a,
190 edges_b,
191 discarded_a,
192 discarded_b,
193) = tree_a.robinson_foulds(
194 tree_b,
195 unrooted_trees=True,
198print(
199 {
200 "rf": rf,
201 "max_rf": max_rf,
202 "normalized_rf": rf / max_rf if max_rf > 0 else None,
203 "common_leaf_count": len(common),
204 "discarded_edges_a": len(discarded_a),
205 "discarded_edges_b": len(discarded_b),
206 }
208```
210Checklist:
212- Rooted and unrooted RF answer different questions.
213- ETE compares the intersection when leaf sets differ; report its size.
214- Require at least two shared tips. With no eligible splits (`max_rf <= 0`),
215 normalized RF is undefined, not zero.
216- Duplicate labels violate the usual tip-identity assumption.
217- Support filtering and polytomy expansion materially change results.
218- A high RF distance does not explain which biological split is preferable.
220For a higher-level result:
222```python
223summary = tree_a.compare(tree_b, unrooted=True)
224print(summary["rf"], summary["max_rf"], summary["norm_rf"])
225```
227Keep labels unique. Do not use `Tree.compare(has_duplications=True)` in ETE
2284.4.0; upstream marks that TreeKO branch as likely broken. The packaged
229`ete4 compare` CLI also fails because it still passes the removed `format=`
230keyword, so use the methods above or `scripts/tree_operations.py compare`.

4. Compare Two Trees

from collections import Counter
from pathlib import Path

from ete4 import Tree


def load(path: Path, parser=1) -> Tree:
    with path.open(encoding="utf-8") as handle:
        return Tree(handle, parser=parser)


def assert_unique_leaf_names(tree: Tree, label: str) -> None:
    counts = Counter(tree.leaf_names())
    if any(not name for name in counts):
        raise ValueError(f"{label} has unnamed leaves")
    duplicates = sorted(name for name, count in counts.items() if count > 1)
    if duplicates:
        raise ValueError(f"{label} has duplicate leaves: {duplicates}")


tree_a = load(Path("method_a.nw"))
tree_b = load(Path("method_b.nw"))
assert_unique_leaf_names(tree_a, "method_a")
assert_unique_leaf_names(tree_b, "method_b")

(
    rf,
    max_rf,
    common,
    edges_a,
    edges_b,
    discarded_a,
    discarded_b,
) = tree_a.robinson_foulds(
    tree_b,
    unrooted_trees=True,
)

print(
    {
        "rf": rf,
        "max_rf": max_rf,
        "normalized_rf": rf / max_rf if max_rf > 0 else None,
        "common_leaf_count": len(common),
        "discarded_edges_a": len(discarded_a),
        "discarded_edges_b": len(discarded_b),
    }
)

Checklist:

  • Rooted and unrooted RF answer different questions.
  • ETE compares the intersection when leaf sets differ; report its size.
  • Require at least two shared tips. With no eligible splits (max_rf <= 0), normalized RF is undefined, not zero.
  • Duplicate labels violate the usual tip-identity assumption.
  • Support filtering and polytomy expansion materially change results.
  • A high RF distance does not explain which biological split is preferable.

For a higher-level result:

summary = tree_a.compare(tree_b, unrooted=True)
print(summary["rf"], summary["max_rf"], summary["norm_rf"])

Keep labels unique. Do not use Tree.compare(has_duplications=True) in ETE 4.4.0; upstream marks that TreeKO branch as likely broken. The packaged ete4 compare CLI also fails because it still passes the removed format= keyword, so use the methods above or scripts/tree_operations.py compare.

Raw lines 157 to 231
Runs code scripts/tree_operations.py :230
232## 5. Annotate a Tree from a Metadata Table
234Keep parsing and schema validation explicit.
236```python
237import csv
238from pathlib import Path
240from ete4 import Tree
242tree = Tree("((sample_1,sample_2),sample_3);")
244metadata: dict[str, dict[str, str]] = {}
245with Path("metadata.tsv").open(encoding="utf-8", newline="") as handle:
246 reader = csv.DictReader(handle, delimiter="\t")
247 required = {"sample", "host", "location"}
248 if not reader.fieldnames or not required.issubset(reader.fieldnames):
249 raise ValueError(f"metadata.tsv must contain columns {sorted(required)}")
250 if len(reader.fieldnames) != len(set(reader.fieldnames)):
251 raise ValueError("metadata.tsv has duplicate column names")
252 for row in reader:
253 if None in row or any(value is None for value in row.values()):
254 raise ValueError(f"metadata.tsv has a malformed row at line {reader.line_num}")
255 sample = row["sample"].strip()
256 if not sample or sample in metadata:
257 raise ValueError(f"Empty or duplicate metadata key: {sample!r}")
258 metadata[sample] = {
259 "host": row["host"].strip(),
260 "location": row["location"].strip(),
261 }
263unmatched_tree = []
264for leaf in tree.leaves():
265 values = metadata.get(leaf.name)
266 if values is None:
267 unmatched_tree.append(leaf.name)
268 continue
269 leaf.add_props(**values)
271unmatched_metadata = sorted(set(metadata) - set(tree.leaf_names()))
272if unmatched_tree or unmatched_metadata:
273 raise ValueError(
274 f"Unmatched tree leaves={unmatched_tree}; "
275 f"unmatched metadata rows={unmatched_metadata}"
276 )
278tree.write(
279 outfile="annotated.nhx",
280 props=["host", "location"],
282```
284Use an explicit `props` list when exporting. `props=None` writes every
285available property and may disclose internal annotations unintentionally.

5. Annotate a Tree from a Metadata Table

Keep parsing and schema validation explicit.

import csv
from pathlib import Path

from ete4 import Tree

tree = Tree("((sample_1,sample_2),sample_3);")

metadata: dict[str, dict[str, str]] = {}
with Path("metadata.tsv").open(encoding="utf-8", newline="") as handle:
    reader = csv.DictReader(handle, delimiter="\t")
    required = {"sample", "host", "location"}
    if not reader.fieldnames or not required.issubset(reader.fieldnames):
        raise ValueError(f"metadata.tsv must contain columns {sorted(required)}")
    if len(reader.fieldnames) != len(set(reader.fieldnames)):
        raise ValueError("metadata.tsv has duplicate column names")
    for row in reader:
        if None in row or any(value is None for value in row.values()):
            raise ValueError(f"metadata.tsv has a malformed row at line {reader.line_num}")
        sample = row["sample"].strip()
        if not sample or sample in metadata:
            raise ValueError(f"Empty or duplicate metadata key: {sample!r}")
        metadata[sample] = {
            "host": row["host"].strip(),
            "location": row["location"].strip(),
        }

unmatched_tree = []
for leaf in tree.leaves():
    values = metadata.get(leaf.name)
    if values is None:
        unmatched_tree.append(leaf.name)
        continue
    leaf.add_props(**values)

unmatched_metadata = sorted(set(metadata) - set(tree.leaf_names()))
if unmatched_tree or unmatched_metadata:
    raise ValueError(
        f"Unmatched tree leaves={unmatched_tree}; "
        f"unmatched metadata rows={unmatched_metadata}"
    )

tree.write(
    outfile="annotated.nhx",
    props=["host", "location"],
)

Use an explicit props list when exporting. props=None writes every available property and may disclose internal annotations unintentionally.

Raw lines 232 to 286
287## 6. Species-Overlap Event Detection
289```python
290from ete4 import PhyloTree
292gene_tree = PhyloTree(
293 "((Hsa|gene1,Ptr|gene1),(Hsa|gene2,Mmu|gene1));",
294 sp_naming_function=lambda name: name.split("|", 1)[0],
297events = gene_tree.get_descendant_evol_events(sos_thr=0.0)
298for event in events:
299 relationship = {
300 "S": "orthology",
301 "D": "paralogy",
302 }[event.etype]
303 print(
304 relationship,
305 sorted(event.in_seqs),
306 sorted(event.out_seqs),
307 )
309for node in gene_tree.traverse():
310 event_type = node.props.get("evoltype")
311 if event_type:
312 print(node.id, event_type)
313```
315Interpretation limits:
317- The gene tree must be rooted and fully bifurcating.
318- Pass the species naming function explicitly; ETE 4.4.0 defaults it to `None`.
319- Results depend on the topology and the species naming function.
320- A single shared species triggers duplication at `sos_thr=0.0`.
321- Gene loss, incomplete lineage sorting, horizontal transfer, and tree error
322 can violate a simple species-overlap interpretation.
323- Keep inferred events separate from curated orthology evidence.

6. Species-Overlap Event Detection

from ete4 import PhyloTree

gene_tree = PhyloTree(
    "((Hsa|gene1,Ptr|gene1),(Hsa|gene2,Mmu|gene1));",
    sp_naming_function=lambda name: name.split("|", 1)[0],
)

events = gene_tree.get_descendant_evol_events(sos_thr=0.0)
for event in events:
    relationship = {
        "S": "orthology",
        "D": "paralogy",
    }[event.etype]
    print(
        relationship,
        sorted(event.in_seqs),
        sorted(event.out_seqs),
    )

for node in gene_tree.traverse():
    event_type = node.props.get("evoltype")
    if event_type:
        print(node.id, event_type)

Interpretation limits:

  • The gene tree must be rooted and fully bifurcating.
  • Pass the species naming function explicitly; ETE 4.4.0 defaults it to None.
  • Results depend on the topology and the species naming function.
  • A single shared species triggers duplication at sos_thr=0.0.
  • Gene loss, incomplete lineage sorting, horizontal transfer, and tree error can violate a simple species-overlap interpretation.
  • Keep inferred events separate from curated orthology evidence.
Raw lines 287 to 324
325## 7. Gene-Tree/Species-Tree Reconciliation
327```python
328from ete4 import PhyloTree
330gene_newick = (
331 "((Dme_001,Dme_002),"
332 "(((Cfa_001,Mms_001),((Hsa_001,Ptr_001),Mmu_001)),"
333 "(Ptr_002,(Hsa_002,Mmu_002))));"
335species_newick = "((((Hsa,Ptr),Mmu),(Mms,Cfa)),Dme);"
337species_from_gene = lambda name: name.split("_", 1)[0]
339gene_tree = PhyloTree(
340 gene_newick,
341 sp_naming_function=species_from_gene,
343species_tree = PhyloTree(
344 species_newick,
345 sp_naming_function=lambda name: name,
348gene_species = {leaf.species for leaf in gene_tree.leaves()}
349species_tree_tips = set(species_tree.leaf_names())
350missing = sorted(gene_species - species_tree_tips)
351if missing:
352 raise ValueError(f"Species absent from species tree: {missing}")
354reconciled_tree, events = gene_tree.reconcile(species_tree)
356for event in events:
357 if event.etype == "S":
358 print("orthology", event.inparalogs, event.orthologs)
359 elif event.etype == "D":
360 print("paralogy", event.inparalogs, event.outparalogs)
362reconciled_tree.write(
363 outfile="reconciled.nhx",
364 props=["evoltype"],
366```
368Reconciliation assumes the species tree and species mapping are correct. State
369the species-tree source and treatment of uncertain branches.

7. Gene-Tree/Species-Tree Reconciliation

from ete4 import PhyloTree

gene_newick = (
    "((Dme_001,Dme_002),"
    "(((Cfa_001,Mms_001),((Hsa_001,Ptr_001),Mmu_001)),"
    "(Ptr_002,(Hsa_002,Mmu_002))));"
)
species_newick = "((((Hsa,Ptr),Mmu),(Mms,Cfa)),Dme);"

species_from_gene = lambda name: name.split("_", 1)[0]

gene_tree = PhyloTree(
    gene_newick,
    sp_naming_function=species_from_gene,
)
species_tree = PhyloTree(
    species_newick,
    sp_naming_function=lambda name: name,
)

gene_species = {leaf.species for leaf in gene_tree.leaves()}
species_tree_tips = set(species_tree.leaf_names())
missing = sorted(gene_species - species_tree_tips)
if missing:
    raise ValueError(f"Species absent from species tree: {missing}")

reconciled_tree, events = gene_tree.reconcile(species_tree)

for event in events:
    if event.etype == "S":
        print("orthology", event.inparalogs, event.orthologs)
    elif event.etype == "D":
        print("paralogy", event.inparalogs, event.outparalogs)

reconciled_tree.write(
    outfile="reconciled.nhx",
    props=["evoltype"],
)

Reconciliation assumes the species tree and species mapping are correct. State the species-tree source and treatment of uncertain branches.

Raw lines 325 to 370
371## 8. Split Duplicated Gene Families
373```python
374from pathlib import Path
376from ete4 import PhyloTree
378tree = PhyloTree(
379 "((Human_1,Chimp_1),(Human_2,(Chimp_2,Mouse_1)));",
380 sp_naming_function=lambda name: name.split("_", 1)[0],
383output_dir = Path("subfamilies")
384output_dir.mkdir(parents=True, exist_ok=True)
386for index, subtree in enumerate(tree.split_by_dups(), start=1):
387 subtree.write(
388 outfile=str(output_dir / f"subfamily_{index:03d}.nw"),
389 props=[],
390 )
391```
393For TreeKO-style enumeration, `get_speciation_trees()` can generate very many
394topologies. Consider `newick_only=True`, monitor output size, and define a
395scientifically justified limit before materializing results.
397```python
398tree_count, duplication_count, newicks = tree.get_speciation_trees(
399 newick_only=True,
401for newick in newicks:
402 process(newick)
403```

8. Split Duplicated Gene Families

from pathlib import Path

from ete4 import PhyloTree

tree = PhyloTree(
    "((Human_1,Chimp_1),(Human_2,(Chimp_2,Mouse_1)));",
    sp_naming_function=lambda name: name.split("_", 1)[0],
)

output_dir = Path("subfamilies")
output_dir.mkdir(parents=True, exist_ok=True)

for index, subtree in enumerate(tree.split_by_dups(), start=1):
    subtree.write(
        outfile=str(output_dir / f"subfamily_{index:03d}.nw"),
        props=[],
    )

For TreeKO-style enumeration, get_speciation_trees() can generate very many topologies. Consider newick_only=True, monitor output size, and define a scientifically justified limit before materializing results.

tree_count, duplication_count, newicks = tree.get_speciation_trees(
    newick_only=True,
)
for newick in newicks:
    process(newick)
Raw lines 371 to 404
405## 9. NCBI and GTDB Taxonomy
407NCBI topology:
409```python
410from ete4 import NCBITaxa
412ncbi = NCBITaxa()
413taxids = [9606, 9598, 10090]
414tree = ncbi.get_topology(taxids, intermediate_nodes=False, annotate=True)
415print(tree.to_str(props=["sci_name", "rank", "taxid"]))
416```
418GTDB topology:
420```python
421from ete4 import GTDBTaxa
423gtdb = GTDBTaxa()
424taxa = ["p__Huberarchaeota", "o__Peptococcales", "f__Korarchaeaceae"]
425tree = gtdb.get_topology(taxa, intermediate_nodes=True, annotate=True)
426print(tree.to_str(props=["sci_name", "rank"]))
427```
429See `taxonomy.md` before downloading, updating, or annotating production data.

9. NCBI and GTDB Taxonomy

NCBI topology:

from ete4 import NCBITaxa

ncbi = NCBITaxa()
taxids = [9606, 9598, 10090]
tree = ncbi.get_topology(taxids, intermediate_nodes=False, annotate=True)
print(tree.to_str(props=["sci_name", "rank", "taxid"]))

GTDB topology:

from ete4 import GTDBTaxa

gtdb = GTDBTaxa()
taxa = ["p__Huberarchaeota", "o__Peptococcales", "f__Korarchaeaceae"]
tree = gtdb.get_topology(taxa, intermediate_nodes=True, annotate=True)
print(tree.to_str(props=["sci_name", "rank"]))

See taxonomy.md before downloading, updating, or annotating production data.

Raw lines 405 to 430
431## 10. Batch Processing
433```python
434from pathlib import Path
436from ete4 import Tree
438input_dir = Path("trees")
439output_dir = Path("processed")
440output_dir.mkdir(parents=True, exist_ok=True)
442for input_path in sorted(input_dir.glob("*.nw")):
443 with input_path.open(encoding="utf-8") as handle:
444 tree = Tree(handle, parser="support")
446 names = list(tree.leaf_names())
447 if len(names) != len(set(names)):
448 raise ValueError(f"{input_path}: duplicate leaf names")
450 tree.set_midpoint_outgroup()
451 tree.ladderize()
453 output_path = output_dir / input_path.name
454 tree.write(
455 outfile=str(output_path),
456 parser="support",
457 props=[],
458 )
459```
461Do not catch and discard parse exceptions in a batch. Fail with the source
462path, or record failures in a structured report and return a nonzero status.

10. Batch Processing

from pathlib import Path

from ete4 import Tree

input_dir = Path("trees")
output_dir = Path("processed")
output_dir.mkdir(parents=True, exist_ok=True)

for input_path in sorted(input_dir.glob("*.nw")):
    with input_path.open(encoding="utf-8") as handle:
        tree = Tree(handle, parser="support")

    names = list(tree.leaf_names())
    if len(names) != len(set(names)):
        raise ValueError(f"{input_path}: duplicate leaf names")

    tree.set_midpoint_outgroup()
    tree.ladderize()

    output_path = output_dir / input_path.name
    tree.write(
        outfile=str(output_path),
        parser="support",
        props=[],
    )

Do not catch and discard parse exceptions in a batch. Fail with the source path, or record failures in a structured report and return a nonzero status.

Raw lines 431 to 463
464## 11. Large Trees
466Prefer generators:
468```python
469for leaf in tree.leaves():
470 process(leaf)
471```
473Materialize only when indexing, sorting, or repeated traversal requires it:
475```python
476leaves = list(tree.leaves())
477```
479Cache descendant content for repeated clade calculations:
481```python
482leaf_cache = tree.get_cached_content(prop="name")
483for node in tree.traverse("postorder"):
484 descendant_names = leaf_cache[node]
485 summarize(node, descendant_names)
486```
488Use SmartView for adaptive exploration. Static rendering of every label on a
489very large tree is usually unreadable and expensive; use collapsed-node layouts
490or render selected subtrees.

11. Large Trees

Prefer generators:

for leaf in tree.leaves():
    process(leaf)

Materialize only when indexing, sorting, or repeated traversal requires it:

leaves = list(tree.leaves())

Cache descendant content for repeated clade calculations:

leaf_cache = tree.get_cached_content(prop="name")
for node in tree.traverse("postorder"):
    descendant_names = leaf_cache[node]
    summarize(node, descendant_names)

Use SmartView for adaptive exploration. Static rendering of every label on a very large tree is usually unreadable and expensive; use collapsed-node layouts or render selected subtrees.

Raw lines 464 to 491
492## 12. Clustering Dendrograms
494ETE 4.4.0 does not export ETE 3's `ClusterTree`. For a clustering workflow:
4961. Compute linkage and validation metrics with SciPy or another maintained
497 clustering package.
4982. Convert the resulting hierarchy to Newick or construct an ETE `Tree`.
4993. Store cluster labels or statistics as node properties.
5004. Use SmartView layouts to display those properties.
502Do not claim that ETE 4 calculated silhouette, Dunn, or matrix-linked
503`ClusterTree` metrics unless another library actually performed those steps.

12. Clustering Dendrograms

ETE 4.4.0 does not export ETE 3's ClusterTree. For a clustering workflow:

  1. Compute linkage and validation metrics with SciPy or another maintained clustering package.
  2. Convert the resulting hierarchy to Newick or construct an ETE Tree.
  3. Store cluster labels or statistics as node properties.
  4. Use SmartView layouts to display those properties.

Do not claim that ETE 4 calculated silhouette, Dunn, or matrix-linked ClusterTree metrics unless another library actually performed those steps.

Raw lines 492 to 504
505## 13. End-to-End Phylogenomic Handoff
507A defensible division of labor is:
5091. Perform sequence quality control outside ETE.
5102. Align sequences with MAFFT or a domain-appropriate aligner.
5113. Infer topology/support with IQ-TREE 2, FastTree, or another explicit method.
5124. Load the inferred Newick into ETE with the correct parser.
5135. Validate tip identity and support scale.
5146. Root, prune, annotate, compare, or reconcile in ETE.
5157. Render and export with explicit properties.
5168. Report both inference-tool settings and ETE transformation settings.
518ETE manipulates and interprets the supplied topology; it does not make upstream
519model choice, alignment quality, or sampling bias disappear.

13. End-to-End Phylogenomic Handoff

A defensible division of labor is:

  1. Perform sequence quality control outside ETE.
  2. Align sequences with MAFFT or a domain-appropriate aligner.
  3. Infer topology/support with IQ-TREE 2, FastTree, or another explicit method.
  4. Load the inferred Newick into ETE with the correct parser.
  5. Validate tip identity and support scale.
  6. Root, prune, annotate, compare, or reconcile in ETE.
  7. Render and export with explicit properties.
  8. Report both inference-tool settings and ETE transformation settings.

ETE manipulates and interprets the supplied topology; it does not make upstream model choice, alignment quality, or sampling bias disappear.

Raw lines 505 to 520
521## 14. Match Repeated Topologies
523Use a topology-only `TreePattern` when the structural pattern is easier to
524state as a small tree:
526```python
527from ete4 import Tree
528from ete4.treematcher import TreePattern
530tree = Tree("((K,((A,B),C),D),(E,F));")
531pattern = TreePattern("(,,)", safer=True)
533for match in pattern.search(tree):
534 print("three-child node:", match.id, match.name)
535```
537The matcher tries child permutations, so sibling order does not prevent a
538topological match. Expression-bearing patterns are executable conditions:
539never construct them from user input, imported data, model output, or other
540untrusted text. Prefer topology-only patterns or explicit traversal predicates
541for dynamic criteria.

14. Match Repeated Topologies

Use a topology-only TreePattern when the structural pattern is easier to state as a small tree:

from ete4 import Tree
from ete4.treematcher import TreePattern

tree = Tree("((K,((A,B),C),D),(E,F));")
pattern = TreePattern("(,,)", safer=True)

for match in pattern.search(tree):
    print("three-child node:", match.id, match.name)

The matcher tries child permutations, so sibling order does not prevent a topological match. Expression-bearing patterns are executable conditions: never construct them from user input, imported data, model output, or other untrusted text. Prefer topology-only patterns or explicit traversal predicates for dynamic criteria.

Raw lines 521 to 542
543## Upstream References
545- Tree tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_trees.html
546- Phylogenetic tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_phylogeny.html
547- Taxonomy tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_taxonomy.html
548- Tree matcher tutorial:
549 https://etetoolkit.github.io/ete/tutorial/tutorial_treematcher.html
550- Tree API: https://etetoolkit.github.io/ete/reference/reference_tree.html

Upstream References

  • Tree tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_trees.html
  • Phylogenetic tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_phylogeny.html
  • Taxonomy tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_taxonomy.html
  • Tree matcher tutorial: https://etetoolkit.github.io/ete/tutorial/tutorial_treematcher.html
  • Tree API: https://etetoolkit.github.io/ete/reference/reference_tree.html
Raw lines 543 to 550