Genome Toolkit. Part 4.3: Building Validated Biological Models

Welcome back to the Genome Toolkit series!

In Part 4.2, we modernized our existing project without throwing away the scientific work we had already done. Genome Toolkit is now an installable Python package managed with uv, and our two k-mer algorithms already live inside:

src/genome_toolkit/algorithms/

Most importantly, our application still produces exactly the same scientific results as before.

But we ended Part 4.2 with one important problem. Our algorithms still accept an ordinary Python string:

sequence: str

And a Python string can contain almost anything:

AATTTTAAAAC
HELLO
12345
???

Python knows that all four examples are strings. It does not know that only one of them looks like DNA.

In this part, we are going to add that biological knowledge to Genome Toolkit.

We will create a reusable Sequence model, build a DNA model on top of it, validate real DNA symbols, test both valid and invalid sequences, and then connect the new model to our existing application.py with only a very small change.

Our k-mer algorithms are already here and already work. We are not replacing them or bringing them back. We are simply adding a validated biological step before the existing algorithms receive their sequence.

The new flow will be:

ordinary Python string
        ↓
DNA(...)
        ↓
validated DNA
        ↓
dna.sequence
        ↓
our existing k-mer algorithms

Later, we will make the connection between our biological models and algorithms even stronger. For now, we will keep the change deliberately small.

Starting With Our Working Project

We continue from exactly where Part 4.2 finished.

Our project currently looks like this:

genome_toolkit/
├── .git/
├── .gitignore
├── .venv/
├── README.md
├── application.py
├── pyproject.toml
├── uv.lock
└── src/
    └── genome_toolkit/
        ├── __init__.py
        ├── py.typed
        └── algorithms/
            ├── __init__.py
            └── kmer.py

Notice that our algorithms are already part of the package:

src/genome_toolkit/algorithms/

And our current application.py is still the familiar example from Part 4.2:

from genome_toolkit.algorithms import (
    count_kmer,
    find_most_frequent_kmers,
)

seq = "AATTTTAAAAC"
kmer = "AA"
k_len = 3


print(f"Sequence: {seq}")
print(f"k-mer: {kmer}")
print(f"Repeats found: {count_kmer(seq, kmer)}")
print(f"Most frequent k-mer: {find_most_frequent_kmers(seq, k_len)}")

Running it gives us:

Sequence: AATTTTAAAAC
k-mer: AA
Repeats found: 4
Most frequent k-mer: ['TTT', 'AAA']

This is our working checkpoint again.

We want to finish Part 4.3 with the same k-mer calculations and the same result. The difference will be that the sequence reaching those algorithms will first pass through our new DNA validation.

What Are We Going to Add?

We are going to add one new package inside Genome Toolkit:

sequence/
├── __init__.py
├── base.py
└── dna.py

Each file has one clear job:

base.py
    ↓
shared biological sequence structure

dna.py
    ↓
DNA-specific normalization and validation

__init__.py
    ↓
clean public imports

The important point is what we are not changing.

Our existing:

algorithms/
├── __init__.py
└── kmer.py

stays in place.

We are adding a new biological layer alongside our existing algorithms, not rebuilding the project again.

Why Do We Need Biological Models?

Right now, this is perfectly valid Python:

seq = "HELLO"

And because our k-mer algorithms currently accept a string, Python would happily allow us to pass that string into them.

The problem is not Python. "HELLO" really is a valid Python string.

The problem is that our program has no biological rule saying:

This value is supposed to represent DNA.

We could manually write checks everywhere we use a sequence:

if not set(sequence.upper()).issubset(set("ACGTN")):
    raise ValueError("Sequence contains invalid DNA symbols.")

This checks whether every symbol in the sequence belongs to our allowed DNA alphabet:

A, C, G, T, N

So:

sequence = "AATTTTAAAAC"

would pass, while:

sequence = "AATTTTZAAAC"

would fail because Z is not part of our supported DNA alphabet.

But then every time we need to check whether a DNA sequence is valid, we would have to write the same validation code again.

Instead, we want one clear boundary:

untrusted/raw value
        ↓
DNA(...)
        ↓
valid biological object

If a DNA object exists successfully, the rest of our program can know that its sequence has already passed the DNA rules we defined.

That is the problem we are solving in this part.

Adding Pydantic

To build these models, we are going to use Pydantic.

Pydantic is a Python data-validation library. It lets us describe what data our model should contain and add our own biological validation rules without writing all of the surrounding validation machinery ourselves.

For Genome Toolkit, it will help us with things such as:

  • required fields;
  • minimum string lengths;
  • rejecting unexpected fields;
  • immutable model objects;
  • useful validation errors;
  • type information;
  • dictionary and JSON serialization.

We still define the biology ourselves. Pydantic gives us a standard way to apply those rules every time we create a model, instead of writing the same validation code in different parts of the project.

And of course, as always, our good friend Corey Schafer has us covered here too. He has an excellent video explaining what Pydantic is and how it works, and I definitely recommend watching it:

Pydantic – Data Validation for Python

Pydantic is useful far beyond Genome Toolkit. It is a great way to keep Python projects structured, validated, and easier to extend later into things like web applications, APIs, and AI or agent-based tools.

Open a terminal in the Genome Toolkit project directory and add Pydantic:

uv add pydantic

Because we are using uv, this command updates both:

pyproject.toml
uv.lock

If we open pyproject.toml, we should now see our first project dependency added:

dependencies = [
    "pydantic>=2.13.5",
]

The exact version may be newer depending on when you run the command, but the important change is that Pydantic now appears inside our project dependencies. uv.lock is updated automatically with the exact resolved package versions used by our environment.

Our package now has its first external runtime dependency because we finally have code that actually needs one.

Creating the sequence Package

Inside:

src/genome_toolkit/

create a new folder:

sequence/

We are going to build this package one file at a time. The first file we need is:

base.py

So our project now becomes:

genome_toolkit/
├── .git/
├── .gitignore
├── .venv/
├── README.md
├── application.py
├── pyproject.toml
├── uv.lock
└── src/
    └── genome_toolkit/
        ├── __init__.py
        ├── py.typed
        ├── algorithms/
        │   ├── __init__.py
        │   └── kmer.py
        └── sequence/            ← NEW
            └── base.py          ← NEW

Our existing algorithms/ package stays exactly where Part 4.2 left it. We are simply adding a new place for biological sequence models.

Starting Our Base Sequence Model

Open:

src/genome_toolkit/sequence/base.py

We will begin with the smallest useful version of our new class:

"""Base models for biological sequences."""

from pydantic import BaseModel, ConfigDict


class Sequence(BaseModel):
    """Base model shared by biological sequence types."""

    model_config = ConfigDict(
        frozen=True,
        extra="forbid",
        str_strip_whitespace=True,
    )

The first important part is:

class Sequence(BaseModel):

BaseModel is Pydantic’s base class. By inheriting from it, our normal Python class becomes a Pydantic model that can validate the data we give it.

Then we configure how our model should behave:

model_config = ConfigDict(
    frozen=True,
    extra="forbid",
    str_strip_whitespace=True,
)

We do not call model_config ourselves anywhere else in the code.

Because Sequence inherits from Pydantic’s BaseModel, Pydantic automatically looks for:

model_config = ConfigDict(...)

when it creates and validates a Sequence object.

So later, when we write:

sequence = Sequence(
    identifier="  example_sequence  ",
    sequence="AATTTTAAAAC",
)

Pydantic automatically applies the rules from model_config while creating that object.

In our case, those rules tell it to:

frozen=True                 → prevent fields from being changed later
extra="forbid"              → reject fields we did not define
str_strip_whitespace=True   → remove surrounding whitespace from strings

So model_config is the place where we define the general behavior that every Sequence object should follow.

Each setting solves a small problem.

Keeping Sequence Objects Stable

frozen=True

means that once we create a sequence object, we cannot simply replace one of its fields later.

Without it, code could potentially do something like:

sequence.sequence = "AAAA"

halfway through an analysis.

With frozen=True, the model prevents that change. This is especially important in scientific workflows, where the same sequence may pass through many steps and algorithms. Unless we deliberately create a new sequence, we want to be sure that nothing accidentally changes the original data halfway through an analysis.

Rejecting Unexpected Fields

extra="forbid"

means that Pydantic will reject fields our model does not know about.

For example, if we accidentally misspell:

identifier

as:

identifer

Pydantic will report the mistake instead of silently accepting an unexpected field.

Cleaning Surrounding Whitespace

str_strip_whitespace=True

removes whitespace from the beginning and end of our string fields.

For example:

"  sample_1  "

becomes:

sample_1

This gives us cleaner values without having to call .strip() manually every time we create a model.

Adding Our Sequence Data

Now our model needs some actual biological sequence data.

First, update the Pydantic import so we can use Field:

from pydantic import BaseModel, ConfigDict, Field

Then add three fields inside Sequence:

identifier: str = Field(min_length=1)
description: str | None = None
sequence: str = Field(min_length=1)

Our class now knows about:

identifier
description
sequence

The identifier gives the sequence a name we can use in our workflow.

The description is optional:

description: str | None = None

because a manually created sequence may not always need one.

And:

sequence: str

stores the actual biological symbols.

For the identifier and sequence, we also use:

Field(min_length=1)

so an empty identifier or empty sequence is rejected immediately.

A Quick Check in application.py

We already have a working application.py, so let us use it as our checkpoint instead of creating a separate test program.

Add this import:

from genome_toolkit.sequence.base import Sequence

Then replace our original sequence string:

seq = "AATTTTAAAAC"

with:

bio_sequence = Sequence(
    identifier="example_sequence",
    description="Part 4.3 demonstration sequence",
    sequence="AATTTTAAAAC",
)

seq = bio_sequence.sequence

Everything else in our Part 4.2 application can stay exactly the same.

Run:

uv run python application.py

Our k-mer results should still be:

Repeats found: 4
Most frequent k-mer: ['TTT', 'AAA']

So even our basic Sequence model can already sit in front of the existing algorithms without changing their calculations.

Now we can make this model nicer to work with.

Adding Simple Sequence Behavior

We will add three small Python dunder methods so our Sequence model behaves more like the sequence string it contains. Most Python developers will already recognize these, so we only need to look at what each one gives us.

__len__()

Without it, we would write:

len(bio_sequence.sequence)

With __len__(), we can simply write:

len(bio_sequence)

Add:

def __len__(self) -> int:
    """Return the number of symbols in the sequence."""
    return len(self.sequence)

This lets the Sequence object report the length of the biological sequence it contains.

__getitem__()

Without it, indexing and slicing would look like:

bio_sequence.sequence[0]
bio_sequence.sequence[:4]

With __getitem__(), we can use the model directly:

bio_sequence[0]
bio_sequence[:4]

Add:

def __getitem__(self, index: int | slice) -> str:
    """Return one symbol or a sequence slice."""
    return self.sequence[index]

This gives us normal Python indexing and slicing without reaching into .sequence every time.

__iter__()

For iteration, we would normally write:

for symbol in bio_sequence.sequence:

With __iter__(), we can write:

for symbol in bio_sequence:

This is the first point where we need Iterator, so add:

from collections.abc import Iterator

Then add:

def __iter__(self) -> Iterator[str]:
    """Iterate over sequence symbols."""
    return iter(self.sequence)

Now our model can be used directly anywhere we want to move through the sequence one symbol at a time.

Our base model is now doing something useful before we have written a single DNA-specific rule.

It gives us structured sequence data and lets us work naturally with:

len(sequence)
sequence[0]
sequence[:4]
for symbol in sequence

Now we are ready to teach Genome Toolkit what makes a sequence specifically DNA.

Building the DNA Model

Create our second file:

src/genome_toolkit/sequence/dna.py

Our project now has:

sequence/
├── base.py
└── dna.py          ← NEW

The DNA model is much smaller, so we can look at it together:

"""DNA-specific sequence validation."""

from pydantic import field_validator

from .base import Sequence


DNA_ALPHABET = frozenset("ACGTN")


class DNA(Sequence):
    """DNA sequence validated against the A, C, G, T, and N alphabet."""

    @field_validator("sequence")
    @classmethod
    def validate_nucleotides(cls, value: str) -> str:
        """Normalize DNA to uppercase and reject unsupported symbols."""
        normalized = value.upper()
        invalid = set(normalized) - DNA_ALPHABET

        if invalid:
            symbols = ", ".join(sorted(invalid))
            raise ValueError(
                f"Sequence contains invalid DNA symbols: {symbols}. "
                "Allowed symbols are A, C, G, T, and N."
            )

        return normalized

Inheriting Everything From Sequence

The first important line is:

class DNA(Sequence):

This means DNA inherits everything we already built in our base Sequence model. We do not need to redefine the identifier, description, sequence field, model configuration, length, indexing, slicing, or iteration again.

So every DNA object already has:

identifier
description
sequence
frozen model configuration
extra-field checks
whitespace stripping
length
indexing
slicing
iteration

DNA only adds the rule that is specific to DNA: which nucleotide symbols are allowed.

Defining Our DNA Alphabet

We start with:

DNA_ALPHABET = frozenset("ACGTN")

A frozenset is simply a Python set that cannot be changed after it is created. That makes it a good fit for our DNA alphabet because these allowed symbols should stay fixed while Genome Toolkit is running.

For this introductory DNA model, we support:

A
C
G
T
N

N represents an unknown or unresolved nucleotide and appears commonly in real biological sequence data.

Keeping the alphabet in one constant also means we have one clear place to expand the rule later if Genome Toolkit needs additional IUPAC symbols.

Connecting Validation to the sequence Field

This line:

@field_validator("sequence")

connects our validation function directly to the inherited sequence field.

We do not call validate_nucleotides() ourselves. When we create:

dna = DNA(
    identifier="example",
    sequence="acgtn",
)

Pydantic automatically runs the checks from our inherited Sequence model and then runs this DNA-specific validator before the final object is created.

The process is:

DNA(...)
   ↓
Sequence fields and model rules are checked
   ↓
validate_nucleotides() runs for sequence
   ↓
sequence is converted to uppercase
   ↓
DNA symbols are checked
   ↓
invalid symbols? → stop with validation error
   ↓
valid sequence? → return normalized sequence
   ↓
DNA object is created

So if the sequence is invalid, we never get a successfully created DNA object. The bad data is stopped before we can pass it deeper into Genome Toolkit.

Why @classmethod?

Right under the field validator we use:

@classmethod

Normally, a method inside a class works with an object that has already been created. For example, we first create:

dna = DNA(...)

and only then call methods on dna.

Our validator has to work earlier than that. It needs to check the sequence before Pydantic finishes creating the DNA object.

That is why we use:

@classmethod

It lets Pydantic call the validation function through the DNA class itself, without needing an already-created dna object first.

Without it, a normal method would expect an existing object to work with, but at this point the object is still being created. So @classmethod solves a simple timing problem: validate first, create the final DNA object only if the data is valid.

Normalizing the Sequence

The validator receives the supplied sequence as:

value

Then:

normalized = value.upper()

converts lowercase DNA such as:

acgtn

into:

ACGTN

This means the rest of Genome Toolkit can work with one consistent uppercase representation.

Finding Invalid Symbols With set()

Next we have:

invalid = set(normalized) - DNA_ALPHABET

A Python set keeps only unique values.

For example, if:

value = "AATTTTZAAAC"

then:

normalized = value.upper()

is still:

AATTTTZAAAC

and:

set(normalized)

contains only the unique symbols:

A, T, Z, C

The order of values inside a set does not matter.

We then subtract our allowed DNA alphabet:

{A, T, Z, C}
-
{A, C, G, T, N}
=
{Z}

So anything left in:

invalid

is a symbol our DNA model does not support.

For a valid sequence such as:

AATTTTAAAAC

the unique symbols are only:

A, T, C

and after subtracting the allowed alphabet, nothing is left.

Stopping Invalid DNA

If invalid contains anything, we create a readable list of those symbols:

symbols = ", ".join(sorted(invalid))

and raise:

ValueError(...)

For:

ACGTZ

the useful part of the error tells us:

Sequence contains invalid DNA symbols: Z.
Allowed symbols are A, C, G, T, and N.

If no invalid symbols are found, we finish with:

return normalized

That returned value becomes the final value stored inside:

dna.sequence

So:

"aCgTn"
    ↓
DNA(...)
    ↓
validation
    ↓
"ACGTN"
    ↓
fully created DNA object

while:

"ACGTZ"
    ↓
DNA(...)
    ↓
validation
    ↓
error
    ↓
no DNA object is created

At this point, our general Sequence model gives us the common sequence behavior, while DNA adds exactly the biological rule we need for DNA.

Exposing Our Sequence API

We now have the two models, but we do not want users of Genome Toolkit to depend on the internal filenames.

Open:

src/genome_toolkit/sequence/__init__.py

Add:

"""Validated biological sequence models."""

from .base import Sequence
from .dna import DNA

__all__ = ["Sequence", "DNA"]

Now we can use the clean import:

from genome_toolkit.sequence import DNA

instead of:

from genome_toolkit.sequence.dna import DNA

This is the same idea we already used for our algorithms in Part 4.2.

Our package exposes a clean public entry point while the internal files remain organized by responsibility.

Expanding Our Existing application.py

Now that the DNA model exists, we do not need a separate test program or a separate Python session.

We already have a working application.py from Part 4.2, so we can use that same application to demonstrate every new feature we have just added.

Our current application starts with:

from genome_toolkit.algorithms import (
    count_kmer,
    find_most_frequent_kmers,
)

seq = "AATTTTAAAAC"

The first change is one new import:

from genome_toolkit.sequence import DNA

Then instead of creating an arbitrary sequence string directly, we create a validated DNA object:

dna = DNA(
    identifier="example_dna",
    description="Part 4.3 demonstration sequence",
    sequence="aattttaaaac",
)

seq = dna.sequence

Notice that we deliberately supplied the sequence in lowercase:

aattttaaaac

Our DNA validator will normalize it to:

AATTTTAAAAC

The rest of our Part 4.2 application can stay exactly where it was. We simply add a few lines before the k-mer output so we can see what our new biological model can do.

Update application.py to:

from genome_toolkit.algorithms import (
    count_kmer,
    find_most_frequent_kmers,
)
from genome_toolkit.sequence import DNA

dna = DNA(
    identifier="example_dna",
    description="Part 4.3 demonstration sequence",
    sequence="aattttaaaac",
)

seq = dna.sequence
kmer = "AA"
k_len = 3


print(f"Identifier: {dna.identifier}")
print(f"Description: {dna.description}")
print(f"Validated sequence: {dna.sequence}")
print(f"Sequence length: {len(dna)}")
print(f"First nucleotide: {dna[0]}")
print(f"First four nucleotides: {dna[:4]}")
print(f"Nucleotides: {' '.join(dna)}")

print("DNA as JSON:")
print(dna.model_dump_json(indent=2))

print()

print(f"Sequence: {seq}")
print(f"k-mer: {kmer}")
print(f"Repeats found: {count_kmer(seq, kmer)}")
print(f"Most frequent k-mer: {find_most_frequent_kmers(seq, k_len)}")

This is still our Part 4.2 application, but now it also demonstrates the new biological model we added in Part 4.3.

The first group of output shows that our DNA object now gives us:

identifier
description
validated sequence
sequence length
indexing
slicing
iteration
JSON serialization

And the second group is still our original k-mer workflow.

The important connection is this line:

seq = dna.sequence

Our algorithms still receive a normal Python string, so we do not need to change them yet.

What changed is that the string now comes from a successfully validated DNA object:

raw sequence
      ↓
DNA(...)
      ↓
validated dna.sequence
      ↓
existing k-mer algorithms

Demonstrating Invalid DNA

Now let us deliberately break the sequence for a moment.

Change only this line:

sequence="aattttaaaac",

to:

sequence="aattttaaaaz",

Then run:

uv run python application.py

The application should stop when it tries to create the DNA object.

Our validator sees:

Z

and rejects it because our current DNA alphabet only allows:

A, C, G, T, N

The useful part of the validation error will tell us that the sequence contains an invalid DNA symbol:

Sequence contains invalid DNA symbols: Z.
Allowed symbols are A, C, G, T, and N.

Notice what happens here: our k-mer algorithms never run.

That is exactly the protection we wanted. Invalid biological data is stopped before it reaches the scientific calculation.

Now fix the sequence again:

sequence="aattttaaaac",

Our application is fully functional again.

Running the Complete Part 4.3 Application

Run:

uv run python application.py

We should now see output similar to:

Identifier: example_dna
Description: Part 4.3 demonstration sequence
Validated sequence: AATTTTAAAAC
Sequence length: 11
First nucleotide: A
First four nucleotides: AATT
Nucleotides: A A T T T T A A A A C
DNA as JSON:
{
  "identifier": "example_dna",
  "description": "Part 4.3 demonstration sequence",
  "sequence": "AATTTTAAAAC"
}

Sequence: AATTTTAAAAC
k-mer: AA
Repeats found: 4
Most frequent k-mer: ['TTT', 'AAA']

This is a much more useful checkpoint than running a separate temporary test.

Our single application.py now demonstrates both generations of Genome Toolkit together:

Part 4.2
existing k-mer algorithms
        +
Part 4.3
validated biological DNA

The original scientific calculations are still unchanged:

Repeats found: 4
Most frequent k-mer: ['TTT', 'AAA']

But now we know that the sequence reaching those calculations has already been normalized and checked against our DNA rules. We also carry useful information such as its identifier and description together with the sequence.

Genome Toolkit can now:

  • keep an identifier and optional description with the sequence;
  • normalize lowercase DNA to uppercase;
  • reject unsupported DNA symbols;
  • report sequence length;
  • support normal indexing, slicing, and iteration;
  • serialize the model to JSON;
  • pass the validated sequence directly into our existing algorithms.

So our workflow has grown from:

string
   ↓
algorithm

to:

raw string
    ↓
validated DNA object
    ↓
dna.sequence
    ↓
existing algorithm

And we achieved that without rewriting the working k-mer code from Part 4.2.

Our New Project Structure

At the end of Part 4.3, our project looks like this:

genome_toolkit/
├── .git/
├── .gitignore
├── .venv/
├── README.md
├── application.py              # <-- UPDATED
├── pyproject.toml              # <-- UPDATED
├── uv.lock                     # <-- UPDATED
└── src/
    └── genome_toolkit/
        ├── __init__.py
        ├── py.typed
        ├── algorithms/
        │   ├── __init__.py
        │   └── kmer.py
        └── sequence/            # <-- NEW
            ├── __init__.py      # <-- NEW
            ├── base.py          # <-- NEW
            └── dna.py           # <-- NEW

The responsibilities are now very clear:

algorithms/
    scientific calculations

sequence/base.py
    shared biological sequence structure

sequence/dna.py
    DNA normalization and validation

sequence/__init__.py
    public sequence imports

application.py
    demonstrates how the pieces work together

And the most important thing is that this is still the same working Genome Toolkit we started with.

We have simply added one new capability.

Why We Keep File Loading Out of DNA

We are still manually typing:

sequence="AATTTTAAAAC"

That is intentional.

It may be tempting to immediately add something like:

DNA.from_fasta("sample.fasta")

But that would make the DNA model responsible for two different things:

understanding DNA
+
understanding FASTA files

Those are separate problems.

We want our DNA model to answer:

Is this valid DNA?

A loader should answer:

How do I read sequence data from this source?

Keeping those responsibilities separate means the same DNA model can later validate data that came from:

plain-text file
FASTA file
database
API
manual input

without changing the DNA class every time we add a new source.

That is exactly where we are going next.

Summary

In Part 4.3, we added Genome Toolkit’s first validated biological models without changing our existing algorithms.

We added Pydantic, created the new sequence/ package, built a reusable Sequence base model, and created a DNA model that normalizes sequences to uppercase and rejects unsupported DNA symbols.

We then expanded our existing application.py so the same program demonstrates the new DNA features and still runs the original k-mer workflow. We deliberately supplied an invalid DNA symbol, watched validation stop the application before the algorithms could run, fixed the sequence, and finished with one fully functional application that combines Part 4.2 and Part 4.3.

Our project has therefore grown from:

string → algorithm

to:

string → validated DNA → algorithm

without breaking the working code we already had.

New Concepts We Learned

  • Pydantic — A Python validation library that gives us reusable model infrastructure such as field validation, useful errors, serialization, and schema information. We use it so Genome Toolkit can focus on biological rules instead of repeatedly rebuilding generic validation code.
  • BaseModel — The Pydantic class our Sequence model inherits from. It turns our typed model fields into validated data when an object is created.
  • Field(min_length=1) — Adds a rule to a model field. We use it to prevent empty identifiers and empty biological sequences from successfully entering our workflow.
  • ConfigDict — Lets us configure shared model behavior. In Sequence, we use it to strip surrounding whitespace, reject unexpected fields, and prevent model fields from being reassigned after creation.
  • field_validator — Lets us attach our own rule to a specific model field. We use it in DNA to normalize the sequence to uppercase and reject symbols outside our supported DNA alphabet.
  • Validation boundary — The point where raw data becomes trusted biological data. Instead of asking every algorithm to validate DNA independently, we validate once when creating DNA.
  • Base model and specialized modelSequence contains behavior shared by biological sequences, while DNA adds the rules that are specific to DNA. This gives us a natural place to add RNA or protein models later without duplicating all of the common sequence behavior.

What is Next?

Our DNA model now knows whether a sequence is biologically valid, but we are still typing that sequence directly into Python:

sequence="AATTTTAAAAC"

Real biological data usually comes from somewhere else.

In Part 4.4, we will add our first simple loading layer and read sequence data from a plain-text file.

The new workflow will become:

plain-text file
      ↓
loaded sequence data
      ↓
DNA(...)
      ↓
validated DNA
      ↓
our existing Genome Toolkit

We will keep the same approach we used here: add one meaningful piece, test it immediately, and then connect it to the working project.

The full source code for Genome Toolkit is available here:

https://github.com/rebelC0der/Genome_Toolkit

I hope adding our first validated biological sequence models and teaching Genome Toolkit what valid DNA looks like was useful for your bioinformatics and programming journey! If you found this article valuable and want to help us continue building rebelScience, please consider supporting our project. You can explore various ways to contribute here:

https://rebelscience.club/cryptocurrency-donations/

Until next time, rebelCoder, signing out.

Video version can be found here:

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.