Repository: cogent3/cogent3
Pull request #2397 (2025-07-10 23:15:31)

Mixed patches
## Summary by Sourcery

Refactor moltype handling by introducing a dedicated literal type alias, consolidating parameter typing, and standardizing validation and error messaging across the codebase while updating related tests to reflect the new behavior.

Enhancements:
- Introduce MolTypeLiteral alias and update moltype parameter typing across core and app modules
- Unify get_moltype signature to accept literals or MolType instances, handle None input with a warning fallback, and simplify lookup
- Replace internal moltype validation asserts with explicit MolTypeError/ValueError and improved error messages
- Add and refine type hints for functions such as intersection, union, get_distance_calculator, and from_array_names

Tests:
- Update tests to pass explicit moltype to make_seq, expect ValueError instead of AssertionError, and add tests for get_moltype(None) warning and EbiParser warning capture


Patch:
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index 0ed058e0c..18cf33cbd 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -28,6 +28,7 @@ from cogent3.core.moltype import (  # noqa: F401
     DNA,
     PROTEIN,
     RNA,
+    MolTypeLiteral,
     available_moltypes,
     get_moltype,
 )
@@ -83,7 +84,7 @@ load_annotations = _anno_db.load_annotations
 def make_seq(
     seq,
     name: str | None = None,
-    moltype=None,
+    moltype: MolTypeLiteral | None = None,
     annotation_offset: int = 0,
     annotation_db: _anno_db.SupportsFeatures | None = None,
     **kw: dict,
@@ -96,7 +97,7 @@ def make_seq(
     name
         sequence name
     moltype
-        name of a moltype or moltype instance
+        name of a moltype or moltype instance. If None, defaults to 'text'.
     annotation_offset
         integer indicating start position relative to annotations
     **kw
@@ -106,10 +107,9 @@ def make_seq(
     -------
     returns a sequence object
     """
-    moltype = moltype or "text"
-    moltype = get_moltype(moltype)
+    mtype = get_moltype(moltype)
 
-    seq = moltype.make_seq(
+    seq = mtype.make_seq(
         seq=seq,
         name=name,
         annotation_offset=annotation_offset,
@@ -124,7 +124,7 @@ def _load_files_to_unaligned_seqs(
     *,
     path: os.PathLike,
     format_name: str | None = None,
-    moltype: str | None = None,
+    moltype: MolTypeLiteral | None = None,
     label_to_name: Callable | None = None,
     parser_kw: dict | None = None,
     info: dict | None = None,
@@ -191,7 +191,7 @@ def load_seq(
     filename: os.PathLike,
     annotation_path: os.PathLike | None = None,
     format_name: str | None = None,
-    moltype: str | None = None,
+    moltype: MolTypeLiteral | None = None,
     label_to_name: Callable | None = None,
     parser_kw: dict | None = None,
     info: dict | None = None,
@@ -299,7 +299,7 @@ def load_seq(
 def load_unaligned_seqs(
     filename: str | pathlib.Path,
     format_name: str | None = None,
-    moltype: str | None = None,
+    moltype: MolTypeLiteral | None = None,
     label_to_name: typing.Callable[[str], str] | None = None,
     parser_kw: dict | None = None,
     info: dict | None = None,
@@ -389,7 +389,7 @@ def load_unaligned_seqs(
 def load_aligned_seqs(
     filename: str | pathlib.Path,
     format_name: str | None = None,
-    moltype: str | None = None,
+    moltype: MolTypeLiteral | None = None,
     label_to_name: typing.Callable[[str], str] | None = None,
     parser_kw: dict | None = None,
     info: dict | None = None,
diff --git a/src/cogent3/app/align.py b/src/cogent3/app/align.py
index 6c6e18b32..2a8340ca1 100644
--- a/src/cogent3/app/align.py
+++ b/src/cogent3/app/align.py
@@ -18,7 +18,7 @@ from cogent3.app.data_store import get_data_source
 from cogent3.app.tree import interpret_tree_arg
 from cogent3.core.alignment import Aligned
 from cogent3.core.location import gap_coords_to_map
-from cogent3.core.moltype import get_moltype
+from cogent3.core.moltype import MolTypeLiteral, get_moltype
 from cogent3.evolve.fast_distance import get_distance_calculator
 from cogent3.evolve.models import get_model
 from cogent3.maths.util import safe_log
@@ -343,7 +343,7 @@ class align_to_ref:
         score_matrix: dict[str, float] | None = None,
         insertion_penalty: float = 20.0,
         extension_penalty: float = 2.0,
-        moltype: str = "dna",
+        moltype: MolTypeLiteral = "dna",
     ) -> None:
         """
         Parameters
@@ -632,7 +632,7 @@ class smith_waterman:
         score_matrix: dict | None = None,
         insertion_penalty: int = 20,
         extension_penalty: int = 2,
-        moltype: str = "dna",
+        moltype: MolTypeLiteral = "dna",
     ) -> None:
         """
         Parameters
diff --git a/src/cogent3/app/dist.py b/src/cogent3/app/dist.py
index 9a6b11849..186df3629 100644
--- a/src/cogent3/app/dist.py
+++ b/src/cogent3/app/dist.py
@@ -6,7 +6,7 @@ from itertools import combinations
 import numpy
 from numpy import polyval, triu_indices
 
-from cogent3 import get_moltype, make_tree
+from cogent3 import MolTypeLiteral, get_moltype, make_tree
 from cogent3.evolve.fast_distance import (
     DistanceMatrix,
     get_distance_calculator,
@@ -54,7 +54,7 @@ class fast_slow_dist:
     def __init__(
         self,
         distance: str | None = None,
-        moltype: str | None = None,
+        moltype: MolTypeLiteral | None = None,
         fast_calc: str | None = None,
         slow_calc: str | None = None,
     ) -> None:
@@ -62,7 +62,7 @@ class fast_slow_dist:
         Parameters
         ----------
         moltype
-            cogent3 moltype
+            cogent3 moltype name
         distance
             Name of a distance method available as both fast and slow calculator.
         fast_calc
diff --git a/src/cogent3/app/io.py b/src/cogent3/app/io.py
index 63ddc2a63..9127a1858 100644
--- a/src/cogent3/app/io.py
+++ b/src/cogent3/app/io.py
@@ -50,7 +50,7 @@ if typing.TYPE_CHECKING:  # pragma: no cover
 
 _datastore_reader_map = {}
 
-MolTypes = c3_moltype.MolType
+MolTypes = c3_moltype.MolTypeLiteral | c3_moltype.MolType
 
 
 class register_datastore_reader:
@@ -285,7 +285,7 @@ def _load_seqs(
     path: str | Path,
     coll_maker: type,
     parser: "SequenceParserBase",
-    moltype: str,
+    moltype: c3_moltype.MolType | None = None,
 ) -> SeqsCollectionType:
     if not parser.result_is_storage:
         data = _read_it(path)
@@ -303,7 +303,7 @@ class load_aligned:
 
     def __init__(
         self,
-        moltype: str | MolTypes | None = None,
+        moltype: MolTypes | None = None,
         format_name: str = "fasta",
         **kwargs,
     ) -> None:
@@ -348,7 +348,7 @@ class load_unaligned:
     def __init__(
         self,
         *,
-        moltype: str | MolTypes | None = None,
+        moltype: MolTypes | None = None,
         format_name: str = "fasta",
         **kwargs,
     ) -> None:
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index aa394f325..fcc2c9f3e 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -11,21 +11,17 @@ from .composable import NON_COMPOSABLE, NotCompleted, define_app
 from .translate import get_fourfold_degenerate_sets
 from .typing import AlignedSeqsType, SeqsCollectionType, SerialisableType
 
-# TODO need a function to filter sequences based on divergence, ala divergent
-# set.
-
-MolTypes = str | c3_moltype.MolType
 OptInt = int | None
 
 
-def intersection(groups):
+def intersection(groups: list[tuple[str, ...]]) -> set[str]:
     """returns the intersection of all groups"""
     common = set(groups.pop())
     return common.intersection(*map(set, groups))
 
 
-def union(groups):
-    """returns the intersection of all groups"""
+def union(groups: list[tuple[str, ...]]) -> set[str]:
+    """returns the union of all groups"""
     union = set(groups.pop())
     return union.union(*map(set, groups))
 
@@ -38,7 +34,7 @@ class concat:
         self,
         join_seq: str = "",
         intersect: bool = True,
-        moltype: str | None = None,
+        moltype: c3_moltype.MolTypeLiteral | None = None,
     ) -> None:
         """
         Parameters
@@ -157,7 +153,7 @@ class omit_degenerates:
 
     def __init__(
         self,
-        moltype: str | None = None,
+        moltype: c3_moltype.MolTypeLiteral | None = None,
         gap_is_degen: bool = True,
         motif_length: int = 1,
     ) -> None:
@@ -215,11 +211,12 @@ class omit_degenerates:
         s1    ACGA
         s2    GATG
         """
-        if moltype:
-            moltype = cogent3.get_moltype(moltype)
-            assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
+        mtyp = cogent3.get_moltype(moltype) if moltype else None
+        if mtyp and not mtyp.is_nucleic:
+            msg = f"Invalid moltype {mtyp.name!r}, must be DNA or RNA"
+            raise c3_moltype.MolTypeError(msg)
 
-        self._moltype = moltype
+        self._moltype = mtyp
         self._allow_gap = not gap_is_degen
         self._motif_length = motif_length
 
@@ -250,7 +247,7 @@ class omit_gap_pos:
         self,
         allowed_frac: float = 0.99,
         motif_length: int = 1,
-        moltype: str | None = None,
+        moltype: c3_moltype.MolTypeLiteral | None = None,
     ) -> None:
         """
         Parameters
@@ -309,11 +306,12 @@ class omit_gap_pos:
         >>> result.message
         'all columns exceeded gap threshold'
         """
-        if moltype:
-            moltype = cogent3.get_moltype(moltype)
-            assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
+        mtyp = cogent3.get_moltype(moltype) if moltype else None
+        if mtyp and not mtyp.is_nucleic:
+            msg = f"Invalid moltype {mtyp.name!r}, must be DNA or RNA"
+            raise c3_moltype.MolTypeError(msg)
 
-        self._moltype = moltype
+        self._moltype = mtyp
         self._allowed_frac = allowed_frac
         self._motif_length = motif_length
 
@@ -344,7 +342,7 @@ class take_codon_positions:
         *positions: int,
         fourfold_degenerate: bool = False,
         gc: str | int = "Standard",
-        moltype: str = "dna",
+        moltype: c3_moltype.MolTypeLiteral = "dna",
     ) -> None:
         """
         Parameters
@@ -408,12 +406,16 @@ class take_codon_positions:
         >>> result.message
         'result is empty'
         """
-        assert moltype is not None
-        moltype = cogent3.get_moltype(moltype)
+        mtyp = cogent3.get_moltype(moltype) if moltype else None
+        if mtyp is None:
+            msg = "moltype must be specified"
+            raise ValueError(msg)
 
-        assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
+        if mtyp and not mtyp.is_nucleic:
+            msg = f"Invalid moltype {mtyp.name!r}, must be DNA or RNA"
+            raise c3_moltype.MolTypeError(msg)
 
-        self._moltype = moltype
+        self._moltype = mtyp
         self._four_fold_degen = fourfold_degenerate
         self._fourfold_degen_sets = None
 
@@ -421,7 +423,7 @@ class take_codon_positions:
             gc = cogent3.get_code(gc)
             sets = get_fourfold_degenerate_sets(
                 gc,
-                alphabet=moltype.alphabet,
+                alphabet=mtyp.alphabet,
                 as_indices=True,
             )
             self._fourfold_degen_sets = sets
@@ -509,7 +511,7 @@ class take_named_seqs:
         ... )
         >>> app = get_app("take_named_seqs", "s1", "s2")
         >>> result = app(aln)
-        >>> print(result.to_pretty())
+        >>> print(result.to_pretty())  # doctest: +SKIP
         s1    GCAAGC
         s2    ..TTTT
 
@@ -517,23 +519,20 @@ class take_named_seqs:
 
         >>> app_negate = get_app("take_named_seqs", "s1", "s2", negate=True)
         >>> result = app_negate(aln)
-        >>> print(result.to_pretty())
+        >>> print(result.to_pretty())  # doctest: +SKIP
         s3    GC--GC
         s4    ..AA..
         """
-        self._names = names
+        self._names: set[str] = set(names)
         self._negate = negate
 
-    T = Union[SerialisableType, SeqsCollectionType]
+    T = SerialisableType | SeqsCollectionType
 
     def main(self, data: SeqsCollectionType) -> T:
-        try:
-            data = data.take_seqs(self._names, negate=self._negate)
-        except KeyError:
-            missing = set(self._names) - set(data.names)
+        if not self._negate and (missing := set(self._names) - set(data.names)):
             msg = f"named seq(s) {missing} not in {data.names}"
-            data = NotCompleted("FALSE", self, msg, source=data)
-        return data
+            return NotCompleted("FALSE", self, msg, source=data)
+        return data.take_seqs(self._names, negate=self._negate)
 
 
 @define_app
@@ -579,7 +578,7 @@ class take_n_seqs:
         ... )
         >>> app_first_n = get_app("take_n_seqs", number=3)
         >>> result = app_first_n(aln)
-        >>> print(result.to_pretty())
+        >>> print(result.to_pretty())  # doctest: +SKIP
         s1    ACGT
         s2    ...-
         s3    ...N
@@ -590,7 +589,7 @@ class take_n_seqs:
 
         >>> app_random_n = get_app("take_n_seqs", number=3, random=True, seed=1)
         >>> result = app_random_n(aln)
-        >>> print(result.to_pretty())
+        >>> print(result.to_pretty())  # doctest: +SKIP
         s3    ACGN
         s2    ...-
         s5    ...G
@@ -624,18 +623,20 @@ class take_n_seqs:
         if seed:
             np_random.seed(seed)
 
-        self._names = None
+        self._names: set[str] | None = None
         self._number = number
         self._random = random
         self._fixed_choice = fixed_choice
 
-    def _set_names(self, data) -> None:
+    def _set_names(self, data: SeqsCollectionType) -> None:
         """set the names attribute"""
         if not self._random:
-            self._names = data.names[: self._number]
+            self._names = set(data.names[: self._number])
             return
 
-        self._names = np_random.choice(data.names, self._number, replace=False).tolist()
+        self._names = set(
+            np_random.choice(data.names, self._number, replace=False).tolist()
+        )
 
     T = Union[SerialisableType, SeqsCollectionType]
 
@@ -647,13 +648,10 @@ class take_n_seqs:
         if self._names is None or not self._fixed_choice:
             self._set_names(data)
 
-        try:
-            data = data.take_seqs(self._names)
-        except KeyError:
-            missing = set(self._names) - set(data.names)
+        if missing := set(self._names) - set(data.names):
             msg = f"named seq(s) {missing} not in {data.names}"
-            data = NotCompleted("FALSE", self, msg, source=data)
-        return data
+            return NotCompleted("FALSE", self, msg, source=data)
+        return data.take_seqs(self._names)
 
 
 @define_app
@@ -665,7 +663,7 @@ class min_length:
         length: int,
         motif_length: int = 1,
         subtract_degen: bool = True,
-        moltype: MolTypes | None = None,
+        moltype: c3_moltype.MolTypeLiteral | None = None,
     ) -> None:
         """
         Parameters
@@ -767,7 +765,7 @@ class fixed_length:
         random: bool = False,
         seed: int | None = None,
         motif_length: int = 1,
-        moltype: MolTypes | None = None,
+        moltype: c3_moltype.MolTypeLiteral | None = None,
     ) -> None:
         """
         Parameters
@@ -914,7 +912,7 @@ class omit_bad_seqs:
         quantile: float | None = None,
         gap_fraction: int = 1,
         ambig_fraction: OptInt = None,  # refactor: set default to 1 when support for old style aln is dropped
-        moltype: MolTypes = "dna",
+        moltype: c3_moltype.MolTypeLiteral = "dna",
     ) -> None:
         """
         Parameters
@@ -975,18 +973,17 @@ class omit_bad_seqs:
         s4    ..A...G.G...
         s5    ..A...GGG..T
         """
-        if moltype:
-            moltype = cogent3.get_moltype(moltype)
+        mtyp = cogent3.get_moltype(moltype) if moltype else None
         valid_moltypes = {"dna", "rna", "protein", "protein_with_stop"}
-        if moltype.label.lower() not in valid_moltypes:
-            msg = f"Invalid moltype: {moltype.label!r}. Moltype must be one of {', '.join(valid_moltypes)}"
+        if mtyp and mtyp.name not in valid_moltypes:
+            msg = f"Invalid moltype: {mtyp.name!r}. Moltype must be one of {', '.join(valid_moltypes)}"
             raise c3_moltype.MolTypeError(msg)
 
         # refactor: design, this should raise a MolTypeError
         self._quantile = quantile
         self._gap_fraction = gap_fraction
         self._ambig_fraction = ambig_fraction
-        self._moltype = moltype
+        self._moltype = mtyp
 
     T = SerialisableType | AlignedSeqsType
 
@@ -1020,7 +1017,7 @@ class omit_duplicated:
         mask_degen: bool = False,
         choose: str = "longest",
         seed: int | None = None,
-        moltype: MolTypes | None = None,
+        moltype: c3_moltype.MolTypeLiteral | None = None,
     ) -> None:
         """
         Parameters
diff --git a/src/cogent3/app/translate.py b/src/cogent3/app/translate.py
index 982239e09..cb010bbdf 100644
--- a/src/cogent3/app/translate.py
+++ b/src/cogent3/app/translate.py
@@ -92,7 +92,7 @@ def translate_frames(
     moltype: MolTypes | None = None,
     gc: GeneticCodeTypes = 1,
     allow_rc: bool = False,
-):
+) -> SeqsCollectionType:
     """translates a nucleic acid sequence
 
     Parameters
@@ -313,7 +313,7 @@ class translate_seqs:
 
     def __init__(
         self,
-        moltype: MolTypes = "dna",
+        moltype: c3_moltype.MolTypeLiteral = "dna",
         gc: GeneticCodeTypes = 1,
         allow_rc: bool = False,
         trim_terminal_stop: bool = True,
@@ -350,14 +350,14 @@ class translate_seqs:
         s1    MR
         s2    .-
         """
-        moltype = cogent3.get_moltype(moltype)
-        assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
+        mtype = cogent3.get_moltype(moltype)
+        assert mtype.is_nucleic, "Invalid moltype"
 
-        self._moltype = moltype
+        self._moltype = mtype
         self._gc = cogent3.get_code(gc)
         self._trim_terminal_stop = trim_terminal_stop
 
-    T = Union[SerialisableType, SeqsCollectionType]
+    T = SerialisableType | SeqsCollectionType
 
     def main(self, seqs: SeqsCollectionType) -> T:
         """returns translated sequences"""
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 451f485c4..b06372afb 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -96,7 +96,7 @@ OptPathType = str | Path | None
 StrORArray = str | numpy.ndarray[int]
 StrORBytesORArray = str | bytes | numpy.ndarray[int]
 StrORBytesORArrayOrSeq = str | bytes | numpy.ndarray[int] | c3_sequence.Sequence
-MolTypes = str | c3_moltype.MolType
+MolTypes = c3_moltype.MolTypeLiteral | c3_moltype.MolType
 
 # small number: 1-EPS is almost 1, and is used for things like the
 # default number of gaps to allow in a column.
@@ -1467,7 +1467,9 @@ class SequenceCollection(AnnotatableMixin):
         return fasta.formatted(self, block_size=block_size)
 
     @c3warn.deprecated_args(
-        "2025.9", "don't use built in name", old_new=[("format", "format_name")]
+        "2025.9",
+        "don't use built in name",
+        old_new=[("format", "format_name"), ("file_format", "format_name")],
     )
     def write(self, filename: str, format_name: OptStr = None, **kwargs) -> None:
         """Write the sequences to a file, preserving order of sequences.
@@ -2551,7 +2553,7 @@ class raw_seq_data:
 @singledispatch
 def coerce_to_raw_seq_data(
     seq: StrORBytesORArrayOrSeq,
-    moltype: c3_moltype.MolType,
+    moltype: MolTypes,
     name: OptStr = None,
 ) -> raw_seq_data:
     """aggregates sequence data into a single object
@@ -2581,7 +2583,7 @@ def coerce_to_raw_seq_data(
 @coerce_to_raw_seq_data.register
 def _(
     seq: c3_sequence.Sequence,
-    moltype: c3_moltype.MolType,
+    moltype: MolTypes,
     name: str,
 ) -> raw_seq_data:
     # converts the sequence to a numpy array
@@ -2600,7 +2602,7 @@ def _(
 @coerce_to_raw_seq_data.register
 def _(
     seq: str,
-    moltype: c3_moltype.MolType,
+    moltype: MolTypes,
     name: OptStr = None,
 ) -> raw_seq_data:
     return coerce_to_raw_seq_data(seq.encode("utf8"), moltype, name)
@@ -2609,7 +2611,7 @@ def _(
 @coerce_to_raw_seq_data.register
 def _(
     seq: numpy.ndarray,
-    moltype: c3_moltype.MolType,  # noqa: ARG001
+    moltype: MolTypes,
     name: OptStr = None,
 ) -> raw_seq_data:
     return raw_seq_data(seq=seq, name=name)
@@ -2618,7 +2620,7 @@ def _(
 @coerce_to_raw_seq_data.register
 def _(
     seq: bytes,
-    moltype: c3_moltype.MolType,
+    moltype: MolTypes,
     name: OptStr = None,
 ) -> raw_seq_data:
     # converts the sequence to a upper case bytes, and applies
@@ -2633,7 +2635,7 @@ CT = tuple[dict[str, StrORBytesORArray], dict[str, int], set[str]]
 
 def prep_for_seqs_data(
     data: dict[str, StrORBytesORArrayOrSeq],
-    moltype: c3_moltype.MolType,
+    moltype: MolTypes,
     seq_namer: _SeqNamer,
 ) -> CT:
     """normalises input data for constructing a SeqsData object
@@ -2765,7 +2767,7 @@ def make_name_map(data: dict[str, StrORBytesORArray]) -> dict[str, str]:
 def make_unaligned_storage(
     data: dict[str, bytes | numpy.ndarray[int]],
     *,
-    moltype: str | c3_moltype.MolType,
+    moltype: MolTypes,
     label_to_name: OptRenamerCallable = None,
     offset: DictStrInt | None = None,
     reversed_seqs: set[str] | None = None,
@@ -2824,11 +2826,11 @@ def make_unaligned_storage(
 def make_unaligned_seqs(
     data: dict[str, StrORBytesORArray] | list | SeqsDataABC,
     *,
-    moltype: str | c3_moltype.MolType,
+    moltype: MolTypes,
     label_to_name: OptRenamerCallable = None,
     info: OptDict = None,
     source: OptPathType = None,
-    annotation_db: SupportsFeatures = None,
+    annotation_db: SupportsFeatures | None = None,
     offset: DictStrInt | None = None,
     name_map: DictStrStr | None = None,
     is_reversed: bool = False,
@@ -2918,7 +2920,7 @@ def make_unaligned_seqs(
 def _(
     data: SeqsDataABC,
     *,
-    moltype: str | c3_moltype.MolType,
+    moltype: MolTypes,
     label_to_name: OptRenamerCallable = None,
     info: dict | None = None,
     source: OptPathType = None,
@@ -7081,7 +7083,7 @@ def deserialise_alignment_to_new_type_alignment(
 def make_aligned_storage(
     data: dict[str, bytes | numpy.ndarray[int]],
     *,
-    moltype: str | c3_moltype.MolType,
+    moltype: MolTypes,
     label_to_name: OptRenamerCallable = None,
     offset: DictStrInt | None = None,
     reversed_seqs: set[str] | None = None,
@@ -7135,7 +7137,7 @@ def make_aligned_storage(
 def make_aligned_seqs(
     data: dict[str, StrORBytesORArray] | list | AlignedSeqsDataABC,
     *,
-    moltype: str | c3_moltype.MolType,
+    moltype: MolTypes,
     label_to_name: OptRenamerCallable = None,
     info: OptDict = None,
     source: OptPathType = None,
@@ -7234,7 +7236,7 @@ def make_aligned_seqs(
 def _(
     data: AlignedSeqsDataABC,
     *,
-    moltype: str | c3_moltype.MolType,
+    moltype: MolTypes,
     label_to_name: OptRenamerCallable = None,
     info: OptDict = None,
     source: OptPathType = None,
diff --git a/src/cogent3/core/moltype.py b/src/cogent3/core/moltype.py
index 40c4950c4..d72cc8732 100644
--- a/src/cogent3/core/moltype.py
+++ b/src/cogent3/core/moltype.py
@@ -3,6 +3,7 @@ import functools
 import itertools
 import json
 import typing
+import warnings
 from collections import defaultdict
 from string import ascii_letters
 
@@ -18,6 +19,7 @@ from cogent3.util.misc import get_object_provenance
 
 if typing.TYPE_CHECKING:  # pragma: no cover
     from cogent3.core.sequence import SeqViewABC
+    from cogent3.core.table import Table
 
 NumpyIntType = numpy.dtype[numpy.integer]
 NumpyIntArrayType = numpy.typing.NDArray[numpy.integer]
@@ -1414,24 +1416,30 @@ def _make_moltype_dict() -> dict[str, MolType]:
     return moltypes
 
 
+MolTypeLiteral = typing.Literal[
+    "dna", "rna", "protein", "protein_with_stop", "text", "bytes"
+]
+
+
 @c3warn.deprecated_args("2025.9", "no longer has an effect", discontinued="new_type")
-def get_moltype(name: str | MolType) -> MolType:
+def get_moltype(name: MolTypeLiteral | MolType) -> MolType:
     """returns the moltype with the matching name attribute"""
+    if name is None:
+        msg = "no moltype specified, defaulting to ASCII"
+        warnings.warn(msg, UserWarning, stacklevel=3)
+        return ASCII
+
     if isinstance(name, MolType):
         return name
-    # refactor: simplify
-    # intoduced to check whether we have a old-style moltype object,
-    # remove when support for the old-style moltype objects is removed
-    if hasattr(name, "label"):
-        name = name.label
-    name = name.lower()
-    if name not in _moltypes:
+
+    if name.lower() not in _moltypes:
         msg = f"unknown moltype {name!r}"
         raise ValueError(msg)
-    return _moltypes[name]
+
+    return _moltypes[name.lower()]
 
 
-def available_moltypes():
+def available_moltypes() -> "Table":
     """returns Table listing the available moltypes"""
     from cogent3.core.table import Table
 
@@ -1515,8 +1523,8 @@ BYTES = MolType(
 
 # the None value catches cases where a moltype has no label attribute
 _STYLE_DEFAULTS = {
-    getattr(mt, "label", ""): defaultdict(
-        _DefaultValue(f"ambig_{getattr(mt, 'label', '')}"),
+    getattr(mt, "name", ""): defaultdict(
+        _DefaultValue(f"ambig_{getattr(mt, 'name', '')}"),
     )
     for mt in (ASCII, BYTES, DNA, RNA, PROTEIN, PROTEIN_WITH_STOP, None)
 }
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index 872e8f02c..0949b3205 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -1185,7 +1185,9 @@ class Sequence(AnnotatableMixin):
             feature_data.pop(discard)
         return self.make_feature(feature_data)
 
-    def to_moltype(self, moltype: str | c3_moltype.MolType) -> Sequence:
+    def to_moltype(
+        self, moltype: c3_moltype.MolTypeLiteral | c3_moltype.MolType
+    ) -> Sequence:
         """returns copy of self with moltype seq
 
         Parameters
diff --git a/src/cogent3/evolve/fast_distance.py b/src/cogent3/evolve/fast_distance.py
index 788f24599..f26effa88 100644
--- a/src/cogent3/evolve/fast_distance.py
+++ b/src/cogent3/evolve/fast_distance.py
@@ -17,6 +17,7 @@ from cogent3.util.misc import get_object_provenance
 from cogent3.util.progress_display import display_wrap
 
 if typing.TYPE_CHECKING:  # pragma: no cover
+    from cogent3.core.alignment import Alignment
     from cogent3.core.tree import PhyloNode
 
 PySeq = typing.Sequence
@@ -331,24 +332,24 @@ class _PairwiseDistance:
 
     def __init__(
         self,
-        moltype,
-        invalid=-9,
-        alignment=None,
-        invalid_raises=False,
+        moltype: c3_moltype.MolTypeLiteral,
+        invalid: int = -9,
+        alignment: typing.Optional["Alignment"] = None,
+        invalid_raises: bool = False,
     ) -> None:
         super().__init__()
-        moltype = cogent3.get_moltype(moltype)
-        if moltype.label not in self.valid_moltypes:
+        mtype = cogent3.get_moltype(moltype)
+        if mtype.label not in self.valid_moltypes:
             name = self.__class__.__name__
             msg = (
-                f"Invalid moltype for {name}: '{moltype.label}' not "
+                f"Invalid moltype for {name}: '{mtype.name}' not "
                 f"in {self.valid_moltypes}"
             )
             raise ValueError(msg)
 
-        self.moltype = moltype
-        self.char_to_indices = get_moltype_index_array(moltype, invalid=invalid)
-        self._dim = len(list(moltype))
+        self.moltype = mtype
+        self.char_to_indices = get_moltype_index_array(mtype, invalid=invalid)
+        self._dim = len(list(mtype))
         self._dists = None
         self._dupes = None
         self._duped = None
@@ -552,7 +553,9 @@ class HammingPair(_PairwiseDistance):
 
     valid_moltypes = ("dna", "rna", "protein", "text", "bytes")
 
-    def __init__(self, moltype="text", *args, **kwargs) -> None:
+    def __init__(
+        self, moltype: c3_moltype.MolTypeLiteral = "text", *args, **kwargs
+    ) -> None:
         """states: the valid sequence states"""
         super().__init__(moltype, *args, **kwargs)
         self.func = _hamming
@@ -563,7 +566,9 @@ class ProportionIdenticalPair(_PairwiseDistance):
 
     valid_moltypes = ("dna", "rna", "protein", "text", "bytes")
 
-    def __init__(self, moltype="text", *args, **kwargs) -> None:
+    def __init__(
+        self, moltype: c3_moltype.MolTypeLiteral = "text", *args, **kwargs
+    ) -> None:
         """states: the valid sequence states"""
         super().__init__(moltype, *args, **kwargs)
         self.func = _hamming
@@ -592,7 +597,9 @@ class _NucleicSeqPair(_PairwiseDistance):
 
     valid_moltypes = ("dna", "rna")
 
-    def __init__(self, moltype="dna", *args, **kwargs) -> None:
+    def __init__(
+        self, moltype: c3_moltype.MolTypeLiteral = "dna", *args, **kwargs
+    ) -> None:
         super().__init__(moltype, *args, **kwargs)
         if not _same_moltype(
             cogent3.get_moltype("dna"),
@@ -608,7 +615,9 @@ class _NucleicSeqPair(_PairwiseDistance):
 class JC69Pair(_NucleicSeqPair):
     """JC69 distance calculator for pairwise alignments"""
 
-    def __init__(self, moltype="dna", *args, **kwargs) -> None:
+    def __init__(
+        self, moltype: c3_moltype.MolTypeLiteral = "dna", *args, **kwargs
+    ) -> None:
         """states: the valid sequence states"""
         super().__init__(moltype, *args, **kwargs)
         self.func = _jc69_from_matrix
@@ -617,7 +626,9 @@ class JC69Pair(_NucleicSeqPair):
 class TN93Pair(_NucleicSeqPair):
     """TN93 calculator for pairwise alignments"""
 
-    def __init__(self, moltype="dna", *args, **kwargs) -> None:
+    def __init__(
+        self, moltype: c3_moltype.MolTypeLiteral = "dna", *args, **kwargs
+    ) -> None:
         """states: the valid sequence states"""
         super().__init__(moltype, *args, **kwargs)
         self._freqs = zeros(self._dim, float64)
@@ -653,7 +664,13 @@ class LogDetPair(_PairwiseDistance):
 
     valid_moltypes = ("dna", "rna", "protein")
 
-    def __init__(self, moltype="dna", use_tk_adjustment=True, *args, **kwargs) -> None:
+    def __init__(
+        self,
+        moltype: c3_moltype.MolTypeLiteral = "dna",
+        use_tk_adjustment=True,
+        *args,
+        **kwargs,
+    ) -> None:
         """Arguments:
         - moltype: string or moltype instance (must be dna or rna)
         - use_tk_adjustment: use the correction of Tamura and Kumar 2002
@@ -674,7 +691,9 @@ class ParalinearPair(_PairwiseDistance):
 
     valid_moltypes = ("dna", "rna", "protein")
 
-    def __init__(self, moltype="dna", *args, **kwargs) -> None:
+    def __init__(
+        self, moltype: c3_moltype.MolTypeLiteral = "dna", *args, **kwargs
+    ) -> None:
         super().__init__(moltype, *args, **kwargs)
         self.func = _paralinear
 
@@ -689,7 +708,7 @@ _calculators = {
 }
 
 
-def get_distance_calculator(name, *args, **kwargs):
+def get_distance_calculator(name: str, *args, **kwargs) -> _PairwiseDistance:
     """returns a pairwise distance calculator
 
     name is converted to lower case"""
@@ -741,7 +760,7 @@ class DistanceMatrix(DictArray):
 
     @classmethod
     def from_array_names(
-        cls, matrix: numpy.ndarray, names: PySeqStr, invalid=None
+        cls, matrix: numpy.ndarray, names: PySeqStr, invalid: bool | None = None
     ) -> "DistanceMatrix":
         """construct a distance matrix from numpy array and names"""
         darr = DictArray.from_array_names(matrix, names, names)


Test patch:
diff --git a/tests/test_align/test_compare.py b/tests/test_align/test_compare.py
index 7f3a5275a..c30289ead 100644
--- a/tests/test_align/test_compare.py
+++ b/tests/test_align/test_compare.py
@@ -149,7 +149,7 @@ def test_get_segments():
 
 
 def test_kmer_one(smallseq):
-    seq = make_seq(seq=smallseq, name="seq1")
+    seq = make_seq(seq=smallseq, name="seq1", moltype="dna")
     kmers = {Kmer(e, seq.name, i) for i, e in enumerate(seq.iter_kmers(k=2))}
     assert kmers == {smallseq[i : i + 2] for i in range(len(smallseq) - 1)}
 
diff --git a/tests/test_app/test_sample.py b/tests/test_app/test_sample.py
index 2f2a26642..cc30ce2b7 100644
--- a/tests/test_app/test_sample.py
+++ b/tests/test_app/test_sample.py
@@ -83,7 +83,7 @@ class TranslateTests(TestCase):
         )
         got = select(aln)
         assert not got
-        assert type(got) == composable.NotCompleted
+        assert type(got) is composable.NotCompleted
 
         # using negate
         select = sample.take_named_seqs("c", negate=True)
@@ -677,7 +677,7 @@ def test_codon_positions_4fold_degen():
     got = ffold(aln)
     assert got.to_dict() == expect
     # error if no moltype
-    with pytest.raises(AssertionError):
+    with pytest.raises(ValueError):
         _ = sample.take_codon_positions(moltype=None)
 
 
@@ -693,3 +693,11 @@ def test_fourfold_empty_alignment():
     result = take_fourfold.main(aln)
     assert isinstance(result, NotCompleted)
     assert result.message == "result is empty"
+
+
+@pytest.mark.parametrize(
+    "app_name", ["omit_degenerates", "omit_gap_pos", "take_codon_positions"]
+)
+def test_invalid_moltype_apps(app_name):
+    with pytest.raises(MolTypeError):
+        _ = cogent3.get_app(app_name, moltype="protein")
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index 085f90220..f2101ef26 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -53,7 +53,7 @@ def seq_db(DATA_DIR):
 
 @pytest.fixture
 def seq():
-    return cogent3.make_seq("ATTGTACGCCTTTTTTATTATT", name="test_seq")
+    return cogent3.make_seq("ATTGTACGCCTTTTTTATTATT", name="test_seq", moltype="dna")
 
 
 @pytest.fixture
@@ -64,7 +64,7 @@ def anno_db() -> BasicAnnotationDb:
 
 @pytest.fixture
 def simple_seq_gff_db(DATA_DIR):
-    seq = cogent3.make_seq("ATTGTACGCCTTTTTTATTATT", name="test_seq")
+    seq = cogent3.make_seq("ATTGTACGCCTTTTTTATTATT", name="test_seq", moltype="dna")
     seq.annotation_db = load_annotations(path=DATA_DIR / "simple.gff")
     return seq
 
@@ -636,7 +636,7 @@ def test_get_features_matching_start_stop_seqview(DATA_DIR, seq):
 
 def test_get_slice():
     """get_slice should return the same as slicing the sequence directly"""
-    seq = cogent3.make_seq("ATTGTACGCCCCTGA", name="test_seq")
+    seq = cogent3.make_seq("ATTGTACGCCCCTGA", name="test_seq", moltype="dna")
     feature_data = {
         "biotype": "CDS",
         "name": "fake",
@@ -652,7 +652,7 @@ def test_get_slice():
 
 def test_get_slice_annotation_offset():
     """get_slice should return the same as slicing the sequence directly"""
-    seq = cogent3.make_seq("ATTGTACGCCCCTGA", name="test_seq")
+    seq = cogent3.make_seq("ATTGTACGCCCCTGA", name="test_seq", moltype="dna")
     feature_data = {
         "biotype": "CDS",
         "name": "fake",
@@ -670,7 +670,7 @@ def test_get_slice_annotation_offset():
 def test_get_slice_annotation_offset_not_set():
     # annotation offset is set to zero if a feature spans disjoint segments
     # plus the annotation db is set to None
-    seq = cogent3.make_seq("ATTGTACGCCCCTGA", name="test_seq")
+    seq = cogent3.make_seq("ATTGTACGCCCCTGA", name="test_seq", moltype="dna")
     feature_data = {
         "biotype": "CDS",
         "name": "fake",
diff --git a/tests/test_core/test_location.py b/tests/test_core/test_location.py
index 25d7efa0a..3e4f6a727 100644
--- a/tests/test_core/test_location.py
+++ b/tests/test_core/test_location.py
@@ -1147,7 +1147,7 @@ def test_get_gap_align_coordinates1(seq, expected):
     expected = numpy.array(expected, dtype=int)
     if not len(expected):
         expected = expected.reshape((0, 2))
-    im, _ = cogent3.make_seq(seq).parse_out_gaps()
+    im, _ = cogent3.make_seq(seq, moltype="dna").parse_out_gaps()
     result = im.get_gap_align_coordinates()
     assert (result == expected).all(), f"{expected=}, {result=}"
 
@@ -1165,7 +1165,7 @@ def test_get_gap_align_coordinates_edge_cases(seq, expected):
     if not len(expected):
         expected = expected.reshape((0, 2))
 
-    im, _ = cogent3.make_seq(seq).parse_out_gaps()
+    im, _ = cogent3.make_seq(seq, moltype="text").parse_out_gaps()
     result = im.get_gap_align_coordinates()
     assert (result == expected).all(), f"{expected=}, {result=}"
 
diff --git a/tests/test_core/test_moltype.py b/tests/test_core/test_moltype.py
index 3f460464e..cf4bd0420 100644
--- a/tests/test_core/test_moltype.py
+++ b/tests/test_core/test_moltype.py
@@ -704,3 +704,8 @@ def test_custom_moltype():
         gap=".",
     )
     assert "." in mt.gaps
+
+
+def test_no_moltype():
+    with pytest.warns(UserWarning):
+        c3_moltype.get_moltype(None)
diff --git a/tests/test_core/test_seq_annotation.py b/tests/test_core/test_seq_annotation.py
index bb6095005..97ad85a54 100644
--- a/tests/test_core/test_seq_annotation.py
+++ b/tests/test_core/test_seq_annotation.py
@@ -156,7 +156,7 @@ def test_slice_seq_with_partial_start(ann_seq, annot_type, num):
 
 
 def test_gbdb_get_children_get_parent(DATA_DIR):
-    seq = load_seq(DATA_DIR / "annotated_seq.gb")
+    seq = load_seq(DATA_DIR / "annotated_seq.gb", moltype="dna")
     seq = seq[2900:6000]
     (orig,) = list(seq.get_features(biotype="gene", name="CNA00110"))
     (child,) = list(orig.get_children("CDS"))
diff --git a/tests/test_parse/test_ebi.py b/tests/test_parse/test_ebi.py
index f7acd4a6e..6ccfb3520 100644
--- a/tests/test_parse/test_ebi.py
+++ b/tests/test_parse/test_ebi.py
@@ -1,8 +1,9 @@
-#!/usr/bin/env python
 """Provides tests for EbiParser and related classes and functions."""
 
 from unittest import TestCase
 
+import pytest
+
 from cogent3.parse.ebi import (
     EbiFinder,
     EbiParser,
@@ -328,20 +329,6 @@ class EbiTests(TestCase):
 
         self.assertRaises(RecordError, list, f(fake_records_valid, strict=True))
 
-    def test_EbiParser(self):
-        """EbiParser:"""
-        f = curry(EbiParser, strict=False)
-        fake_records_valid[:-5]
-
-        # test valid
-        assert len(list(f(fake_records_valid))) == 2
-        # test skipping bad record which strict=False
-        # self.assertEqual(len(list(f(fake_records_valid[:-1] +
-        #    ['OX   xx=no equal.', '//']))), 1)
-        # test Raise RecordError from parse_head when strict=True
-        # self.assertRaises(RecordError, list, f(first_valid[:-1] +
-        #    ['OX   xx=no equal.', '//'], strict=True))
-
 
 class RootParsersKnownValues(TestCase):
     """Test most xx_parsers with known value"""
@@ -1010,4 +997,12 @@ DE   dede.
 //""".splitlines()
 
 
-# Run tests if called from the command line
+def test_EbiParser():
+    """EbiParser:"""
+    f = curry(EbiParser, strict=False)
+    fake_records_valid[:-5]
+
+    # test valid
+    # trapping the warning from not specifying a moltype
+    with pytest.warns(UserWarning):
+        assert len(list(f(fake_records_valid))) == 2