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 algorithmsLater, 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.pyNotice 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 importsThe 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 objectIf 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 ← NEWOur 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.sequenceEverything 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 normalizedInheriting 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 objectwhile:
"ACGTZ"
↓
DNA(...)
↓
validation
↓
error
↓
no DNA object is createdAt 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.sequenceNotice 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 algorithmsDemonstrating 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 DNAThe 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 algorithmAnd 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 # <-- NEWThe 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 togetherAnd 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 ourSequencemodel 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. InSequence, 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 inDNAto 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 model —
Sequencecontains behavior shared by biological sequences, whileDNAadds 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 ToolkitWe 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: