Slice, extract, and concatenate biological sequences and annotated records using Biopython. Use when extracting subsequences by position, splicing exons into a transcript, joining sequences, or carrying a sub-region of an annotated record (with quality scores and features) into a new record.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add FridrichMethod/awesome-skills --skill sequence-slicing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Sequence Slicing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fridrichmethod-sequence-slicing)More formats (shields.io, HTML) on the badges page.
---
name: bio-sequence-slicing
description: Slice, extract, and concatenate biological sequences and annotated records using Biopython. Use when extracting subsequences by position, splicing exons into a transcript, joining sequences, or carrying a sub-region of an annotated record (with quality scores and features) into a new record.
tool_type: python
primary_tool: Bio.Seq
---
## Version Compatibility
Reference examples tested with: BioPython 1.83+
Before using code patterns, verify installed versions match. If versions differ:
- Python: `pip show <package>` then `help(module.function)` to check signatures
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
# Sequence Slicing
Extract sub-regions, splice non-contiguous regions, and concatenate sequences and annotated records.
**"Extract a subsequence"** -> Slice a Seq with 0-based half-open coordinates.
- Python: `seq[start:end]` (Bio.Seq)
**"Pull out a sub-region but keep its quality scores and features"** -> Slice the SeqRecord, not the bare Seq.
- Python: `record[start:end]` (Bio.SeqRecord)
**"Splice exons into a transcript"** -> Extract each region and concatenate.
- Python: `sum((seq[s:e] for s, e in coords), Seq(''))` or the `+` operator
## The Governing Principle
Slicing a bare `Seq` is pure string math: `seq[start:end]` returns a new `Seq`, half-open, with no metadata to lose. Slicing a `SeqRecord` carries metadata, and the rule for WHAT survives is the single most error-prone part of this skill:
`record[start:end]` (verified against `Bio/SeqRecord.__getitem__`):
- PRESERVES `id`, `name`, `description`, and `molecule_type`.
- AUTO-SLICES `letter_annotations` (per-letter data such as PHRED `phred_quality`) to match the new coordinates -- this is why a FASTQ slice keeps the right per-base qualities for free.
- KEEPS only features FULLY CONTAINED in `[start:end]`; their locations are recalculated relative to the new start.
- SILENTLY DROPS the `annotations` dict (organism, taxonomy, references, comments), the `dbxrefs` list, and any feature that STRADDLES the slice boundary (dropped whole, never truncated). A non-trivial stride (`record[::2]`) drops features entirely.
Nothing warns when annotations vanish. The GenBank `source` feature spans the whole record, so it straddles almost any slice and disappears along with organism/taxonomy. To carry that metadata across, copy it explicitly:
```python
sub = record[start:end]
sub.annotations = record.annotations.copy()
```
and re-add any boundary-straddling feature manually (with a clamped, recalculated location) if a truncated copy is needed.
## Required Imports
```python
from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord
from Bio import SeqIO
```
## Coordinate Systems: the 0-based vs 1-based trap
Python and Biopython slicing is 0-based and half-open: `seq[start:end]` includes `start`, excludes `end`, and returns `end - start` letters. File formats disagree, and mixing them is a SILENT off-by-one (no error, just the wrong bases):
| Source | Convention | Position 1234..5678 means |
|--------|------------|---------------------------|
| Python / Bio.Seq slice | 0-based, half-open | `seq[1234:5678]` |
| GenBank / EMBL / GFF / VCF feature line | 1-based, INCLUSIVE | `seq[1233:5678]` (subtract 1 from start only) |
| BED file | 0-based, half-open | `seq[1234:5678]` (already matches Python) |
The asymmetry is the catch: convert a 1-based inclusive interval by subtracting 1 from the START only; the end already lands correctly because Python's exclusive end cancels the inclusive end. Reading a coordinate straight off a GFF and slicing `seq[start:end]` without the `-1` silently shifts everything one base left.
`Bio.SeqFeature` locations sidestep this entirely: they store a 0-based start and a Python-style end, so `int(feature.location.start):int(feature.location.end)` slices the parent directly, and `feature.extract(record.seq)` does the same automatically (handling strand and compound/joined locations).
```python
def extract_1based(seq, start, end):
'''Extract a 1-based inclusive interval (GenBank/GFF style).'''
return seq[start - 1:end]
```
## Slicing a Bare Seq
Slicing returns a `Seq` (not a string); negative indices and strides behave exactly like `str` (Seq has behaved like `str` since BioPython 1.78).
```python
seq = Seq('ATGCGATCGATCG')
seq[0] # 'A' single base, 0-indexed -> returns a str
seq[-1] # 'G' last base
seq[0:3] # Seq('ATG') first 3 bases
seq[-5:] # Seq('GATCG') last 5
seq[::2] # Seq('AGGTGTG') every 2nd base (stride)
seq[::-1] # Seq('GCTAGCTAGCGTA') reversed (not the reverse complement)
```
`str(record.seq)` returns the raw string, but raises `UndefinedSequenceError` when the record's sequence content is undefined (e.g. `Seq(None, length=n)` from a header-only FASTA or a pysam-backed record). Guard with `len()` (always defined) before forcing the content to a string.
## Code Patterns
### Splice Non-Contiguous Regions (Exons -> Transcript)
**Goal:** Join several separated regions of a genomic sequence into one continuous sequence.
**Approach:** Extract each region with half-open coordinates and concatenate. `sum()` needs an explicit `Seq('')` start value because the default `0` cannot be added to a `Seq`.
```python
def extract_regions(seq, regions):
'''Concatenate multiple [start, end) regions in order.'''
return sum((seq[start:end] for start, end in regions), Seq(''))
exon_coords = [(0, 50), (100, 150), (200, 250)]
mrna = extract_regions(genomic_seq, exon_coords)
```
For a real annotated transcript, let the feature do the work -- `feature.extract` honors strand and joined exon locations:
```python
for feature in record.features:
if feature.type == 'mRNA':
transcript = feature.extract(record.seq)
```
### Carry a Sub-Region into a New Annotated Record
**Goal:** Keep id, per-base quality, and contained features when extracting a window, and decide deliberately what metadata to carry.
**Approach:** Slice the `SeqRecord` (qualities and contained features ride along automatically), then explicitly copy the `annotations` dict, which slicing always drops.
```python
sub = record[100:400] # qualities + contained features auto-sliced
sub.annotations = record.annotations.copy() # organism/taxonomy/refs would be lost otherwise
sub.id = f'{record.id}:101-400' # 1-based label for humans
```
To build a fresh record from a bare Seq slice instead (no source metadata to carry):
```python
sub = SeqRecord(record.seq[100:400], id=f'{record.id}_sub', description='positions 101-400')
```
### Extract a Feature by Type
```python
for record in SeqIO.parse('sequence.gb', 'genbank'):
for feature in record.features:
if feature.type == 'CDS':
cds = feature.extract(record.seq) # strand-aware
gene = feature.qualifiers.get('gene', ['?'])[0]
```
### Concatenate Sequences and Records
```python
seq1 + seq2 # Seq + Seq -> Seq
seq1 + 'NNNN' # Seq + str -> Seq
Seq('NNN').join([s1, s2, s3]) # linker between each -> Seq
```
Adding `SeqRecord` objects works (`rec1 + rec2` concatenates sequences and per-letter annotations), but follows the same rule as slicing: the result keeps `id`/`name`/`description` only when both share them, and the `annotations` dict is reset. Set metadata on the result explicitly.
### Split into Codons or Fixed Chunks
```python
def split_codons(seq):
'''Whole codons only; trailing 1-2 nt remainder is dropped.'''
return [seq[i:i + 3] for i in range(0, len(seq) - len(seq) % 3, 3)]
def chunk_sequence(seq, size):
'''Fixed-size chunks; final chunk may be shorter.'''
return [seq[i:i + size] for i in range(0, len(seq), size)]
```
### Tile Overlapping Windows
```python
def sliding_windows(seq, window_size, step=1):
for i in range(0, len(seq) - window_size + 1, step):
yield i, seq[i:i + window_size]
```
### Flanking Region Around a Position
```python
def get_flanking(seq, position, flank):
'''Clamp to sequence ends so the slice never runs past the edges.'''
start = max(0, position - flank)
end = min(len(seq), position + flank + 1)
return seq[start:end]
```
## Common Errors
| Symptom | Cause | Fix |
|---------|-------|-----|
| Organism/taxonomy/references gone from a sub-record | `record[start:end]` silently drops the `annotations` dict and `dbxrefs` | `sub.annotations = record.annotations.copy()` after slicing |
| A feature spanning the cut is missing from the slice | Features straddling the boundary are dropped whole, not truncated | Re-add manually with a clamped, recalculated location |
| All features gone after `record[::2]` | A non-trivial stride drops features entirely | Slice without a stride, or rebuild features by hand |
| Everything shifted one base left | GFF/GenBank 1-based start sliced as if 0-based | Subtract 1 from the START only: `seq[start-1:end]` |
| `UndefinedSequenceError` on `str(record.seq)` | Sequence content undefined (`Seq(None, length=n)`) | Use `len(record)`; do not force undefined content to a string |
| `TypeError` from `sum(slices)` | Default start `0` cannot add to a `Seq` | Pass a start: `sum(slices, Seq(''))` |
| Reversed but wrong strand | `seq[::-1]` reverses only; it does not complement | Use `seq.reverse_complement()` (see reverse-complement) |
| `IndexError` on single-base index | Position past the end | Check `len(seq)` first; slices clamp but `seq[i]` does not |
## Decision Guide
- Bare sequence, no metadata to keep -> slice the `Seq`: `seq[start:end]`.
- Need per-base quality or contained features to ride along -> slice the `SeqRecord`: `record[start:end]`, then copy `annotations`.
- Coordinates came from a GFF/GenBank/EMBL/VCF line -> subtract 1 from the start before slicing.
- Coordinates came from a BED file -> use as-is (already 0-based half-open).
- Strand-aware or joined/compound location -> `feature.extract(record.seq)`, never a manual slice.
- Joining separated regions -> `sum((seq[s:e] for s, e in coords), Seq(''))`.
## Related Skills
- seq-objects - Create Seq/SeqRecord objects and handle undefined sequence content
- reverse-complement - Reverse-complement an extracted region (slicing reverses but does not complement)
- transcription-translation - Translate an extracted CDS or spliced transcript
- sequence-io/read-sequences - Parse GenBank/FASTQ records (with features and qualities) to slice
- genome-intervals/gtf-gff-handling - Read 1-based GFF/GTF feature coordinates before slicing
- alignment-files/sam-bam-basics - Extract sequences from BAM regions with samtools
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!