# Adding Logging to Your Bioinformatics Python Scripts: Best Practices

By the end of this guide you will be able to replace scattered `print()` calls in your bioinformatics Python scripts with a proper logging setup that writes timestamped messages to both the terminal and a rotating log file, records full tracebacks when a script fails mid-analysis, captures the output of external tools such as BLAST, and produces a permanent record of every run that you can attach to a [lab notebook](/knowledge/molecular-biology/lab-notebook) or a manuscript revision. Everything here uses the standard library `logging` module plus one worked example built on Biopython, the mature open-source Python library for sequence file parsing, alignments, and database access [1].

To follow along you need Python 3.8 or newer installed, a terminal, and a text editor. For the worked example you also need Biopython, which you can install with pip:

```bash
pip install biopython
```

Verify the install:

```bash
python -c "import Bio; print(Bio.__version__)"
```

Any Biopython version from the last several years will run the example unchanged. If the command prints a version number such as `1.83`, you are ready.

## Why Logging Matters in Bioinformatics Workflows

Bioinformatics scripts are unusual among Python programs in three ways that make logging valuable. First, they are long-running. A genome-wide scan, a docking screen, or a [multiple sequence alignment](/blog/guides/multiple-sequence-alignment) over thousands of records can run for hours or days, and you cannot sit in front of the terminal watching it. Second, they fail late and partially. A malformed FASTA record on line 4,000 of a 10,000-record file should not cost you the whole run, but you need a record of which records were skipped and why. Third, they must be reproducible. Reproducible, reusable pipelines are now an explicit goal of community teaching projects such as TeachOpenCADD, which applies software best practices including testing and idiomatic Python style across its notebook collection [2], and libraries such as Biotite are explicitly designed so researchers can write self-contained applications rather than isolated snippets [3].

A log file is the cheapest provenance system you will ever build. When a reviewer asks which version of a reference database produced a result, or when a collaborator reports that the script "did not work last Tuesday", the log answers the question. This pattern is not theoretical. The pyPaSWAS project, a Python wrapper for parallel Smith-Waterman sequence alignment on GPUs and multi-core CPUs, uses Python libraries for automated system configuration, I/O, and logging precisely so that alignments can be inspected and retraced after the fact [4]. Production-scale pipelines behave the same way. The Reference Viral Database (RVDB) project describes its production scripts as a semantic pipeline with a quality-check step, the kind of multi-stage workflow where each stage should emit its own log record [5].

There is also a software engineering argument. An empirical study of 1,166 containerized Python projects on GitHub found that over 99 percent use the built-in Python logging library, and that the debug and info verbosity levels are used almost twice as much as warning and error [6]. The built-in module is the de facto standard, and the natural verbosity distribution leans toward diagnostic detail. Both facts shape the recommendations below.

## What Logging Replaces: The print() Habit

![Side-by-side comparison of print and logging across timestamps, severity levels, destinations, filtering and whether a record survives after the run.](/article-images/illustrations/python-logging-bioinformatics-print-vs-logging.webp)

Most researchers start with `print()`. It works until it does not. Consider the typical failure mode of a sequence screening script:

```python
print("reading file")
print("record", i)
print("low complexity, skipping")
```

Six months later you cannot answer any of the questions that matter. When did this run start? Which file was being read? How many records were skipped, out of how many? Was "low complexity, skipping" a warning or normal behavior? Printed output has no timestamps, no severity, no destination other than the terminal, and no way to separate what the developer wants to see while debugging from what the user needs to see while running.

The `logging` module, documented thoroughly in dedicated Python texts on testing and tooling [7], solves all four problems with one mental model: your script emits messages at severity levels, and configuration code elsewhere decides which messages go where and in what format. This separation is the entire trick. Learning Python for bioinformatics work increasingly includes exactly this kind of software hygiene, whether the material is a dedicated scripting textbook, a university course adapted to remote Jupyter-based teaching [8], or an outreach program teaching Python syntax through simple biological tools [9]. Logging is one of the habits that separates a script that works today from a tool your lab can reuse next year.

## The logging Module in Two Minutes

The module has four objects you need to know.

| Object | Role | Example |
|--|--|--|
| Logger | The object your code calls to emit messages | `logger = logging.getLogger("fasta_qc")` |
| Handler | Decides where a message goes (terminal, file, network) | `logging.StreamHandler()`, `RotatingFileHandler()` |
| Formatter | Decides what each line looks like | `"%(asctime)s %(levelname)s %(message)s"` |
| Level | Decides which messages pass the filter | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |

The five standard levels have integer values 10, 20, 30, 40, and 50 respectively, and each logger and handler carries a level filter: a message below the handler's level is dropped. A practical translation for bioinformatics work looks like this.

| Level | Use it for | Bioinformatics example |
|--|--|--|
| DEBUG | Step-by-step internal detail you enable only when debugging | Raw BLAST tabular output lines, per-record parse timing |
| INFO | Normal progress a user should see | "Parsed 4,812 records from contigs.fa in 3.2 s" |
| WARNING | Something odd but survivable | "Record 317 has 12 percent ambiguous bases" |
| ERROR | A step failed but the script can continue with other inputs | "Failed to parse malformed.fa, skipping file" |
| CRITICAL | The run cannot continue | "Reference database missing, aborting" |

The numbers matter for one practical reason: level filters are numeric comparisons, so a handler set to `INFO` (20) passes INFO, WARNING, ERROR, and CRITICAL, and drops DEBUG.

## Step-by-Step: Adding Logging to a Sequence QC Script

![Six ordered steps for adding logging to a script: basicConfig, timestamps, dual handlers, named loggers, exception logging and external tool capture.](/article-images/illustrations/python-logging-bioinformatics-logging-setup-steps.webp)

This walkthrough builds the logging setup in six increments, each with runnable code and expected output. Save each snippet as `qc_step.py` (or a separate file per step) and run it with `python qc_step.py`.

### Step 1: The Minimal Useful Setup

The fastest way to add logging to an existing script is `basicConfig()`, which configures the root logger in one call.

```python
import logging

logging.basicConfig(level=logging.INFO)
logging.info("Script started")

records = ["ATCG", "ATGN", "GGGC"]
logging.info("Loaded %d records", len(records))
```

Expected output:

```
INFO:root:Script started
INFO:root:Loaded 3 records
```

Two details worth absorbing now. The `%d` placeholder passes the count lazily, meaning Python formats the string only if the message actually gets emitted. Second, the root logger's default format includes the logger name (`root` here). You will usually want a timestamp, which is the next step.

### Step 2: Timestamps and a Readable Format

Pass a format string to `basicConfig()`. The four fields below cover nearly every script you will write.

```python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logging.info("QC run starting")
```

Expected output:

```
2026-11-11 09:14:03 INFO QC run starting
```

The most useful `%(field)s` tokens for bioinformatics logs are:

| Token | Meaning | Why you want it |
|--|--|--|
| `%(asctime)s` | Human-readable timestamp | Correlate runs with wet-lab dates and instrument runs |
| `%(levelname)s` | Severity of the message | Grep for WARNING or ERROR after a batch of runs |
| `%(name)s` | Name of the emitting logger | Track which module or stage produced the line |
| `%(message)s` | The message itself | Keep lines greppable |
| `%(funcName)s` | Function that emitted the message | Optional, useful in DEBUG output |
| `%(lineno)d` | Source line number | Optional, useful in DEBUG output |

One warning about `basicConfig()`: it only works if the root logger has no handlers yet. If a library you import has already configured logging, your call is silently ignored. Since Python 3.8 you can force reconfiguration with `logging.basicConfig(..., force=True)`, which removes existing root handlers first.

### Step 3: Write to the Console and a File at Different Verbosities

The setup you will actually want keeps two channels: a console handler showing INFO and above for the person running the script, and a file handler capturing DEBUG and above for the permanent record. This mirrors the verbosity distribution observed in real Python projects, where diagnostic-level messages dominate the log volume [6].

```python
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("fasta_qc")
logger.setLevel(logging.DEBUG)

console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s %(message)s"))

logfile = RotatingFileHandler(
    "fasta_qc.log",
    maxBytes=1_000_000,   # about 1 MB per file
    backupCount=5,        # keep 5 rotated copies
)
logfile.setLevel(logging.DEBUG)
logfile.setFormatter(logging.Formatter(
    "%(asctime)s %(levelname)s %(name)s %(message)s"
))

logger.addHandler(console)
logger.addHandler(logfile)

logger.info("Console and file handlers attached")
logger.debug("This line appears only in fasta_qc.log")
```

Expected console output:

```
INFO Console and file handlers attached
```

Expected content of `fasta_qc.log`:

```
2026-11-11 09:14:03 INFO fasta_qc Console and file handlers attached
2026-11-11 09:14:03 DEBUG fasta_qc This line appears only in fasta_qc.log
```

`RotatingFileHandler` starts a new file when the current one exceeds `maxBytes` and keeps up to `backupCount` old copies as `fasta_qc.log.1`, `fasta_qc.log.2`, and so on. For a batch of scripts running over weeks, this single choice prevents the classic failure of a multi-gigabyte log file filling a scratch partition mid-run.

### Step 4: One Logger Per Module

In any script longer than about 100 lines, stop calling the module-level `logging.info()` convenience functions and create named loggers instead:

```python
import logging

logger = logging.getLogger(__name__)
```

`__name__` evaluates to the module's import path, for example `parsers` or `fasta_qc.filters`, and that name flows into the `%(name)s` field. In a multi-stage pipeline this is how you tell from the log alone whether the alignment stage or the filtering stage emitted a warning. Setting the level on the top-level logger and leaving child loggers at `NOTSET` (their default, meaning inherit) is usually all the configuration you need.

### Step 5: Log Exceptions With Full Tracebacks

This is the single highest-value change most bioinformatics scripts can make. When a parser hits a malformed record, an unhandled exception kills the run and prints a traceback to the terminal that vanishes the moment the window closes. Catch it and log it instead:

```python
import logging
from Bio import SeqIO

logger = logging.getLogger("fasta_qc")

def first_record_length(path):
    try:
        record = next(SeqIO.parse(path, "fasta"))
        return len(record.seq)
    except Exception:
        logger.exception("Failed to parse %s", path)
        return None
```

`logger.exception()` is `logger.error()` plus the full traceback, attached automatically. In the log file you get:

```
2026-11-11 09:20:11 ERROR fasta_qc Failed to parse broken.fa
Traceback (most recent call last):
  File "qc.py", line 9, in first_record_length
    record = next(SeqIO.parse(path, "fasta"))
StopIteration
```

The traceback tells you the exact line and the exception type, which is what you need to fix the file or the code. Returning `None` (or a sentinel) lets the calling loop skip that input and continue, which is the right behavior for batch processing where one bad file should not destroy a 12-hour run.

### Step 6: Capture External Tool Output in the Log

Bioinformatics scripts routinely shell out to external tools. Biopython's wrappers around programs such as BLAST and EMBOSS are one of its core features [1], and the same pattern applies to any command-line tool. Capture the tool's standard error stream into your log at DEBUG level so it is available without cluttering the console:

```python
import logging
import subprocess

logger = logging.getLogger("fasta_qc.blast")

def run_blastn(query, subject):
    cmd = ["blastn", "-query", query, "-subject", subject, "-outfmt", "6"]
    logger.info("Running: %s", " ".join(cmd))
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        logger.error("blastn failed with return code %d", result.returncode)
        logger.error("stderr: %s", result.stderr)
        return None
    n_lines = len(result.stdout.strip().splitlines())
    logger.info("blastn returned %d tabular hits", n_lines)
    logger.debug("blastn stderr: %s", result.stderr)
    return result.stdout
```

Logging the full command string before execution is one of the most effective reproducibility habits available. It costs one line and later answers the question "exactly which parameters produced these hits?" without relying on memory.

## A Complete Worked Example: Logging a Biopython FASTA Screen

The following script combines every element above into a realistic quality-control screen over FASTA files. It reports record counts, sequence length ranges, ambiguous base content, and per-file checksums, all through a dual-handler logger. This is the level of bookkeeping that large-scale analysis libraries bake in by design. The rstoolbox project, built for large-scale analysis of computational protein design decoys, is organized around profiling and selecting from large populations of models, a workflow that is unmanageable without per-item records of what ran and what was kept [10]. Your QC script should behave the same way at whatever scale you work.

```python
#!/usr/bin/env python3
"""fasta_qc.py: logged quality-control screen for FASTA files."""

import hashlib
import logging
import time
from logging.handlers import RotatingFileHandler
from pathlib import Path

from Bio import SeqIO

# Input files live next to the script in this example.
# In production, take paths from a config file or CLI arguments.
INPUT_DIR = Path("fasta_in")
LOG_FILE = "fasta_qc.log"
AMBIGUITY_THRESHOLD = 0.10  # fraction of non-ACGT characters

def build_logger(name, log_path):
    """Attach a console handler (INFO+) and a rotating file handler (DEBUG+)."""
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)

    if not logger.handlers:  # guard against duplicate handlers on re-import
        console = logging.StreamHandler()
        console.setLevel(logging.INFO)
        console.setFormatter(
            logging.Formatter("%(levelname)s %(message)s")
        )
        logfile = RotatingFileHandler(
            log_path, maxBytes=1_000_000, backupCount=5
        )
        logfile.setLevel(logging.DEBUG)
        logfile.setFormatter(logging.Formatter(
            "%(asctime)s %(levelname)s %(name)s %(message)s"
        ))
        logger.addHandler(console)
        logger.addHandler(logfile)
    return logger

def sha256_of(path, chunk_size=65536):
    digest = hashlib.sha256()
    with open(path, "rb") as handle:
        for chunk in iter(lambda: handle.read(chunk_size), b""):
            digest.update(chunk)
    return digest.hexdigest()

def screen_fasta(path, logger):
    """Return (kept, skipped) record counts for one FASTA file."""
    kept, skipped = 0, 0
    lengths = []
    for record in SeqIO.parse(path, "fasta"):
        seq = record.seq.upper()
        n_ambig = sum(seq.count(base) for base in "NRYSWKMBDHVX")
        frac_ambig = n_ambig / len(seq) if len(seq) else 1.0
        lengths.append(len(seq))
        if frac_ambig > AMBIGUITY_THRESHOLD:
            logger.warning(
                "%s: skipping %s (id %s), ambiguity %.3f above %.2f",
                path.name, record.name, record.id,
                frac_ambig, AMBIGUITY_THRESHOLD,
            )
            skipped += 1
        else:
            kept += 1
    if lengths:
        logger.info(
            "%s: %d records, length %d to %d, kept %d, skipped %d",
            path.name, len(lengths), min(lengths), max(lengths),
            kept, skipped,
        )
    else:
        logger.error("%s: no records parsed, file may be empty or corrupt", path)
    return kept, skipped

def main():
    logger = build_logger("fasta_qc", LOG_FILE)
    start = time.time()
    logger.info("fasta_qc starting, input dir %s", INPUT_DIR)

    total_kept, total_skipped = 0, 0
    fasta_files = sorted(INPUT_DIR.glob("*.fa")) + sorted(INPUT_DIR.glob("*.fasta"))
    logger.debug("Found %d FASTA files", len(fasta_files))

    for path in fasta_files:
        logger.debug("Checksum %s sha256=%s", path, sha256_of(path))
        try:
            kept, skipped = screen_fasta(path, logger)
        except Exception:
            logger.exception("Unrecoverable error while screening %s", path)
            continue
        total_kept += kept
        total_skipped += skipped

    elapsed = time.time() - start
    logger.info(
        "Done: %d files, %d records kept, %d skipped, %.1f s elapsed",
        len(fasta_files), total_kept, total_skipped, elapsed,
    )

if __name__ == "__main__":
    main()
```

Suppose `fasta_in/` contains `contigs.fa` with 4,812 records and `broken.fa` with a truncated final record. Expected console output:

```
INFO fasta_qc starting, input dir fasta_in
WARNING contigs.fa: skipping rec_0317 (id rec_0317), ambiguity 0.144 above 0.10
INFO contigs.fa: 4812 records, length 212 to 18439, kept 4811, skipped 1
ERROR broken.fa: no records parsed, file may be empty or corrupt
INFO Done: 2 files, 4811 records kept, 1 skipped, 1.4 s elapsed
```

The log file additionally contains the DEBUG lines, the SHA-256 checksum of each input file, and any tracebacks from the exception handler. Checksums deserve a word of explanation: hashing the input and writing the digest to the log proves which exact bytes produced the result, so a rerun after the input file changed is immediately detectable. This is the same provenance instinct behind pipelines that log per-stage configuration for batch-effect analyses such as pyComBat, a Python implementation of ComBat and ComBat-Seq correction methods whose settings must be traceable to the data they transformed [11].

## Verifying Your Logging Setup Works

Do not trust a logger you have not tested. Run this four-point check after wiring logging into any script.

1. Run the script once against a small input file with a deliberately corrupted copy included. Confirm the console shows INFO lines and above, and confirm the traceback from the corrupted file appears.
2. Open the log file and confirm DEBUG lines are present. On Linux or macOS:

```bash
tail -n 5 fasta_qc.log
grep -c "ERROR" fasta_qc.log
```

3. Cross-check the record count against the input directly. For a standard FASTA file, each record starts with a `>` line:

```bash
grep -c ">" contigs.fa
```

The count should equal the kept plus skipped total in your log for that file. If it does not, your parser or your counting logic has a gap.

4. Confirm rotation. If you lower `maxBytes` temporarily to a tiny value such as 2,000 bytes and run repeatedly, you should see `fasta_qc.log.1`, `fasta_qc.log.2`, and so on appear, then delete the test rotations and restore the real value.

A logging setup that passes all four checks is production-ready for a research script.

## Python Logging Bioinformatics Tips for Everyday Scripts

These are the habits that separate adequate logging from logging you will thank yourself for later.

**Log the run configuration once, at startup, in one block.** Emit a single INFO message listing every parameter: input paths, thresholds, reference versions, random seeds, Python and library versions. Teaching pipelines aimed at reproducible research make this kind of metadata capture an explicit design goal [2]. A version stamp for Biopython takes one line:

```python
import Bio
logger.info("Biopython version %s", Bio.__version__)
```

**Log counts, not just events.** "Skipping record" repeated 9,000 times is noise. A summary line at the end ("kept 4,811, skipped 1, 3 files failed") is information. Emit per-record warnings only when the condition is genuinely rare and surprising.

**Use lazy formatting in hot loops.** `logger.debug("record %s", record.id)` evaluates cheaply when DEBUG is filtered out, while `logger.debug(f"record {record.id}")` builds the string every time regardless. Over a few million records this difference is measurable.

**Timestamp each stage boundary.** Wrap each pipeline stage with a start and end INFO line, or time it directly:

```python
stage_start = time.perf_counter()
# ... alignment or filtering work ...
logger.info("Alignment stage took %.1f s", time.perf_counter() - stage_start)
```

This turns the log into a free performance profile, which is how you discover that 90 percent of your run time is in parsing rather than computation. Performance-aware bioinformatics libraries think in exactly these terms, vectorizing hot paths and profiling GPU versus CPU execution [12].

**Treat warnings as data.** When a WARNING condition occurs (high ambiguity, an unusually short sequence, an unrecognized header format), consider also appending the affected record identifiers to a side file. Logs answer "what happened?" and side files answer "give me the list", and analyses downstream of QC routinely need the list.

**Keep the console output readable for the user.** Someone running your script in a SLURM job or over SSH sees the console stream. INFO-level progress every few thousand records plus a final summary is friendly. Everything more granular belongs in the file.

**Log in notebooks too, differently.** In exploratory Jupyter work, the interactive display of results is the point, and courses built on notebooks lean into that interactivity [8]. A `print()` during exploration is fine. The rule is to convert to logging when the notebook code graduates into a reusable script or pipeline stage, which is the same graduation step that notebook-based teaching platforms describe when their materials move from exploration toward FAIR, reusable pipelines [2].

**Name loggers after pipeline stages, not files.** `logging.getLogger("qc.ambiguity_filter")` survives file renames and immediately tells a reader which logical stage spoke.

## Common Mistakes and Limitations

Every one of the following appears repeatedly in real research code. Each has a concrete fix.

**Duplicate log lines.** Your script prints every message twice. Cause: your named logger has a handler and also propagates the message to the root logger, which has its own handler. Fix either by setting `logger.propagate = False` or by configuring only the root logger and letting child loggers inherit. Test with a single INFO call at startup and count the lines.

**`basicConfig()` silently doing nothing.** Symptom: your format string never appears. Cause: the root logger already had handlers, usually because an imported library configured logging at import time. Fix: pass `force=True` (Python 3.8 and later) or configure your own named logger with explicit handlers as in the worked example.

**`ModuleNotFoundError: No module named 'Bio'`** when running the Biopython example. Cause: the interpreter running the script is not the environment where Biopython is installed, a frequent issue on systems with multiple Python installs or conda environments. Fix: confirm with `which python` (Linux, macOS) or `where python` (Windows), and install into that interpreter with `python -m pip install biopython`.

**Handlers writing to closed files.** Symptom: `ValueError: I/O operation on closed file` late in a long run. Cause: a handler was closed (often after a `logging.shutdown()` call or an interpreter restart within the same process) but messages still flow. Fix: create handlers inside a setup function, add each handler exactly once, and never re-add handlers on module reimport. The guard `if not logger.handlers:` in the worked example exists for this reason.

**UnicodeEncodeError in log output.** Symptom: `UnicodeEncodeError: 'charmap' codec can't encode character ...` on Windows consoles when sequences or descriptions contain non-ASCII characters. Fix: set the environment variable `PYTHONIOENCODING=utf-8` before running, or configure the `FileHandler` with `encoding="utf-8"` (the default is locale-dependent).

**Logging inside per-record loops at INFO level.** Symptom: a script that took minutes now takes an hour. Every log call with a handler attached performs I/O, and I/O on millions of records dominates compute. Fix: aggregate. Count conditions and emit one summary line per file or per stage, and keep per-record detail at DEBUG, where it is cheap when disabled because the message is filtered before formatting.

**F-strings in log calls.** Symptom: no visible problem, just slower runs and, with some configurations, formatting errors triggered even for filtered levels. Fix: pass arguments lazily (`logger.info("parsed %d records", n)`) rather than interpolating up front.

**A log file that grows without bound.** Symptom: a 40 GB `run.log` on the scratch volume. Fix: `RotatingFileHandler` with a sane `maxBytes` and `backupCount`, as in Step 3. The empirical survey of Python projects referenced earlier found that diagnostic-level verbosity dominates log volume in practice [6], which is precisely the pattern that fills disks when rotation is absent.

**Legacy Python 2 scripts left unlogged or unrunnable.** A meaningful share of older bioinformatics scripts still in circulation were written for Python 2, and large projects have had to migrate production pipelines to Python 3 explicitly, as the RVDB database project describes for its production scripts [5]. If you inherit such a script, add logging during the Python 3 migration rather than after, because you will want the run records while validating that the migrated pipeline reproduces old results.

**Logging secrets or sensitive data.** Scripts that query online services sometimes embed API keys or tokens. Log the fact that a request happened, its parameters, and its status, never the credential. This matters increasingly as bioinformatics work moves toward web-connected pipelines.

**One honest limitation.** Logging records what your code did, not whether your code was correct. A log full of green INFO lines can document a systematic error executed flawlessly. Validation of results against known controls or reference answers remains a separate scientific step, and libraries exist to help with the analysis side of that work [3].

## Frequently Asked Questions

### Should I use print() or logging in my bioinformatics scripts?

Use logging for anything you might need to reconstruct later, and keep `print()` only for the final result a user is meant to read directly, such as a single answer or a table written to standard output. Logging gives you timestamps, severity levels, and dual destinations, none of which `print()` provides.

### What logging level should my script use by default?

Set the logger to DEBUG and let the console handler show INFO and above, so the terminal stays readable while the file captures everything. The survey data on real Python projects shows debug and info dominate actual log volume, so your defaults should expect the same distribution [6].

### How do I send log messages to both the console and a file?

Attach two handlers to one logger: a `StreamHandler` for the console and a `RotatingFileHandler` for the file, each with its own level and formatter. The worked example in Step 3 shows the complete pattern, including the duplicate-handler guard.

### How do I capture warnings and messages from Biopython or other libraries?

Get the library's logger by name (for example `logging.getLogger("Bio")`) and attach a handler to it or set its level, since library messages flow through the same logging hierarchy. Alternatively, configure the root logger so everything, including library messages, reaches your handlers.

### How do I log a crash with the full traceback?

Call `logger.exception("message")` inside an `except` block, which logs at ERROR level and attaches the traceback automatically. You can also pass `exc_info=True` to any logging call to include the current exception.

### Does logging slow my scripts down?

Filtered-out messages cost almost nothing, but every emitted message performs I/O, so per-record logging in hot loops is genuinely expensive. Aggregate per-record conditions into counts and summaries, keep per-record detail at DEBUG, and your scripts will run at essentially full speed.

### Can I use logging inside Jupyter notebooks?

Yes, and it works best for tracking long cells and capturing tracebacks, though for quick exploration `print()` remains the natural tool. The practical rule is that notebook code converts to logging when it graduates into a reusable script, mirroring how notebook-based teaching materials distinguish exploration from production practice [8].

### How do I keep log files from filling up my disk?

Use `RotatingFileHandler` with a `maxBytes` limit around 1 to 10 MB and a `backupCount` of about 5, which caps total disk use at a known ceiling. For time-based rotation across scheduled jobs, `TimedRotatingFileHandler` with a `when` value such as `"midnight"` rolls the file daily instead.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Should I use print() or logging in my bioinformatics scripts?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use logging for anything you might need to reconstruct later, and keep print() only for the final result a user reads directly. Logging provides timestamps, severity levels, and dual destinations that print() cannot."
      }
    },
    {
      "@type": "Question",
      "name": "What logging level should my script use by default?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Set the logger to DEBUG and let the console handler show INFO and above, so the terminal stays readable while the file captures everything."
      }
    },
    {
      "@type": "Question",
      "name": "How do I send log messages to both the console and a file?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Attach two handlers to one logger: a StreamHandler for the console and a RotatingFileHandler for the file, each with its own level and formatter. Guard against duplicate handlers when re-importing."
      }
    },
    {
      "@type": "Question",
      "name": "How do I capture warnings and messages from Biopython or other libraries?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Get the library logger by name, such as logging.getLogger('Bio'), and attach a handler to it or set its level. Alternatively configure the root logger so all library messages reach your handlers."
      }
    },
    {
      "@type": "Question",
      "name": "How do I log a crash with the full traceback?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Call logger.exception() inside an except block. It logs at ERROR level and attaches the traceback automatically. You can also pass exc_info=True to any logging call."
      }
    },
    {
      "@type": "Question",
      "name": "Does logging slow my scripts down?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Filtered-out messages cost almost nothing, but every emitted message performs I/O. Aggregate per-record conditions into summary counts, keep per-record detail at DEBUG, and runs stay at full speed."
      }
    },
    {
      "@type": "Question",
      "name": "Can I use logging inside Jupyter notebooks?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes, it works well for tracking long cells and capturing tracebacks, though print() is natural for quick exploration. Convert to logging when notebook code graduates into a reusable script."
      }
    },
    {
      "@type": "Question",
      "name": "How do I keep log files from filling up my disk?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use RotatingFileHandler with a maxBytes limit around 1 to 10 MB and a backupCount of about 5 to cap disk use. For scheduled jobs, TimedRotatingFileHandler with when set to midnight rolls files daily."
      }
    }
  ]
}
</script>

## Sources

1. [Biopython: freely available Python tools for computational molecular biology and bioinformatics](https://pubmed.ncbi.nlm.nih.gov/19304878/)
2. [TeachOpenCADD 2022: open source and FAIR Python pipelines to assist in structural bioinformatics and cheminformatics research](https://pubmed.ncbi.nlm.nih.gov/35524571/)
3. [Biotite: new tools for a versatile Python bioinformatics library](https://pubmed.ncbi.nlm.nih.gov/37277726/)
4. [pyPaSWAS: Python-based multi-core CPU and GPU sequence alignment](https://pubmed.ncbi.nlm.nih.gov/29293576/)
5. [Refinement of the Reference Viral Database (RVDB) for improving bioinformatics analysis of virus detection by high-throughput sequencing (HTS)](https://pubmed.ncbi.nlm.nih.gov/40548747/)
6. [Identifying Logging Practices in Open Source Python Containerized Application Projects](https://doi.org/10.1145/3474624.3474631)
7. [Logging in Python](https://doi.org/10.1007/978-1-4842-7854-3_7)
8. [Teaching Python programming for bioinformatics with Jupyter notebook in the Post-COVID-19 era](https://pubmed.ncbi.nlm.nih.gov/37199252/)
9. [Bioinformatics calls the school: Use of smartphones to introduce Python for bioinformatics in high schools](https://pubmed.ncbi.nlm.nih.gov/30763318/)
10. [rstoolbox - a Python library for large-scale analysis of computational protein design data and structural bioinformatics](https://pubmed.ncbi.nlm.nih.gov/31092198/)
11. [pyComBat, a Python tool for batch effects correction in high-throughput molecular data using empirical Bayes methods](https://pubmed.ncbi.nlm.nih.gov/38057718/)
12. [ZMPY3D: accelerating protein structure volume analysis through vectorized 3D Zernike moments and Python-based GPU integration](https://pubmed.ncbi.nlm.nih.gov/39100546/)