Engineering habits for trustworthy scientific software
2026-10-18
Practical notes on Git, UNIX, maintainable Python, Julia, reliable data products, and the engineering ideas that keep bioinformatics work reproducible.
These chapters started as learning notes and primers.
Reliable data products are more than pipelines that succeed, they need clear consumers, contracts, quality gates, and a rebuild path. A practical primer from a data analysis and bioinformatics lens.
Reliable data products are not just pipelines that finish without errors. They are artefacts that stakeholders can trust on Monday morning, tables, dashboards, feature stores, and analysis-ready datasets whose meaning, freshness, and lineage are clear. In bioinformatics and broader data engineering work, that trust is earned through deliberate design, not luck.
This primer shares a practical framing I use when building data products: treat reliability as a product property, not a last-mile ops concern.
A data product has a defined consumer, a contract, and a lifecycle. The consumer might be a wet-lab collaborator reading a differential expression table, a downstream ML job, or a clinical operations dashboard. The contract covers schema, grain, update cadence, and known limitations. The lifecycle covers ownership, versioning, and retirement.
If you cannot name the consumer and the contract in a few sentences, you probably have a pipeline, not yet a product.
Before writing transforms, write down:
These decisions prevent the classic failure mode where two teams build slightly different “truth” tables and argue in meetings instead of analysing data.
Reliability means detecting broken assumptions early. Prefer small, readable checks over elaborate frameworks at the start:
In practice, a handful of assertions in your orchestration layer (or a simple validation script) catches most silent corruption before it reaches a notebook.
Experiments get re-run. Metadata gets corrected. A reliable product can be rebuilt from raw inputs without archaeology. That usually means:
Reproducibility is not only a publication requirement, it is how you sleep when someone asks for last quarter’s cohort definition again.
You do not need a wall of red-green panels on day one. You do need answers to three questions when something fails:
Log structured run metadata. Alert on consumer-facing freshness and failed quality checks first; refine later.
Every dataset has blind spots, batch effects, missing covariates, delayed labels. Document them next to the product, not in a private Slack thread. A short README with known issues often prevents more harm than another chart.
Reliable data products grow from this discipline. The tooling will change; the product mindset will not. Build for the person who has to trust your table under time pressure, that is usually the best design constraint you can pick.
Practical habits for readable, reusable Python in analysis and bioinformatics, project layout, pinned environments, small tests, and clarity over cleverness.
Maintainable Python is less about cleverness and more about kindness to your future self, and to whoever inherits your analysis next quarter. In data analysis and bioinformatics, scripts often start as exploratory notebooks and quietly become production. A few habits keep that transition from becoming painful.
Readable code wins. Use descriptive names
(filtered_counts over df2), keep functions
short enough to skim, and avoid nesting that needs a map to parse. If a
line needs a comment to explain what it does, rewrite the line.
Reserve comments for why, the scientific rationale, the quirk
in an upstream file, the reason a threshold exists.
def filter_low_counts(counts, min_cpm=1.0, min_samples=3):
"""Keep genes with CPM above threshold in enough samples."""
cpm = counts.div(counts.sum(axis=0), axis=1) * 1e6
keep = (cpm > min_cpm).sum(axis=1) >= min_samples
return counts.loc[keep]
The docstring and names already carry most of the story.
Even small analyses benefit from a predictable layout:
data/raw and data/processed (never
overwrite raw).src/ for reusable functions.notebooks/ for exploration only.scripts/ or pipelines/ for runnable entry
points.tests/ for the bits that must not silently drift.Move logic out of notebooks into modules as soon as you find yourself copying cells between projects. Notebooks are wonderful for thinking; modules are better for repeating.
Pin versions for anything that can change numerical results,
numpy, pandas, Bioconductor bridges, plotting
stacks. Record the environment with requirements.txt,
environment.yml, or a lock file. In collaborative
bioinformatics work, “it worked on my machine” is not a methodology
section.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
You do not need heavyweight typing everywhere. A few annotations on public functions make IDEs and reviewers more effective:
from pathlib import Path
import pandas as pd
def load_sample_sheet(path: Path) -> pd.DataFrame:
return pd.read_csv(path, dtype={"sample_id": "string"})
Keep function signatures boring: explicit inputs, explicit outputs, minimal hidden global state.
Hard-coded absolute paths break the moment someone else clones the repo. Prefer project-relative paths, environment variables for secrets and machine-specific roots, and a single config module or YAML file for analysis parameters (thresholds, genome build, output directories).
You do not need 100% coverage. Test joins that drop rows, ID mapping,
date parsing, and any custom statistic you would be embarrassed to get
wrong. A tiny pytest suite on those functions pays for
itself the first time a metadata column changes type.
def test_filter_low_counts_keeps_expressed_genes():
counts = make_toy_counts()
out = filter_low_counts(counts, min_cpm=1.0, min_samples=2)
assert "highly_expressed_gene" in out.index
Use a formatter (ruff format or black), a
linter (ruff), and consistent import ordering. Agree once
as a team and stop debating whitespace. Consistency lowers cognitive
load when reviewing scientific code.
Maintainable Python is a habit, not a rewrite project. Apply these practices incrementally, each cleaned function and pinned environment makes the next analysis safer and faster.
A compact UNIX toolkit for analysts and bioinformaticians, inspecting large files, composing pipes, compression, search, and everyday shell hygiene.
UNIX command-line tools remain the fastest way to inspect files, glue workflows together, and survive large bioinformatics datasets without opening a GUI. You do not need to memorise everything, a compact toolkit covers most day-to-day work for analysts and data engineers.
This primer focuses on commands I reach for constantly when exploring sequencing outputs, logs, and tabular intermediates.
The command line is a text interface to your computer. It lets you interact with your computer using text-based commands. It works on macOS, Windows, and Linux, and gives you speed, efficiency, and access to advanced tools that graphical interfaces cannot provide.
Bioinformaticians don’t have to start with the command line, but in practice it quickly becomes essential because most real biological data analysis is built around tools that assume you can use it. Here’s why it matters.
First, scale. Bioinformatics deals with large datasets like whole genomes, RNA-seq reads, or metagenomic samples. These files are often too big for graphical tools to handle efficiently. The command line lets you process them directly on servers or high-performance computing (HPC) systems without loading everything into memory-heavy interfaces.
Second, reproducibility. Command-line workflows are scriptable, which means you can record every step of an analysis exactly. That’s a big deal in science because it allows others (or your future self) to reproduce results. Instead of clicking through menus, you can run a pipeline with a script and get the same output every time.
Third, automation. Bioinformatics often involves repeating similar steps across hundreds or thousands of samples: quality control, alignment, variant calling, and so on. With command-line tools and scripting (bash, Python), you can automate this instead of doing it manually, saving huge amounts of time and reducing errors.
Fourth, tool availability. Most widely used bioinformatics software (e.g. FastQC, BWA, SAMtools, Bowtie2, GATK) is designed primarily for the command line. Even when a GUI exists, the full functionality is usually in the terminal version.
Fifth, working on remote systems. A lot of real bioinformatics work is done on cloud servers or HPC clusters. You typically access these through SSH, which is entirely command-line based. Without it, you simply can’t run many large-scale analyses.
Finally, flexibility and control. The command line allows you to combine tools in powerful ways using pipes, filters, and scripts. You can build custom pipelines tailored to your research question rather than relying on fixed GUI workflows.
pwd
ls -lah
cd /path/to/project
tree -L 2 # if installed
ls -lah shows hidden files, human-readable sizes, and
permissions. Permissions matter when shared cluster directories refuse
to write.
Never open a multi-gigabyte FASTQ in a graphical editor. Use streaming viewers:
head -n 20 sample.fastq
tail -n 50 run.log
less big_matrix.tsv
wc -l counts.csv
less lets you search with / and quit with
q. wc -l is a quick sanity check after
filters.
find data/raw -name "*.fastq.gz"
grep -n "ERROR" logs/pipeline.log
rg "sample_id" metadata/ # ripgrep, if available
find locates files by name or pattern;
grep/rg locate text inside files. Prefer
ripgrep on large codebases, it is fast and respects ignore rules.
Omics data lives compressed. Essential patterns:
gzip file.txt
gunzip file.txt.gz
zcat sample.fastq.gz | head
tar -xzf archive.tar.gz
tar -czf backup.tar.gz results/
zcat (or gzip -dc) streams compressed text
without writing an uncompressed copy to disk, invaluable on clusters
with tight quotas.
The UNIX philosophy shines when you chain small tools:
zcat reads.fastq.gz | head -n 400 | grep "^@" | wc -l
cut -f1,3 counts.tsv | sort | uniq -c | sort -nr | head
Stdout of one command feeds stdin of the next. Redirect with
> (overwrite) or >> (append); send
errors aside with 2> when needed.
cut -d',' -f1,4 samples.csv
awk -F'\t' 'NR==1 || $3 > 10' genes.tsv
sort -k2,2nr results.tsv | head
cut selects fields; awk filters and
transforms; sort orders. For serious tabular work you will
still use Python or R, but these one-liners are perfect for smoke
tests.
chmod u+x run.sh
df -h
du -sh data/*
top
htop # if installed
kill PID
Know when a job is CPU-bound versus waiting on disk. du
finds which directory ate the quota before someone else’s job fails.
ssh user@cluster
scp local.file user@cluster:/path/
rsync -avP data/ user@cluster:/path/data/
rsync is usually kinder than scp for large
directories, it resumes and shows progress.
"$file", spaces in filenames are
common.set -euo pipefail.UNIX fluency is a force multiplier for data work. Learn the small set above deeply, composition, compression, inspection, and search, and you will spend less time fighting files and more time analysing them. Everything else can be looked up as needed.
A beginner reference for navigating, managing, viewing, and searching files from the terminal, with bioinformatics examples.
File management is one of the first things you’ll do in the command line, whether you’re organizing scripts, moving sequencing data, or setting up analysis folders. These commands help you navigate and manipulate files efficiently.
| Command | Description |
|---|---|
pwd |
Show current directory path |
ls |
List files and directories |
cd |
Change directory |
touch |
Create empty files |
mkdir |
Create directories |
cp |
Copy files or directories |
mv |
Move or rename files or directories |
rm |
Remove files or directories |
cat |
View or concatenate files |
grep |
Search for a text pattern within a file or output |
head and tail |
View the top and bottom of a file |
less |
View a file one page at a time |
man |
Show a command’s manual |
1. The command: the “what”
The command is the name of the program or tool you want to run. It represents the action you want to take.
ls (list files),
rm (remove), or mkdir (make directory).2. Options: the “how”
Options (also called flags) modify how the command behaves. They
allow you to tweak the output or the intensity of the action. They
usually start with a single dash (-) for the short form or
two dashes (--) for full words.
ls -l. Here, -l
tells the computer to show the list in “long” format, giving you extra
details like file size and modification date.Use man [command] (manual) or
[command] --help to find every available option for a given
command.
3. Arguments: the “target”
The argument is the subject of your command. It tells the program what it should act on, such as a specific file, a folder, or a path.
mkdir Projects. Here,
mkdir is the command, and Projects is the
argument: the name of the folder you want to create.Some commands require at least one argument
(e.g. mkdir Projects), while others don’t need any at all
(e.g. cd).
Cancelling a command: press Ctrl + C to
cancel a running command.
pwd: print working
directoryDisplays the full path of your current location.
pwd
# /home/user/projects/genomicsImportant consideration: this is especially useful when writing scripts or running pipelines, so you always know where files are being read from or saved to.
ls: list files
and directoriesLists the files and folders in the current directory.
ls [options]Common useful options:
ls -l: long format, including permissions, size, owner,
and modification datels -a: shows hidden files (files starting with
.)ls -lh: human-readable file sizes (KB, MB, GB)Important consideration: hidden files (like
.bashrc or .config) often store system or
environment settings, so ls -a is useful when
troubleshooting or configuring tools.
cd: change directoryMoves you between folders.
cd [directory_path]Basic navigation:
cd ~: goes to your home directory (the base folder for
your user)cd /: moves to the root directory (the very top of the
entire system)cd ..: moves one level upcd -: takes you back to the directory you were just in
(like a “back” button)Relative vs. absolute paths:
cd folder_name: moves down into a folder inside your
current locationcd ../..: moves up two levels at oncecd ~/Desktop/Project: jumps directly to a specific
folder, regardless of where you are starting fromImportant consideration: most bioinformatics
workflows involve moving through nested folders (raw data → processed
data → results), so mastering cd is essential for not
getting “lost” in directory structures.
touch: create empty
filesCreates a new empty file.
touch [file_name]Best practices for naming files and folders. Follow these conventions to avoid data loss and syntax errors:
File.txt and file.txt
are the same item._), and hyphens (-).my_project instead of
my project)..txt or .sh) so both you
and the system know what the file is.mkdir: create
directoriesCreates new folders.
mkdir [folder_name]
mkdir -p [directory_path] # create nested directoriesExample:
mkdir -p project/raw_data/fastq_files
Important consideration: -p is short
for “parents”. It tells the system to create any missing parent folders
in the path. Without it, the command would fail if project
or raw_data didn’t already exist. This is very useful in
bioinformatics, where projects often need deeply nested directories for
raw data, scripts, results, and metadata.
cp: copy files or
directoriesCopies a file to a new name, or into another folder.
cp [options] source destination
cp file.txt new_file.txt
cp file.txt /home/user/backup/Common useful options:
-r (recursive): copies an entire folder and all its
contents, e.g. cp -r Projects/ Backup-i (interactive): asks before overwriting an existing
file, e.g. cp -i file.txt destination/-v (verbose): shows exactly what is being copied,
e.g. cp -v file.txt backup/Important consideration: always double-check destination paths. Unlike a graphical interface, the command line will not ask for confirmation in most cases.
mv: move or rename
filesUsed for both moving and renaming.
mv oldname.txt newname.txt # rename
mv file.txt /home/user/Documents/ # moveImportant consideration: mv does not
make a copy; it removes the original. If the destination
already has a file with the same name, it may be overwritten without
warning.
rm: remove files
or directoriesDeletes files or folders.
rm [file_name]Common useful options:
-r (recursive): removes a directory and everything in
it, including sub-folders-i (interactive): asks for confirmation before deleting
each fileCritical warning: deleted files are not sent to a
recycle bin. They are usually gone for good. Use rm -r with
extreme caution, especially around important data or project
directories.
cat: display and
concatenateShows the contents of a file, or joins several files together.
cat [file_name]
cat [file_1] [file_2] [file_3]grep: search for
textSearches for a pattern or text in files or command output.
grep [options] "pattern" [file_name]Common useful options:
-c: counts matching lines (not individual
occurrences)-i: case-insensitive search-r (recursive): searches all files in a folder and its
sub-foldersIn bioinformatics, grep is commonly used for counting
sequences and finding motifs:
grep -c ">" sequences.fasta # count the sequences in a FASTA file
grep "ATCTGACC" sample.fastq # find reads containing a motifhead and tail: view the top and bottom of a
filehead and tail are quick sanity checks when
working with big files. By default they show the first and last 10
lines, and they work on multiple files.
head [options] [file_name]
tail [options] [file_name]
head -n 20 sample_file # show the first 20 linesless: page-by-page
viewerLike head and tail, less
previews large files. It opens one page at a time and lets you scroll
forward and backward.
less [options] [file_name]Navigation shortcuts: space for the next page,
b for the previous page, and q to quit.
Directory separator. Linux and macOS use the forward
slash (/) to separate folder names in a path, or a folder
from a file.
/ at the
very start of a path is the top-level directory of the entire
system.project/raw_data/sample.fastq, the slashes tell Linux that
sample.fastq is inside raw_data, which is
inside project.\) instead.Tip: in the terminal, always use the forward slash. Linux treats a backslash as an “escape character” (telling the system to ignore the special meaning of the next character), so a backslash in a path will make your command fail.
Absolute vs. relative paths
/) and works no
matter where you are in the terminal. Example:
/home/user/project/data.txtpwd). If you are already in the
project folder, the relative path to your file is just
data.txt.The Git daily loop for scientific and data work, status, diff, commit, branch, and safe undo, without drowning in advanced history surgery.
Git is the shared memory of a software or analysis project. For data analysts and bioinformaticians, it is how you recover yesterday’s working script, review a colleague’s change to a pipeline, and explain exactly which code produced a figure. You do not need every advanced command, a solid essentials kit covers most real work.
Analyses evolve. Parameters change. Reviewers ask for a tweak six months later. Git gives you a chronological story of those decisions. Pair it with clear commits and you can reconstruct not only what changed, but often why.
Keep large raw data out of Git. Version the code, configs, and small metadata; store bulky omics files in appropriate data stores and record accessions or checksums in the repo.
git status
git diff
git add path/to/file
git commit -m "Describe why this change exists"
git push
status and diff are your habits before
every commit. Read the diff. Accidental notebook outputs and credentials
show up there more often than anyone likes to admit.
git clone git@example.com:org/project.git
git checkout -b feature/qc-filters
# ... edit ...
git add -p
git commit -m "Tighten CPM filter for low-depth libraries"
git checkout main
git pull
git merge feature/qc-filters
Branches let you try ideas without breaking main. Name
them for intent (fix/sample-sheet-join,
docs/readme-setup). Prefer small branches that are easy to
review.
A good commit message completes the sentence “This commit will…”. Focus on motivation:
Atomic commits, one logical change each, make reverts and archaeology easier.
git log --oneline -n 20
git log -p -- path/to/file
git blame path/to/file
git show COMMIT
blame is not about blame, it answers “when did this line
arrive?”. Combined with log -p, it is the fastest way to
understand surprising behaviour in a shared pipeline.
Common, relatively safe moves:
git restore file.py # discard unstaged edits to a file
git restore --staged file.py # unstage
git revert COMMIT # add a new commit that undoes a past one
git reset --soft HEAD~1 # undo last commit, keep changes staged
Avoid rewriting published history on shared branches
(reset --hard, force-push) unless the team explicitly
agrees. Prefer revert for changes already on
main.
Ignore the noise: virtualenvs, __pycache__, large
intermediates, local secrets, and notebook checkpoint folders. Commit an
example env file (.env.example) instead of real
credentials. Never commit API keys or private key files, rotate
immediately if you slip.
.venv/
__pycache__/
data/raw/
*.h5ad
.env
.ipynb_checkpoints/
v1.2.0).Master moving changes deliberately between those layers and Git stops feeling mystical. Essentials first; rebases and advanced history surgery can wait until the daily loop is second nature.
A quick tour of the Julia programming language, its performance story, its scientific-computing ecosystem, and how to get started with Pluto notebooks.
Experts are saying Julia “looks like Python, feels like Lisp, runs like Fortran.” That made me curious, so I decided to explore more. Here is a summary of what I found.
Julia is a general-purpose, interpreted language developed by a group at MIT, with numerical computing at the core of its design. It was first released in 2012 and slowly matured to a point where version 1.0 was launched in 2018 (currently at v1.7). At its core, Julia is a high-level, dynamic programming language that offers performance approaching that of statically-typed languages like C++, and is increasingly being used by a huge number of universities and tech companies.
Some bragging rights highlighted by MIT at the v1.0 launch:
Julia is the only high-level dynamic programming language in the “petaflop club,” having achieved 1.5 petaflop/s using 1.3 million threads, 650,000 cores and 9,300 Knights Landing (KNL) nodes to catalogue 188 million stars, galaxies, and other astronomical objects in 14.6 minutes on the world’s sixth-most powerful supercomputer.
“The release of Julia 1.0 signals that Julia is now ready to change the technical world by combining the high-level productivity and ease of use of Python and R with the lightning-fast speed of C++,” Edelman says.
Rich data science and visualisation libraries: From a data science perspective, anyone who is already pushing the boundaries of Python and is interested in high-performance numerical computing should definitely give Julia a try. Developing high-performance libraries focusing on scientific computing and numerical computation has been prioritised since day one. This means Julia solves the “two language problem,” letting you quickly convert prototypes into high-performance implementations without the extra overhead of shifting between two different languages. It is quite similar to Python syntactically, plus you can use all your existing Python libraries in it. You can also call C and Fortran libraries natively.
Try Julia for data science: How to get started? MIT has made available a free online course called Introduction to Computational Thinking to help you get started with fundamental concepts of programming with Julia. In addition, the official YouTube channel of Julia is a treasure trove of discussions on advanced topics such as dynamic type systems, parametric polymorphism/multiple dispatch, Lisp-like macros, distributed computing, garbage collection, and libraries for floating point calculations.
I would also recommend installing Pluto, an interactive-computing environment (reactive notebook) for Julia with intelligent syntax analysis. It also comes with an important guarantee: “At any instant, the program state is completely described by the code you see.” That makes it more promising than using Jupyter notebooks or MATLAB. Lastly, this cheatsheet will come in handy: github.com/mitmath/julia-mit
Modularity, reproducibility, testing, configuration, and workflows, the software engineering ideas that keep bioinformatics analyses trustworthy and reusable.
Bioinformatics sits at the intersection of biology and software. The biology may be your passion, but the software engineering habits you bring to scripts, pipelines, and shared tools largely decide whether results are trustworthy and reusable. You do not need to become a full-time software engineer, you do need a few durable concepts.
Split work into stages with clear inputs and outputs: ingest, QC, transform, analyse, report. Each stage should be runnable on its own with documented artefacts. When QC is tangled inside differential expression code, every change risks breaking both.
In practice, modularity looks like small functions or workflow steps, stable intermediate files or tables, and interfaces that do not leak temporary assumptions.
A result you cannot regenerate is an anecdote. Aim for:
# Example: record the stack beside results
python -m pip freeze > results/run_2026-09-06/requirements.lock
Containers (Docker/Singularity) are especially helpful on shared HPCs where “module load” combinations drift over time.
Scientific code fails quietly, joins drop samples, gene ID maps collide, strandedness flips. Add automated checks where mistakes are costly:
Even a handful of tests around sample-sheet parsing will save days over a project’s life.
Use Git for code and configuration. Review changes that affect analysis logic the same way you would review a methods paragraph, because they are methods. Pull requests create a paper trail of design decisions that notebooks alone rarely capture.
Thresholds, reference paths, and cohort definitions change. Put them in config files or explicit parameters rather than burying magic numbers in deep functions. Your future self will need to rerun sensitivity analyses; make that a one-line edit, not a scavenger hunt.
# config.yaml
min_cpm: 1.0
min_samples: 3
genome_build: GRCh38
strandedness: reverse
Fail loudly with useful messages. Capture run IDs, input hashes, and key counts at each stage. When a collaborator says “the heatmap looks wrong”, logs that show how many genes survived filtering are worth more than a vague stack trace.
Agree on sample identifiers, ontology for conditions, and units. Treat metadata as part of the product: inconsistent labels cause more incorrect biology papers than exotic statistical choices. Validate early that every assay file has a matching metadata row.
Bioinformatics datasets can be large, but premature optimisation wastes time. Profile when something is slow. Prefer efficient formats (Parquet, indexed BAM/CRAM) and vectorised operations before rewriting algorithms. Parallelise embarrassingly parallel sample-level steps with workflow managers rather than ad-hoc background jobs.
Tools like Snakemake, Nextflow, or CWL encode dependencies between steps so rebuilds are incremental and transparent. They also make cluster submission less fragile than a nest of shell scripts. Learn one well enough to express a linear pipeline cleanly, that skill transfers.
Access controls, de-identification, and data retention are software concerns too. Keep secrets out of repos. Know which datasets are shareable under which licences. Engineering discipline includes protecting participants and collaborators, not only shipping plots.
Software engineering concepts are amplifiers for biological insight. Adopt them gradually, with empathy for scientific iteration, and your pipelines will stay trustworthy as questions get harder.