Skill Tracer
Skills › Scientific Agent Skills › pysam › sequence_files.md

sequence_files.md

318 lines · skills/pysam/references/sequence_files.md at 5f2bc03

Open on GitHub
1# FASTA, FASTQ, and Tabix-Indexed Files
2
3This reference targets pysam 0.24.1.
4

FASTA, FASTQ, and Tabix-Indexed Files

This reference targets pysam 0.24.1.

Raw lines 1 to 4
5## Indexed FASTA
6
7`FastaFile` provides random access through a faidx index.
8
9```python
10import pysam
12pysam.faidx("reference.fa")
14with pysam.FastaFile("reference.fa") as fasta:
15 print(fasta.references)
16 print(fasta.lengths)
17 print(fasta.get_reference_length("chr1"))
18```
20An uncompressed FASTA uses `<name>.fai`. With a default index path, `FastaFile`
21can build a missing index on open. Pass an existing `filepath_index` to prevent
22implicit index creation (as the bundled inspector does). A BGZF-compressed FASTA also needs a
23`.gzi` compressed-offset index. Ordinary gzip is not suitable for indexed
24random access.
26The constructor can use nonstandard index paths:
28```python
29fasta = pysam.FastaFile(
30 "reference.fa.gz",
31 filepath_index="indexes/reference.fa.gz.fai",
32 filepath_index_compressed="indexes/reference.fa.gz.gzi",
34```

Indexed FASTA

FastaFile provides random access through a faidx index.

import pysam

pysam.faidx("reference.fa")

with pysam.FastaFile("reference.fa") as fasta:
    print(fasta.references)
    print(fasta.lengths)
    print(fasta.get_reference_length("chr1"))

An uncompressed FASTA uses <name>.fai. With a default index path, FastaFile can build a missing index on open. Pass an existing filepath_index to prevent implicit index creation (as the bundled inspector does). A BGZF-compressed FASTA also needs a .gzi compressed-offset index. Ordinary gzip is not suitable for indexed random access.

The constructor can use nonstandard index paths:

fasta = pysam.FastaFile(
    "reference.fa.gz",
    filepath_index="indexes/reference.fa.gz.fai",
    filepath_index_compressed="indexes/reference.fa.gz.gzi",
)
Raw lines 5 to 35
36### Numeric and string coordinates
38```python
39with pysam.FastaFile("reference.fa") as fasta:
40 # Numeric: 0-based, half-open
41 sequence = fasta.fetch("chr1", 999, 1_099)
43 # Region string: 1-based, inclusive
44 same_sequence = fasta.fetch(region="chr1:1000-1099")
45```
47If start or end is omitted, pysam uses the sequence boundary. Negative or
48inverted coordinates raise, but an end beyond the contig clips and a start
49at/beyond the end returns an empty string in 0.24.1. Check
50`0 <= start <= stop <= fasta.get_reference_length(contig)` when exact length
51is required; absence of an exception is not a bounds check.

Numeric and string coordinates

with pysam.FastaFile("reference.fa") as fasta:
    # Numeric: 0-based, half-open
    sequence = fasta.fetch("chr1", 999, 1_099)

    # Region string: 1-based, inclusive
    same_sequence = fasta.fetch(region="chr1:1000-1099")

If start or end is omitted, pysam uses the sequence boundary. Negative or inverted coordinates raise, but an end beyond the contig clips and a start at/beyond the end returns an empty string in 0.24.1. Check 0 <= start <= stop <= fasta.get_reference_length(contig) when exact length is required; absence of an exception is not a bounds check.

Raw lines 36 to 52
53### Fetch variant context
55```python
56def variant_context(
57 fasta: pysam.FastaFile,
58 contig: str,
59 pos_1based: int,
60 ref: str,
61 flank: int = 20,
62) -> tuple[str, bool]:
63 if pos_1based < 1 or flank < 0 or not ref:
64 raise ValueError("positive POS, nonempty REF, and nonnegative flank required")
65 start = pos_1based - 1
66 if start + len(ref) > fasta.get_reference_length(contig):
67 raise ValueError("REF extends beyond the contig")
68 context_start = max(0, start - flank)
69 context_stop = min(
70 fasta.get_reference_length(contig),
71 start + len(ref) + flank,
72 )
73 context = fasta.fetch(contig, context_start, context_stop)
74 observed_ref = fasta.fetch(contig, start, start + len(ref))
75 return context, observed_ref.upper() == ref.upper()
76```
78Validate the entire REF allele, not only its first base. A mismatch often
79means the VCF and FASTA use different assemblies, contig aliases, or
80normalization.

Fetch variant context

def variant_context(
    fasta: pysam.FastaFile,
    contig: str,
    pos_1based: int,
    ref: str,
    flank: int = 20,
) -> tuple[str, bool]:
    if pos_1based < 1 or flank < 0 or not ref:
        raise ValueError("positive POS, nonempty REF, and nonnegative flank required")
    start = pos_1based - 1
    if start + len(ref) > fasta.get_reference_length(contig):
        raise ValueError("REF extends beyond the contig")
    context_start = max(0, start - flank)
    context_stop = min(
        fasta.get_reference_length(contig),
        start + len(ref) + flank,
    )
    context = fasta.fetch(contig, context_start, context_stop)
    observed_ref = fasta.fetch(contig, start, start + len(ref))
    return context, observed_ref.upper() == ref.upper()

Validate the entire REF allele, not only its first base. A mismatch often means the VCF and FASTA use different assemblies, contig aliases, or normalization.

Raw lines 53 to 81
82### Strand-aware extraction
84Coordinates do not encode strand. Reverse-complement after fetching:
86```python
87sequence = fasta.fetch("chr1", start, stop)
88if strand == "-":
89 sequence = pysam.reverse_complement(sequence)
90```
92Confirm annotation coordinates before conversion: BED is normally 0-based
93half-open; GFF/GTF text is normally 1-based inclusive.

Strand-aware extraction

Coordinates do not encode strand. Reverse-complement after fetching:

sequence = fasta.fetch("chr1", start, stop)
if strand == "-":
    sequence = pysam.reverse_complement(sequence)

Confirm annotation coordinates before conversion: BED is normally 0-based half-open; GFF/GTF text is normally 1-based inclusive.

Raw lines 82 to 94
95## Sequential FASTA and FASTQ
97`FastxFile` streams FASTA, FASTQ, or mixed FASTX records. It does not implement
98random access.
100```python
101with pysam.FastxFile("reads.fastq.gz") as reads:
102 for record in reads:
103 print(record.name)
104 print(record.sequence)
105 print(record.comment)
106 print(record.quality)
107```
109The current class is `FastxFile`; `FastqFile` is an old compatibility name.

Sequential FASTA and FASTQ

FastxFile streams FASTA, FASTQ, or mixed FASTX records. It does not implement random access.

with pysam.FastxFile("reads.fastq.gz") as reads:
    for record in reads:
        print(record.name)
        print(record.sequence)
        print(record.comment)
        print(record.quality)

The current class is FastxFile; FastqFile is an old compatibility name.

Raw lines 95 to 110
111### Record persistence
113`persist=True` is the default and copies each record so it remains valid.
114For high-throughput scans, `persist=False` avoids the copy:
116```python
117with pysam.FastxFile("reads.fastq.gz", persist=False) as reads:
118 for record in reads:
119 process(record.name, record.sequence)
120 # Do not save `record` for use after the iterator advances.
121```
123With `persist=False`, records are read-only proxies and cease to be valid after
124iteration advances. Copy needed strings or use the default when retaining
125records.

Record persistence

persist=True is the default and copies each record so it remains valid. For high-throughput scans, persist=False avoids the copy:

with pysam.FastxFile("reads.fastq.gz", persist=False) as reads:
    for record in reads:
        process(record.name, record.sequence)
        # Do not save `record` for use after the iterator advances.

With persist=False, records are read-only proxies and cease to be valid after iteration advances. Copy needed strings or use the default when retaining records.

Raw lines 111 to 126
127### Quality scores
129```python
130with pysam.FastxFile("reads.fastq") as reads:
131 for record in reads:
132 if record.quality is None: # FASTA record
133 continue
134 qualities = record.get_quality_array()
135 mean_quality = (
136 sum(qualities) / len(qualities) if qualities else None
137 )
138```
140Pysam converts the FASTQ quality string to numeric Phred scores. Confirm the
141source encoding for legacy FASTQ; modern data is normally Phred+33.

Quality scores

with pysam.FastxFile("reads.fastq") as reads:
    for record in reads:
        if record.quality is None:  # FASTA record
            continue
        qualities = record.get_quality_array()
        mean_quality = (
            sum(qualities) / len(qualities) if qualities else None
        )

Pysam converts the FASTQ quality string to numeric Phred scores. Confirm the source encoding for legacy FASTQ; modern data is normally Phred+33.

Raw lines 127 to 142
143### Safe writing
145`str(record)` preserves whether the record is FASTA or FASTQ and includes the
146comment and quality line correctly:
148```python
149with pysam.FastxFile("reads.fastq.gz") as source, open(
150 "filtered.fastq", "x", encoding="utf-8"
151) as destination:
152 for record in source:
153 qualities = record.get_quality_array()
154 if qualities and sum(qualities) / len(qualities) >= 20:
155 destination.write(str(record) + "\n")
156```
158Use exclusive creation (`"x"`) when replacement was not requested. Manual
159four-line FASTQ formatting can lose comments or create sequence/quality length
160mismatches.

Safe writing

str(record) preserves whether the record is FASTA or FASTQ and includes the comment and quality line correctly:

with pysam.FastxFile("reads.fastq.gz") as source, open(
    "filtered.fastq", "x", encoding="utf-8"
) as destination:
    for record in source:
        qualities = record.get_quality_array()
        if qualities and sum(qualities) / len(qualities) >= 20:
            destination.write(str(record) + "\n")

Use exclusive creation ("x") when replacement was not requested. Manual four-line FASTQ formatting can lose comments or create sequence/quality length mismatches.

Raw lines 143 to 161
162### Streaming statistics
164```python
165from __future__ import annotations
168def fastx_stats(path: str) -> dict[str, float | int | None]:
169 record_count = 0
170 base_count = 0
171 quality_sum = 0
172 quality_count = 0
174 with pysam.FastxFile(path, persist=False) as records:
175 for record in records:
176 record_count += 1
177 base_count += len(record.sequence)
178 if record.quality is not None:
179 qualities = record.get_quality_array()
180 quality_sum += sum(qualities)
181 quality_count += len(qualities)
183 return {
184 "records": record_count,
185 "bases": base_count,
186 "mean_length": (
187 base_count / record_count if record_count else None
188 ),
189 "mean_quality": (
190 quality_sum / quality_count if quality_count else None
191 ),
192 }
193```
195This is a full scan but has constant memory.

Streaming statistics

from __future__ import annotations


def fastx_stats(path: str) -> dict[str, float | int | None]:
    record_count = 0
    base_count = 0
    quality_sum = 0
    quality_count = 0

    with pysam.FastxFile(path, persist=False) as records:
        for record in records:
            record_count += 1
            base_count += len(record.sequence)
            if record.quality is not None:
                qualities = record.get_quality_array()
                quality_sum += sum(qualities)
                quality_count += len(qualities)

    return {
        "records": record_count,
        "bases": base_count,
        "mean_length": (
            base_count / record_count if record_count else None
        ),
        "mean_quality": (
            quality_sum / quality_count if quality_count else None
        ),
    }

This is a full scan but has constant memory.

Raw lines 162 to 196
197## BGZF and Tabix
199`TabixFile` provides random access to coordinate-sorted, BGZF-compressed
200tabular files.

BGZF and Tabix

TabixFile provides random access to coordinate-sorted, BGZF-compressed tabular files.

Raw lines 197 to 201
202### Non-destructive compression and indexing
204```python
205import pysam
207pysam.tabix_compress("regions.bed", "regions.bed.gz")
208pysam.tabix_index("regions.bed.gz", preset="bed")
209```
211This leaves `regions.bed` intact. By contrast, calling `tabix_index()` directly
212on an uncompressed file may automatically compress it and remove the original
213unless `keep_original=True`.
215Do not use `force=True` unless replacing existing compressed data or indexes is
216explicitly intended.

Non-destructive compression and indexing

import pysam

pysam.tabix_compress("regions.bed", "regions.bed.gz")
pysam.tabix_index("regions.bed.gz", preset="bed")

This leaves regions.bed intact. By contrast, calling tabix_index() directly on an uncompressed file may automatically compress it and remove the original unless keep_original=True.

Do not use force=True unless replacing existing compressed data or indexes is explicitly intended.

Raw lines 202 to 217
218### Input requirements
220- Sort by contig and coordinate first. The wrapper does not sort or perform
221 a full preflight; HTSlib rejects detected ordering errors during indexing.
222- Use BGZF, not ordinary gzip.
223- Select the correct preset: commonly `bed`, `gff`, `sam`, or `vcf`.
224- Presets define columns and coordinate conventions.
226For a custom table:
228```python
229pysam.tabix_index(
230 "custom.tsv.gz",
231 seq_col=0,
232 start_col=1,
233 end_col=2,
234 zerobased=True,
236```
238Python column indices are 0-based. File coordinates are assumed 1-based unless
239`zerobased=True`. This is separate from query coordinates, which are always
240numeric 0-based in the Python API.
242Use CSI for references beyond legacy TBI limits, then pass its path to
243`TabixFile(..., index="regions.bed.gz.csi")` (the default only looks for TBI):
245```python
246pysam.tabix_index(
247 "regions.bed.gz",
248 preset="bed",
249 csi=True,
250 min_shift=14,
252```

Input requirements

  • Sort by contig and coordinate first. The wrapper does not sort or perform a full preflight; HTSlib rejects detected ordering errors during indexing.
  • Use BGZF, not ordinary gzip.
  • Select the correct preset: commonly bed, gff, sam, or vcf.
  • Presets define columns and coordinate conventions.

For a custom table:

pysam.tabix_index(
    "custom.tsv.gz",
    seq_col=0,
    start_col=1,
    end_col=2,
    zerobased=True,
)

Python column indices are 0-based. File coordinates are assumed 1-based unless zerobased=True. This is separate from query coordinates, which are always numeric 0-based in the Python API.

Use CSI for references beyond legacy TBI limits, then pass its path to TabixFile(..., index="regions.bed.gz.csi") (the default only looks for TBI):

pysam.tabix_index(
    "regions.bed.gz",
    preset="bed",
    csi=True,
    min_shift=14,
)
Raw lines 218 to 253
254### Querying
256```python
257with pysam.TabixFile(
258 "regions.bed.gz",
259 parser=pysam.asBed(),
260) as regions:
261 for row in regions.fetch("chr1", 1_000, 2_000):
262 print(row.contig, row.start, row.end, row.name)
263```
265Numeric query coordinates are 0-based, half-open. Region strings are
266samtools-style 1-based inclusive.
268Useful parsers:
270- `pysam.asTuple()`: tuple-like fields
271- `pysam.asBed()`: BED fields with 0-based start/end
272- `pysam.asGTF()`: GTF fields and quoted attributes
273- `pysam.asGFF3()`: GFF3 fields and `key=value` attributes
274- `pysam.asVCF()`: lightweight tabix VCF parser
276For complete VCF semantics, use `VariantFile`, not `TabixFile(asVCF())`.
278Without a parser, each result is the raw tab-delimited string:
280```python
281with pysam.TabixFile("annotations.tsv.gz") as table:
282 for line in table.fetch("chr1", 1_000, 2_000):
283 fields = line.split("\t")
284```

Querying

with pysam.TabixFile(
    "regions.bed.gz",
    parser=pysam.asBed(),
) as regions:
    for row in regions.fetch("chr1", 1_000, 2_000):
        print(row.contig, row.start, row.end, row.name)

Numeric query coordinates are 0-based, half-open. Region strings are samtools-style 1-based inclusive.

Useful parsers:

  • pysam.asTuple(): tuple-like fields
  • pysam.asBed(): BED fields with 0-based start/end
  • pysam.asGTF(): GTF fields and quoted attributes
  • pysam.asGFF3(): GFF3 fields and key=value attributes
  • pysam.asVCF(): lightweight tabix VCF parser

For complete VCF semantics, use VariantFile, not TabixFile(asVCF()).

Without a parser, each result is the raw tab-delimited string:

with pysam.TabixFile("annotations.tsv.gz") as table:
    for line in table.fetch("chr1", 1_000, 2_000):
        fields = line.split("\t")
Raw lines 254 to 285
286### Headers and multiple iterators
288```python
289with pysam.TabixFile("annotations.gff.gz") as table:
290 header_lines = list(table.header)
291 first = table.fetch("chr1", multiple_iterators=True)
292 second = table.fetch("chr2", multiple_iterators=True)
293```
295Header lines are yielded without trailing newlines. Each
296`multiple_iterators=True` iterator reopens the file and adds overhead.

Headers and multiple iterators

with pysam.TabixFile("annotations.gff.gz") as table:
    header_lines = list(table.header)
    first = table.fetch("chr1", multiple_iterators=True)
    second = table.fetch("chr2", multiple_iterators=True)

Header lines are yielded without trailing newlines. Each multiple_iterators=True iterator reopens the file and adds overhead.

Raw lines 286 to 297
298## Choosing the Right Interface
300| Data | Interface | Access |
301|---|---|---|
302| Reference FASTA | `FastaFile` | indexed random access |
303| FASTA/FASTQ reads | `FastxFile` | sequential |
304| BED/GFF/GTF/custom table | `TabixFile` | BGZF + tabix/CSI random access |
305| VCF/BCF | `VariantFile` | structured records, sequential or indexed |

Choosing the Right Interface

DataInterfaceAccess
Reference FASTAFastaFileindexed random access
FASTA/FASTQ readsFastxFilesequential
BED/GFF/GTF/custom tableTabixFileBGZF + tabix/CSI random access
VCF/BCFVariantFilestructured records, sequential or indexed
Raw lines 298 to 306
307## Common Pitfalls
309- Mixing numeric 0-based coordinates with 1-based region strings
310- Unexpected `.fai` creation when opening `FastaFile` without an explicit index
311- Using ordinary gzip for indexed FASTA or tabix data
312- Retaining a `persist=False` FASTX proxy
313- Assuming every FASTX record has qualities
314- Manually formatting FASTQ and losing comments or quality alignment
315- Letting `tabix_index()` remove the uncompressed source unexpectedly
316- Indexing an unsorted file
317- Forgetting `zerobased=True` for custom 0-based table coordinates
318- Using a TBI index for coordinates beyond its legacy range

Common Pitfalls

  • Mixing numeric 0-based coordinates with 1-based region strings
  • Unexpected .fai creation when opening FastaFile without an explicit index
  • Using ordinary gzip for indexed FASTA or tabix data
  • Retaining a persist=False FASTX proxy
  • Assuming every FASTX record has qualities
  • Manually formatting FASTQ and losing comments or quality alignment
  • Letting tabix_index() remove the uncompressed source unexpectedly
  • Indexing an unsorted file
  • Forgetting zerobased=True for custom 0-based table coordinates
  • Using a TBI index for coordinates beyond its legacy range
Raw lines 307 to 318