Welcome back to the Genome Toolkit series!
In Part 4.1, we looked at where Genome Toolkit is going and why it makes sense to turn our small educational project into a proper scientific Python package. Now we are finally going to start doing it.
The important thing is that our scientific calculations already work. We have two k-mer algorithms, we know what input they receive, and we know exactly what output our current application produces. So before we change anything, we are going to use that working application as our reference point.
Starting With the Working Project
We are continuing inside the same Genome Toolkit Git repository:
genome_toolkit/ ├── .git/ ├── .gitignore ├── application.py ├── genome_toolkit.py ├── Pipfile └── Pipfile.lock
The .git/ directory is important because it contains the history of our project. We are improving the same Genome Toolkit we have already been building, so we keep that history and continue working in the same repository.
Before touching the structure, let us run the original application one more time:
python application.py
Or, if you are using the Code Runner extension in Visual Studio Code, you can run it with:
Ctrl + Alt + N
We should get:
Sequence: AATTTTAAAAC k-mer: AA Repeats found: 4 Most frequent k-mer: ['TTT', 'AAA']
This output is our working checkpoint. We already know these calculations are correct for the example we have been using throughout the series, so after we reorganize the project, we will run the application again and compare the result with this exact output.
That gives us a very simple goal for this article:
old project structure
↓
modern Python package
↓
same scientific calculationsWe are changing how Genome Toolkit is organized, not what our k-mer algorithms calculate.
What Are We Going to Improve?
Right now, Genome Toolkit is still a very small Python project. We have our environment managed with Pipenv, our scientific code lives in one genome_toolkit.py file, and we create a genomeToolkit object before calling our algorithms.
That worked perfectly for the first few parts of the series, but now we want to give Genome Toolkit a cleaner structure that will be easier to grow.
By the end of this article, we will have:
uv
↓
a modern Python project
src/genome_toolkit/
↓
our installable Genome Toolkit package
algorithms/
↓
our k-mer functionsWe are not going to learn all of Python packaging at once. We will change one small thing at a time, explain why it helps, and keep checking that our original scientific calculations still work.
The first thing we are going to modernize is how we manage the Python project itself.
Moving From Pipenv to uv
When we started Genome Toolkit, we used Pipenv to create a Python environment and manage our project dependencies. That is why our repository currently contains:
Pipfile Pipfile.lock
For the modern version of Genome Toolkit, we are going to use uv.
uv is a modern Python project and package manager. It can create projects, manage Python environments, install dependencies, generate lock files, build packages, and run Python commands for us.
If uv is new to you, Corey Schafer has an excellent detailed tutorial on it. We are going to use only the parts we need for Genome Toolkit, but if you want a deeper introduction to the tool, I highly recommend watching his video:
Python Tutorial: UV – A Faster, All-in-One Package Manager to Replace Pip and Venv
In practical terms, many of the jobs for which we previously used several Python tools can now be handled through one tool:
project setup
environment
dependencies
lock file
package build
running commands
↓
uvFor Genome Toolkit, this gives us a clean modern starting point without adding unnecessary tools.
Installing uv
Before we start using uv, we first need to install it.
On macOS and Linux, run:
curl -LsSf https://astral.sh/uv/install.sh | sh
On Windows, run:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
That installs uv on our system so we can use commands such as:
uv init uv sync uv run
If you already have uv installed, you can skip this step.
Removing the Old Environment Files
We begin by removing the old Pipenv declarations:
rm Pipfile Pipfile.lock
We are not deleting our Python code. application.py and genome_toolkit.py stay in place while we create the new package around them.
Our existing .gitignore also needs a small update.
Because this project already has its existing:
.git/
Git repository, uv does not generate a new .gitignore for us. So instead of deleting the file, open the existing .gitignore and replace its contents with:
# Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv
This keeps temporary Python files, build files, and our local virtual environment out of the Git repository.
We keep .git/ exactly where it is because it contains the existing history of Genome Toolkit. At this point, the scientific code has not changed at all; we have only removed the old Pipenv configuration and updated the files that control our development environment.
Initializing Genome Toolkit as a Library
Now we can initialize the existing directory as a uv library:
uv init --lib --name genome-toolkit
There are two useful parts in this command. --name genome-toolkit gives the project its package distribution name, while --lib tells uv that Genome Toolkit is a library: reusable Python code that other programs can import and use.
That distinction is useful for our project:
library provides reusable scientific functions and objects application uses those functions and decides what to show the user
Genome Toolkit itself is becoming the reusable library. Our existing application.py stays at the top level as a simple development and demonstration program that uses that library.
After running the command, uv creates the modern project files and a src/ package structure for us. The exact small details generated by uv can change between versions, so the important thing is to understand the structure rather than memorize every generated line.
We should now see the beginning of a package layout similar to:
genome_toolkit/
├── .git/
├── .gitignore
├── application.py
├── genome_toolkit.py
├── pyproject.toml
├── README.md
└── src/
└── genome_toolkit/
├── __init__.py
└── py.typedThis is our first major improvement. We still have our original files at the repository root, but now we also have the place where our real installable genome_toolkit package will live:
src/genome_toolkit/
What Is the src/ Layout?
The new src/ directory may look like an unnecessary extra folder at first. Why not simply put the package directly in the repository root?
The basic idea is separation:
repository files
↓
src/
↓
installable Python packageThe repository contains things such as README.md, configuration files, tests, sample data, and our development application. The actual Python package that another project imports lives under src/.
This gives us a clearer distinction between:
the project repository and the installed genome_toolkit package
Later, when we write:
import genome_toolkit
we want Python to use the installed package under src/, not accidentally find some unrelated file at the repository root.
We will see one very practical example of why that matters later in this article when we remove the old genome_toolkit.py file.
The New pyproject.toml
One of the most important files created by uv is:
pyproject.toml
This is the main configuration file for a modern Python project. Our old setup used Pipfile for environment and dependency information, while pyproject.toml gives us one standard place for the package information that Python tools need.
For Genome Toolkit, we keep the structure generated by uv and set the project information we actually own:
[project] name = "genome-toolkit" version = "0.1.0" description = "A small, typed bioinformatics toolkit for validated sequences and structured analyses." readme = "README.md" requires-python = ">=3.12" dependencies = []
Let us go through this from top to bottom.
name = "genome-toolkit"
This is the package distribution name.
version = "0.1.0"
Genome Toolkit is still at the beginning of its development, so we keep our current 0.1.0 version.
description = "A small, typed bioinformatics toolkit for validated sequences and structured analyses."
This gives package tools a short description of what the project is.
readme = "README.md"
This tells packaging tools which file contains the longer project description.
Then we have:
requires-python = ">=3.12"
This tells users and package tools which Python versions Genome Toolkit supports. Notice that this does not say that every developer must use one exact Python 3.12 installation; it says that the package requires Python 3.12 or newer.
Finally:
dependencies = []
At this point Genome Toolkit has no external runtime dependencies, so the list is empty. That will change soon, but we do not add dependencies before we actually need them.
Removing the Local Python Version Pin
Depending on the current uv template, project initialization may also create:
.python-version
That file can pin a local checkout to one particular development Python version.
For Genome Toolkit, the compatibility rule we care about is already declared in pyproject.toml:
requires-python = ">=3.12"
So if .python-version was generated, we remove it:
rm .python-version
This keeps the package rule simple: Python 3.12 or newer.
How Does Genome Toolkit Become an Installable Package?
When we ran:
uv init --lib --name genome-toolkit
uv also added the small piece of configuration Python needs to turn our source code into an installable package.
You will see it inside pyproject.toml under:
[build-system]
We do not need to change it.
For Genome Toolkit, our setup is very simple:
Python code
↓
uv
↓
installable genome_toolkit packageWe are not compiling C or C++ code, creating special extensions, or doing anything unusual. Genome Toolkit is just a normal Python package, so the setup that uv generated for us already does the job.
This is all we need to understand for now. Later, if our package ever needs a more complicated build process, we can learn about that when we actually need it.
Moving Our Algorithms Into the Package
Now our package structure is ready, but both of our scientific algorithms are still sitting in the old root-level file:
genome_toolkit.py
There is no reason to recreate them one at a time. They already belong together, so we can simply move the existing file into our new package.
Inside:
src/genome_toolkit/
create a new folder called:
algorithms
Then move:
genome_toolkit.py
into that folder and rename it to:
kmer.py
Finally, create an empty:
__init__.py
inside the new algorithms/ folder.
We should now have:
src/
└── genome_toolkit/
└── algorithms/
├── __init__.py
└── kmer.pyThat already makes the purpose of the file much clearer:
genome_toolkit
↓
algorithms
↓
kmer.py
↓
our k-mer algorithmsBoth of our existing algorithms have now moved into the new package together. We have not changed how they work yet; we have simply given them a better home.
Simplifying kmer.py
Our old file wrapped both algorithms inside the genomeToolkit class.
Originally, the two methods begin like this:
def count_kmer(self, sequence, kmer):
and:
def find_most_frequent_kmers(self, sequence, k_len):
For Part 4.2, we are making only one small structural change to them: they become plain functions instead of class methods.
We will also add type hints to the function signatures:
def count_kmer(sequence: str, kmer: str) -> int:
and:
def find_most_frequent_kmers(
sequence: str,
k_len: int,
) -> list[str]:That means:
remove self
+
add type hintsThe actual algorithms stay exactly the same.
Update kmer.py to:
"""K-mer analysis algorithms."""
def count_kmer(sequence: str, kmer: str) -> int:
"""
Counts the number of times a specific k-mer appears in a given sequence,
including overlapping k-mers.
Parameters:
sequence (str): The DNA sequence to search in.
kmer (str): The specific k-mer to search for in the sequence.
Returns:
int: The number of times the k-mer appears in the sequence.
"""
kmer_count = 0
for position in range(len(sequence) - (len(kmer) - 1)):
if sequence[position : position + len(kmer)] == kmer:
kmer_count += 1
return kmer_count
def find_most_frequent_kmers(
sequence: str,
k_len: int,
) -> list[str]:
"""
Finds the most frequent k-mers of a given length in a DNA string.
Parameters:
sequence (str): The DNA string to search.
k_len (int): The length of the k-mers to search for.
Returns:
list: A list of the most frequent k-mers in the DNA string.
"""
kmer_frequencies = {}
for i in range(len(sequence) - k_len + 1):
kmer = sequence[i : i + k_len]
if kmer in kmer_frequencies:
kmer_frequencies[kmer] += 1
else:
kmer_frequencies[kmer] = 1
highest_frequency = max(kmer_frequencies.values())
return [
kmer
for kmer, frequency in kmer_frequencies.items()
if frequency == highest_frequency
]Notice what did not change:
kmer_count = 0
kmer_frequencies = {}
the loops
the comparisons
the counting logic
the return logicWe are not improving or modernizing the algorithms themselves in this part. We are moving the same calculations into a proper package and changing the way we call them.
Exposing Our Algorithm API
We now have:
algorithms/ ├── __init__.py └── kmer.py
We could import directly from the kmer.py module:
from genome_toolkit.algorithms.kmer import count_kmer
That works, but we can give users a cleaner public entry point through:
src/genome_toolkit/algorithms/__init__.py
Add:
"""Bioinformatics algorithms.""" from .kmer import count_kmer, find_most_frequent_kmers __all__ = ["count_kmer", "find_most_frequent_kmers"]
Now both algorithms can be imported from:
from genome_toolkit.algorithms import (
count_kmer,
find_most_frequent_kmers,
)This is the public algorithm interface we want to expose.
The line:
from .kmer import count_kmer, find_most_frequent_kmers
re-exports the two functions from kmer.py through the algorithms package.
Then:
__all__ = ["count_kmer", "find_most_frequent_kmers"]
makes our intention explicit: these are the names this package is deliberately exposing as its public API.
For now, we only expose the two algorithms we actually have.
That is enough to reconnect our application immediately and check whether the refactor still produces the exact same result.
Updating application.py
Our algorithms are now inside the new package, so the next step is to update application.py to use them.
We only need to change the import, remove the old gt object, and update the two places where we call our algorithms:
from genome_toolkit.algorithms import ( # CHANGE TO
count_kmer,
find_most_frequent_kmers,
)
# gt = genomeToolkit() # REMOVE
seq = "AATTTTAAAAC"
kmer = "AA"
k_len = 3
print(f"Sequence: {seq}")
print(f"k-mer: {kmer}")
print(f"Repeats found: {count_kmer(seq, kmer)}") # UPDATED
print(
"Most frequent k-mer:",
find_most_frequent_kmers(seq, k_len), # UPDATED
)That is it.
We no longer import the old genomeToolkit class or create a gt object. Instead, we import our two functions directly from the new algorithms package and call them directly.
Everything else stays exactly the same.
Now we are ready to synchronize the new package and test it.
Synchronizing and Testing the New Project
Now we want to test our changes immediately.
Because Genome Toolkit is now an installable package under src/, we first synchronize the project environment, run this command from the project root:
uv sync
The first time we run uv sync, uv creates our local virtual environment:
.venv/
It also creates:
uv.lock
uv.lock records the exact package versions used by the project so the same environment can be recreated consistently later.
uv sync then installs our current Genome Toolkit package into the new .venv according to pyproject.toml.
The first time this environment is created, VS Code or VSCodium may show a message like:
We noticed a new environment has been created. Do you want to select it for the workspace folder?
Just select Yes.
If the message does not appear, or the editor is still using another Python installation, check the Python interpreter shown in the bottom-right corner of VS Code/VSCodium and select our new project environment. It should point to the .venv created inside the Genome Toolkit project, usually shown as something similar to:
('.venv': venv)After this step, both the editor and our project environment know where:
genome_toolkit
actually lives, so our new import can work normally.
If you use uv run, uv can also synchronize the environment automatically before running the command. We are running uv sync explicitly here because it gives us a clear checkpoint and also prepares the .venv environment for editors and Code Runner.
Now for the important part.
Run:
uv run python application.py
We should see:
Sequence: AATTTTAAAAC k-mer: AA Repeats found: 4 Most frequent k-mer: ['TTT', 'AAA']
The scientific output is exactly the same as before.
That is the result we wanted. We changed the environment manager, package structure, imports, and the way our algorithms are exposed, and we also replaced a stateless class with plain functions.
But count_kmer() still returns:
4
and find_most_frequent_kmers() still returns:
['TTT', 'AAA']
Perfect. This is the checkpoint we wanted: we moved the algorithms, simplified how we call them, reconnected application.py, and the scientific result is still exactly the same.
Now that the important path works again, we can take care of two small package details before we finish.
Keeping One Package Version
Now that our application works again, let us clean up one small package detail. The uv template also created:
src/genome_toolkit/__init__.py
This is the top-level package initializer.
We already declared our package version in pyproject.toml:
version = "0.1.0"
We could write the same version again inside Python:
__version__ = "0.1.0"
but then we would have two places to keep synchronized. Instead, we keep one source of truth, so replace the generated src/genome_toolkit/__init__.py with:
"""Genome Toolkit package."""
from importlib.metadata import version
__version__ = version("genome-toolkit")
__all__ = ["__version__"]The new import:
from importlib.metadata import version
comes from Python’s standard library. It lets our package read the version stored in the installed package metadata.
Then:
__version__ = version("genome-toolkit")means Python can still tell us the Genome Toolkit version, but the actual version number remains declared in one place:
pyproject.toml
↓
installed package metadata
↓
genome_toolkit.__version__That is easier to maintain than copying "0.1.0" into multiple files.
This may look like a small packaging detail now, but the version number will become much more important once we start using Genome Toolkit for real biological experiments.
Imagine that later we analyze a real genome and save the result. If Genome Toolkit changes over time, we will want to know exactly which version of the toolkit produced that result:
Genome Toolkit 0.1.0
↓
biological data
↓
scientific analysis
↓
saved resultThat version becomes part of the scientific context of the experiment. If we return to the same data months later, or somebody else wants to repeat our analysis, knowing which Genome Toolkit version was used helps us reproduce the same computational setup.
This connects directly to two ideas we introduced in Part 4.1: reproducibility and provenance. We will come back to both in much more detail in the upcoming articles, when Genome Toolkit starts returning structured scientific results and we begin recording where those results came from.
For now, the important thing is simple: Genome Toolkit should have one reliable version number, and every part of the package should read that same version.
What Is py.typed? In Simple Terms
There is one more small file that uv created for us:
src/genome_toolkit/py.typed
The file is empty, and we leave it empty.
So what does it actually do?
In simple terms, py.typed is just a small marker file. It tells Python development tools:
Genome Toolkit includes type hints in its code, so you can use them.
For example, we now have functions like:
def count_kmer(sequence: str, kmer: str) -> int:
The type hints tell us that:
sequence → string kmer → string result → integer
And:
def find_most_frequent_kmers(
sequence: str,
k_len: int,
) -> list[str]:tells us that the function expects a string and an integer, and returns a list of strings.
The py.typed file simply tells compatible editors and type-checking tools that these type hints are intentionally part of Genome Toolkit.
Without py.typed, another project may import Genome Toolkit and still run perfectly, but some type-checking tools may not automatically treat our package’s type hints as part of its public interface.
With py.typed, we are explicitly telling those tools:
Genome Toolkit includes type information. Please use it.
So if another developer passes the wrong kind of value to one of our functions, their editor or type checker has a better chance of warning them before the code even runs.
We do not need to write anything inside py.typed. We just keep the empty file in the package.
A Quick README.md Update
Before we finish the main part of Part 4.2, let us make one small but useful update to:
README.md
uv created this file when we initialized the project, and now we can add a few simple instructions so anyone who finds Genome Toolkit on GitHub knows how to run it after cloning the repository.
Add:
```markdown ## Running Genome Toolkit After cloning the repository, synchronize the project environment: ```bash uv sync ``` Then run Genome Toolkit: ```bash uv run application.py ```
That is enough for now.
Anyone cloning the project can immediately see the two steps they need:
clone repository
↓
uv sync
↓
uv run application.pyAs Genome Toolkit grows, we can keep expanding the README with installation instructions, examples, supported biological data, and other useful project information. For now, we only add what a new user actually needs to run the project.
Before We Finish Part 4.2
And that is it for the main part of Part 4.2. Genome Toolkit is now running as a modern Python package, our two original k-mer algorithms are still producing the same results, and we have a much better foundation for everything we are going to build next.
Before we close this part, however, I want to introduce two optional Pro Tips that will help us a lot as Genome Toolkit grows. They are not required for the project, so you can skip them completely if you want, but they are also useful habits that can serve you well in your own Python projects.
The first is for those of us who use the Code Runner extension. Because our project now uses uv and its own .venv, Code Runner may need a small configuration change so it runs our code inside the correct environment.
The second is about Ruff, a modern Python linter and formatter. It can automatically keep our code clean and consistently formatted as the project grows.
If you want to keep the setup minimal, feel free to skip both. If you want a smoother development workflow going forward, I recommend following them.
Pro Tip #1: Code Runner With uv
If you have been following rebelScience for a while, you may already use the Code Runner extension in VS Code or VSCodium to run Python files quickly without typing a terminal command every time.
The default shortcuts are:
Linux / Windows: Ctrl + Alt + N macOS: Cmd + Option + N
After moving Genome Toolkit to uv, there is one small thing we need to check.
uv created our local Python environment inside:
.venv/
But Code Runner may still try to use the global Python installation on your computer. If that happens, it may not be able to find our new installed genome_toolkit package.
Fortunately, this is a simple one-time fix. We just need to tell Code Runner to use the Python interpreter already selected by VS Code or VSCodium.
- Open the Command Palette:
Ctrl + Shift + P
or on macOS:
Cmd + Shift + P
- Search for Open User Settings and select the option with (JSON) in its name.
- Find the existing:
"code-runner.executorMap"
section.
- Update only the Python entry to:
"code-runner.executorMap": {
"python": "$pythonPath -u $fullFileName"
}Leave your other language entries unchanged and save the file.
Code Runner will now use the Python interpreter selected by the editor, which for our project should be the one inside .venv/.
That means our quick:
Ctrl + Alt + N
shortcut can keep working exactly as before, but now it runs Genome Toolkit inside our new uv environment.
Pro Tip #2: Automatic Formatting With Ruff
Here is another small setup improvement that can save us a lot of repetitive work as Genome Toolkit grows.
When we work on the same project across many files and many sessions, formatting can slowly become inconsistent. Spacing changes, imports move around, and a tiny code change can create a much larger Git diff than it really needs to.
For Python, we are going to use Ruff.
Ruff is a modern Python linter and formatter built by Astral, the same team behind uv. A linter helps spot common code problems, while a formatter automatically keeps our Python code laid out consistently.
And as always, Corey Schafer has an excellent detailed walkthrough if you want to understand Ruff beyond the small setup we need here:
Python Tutorial: Ruff – A Fast Linter & Formatter to Replace Multiple Tools and Improve Code Quality
If you use VS Code or VSCodium, the easiest setup is through the official Ruff extension published by Astral Software.
Open the Extensions panel:
Ctrl + Shift + X
or on macOS:
Cmd + Shift + X
Search for Ruff and install or enable the official extension.
Then open your User Settings JSON and add or verify:
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"ruff.nativeServer": "on"
}The most important setting for our everyday workflow is:
"editor.formatOnSave": true
Now every time we save a Python file, Ruff can automatically keep the formatting clean and consistent for us.
Ruff is a development tool, not something Genome Toolkit needs in order to run. We therefore do not add it to the package’s normal runtime dependencies just to format our source code.
Our New Project Structure
After synchronizing the project, the repository should now be approximately:
genome_toolkit/
├── .git/
├── .gitignore
├── .venv/
├── README.md
├── application.py
├── pyproject.toml
├── uv.lock
└── src/
└── genome_toolkit/
├── __init__.py
├── py.typed
└── algorithms/
├── __init__.py
└── kmer.pyThis is still a very small project, which is good.
We now have a proper place for the scientific package:
src/genome_toolkit/
and a dedicated place for our current algorithms:
src/genome_toolkit/algorithms/
As we continue through Parts 4.x, we can add the next pieces only when we actually need them.
What Changed and What Stayed the Same?
Quite a lot changed structurally in this article.
We moved from:
Pipenv
↓
uvWe moved from:
loose genome_toolkit.py
↓
installable src/genome_toolkit packageAnd we moved from:
stateless genomeToolkit object
↓
plain algorithm functionsOur application also imports the scientific functions from the package rather than constructing an object first.
But the important scientific parts did not change:
same input strings same k-mer calculations same int/list return values same scientific results
That distinction is exactly what we wanted to see when we introduced refactoring in Part 4.1. We can improve the way our software is organized while keeping the scientific behavior we already understand.
Summary
In Part 4.2, we took our original working Genome Toolkit and turned it into a modern installable Python package. We moved from Pipenv to uv, created the src/ package structure, moved our existing k-mer algorithms into their new home, simplified the old class methods into plain typed functions, and updated application.py to use them.
Most importantly, the scientific calculations did not change. We reorganized the project, synchronized the new environment, and then confirmed that Genome Toolkit still produces exactly the same k-mer results as before. We now have a much cleaner foundation that will be easier to expand, test, share, and eventually use in real biological experiments.
New Concepts We Learned
uv— Our new Python project and package manager. It creates and manages the virtual environment, installs dependencies, generates the lock file, runs our project, and helps build the package. It replaces several separate project-management tasks with one modern tool.pyproject.toml— The main project configuration file. It tells Python tools what Genome Toolkit is, which Python version and dependencies it needs, its package version and description, and how the project should be built. It solves the problem of keeping the important project and package configuration in one standard place.README.md— Human-facing project documentation. It explains what the project does and, in our case, now includes the basic commands needed to run Genome Toolkit after cloning the repository. It helps other people, and future us, understand and use the project without first reading through the source code.uv.lock— Records the exact dependency versions selected byuv. For example, ifpyproject.tomllater allows a range of Pydantic versions,uv.lockrecords the exact version actually chosen for our project. This helps different machines recreate the same environment instead of silently installing slightly different versions..venv/— Genome Toolkit’s private Python environment. It contains the Python environment and installed packages used by this project, keeping them isolated from other Python projects on the same computer. This prevents one project’s dependencies from easily interfering with another.src/— Contains the actual installable source code, such assrc/genome_toolkit/. It separates the Python package itself from project files such asREADME.md,pyproject.toml, andapplication.py, and helps us work with Genome Toolkit the same way another installed project would.__init__.py— Used inside package directories such asgenome_toolkit/andalgorithms/. In our project, it helps define the package structure and can expose a cleaner public API. For example, instead of importingcount_kmerfromgenome_toolkit.algorithms.kmer, we can expose it throughgenome_toolkit.algorithmsand use the shorter import.__all__— Makes the names we intentionally expose from a module or package explicit. Inalgorithms/__init__.py, it tells readers and development tools thatcount_kmerandfind_most_frequent_kmersare part of the public algorithm API we want people to use.py.typed— Usually an empty marker file saying that the installed package officially provides Python type annotations. It helps external tools such as Pyright, mypy, and IDEs understand that they can use Genome Toolkit’s type hints when checking code that imports our package.- Type hints — Extra information in function signatures that describes the kinds of values a function expects and returns. For example,
sequence: strtells us thatcount_kmer()expects a string, while-> inttells us that it returns an integer. They make our code easier to understand and allow editors and type-checking tools to catch some mistakes earlier. - Plain functions instead of class methods — Our original k-mer algorithms lived inside the
genomeToolkitclass even though they did not need to store any object state. Moving them to plain functions makes them simpler to call and keeps the scientific code focused on the calculation itself, while preserving the exact same algorithm behavior. - Package versioning — Genome Toolkit now keeps one authoritative version in
pyproject.tomland reads that version from the installed package metadata. Later, when we run experiments on real biological data, recording which Genome Toolkit version produced a result will become important for reproducibility and provenance.
The most important thing we learned is that modernizing the structure of a scientific project does not mean rewriting the science. Our algorithms still perform the same calculations, but they now live inside a project that is much easier to install, understand, maintain, and grow.
What is Next?
Genome Toolkit is now a real modern Python package.
But our algorithms still accept this:
sequence: str
And a Python string can contain almost anything:
AATTTTAAAAC HELLO 12345 ???
Python does not know which one is a biological sequence.
So in Part 4.3, we are going to solve the next problem:
If an algorithm is supposed to work on biological sequences, should any random string be allowed to reach it?
We will introduce our first validated biological sequence models and start teaching Genome Toolkit what DNA actually looks like.
The full source code for this part of Genome Toolkit is available here:
https://github.com/rebelC0der/Genome_Toolkit/commits/main
I hope this next step in building Genome Toolkit 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.
Until next time, rebelCoder, signing out.
Video version of this article is available here: https://youtu.be/fPLjrExTQTM