# Python Bio Decorators

> Write @decorators (functools.wraps, @lru_cache, factories) to time, validate, and memoize bio functions. Use for pipeline timing/logging, DNA/protein alphabet checks, caching codon/alignment calls, or decorator stacking.

- Skill: `pavel-kravchenko/python-bio-decorators` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/python-bio-decorators`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/python-bio-decorators/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/python-bio-decorators

---


# Decorators for Bioinformatics

`@decorator` above `def f():` is exactly `f = decorator(f)` at definition time.

## When to Use

- Adding timing/logging around a pipeline step (e.g. `@timer` on a parsing or alignment function) without editing its body.
- Validating that a function's input is a legal DNA/RNA/protein alphabet before running expensive logic.
- Caching pure functions with repeated inputs — codon-to-amino-acid lookups, recursive alignment scoring, k-mer counting.
- Building reusable "specialized function" closures, e.g. a motif counter factory (`make_motif_counter("CG")`).
- Explaining or debugging why a decorated function lost its `__name__`/docstring, or why decorator stacking order changed behavior.

## Version Compatibility

Pure standard library — `functools` and closures work unchanged on Python ≥3.8 (examples use f-strings and `functools.wraps`, both stable since 3.6+). No third-party dependency required.

## Prerequisites

- Comfort with Python functions as first-class objects (functions passed as arguments, returned from functions).
- `*args, **kwargs` syntax for variadic wrappers.
- Related: `python-bio-functions` (higher-order functions, closures basics), `python-bio-error-handling` (raising/catching inside wrappers), `python-bio-context-managers` (the sibling resource-management pattern).

## Core Pattern: Basic Decorator

**Goal:** measure and log how long a bioinformatics function takes to run, without modifying its body.
**Approach:** wrap the function in a closure that times the call and forwards `*args, **kwargs`; use `functools.wraps` to preserve identity.

```python
import functools
import time

def timer(func):
    """Decorator: measure and print execution time of a function."""
    @functools.wraps(func)   # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"[timer] {func.__name__}: {elapsed:.4f}s")
        return result
    return wrapper

@timer
def gc_content(seq: str) -> float:
    """Calculate GC content of a DNA sequence."""
    seq = seq.upper()
    return (seq.count('G') + seq.count('C')) / len(seq) * 100

gc_content("ATGCGATCGATCGTAGC")  # prints "[timer] gc_content: 0.0000s", returns 47.05...
```

## Decorator Factory (with arguments)

**Goal:** validate that a sequence only contains legal characters for its type (DNA vs. protein) before the wrapped function runs.
**Approach:** a decorator that itself takes arguments needs three nested levels — factory(args) → decorator(func) → wrapper(*args); the factory closes over `valid_chars`.

```python
def validate_sequence(valid_chars: str, seq_type: str = "DNA"):
    """Decorator factory: validate that the first argument is a valid sequence."""
    valid_set = set(valid_chars.upper())

    def decorator(func):
        @functools.wraps(func)
        def wrapper(seq, *args, **kwargs):
            invalid = set(seq.upper()) - valid_set
            if invalid:
                raise ValueError(
                    f"Invalid {seq_type} characters {invalid} in input to {func.__name__}()"
                )
            return func(seq, *args, **kwargs)
        return wrapper
    return decorator

@validate_sequence('ATGC', seq_type='DNA')
def complement(seq: str) -> str:
    """Return the DNA complement."""
    comp_map = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G'}
    return ''.join(comp_map[b] for b in seq.upper())

complement("ATGCGATC")     # "TACGCTAG"
complement("ATGXYZ")       # raises ValueError: Invalid DNA characters {'X','Y','Z'} ...
```

## Memoization

**Goal:** avoid recomputing expensive pure functions (codon lookup, recursive alignment) for repeated inputs.
**Approach:** prefer the stdlib `functools.lru_cache`; write a manual cache only when you need custom eviction or a `.clear()` hook exposed on the function.

```python
from functools import lru_cache

@lru_cache(maxsize=None)
def translate_codon(codon: str) -> str:
    """Translate a single codon to its amino acid (cached)."""
    table = {
        'TTT':'F','TTC':'F','TTA':'L','TTG':'L','TCT':'S','TCC':'S','TCA':'S','TCG':'S',
        'TAT':'Y','TAC':'Y','TAA':'*','TAG':'*','TGT':'C','TGC':'C','TGA':'*','TGG':'W',
        'CTT':'L','CTC':'L','CTA':'L','CTG':'L','CCT':'P','CCC':'P','CCA':'P','CCG':'P',
        'CAT':'H','CAC':'H','CAA':'Q','CAG':'Q','CGT':'R','CGC':'R','CGA':'R','CGG':'R',
        'ATT':'I','ATC':'I','ATA':'I','ATG':'M','ACT':'T','ACC':'T','ACA':'T','ACG':'T',
        'AAT':'N','AAC':'N','AAA':'K','AAG':'K','AGT':'S','AGC':'S','AGA':'R','AGG':'R',
        'GTT':'V','GTC':'V','GTA':'V','GTG':'V','GCT':'A','GCC':'A','GCA':'A','GCG':'A',
        'GAT':'D','GAC':'D','GAA':'E','GAG':'E','GGT':'G','GGC':'G','GGA':'G','GGG':'G',
    }
    return table.get(codon.upper(), 'X')

def translate_sequence(dna: str) -> str:
    """Translate a DNA sequence codon-by-codon using the cached lookup."""
    protein = []
    for i in range(0, len(dna) - 2, 3):
        aa = translate_codon(dna[i:i + 3])
        if aa == '*':
            break
        protein.append(aa)
    return ''.join(protein)

translate_sequence("ATGGCTGCTTAG")   # "MAA"
translate_codon.cache_info()          # CacheInfo(hits=..., misses=..., maxsize=None, currsize=...)
```

For recursive functions (e.g. a naive Needleman-Wunsch scorer), a manual `dict`-backed memoizer works the same way but exposes `.cache` and `.clear_cache` directly on the wrapper — useful when args aren't hashable via `lru_cache` alone or you need to inspect hit count mid-run.

## Stacking Decorators

Applied bottom-up (closest to the function runs first): `@A @B def f` is `A(B(f))`.

```python
@timer                              # applied second — measures total time, including validation
@validate_sequence('ATGC')          # applied first — runs before the body, and before timing starts
def analyze(seq: str) -> dict:
    """Full analysis of a DNA sequence."""
    seq = seq.upper()
    return {'length': len(seq), 'gc': (seq.count('G') + seq.count('C')) / len(seq) * 100}

analyze("ATGCGATCGATCGATCGATCG")
analyze("ATGXYZ")   # raises before the timer ever prints
```

## Closure Pattern

**Goal:** build a family of specialized functions (e.g. one motif counter per motif) without repeating code.
**Approach:** an inner function defined inside an outer one keeps a live reference to the outer function's local variables even after the outer call returns.

```python
def make_motif_counter(motif: str):
    """Return a function that counts a specific motif in any sequence."""
    motif = motif.upper()
    def counter(sequence: str) -> int:
        return sequence.upper().count(motif)
    return counter

count_cpg = make_motif_counter("CG")   # counter "remembers" motif via closure
count_cpg("GCGCGCATCG")                # 3
```

## Pitfalls

- **Always use `functools.wraps`**: without it, `func.__name__` becomes `'wrapper'`, breaking logging, `help()`, and stack traces.
- **Decorator factories need 3 levels**: `@validate_sequence('ATGC')` requires factory → decorator → wrapper; a 2-level decorator receives the *argument* as the function, causing a confusing `TypeError`.
- **Stacking order matters**: `@A @B def f` = `A(B(f))`; validation should be inner (runs first, fails fast), timing outer (measures total including validation).
- **`lru_cache` requires hashable arguments**: lists, dicts, and numpy arrays cannot be cached; convert to `tuple` or `bytes` before passing.
- **`lru_cache` holds strong references**: cached results are never GC'd until the cache is cleared; set a bounded `maxsize` and call `.cache_clear()` on long-running processes handling many distinct sequences.

## See Also

- `python-bio-functions` — higher-order functions and closures without the decorator syntax sugar.
- `python-bio-error-handling` — designing the exceptions raised inside a validation wrapper.
- `python-bio-context-managers` — the `with`-statement analog for setup/teardown around code.
- `python-bio-classes` — combining decorators (`@property`, `@staticmethod`) with class-based sequence objects.

