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

alignment_files.md

401 lines · skills/pysam/references/alignment_files.md at 5f2bc03

Open on GitHub
1# Alignment Files: SAM, BAM, and CRAM
2
3This reference targets pysam 0.24.1. All numeric coordinates shown here are
40-based, half-open.
5

Alignment Files: SAM, BAM, and CRAM

This reference targets pysam 0.24.1. All numeric coordinates shown here are 0-based, half-open.

Raw lines 1 to 5
6## Open Modes and Handles
7
8| Format | Read | Write |
9|---|---:|---:|
10| SAM text | `r` | `w` |
11| BAM | `rb` | `wb` |
12| CRAM | `rc` | `wc` |
14```python
15import pysam
17with pysam.AlignmentFile("input.bam", "rb", threads=4) as alignments:
18 print(alignments.references)
19```
21For CRAM, supply the exact reference where possible:
23```python
24with pysam.AlignmentFile(
25 "input.cram",
26 "rc",
27 reference_filename="GRCh38.fa",
28 threads=4,
29) as alignments:
30 ...
31```
33`AlignmentFile` can use a path or a real file object that exposes `fileno()`.
34In-memory objects such as `io.BytesIO` are not supported by HTSlib. Use `"-"`
35for stdin/stdout. When an existing file object is accepted,
36`duplicate_filehandle=True` (the default) prevents pysam from closing the
37caller's descriptor.
39Useful constructor options:
41- `index_filename=`: nonstandard, remote, or separately named index
42- `require_index=True`: fail early if random access is required
43- `reference_filename=`: CRAM reference FASTA
44- `threads=`: compression/decompression threads
45- `format_options=["key=value"]`: HTSlib format options
46- `ignore_truncation=True`: downgrade a missing BGZF EOF marker to a warning;
47 do not combine with `threads > 1`

Open Modes and Handles

FormatReadWrite
SAM textrw
BAMrbwb
CRAMrcwc
import pysam

with pysam.AlignmentFile("input.bam", "rb", threads=4) as alignments:
    print(alignments.references)

For CRAM, supply the exact reference where possible:

with pysam.AlignmentFile(
    "input.cram",
    "rc",
    reference_filename="GRCh38.fa",
    threads=4,
) as alignments:
    ...

AlignmentFile can use a path or a real file object that exposes fileno(). In-memory objects such as io.BytesIO are not supported by HTSlib. Use "-" for stdin/stdout. When an existing file object is accepted, duplicate_filehandle=True (the default) prevents pysam from closing the caller's descriptor.

Useful constructor options:

  • index_filename=: nonstandard, remote, or separately named index
  • require_index=True: fail early if random access is required
  • reference_filename=: CRAM reference FASTA
  • threads=: compression/decompression threads
  • format_options=["key=value"]: HTSlib format options
  • ignore_truncation=True: downgrade a missing BGZF EOF marker to a warning; do not combine with threads > 1
Raw lines 6 to 48
49## Headers and Index State
51`AlignmentFile.header` is an `AlignmentHeader`, not a plain dictionary.
53```python
54with pysam.AlignmentFile("input.bam", "rb") as bam:
55 header_dict = bam.header.to_dict()
56 header_text = str(bam.header)
57 contigs = dict(zip(bam.references, bam.lengths))
58 has_index = bam.has_index()
59```
61Use `check_index()` when a missing index should be an error. It raises for SAM,
62closed files, or unusable indexes. `get_index_statistics()` exposes per-contig
63mapped/unmapped counts recorded in an available index; these are index
64statistics, not a fresh scan of every record. CRAI does not store these counts:
65CRAM `mapped`, `unmapped`, `nocoordinate`, and index-statistic values return zero
66and must not be interpreted as an empty CRAM. Use a sequential scan for counts.

Headers and Index State

AlignmentFile.header is an AlignmentHeader, not a plain dictionary.

with pysam.AlignmentFile("input.bam", "rb") as bam:
    header_dict = bam.header.to_dict()
    header_text = str(bam.header)
    contigs = dict(zip(bam.references, bam.lengths))
    has_index = bam.has_index()

Use check_index() when a missing index should be an error. It raises for SAM, closed files, or unusable indexes. get_index_statistics() exposes per-contig mapped/unmapped counts recorded in an available index; these are index statistics, not a fresh scan of every record. CRAI does not store these counts: CRAM mapped, unmapped, nocoordinate, and index-statistic values return zero and must not be interpreted as an empty CRAM. Use a sequential scan for counts.

Raw lines 49 to 67
68## Iteration Choices

Iteration Choices

Raw lines 68 to 69
70### Indexed region query
72```python
73with pysam.AlignmentFile("input.bam", "rb") as bam:
74 for read in bam.fetch("chr1", 1_000, 2_000):
75 ...
76```
78- Requires BAI/CSI for BAM or CRAI for CRAM.
79- Returns reads overlapping the interval, including reads that start before it
80 or end after it.
81- Records are returned in coordinate/index order.
82- BAM/CRAM `fetch()` with no region requires an index and visits placed records
83 by reference. Placed unmapped records can appear in both this and region
84 queries; test `read.is_unmapped`. Unplaced reads need sequential scanning.
85- SAM `fetch()` without a region is sequential; SAM region access is unsupported
86 by `AlignmentFile` even though standalone samtools can index BGZF SAM.

Indexed region query

with pysam.AlignmentFile("input.bam", "rb") as bam:
    for read in bam.fetch("chr1", 1_000, 2_000):
        ...
  • Requires BAI/CSI for BAM or CRAI for CRAM.
  • Returns reads overlapping the interval, including reads that start before it or end after it.
  • Records are returned in coordinate/index order.
  • BAM/CRAM fetch() with no region requires an index and visits placed records by reference. Placed unmapped records can appear in both this and region queries; test read.is_unmapped. Unplaced reads need sequential scanning.
  • SAM fetch() without a region is sequential; SAM region access is unsupported by AlignmentFile even though standalone samtools can index BGZF SAM.
Raw lines 70 to 87
88### Sequential scan
90```python
91with pysam.AlignmentFile("input.bam", "rb") as bam:
92 for read in bam.fetch(until_eof=True):
93 ...
94```
96- Does not require an index.
97- Starts at the current file position.
98- Preserves file order.
99- Includes unplaced unmapped records.
101`fetch("*")` requests only unplaced unmapped records at the end of a
102coordinate-sorted alignment file.

Sequential scan

with pysam.AlignmentFile("input.bam", "rb") as bam:
    for read in bam.fetch(until_eof=True):
        ...
  • Does not require an index.
  • Starts at the current file position.
  • Preserves file order.
  • Includes unplaced unmapped records.

fetch("*") requests only unplaced unmapped records at the end of a coordinate-sorted alignment file.

Raw lines 88 to 103
104### Multiple active iterators
106`multiple_iterators` belongs to `fetch()`, not the `AlignmentFile`
107constructor:
109```python
110with pysam.AlignmentFile("input.bam", "rb") as bam:
111 chr1_reads = bam.fetch("chr1", multiple_iterators=True)
112 chr2_reads = bam.fetch("chr2", multiple_iterators=True)
113```
115Each such iterator reopens the file and has overhead. Prefer one ordered pass
116when possible.

Multiple active iterators

multiple_iterators belongs to fetch(), not the AlignmentFile constructor:

with pysam.AlignmentFile("input.bam", "rb") as bam:
    chr1_reads = bam.fetch("chr1", multiple_iterators=True)
    chr2_reads = bam.fetch("chr2", multiple_iterators=True)

Each such iterator reopens the file and has overhead. Prefer one ordered pass when possible.

Raw lines 104 to 117
118## `AlignedSegment` Essentials

AlignedSegment Essentials

Raw lines 118 to 119
120### Identity and sequence
122- `query_name`
123- `query_sequence`
124- `query_qualities`: numeric Phred scores, or `None`
125- `query_length`
126- `query_alignment_sequence`: query bases participating in the alignment
127- `query_alignment_qualities`
128- `get_forward_sequence()` / `get_forward_qualities()`: original sequencer
129 orientation of the retained sequence (hard-clipped bases cannot be recovered)
131Assigning `query_sequence` invalidates `query_qualities`. Save and reassign
132qualities after changing sequence.

Identity and sequence

  • query_name
  • query_sequence
  • query_qualities: numeric Phred scores, or None
  • query_length
  • query_alignment_sequence: query bases participating in the alignment
  • query_alignment_qualities
  • get_forward_sequence() / get_forward_qualities(): original sequencer orientation of the retained sequence (hard-clipped bases cannot be recovered)

Assigning query_sequence invalidates query_qualities. Save and reassign qualities after changing sequence.

Raw lines 120 to 133
134### Reference placement
136- `reference_name`
137- `reference_id`
138- `reference_start`: 0-based inclusive
139- `reference_end`: 0-based exclusive; derived from CIGAR
140- `mapping_quality`
141- `next_reference_name`, `next_reference_start`
142- `template_length`
144Many placement-derived properties are `None` or sentinel values for unmapped
145or CIGAR-less records. Check `is_unmapped` before using them.

Reference placement

  • reference_name
  • reference_id
  • reference_start: 0-based inclusive
  • reference_end: 0-based exclusive; derived from CIGAR
  • mapping_quality
  • next_reference_name, next_reference_start
  • template_length

Many placement-derived properties are None or sentinel values for unmapped or CIGAR-less records. Check is_unmapped before using them.

Raw lines 134 to 146
147### Flags
149Common boolean properties:
151- pairing: `is_paired`, `is_proper_pair`, `is_read1`, `is_read2`
152- orientation: `is_reverse`, `mate_is_reverse`
153- mapping: `is_unmapped`, `mate_is_unmapped`
154- status: `is_secondary`, `is_supplementary`, `is_qcfail`, `is_duplicate`
156For a typical primary mapped-read filter:
158```python
159def keep_primary(read: pysam.AlignedSegment) -> bool:
160 return (
161 not read.is_unmapped
162 and not read.is_secondary
163 and not read.is_supplementary
164 and not read.is_qcfail
165 and not read.is_duplicate
166 and read.mapping_quality >= 20
167 )
168```
170State whether supplementary alignments and duplicates are intentionally
171excluded; there is no universal filter for every analysis. MAPQ 255 denotes
172unavailable quality, and the numeric threshold above keeps it. Exclude it
173explicitly when the assay requires measured MAPQ. Proper-pair flags describe
174the aligner's criteria, not independent proof of a valid biological fragment.

Flags

Common boolean properties:

  • pairing: is_paired, is_proper_pair, is_read1, is_read2
  • orientation: is_reverse, mate_is_reverse
  • mapping: is_unmapped, mate_is_unmapped
  • status: is_secondary, is_supplementary, is_qcfail, is_duplicate

For a typical primary mapped-read filter:

def keep_primary(read: pysam.AlignedSegment) -> bool:
    return (
        not read.is_unmapped
        and not read.is_secondary
        and not read.is_supplementary
        and not read.is_qcfail
        and not read.is_duplicate
        and read.mapping_quality >= 20
    )

State whether supplementary alignments and duplicates are intentionally excluded; there is no universal filter for every analysis. MAPQ 255 denotes unavailable quality, and the numeric threshold above keeps it. Exclude it explicitly when the assay requires measured MAPQ. Proper-pair flags describe the aligner's criteria, not independent proof of a valid biological fragment.

Raw lines 147 to 175
176## CIGAR Operations and Alignment Geometry
178`cigartuples` stores `(operation, length)`, the reverse of textual SAM CIGAR
179notation.
181| Enum | Code | SAM op | Consumes query | Consumes reference |
182|---|---:|---:|---:|---:|
183| `CIGAR_OPS.CMATCH` | 0 | `M` | yes | yes |
184| `CIGAR_OPS.CINS` | 1 | `I` | yes | no |
185| `CIGAR_OPS.CDEL` | 2 | `D` | no | yes |
186| `CIGAR_OPS.CREF_SKIP` | 3 | `N` | no | yes |
187| `CIGAR_OPS.CSOFT_CLIP` | 4 | `S` | yes | no |
188| `CIGAR_OPS.CHARD_CLIP` | 5 | `H` | no | no |
189| `CIGAR_OPS.CPAD` | 6 | `P` | no | no |
190| `CIGAR_OPS.CEQUAL` | 7 | `=` | yes | yes |
191| `CIGAR_OPS.CDIFF` | 8 | `X` | yes | yes |
192| `CIGAR_OPS.CBACK` | 9 | `B` | legacy | legacy |
194Prefer enum members in new code. Top-level aliases such as `pysam.CMATCH`
195remain in 0.24 for compatibility but are expected to be removed in a future
196release.
198Useful geometry methods:
200```python
201blocks = read.get_blocks() # M/=/X blocks; D/N gaps, I can split adjacent blocks
202pairs = read.get_aligned_pairs(matches_only=False, with_cigar=True)
203reference_positions = read.get_reference_positions(full_length=True)
204```
206`get_aligned_pairs(with_seq=True)` needs an MD tag and returns reference bases
207derived from that tag. It does not consult a separately opened FASTA.

CIGAR Operations and Alignment Geometry

cigartuples stores (operation, length), the reverse of textual SAM CIGAR notation.

EnumCodeSAM opConsumes queryConsumes reference
CIGAR_OPS.CMATCH0Myesyes
CIGAR_OPS.CINS1Iyesno
CIGAR_OPS.CDEL2Dnoyes
CIGAR_OPS.CREF_SKIP3Nnoyes
CIGAR_OPS.CSOFT_CLIP4Syesno
CIGAR_OPS.CHARD_CLIP5Hnono
CIGAR_OPS.CPAD6Pnono
CIGAR_OPS.CEQUAL7=yesyes
CIGAR_OPS.CDIFF8Xyesyes
CIGAR_OPS.CBACK9Blegacylegacy

Prefer enum members in new code. Top-level aliases such as pysam.CMATCH remain in 0.24 for compatibility but are expected to be removed in a future release.

Useful geometry methods:

blocks = read.get_blocks()  # M/=/X blocks; D/N gaps, I can split adjacent blocks
pairs = read.get_aligned_pairs(matches_only=False, with_cigar=True)
reference_positions = read.get_reference_positions(full_length=True)

get_aligned_pairs(with_seq=True) needs an MD tag and returns reference bases derived from that tag. It does not consult a separately opened FASTA.

Raw lines 176 to 208
209## Optional Tags and Modified Bases
211```python
212if read.has_tag("NM"):
213 edit_distance = read.get_tag("NM")
215read.set_tag("XX", 7, value_type="i")
216all_tags = read.get_tags(with_value_type=True)
217```
219Use standard tags according to the SAM tags specification. Avoid changing
220alignment-derived tags such as NM/MD without recomputing them.
222For base modifications encoded by MM/ML:
224```python
225for (canonical_base, strand, modification), calls in (
226 read.modified_bases or {}
227).items():
228 for query_position, quality in calls:
229 probability_lower_bound = None if quality < 0 else quality / 256.0
230```
232The key is `(canonical base, strand, modification)`, where strand is `0`
233forward or `1` reverse. pysam 0.24 removed the earlier five-modification-type
234limit and fixed crashes on degenerate empty MM calls. ML values are quantized:
235`quality / 256` is a lower bound, not an exact posterior. These query positions
236refer to stored SEQ; use `modified_bases_forward` with `get_forward_sequence()`
237when the original orientation is required.

Optional Tags and Modified Bases

if read.has_tag("NM"):
    edit_distance = read.get_tag("NM")

read.set_tag("XX", 7, value_type="i")
all_tags = read.get_tags(with_value_type=True)

Use standard tags according to the SAM tags specification. Avoid changing alignment-derived tags such as NM/MD without recomputing them.

For base modifications encoded by MM/ML:

for (canonical_base, strand, modification), calls in (
    read.modified_bases or {}
).items():
    for query_position, quality in calls:
        probability_lower_bound = None if quality < 0 else quality / 256.0

The key is (canonical base, strand, modification), where strand is 0 forward or 1 reverse. pysam 0.24 removed the earlier five-modification-type limit and fixed crashes on degenerate empty MM calls. ML values are quantized: quality / 256 is a lower bound, not an exact posterior. These query positions refer to stored SEQ; use modified_bases_forward with get_forward_sequence() when the original orientation is required.

Raw lines 209 to 238
239## Counting and Coverage

Counting and Coverage

Raw lines 239 to 240
241### Record count
243```python
244with pysam.AlignmentFile("input.bam", "rb") as bam:
245 raw_overlap_count = bam.count("chr1", 1_000, 2_000)
246 filtered_count = bam.count(
247 "chr1",
248 1_000,
249 2_000,
250 read_callback="all",
251 )
252```
254`count()` defaults to `read_callback="nofilter"`. `"all"` excludes reads with
255unmapped, secondary, QC-fail, or duplicate flags. It does not accept a
256`quality=` argument. Use a callback for custom record filtering:
258```python
259count = bam.count(
260 "chr1",
261 1_000,
262 2_000,
263 read_callback=lambda read: keep_primary(read),
265```

Record count

with pysam.AlignmentFile("input.bam", "rb") as bam:
    raw_overlap_count = bam.count("chr1", 1_000, 2_000)
    filtered_count = bam.count(
        "chr1",
        1_000,
        2_000,
        read_callback="all",
    )

count() defaults to read_callback="nofilter". "all" excludes reads with unmapped, secondary, QC-fail, or duplicate flags. It does not accept a quality= argument. Use a callback for custom record filtering:

count = bam.count(
    "chr1",
    1_000,
    2_000,
    read_callback=lambda read: keep_primary(read),
)
Raw lines 241 to 266
267### A/C/G/T base coverage
269```python
270a, c, g, t = bam.count_coverage(
271 "chr1",
272 1_000,
273 2_000,
274 quality_threshold=20,
275 read_callback="all",
277depth = [sum(values) for values in zip(a, c, g, t)]
278```
280For `0 <= start < stop <= contig_length`, the result has `stop - start`
281positions, including zero coverage. Validate these bounds: an end past the
282contig is clipped. In 0.24.1, the array allocation does not derive its span
283from `region=` or the `end=` alias; use numeric `contig`, `start`, and `stop`
284for this method. Only A/C/G/T bases count. Deletions, reference skips, and
285ambiguous query bases do not contribute. Overlapping mates count separately;
286this method does not apply pileup BAQ or pair-overlap adjustments.

A/C/G/T base coverage

a, c, g, t = bam.count_coverage(
    "chr1",
    1_000,
    2_000,
    quality_threshold=20,
    read_callback="all",
)
depth = [sum(values) for values in zip(a, c, g, t)]

For 0 <= start < stop <= contig_length, the result has stop - start positions, including zero coverage. Validate these bounds: an end past the contig is clipped. In 0.24.1, the array allocation does not derive its span from region= or the end= alias; use numeric contig, start, and stop for this method. Only A/C/G/T bases count. Deletions, reference skips, and ambiguous query bases do not contribute. Overlapping mates count separately; this method does not apply pileup BAQ or pair-overlap adjustments.

Raw lines 267 to 287
288## Pileup Semantics
290```python
291with pysam.FastaFile("reference.fa") as fasta, pysam.AlignmentFile(
292 "input.bam", "rb"
293) as bam:
294 iterator = bam.pileup(
295 "chr1",
296 1_000,
297 2_000,
298 truncate=True,
299 stepper="samtools",
300 fastafile=fasta,
301 min_mapping_quality=20,
302 min_base_quality=20,
303 max_depth=100_000,
304 ignore_overlaps=True,
305 ignore_orphans=True,
306 )
307 for column in iterator:
308 for pileup_read in column.pileups:
309 if pileup_read.is_del or pileup_read.is_refskip:
310 continue
311 query_position = pileup_read.query_position
312 base = pileup_read.alignment.query_sequence[query_position]
313```
315Key defaults and behaviors:
317- Without `truncate=True`, columns outside the requested interval can appear
318 when overlapping reads extend beyond the interval.
319- `stepper="all"` filters unmapped, secondary, QC-fail, and duplicate reads.
320- `stepper="nofilter"` disables the read callback's flag/MAPQ filtering;
321 base-quality thresholds, the depth cap, and overlap handling still apply.
322- `stepper="samtools"` applies samtools-style processing; provide `fastafile`
323 for full BAQ/reference behavior.
324- Default `min_base_quality` is 13.
325- Default `max_depth` is 8000.
326- Paired overlap detection and orphan filtering are enabled by default.
327- `nsegments` precedes the base-quality filter. Despite its name,
328 `get_num_aligned()` also counts deletion/reference-skip entries that pass the
329 quality check in 0.24.1. For observed base depth, count `column.pileups` entries
330 with `not is_del`, `not is_refskip`, and a non-`None` `query_position`.
331- Overlap handling adjusts base qualities; with `min_base_quality=0`, a
332 zero-quality mate can reappear. It does not universally remove one record.
334Consume each `PileupColumn` before advancing the iterator: its backing buffer
335changes on advance, not only on iterator destruction. Copy primitive values
336needed later. For concurrent CRAM pileups open separate file handles;
337`multiple_iterators=True` is not implemented for CRAM pileup.
339For SNP support, inspect base and quality. For insertions/deletions, use
340`PileupRead.indel`, deletion/refskip state, CIGAR, and normalized alleles.
341Single-base counting is not an indel caller.

Pileup Semantics

with pysam.FastaFile("reference.fa") as fasta, pysam.AlignmentFile(
    "input.bam", "rb"
) as bam:
    iterator = bam.pileup(
        "chr1",
        1_000,
        2_000,
        truncate=True,
        stepper="samtools",
        fastafile=fasta,
        min_mapping_quality=20,
        min_base_quality=20,
        max_depth=100_000,
        ignore_overlaps=True,
        ignore_orphans=True,
    )
    for column in iterator:
        for pileup_read in column.pileups:
            if pileup_read.is_del or pileup_read.is_refskip:
                continue
            query_position = pileup_read.query_position
            base = pileup_read.alignment.query_sequence[query_position]

Key defaults and behaviors:

  • Without truncate=True, columns outside the requested interval can appear when overlapping reads extend beyond the interval.
  • stepper="all" filters unmapped, secondary, QC-fail, and duplicate reads.
  • stepper="nofilter" disables the read callback's flag/MAPQ filtering; base-quality thresholds, the depth cap, and overlap handling still apply.
  • stepper="samtools" applies samtools-style processing; provide fastafile for full BAQ/reference behavior.
  • Default min_base_quality is 13.
  • Default max_depth is 8000.
  • Paired overlap detection and orphan filtering are enabled by default.
  • nsegments precedes the base-quality filter. Despite its name, get_num_aligned() also counts deletion/reference-skip entries that pass the quality check in 0.24.1. For observed base depth, count column.pileups entries with not is_del, not is_refskip, and a non-None query_position.
  • Overlap handling adjusts base qualities; with min_base_quality=0, a zero-quality mate can reappear. It does not universally remove one record.

Consume each PileupColumn before advancing the iterator: its backing buffer changes on advance, not only on iterator destruction. Copy primitive values needed later. For concurrent CRAM pileups open separate file handles; multiple_iterators=True is not implemented for CRAM pileup.

For SNP support, inspect base and quality. For insertions/deletions, use PileupRead.indel, deletion/refskip state, CIGAR, and normalized alleles. Single-base counting is not an indel caller.

Raw lines 288 to 342
343## Writing Alignments

Writing Alignments

Raw lines 343 to 344
345### Preserve an input header
347```python
348with pysam.AlignmentFile("input.bam", "rb") as source, pysam.AlignmentFile(
349 "filtered.bam", "wb", template=source, threads=4
350) as destination:
351 for read in source.fetch(until_eof=True):
352 if keep_primary(read):
353 destination.write(read)
354```
356The output retains input order and `@SQ`, `@RG`, `@PG`, and optional tags.
357Keep the reference order intact: record reference IDs are numeric. Check each
358`RG` tag against a defined `@RG ID` and its intended sample before combining
359files. Per-record filters can drop one mate; they do not repair flags, mate
360coordinates, TLEN, or fetch a mate outside a requested region. Only index
361coordinate-sorted output with unmapped records in valid positions.

Preserve an input header

with pysam.AlignmentFile("input.bam", "rb") as source, pysam.AlignmentFile(
    "filtered.bam", "wb", template=source, threads=4
) as destination:
    for read in source.fetch(until_eof=True):
        if keep_primary(read):
            destination.write(read)

The output retains input order and @SQ, @RG, @PG, and optional tags. Keep the reference order intact: record reference IDs are numeric. Check each RG tag against a defined @RG ID and its intended sample before combining files. Per-record filters can drop one mate; they do not repair flags, mate coordinates, TLEN, or fetch a mate outside a requested region. Only index coordinate-sorted output with unmapped records in valid positions.

Raw lines 345 to 362
363### Construct a new record
365```python
366header = pysam.AlignmentHeader.from_dict(
367 {
368 "HD": {"VN": "1.6", "SO": "coordinate"},
369 "SQ": [{"SN": "chr1", "LN": 248_956_422}],
370 }
373with pysam.AlignmentFile("new.bam", "wb", header=header) as output:
374 read = pysam.AlignedSegment(output.header)
375 read.query_name = "read001"
376 read.query_sequence = "ACGTACGTAA"
377 read.flag = 0
378 read.reference_id = output.get_tid("chr1")
379 read.reference_start = 100
380 read.mapping_quality = 60
381 read.cigartuples = [(pysam.CIGAR_OPS.CMATCH, 10)]
382 read.query_qualities = pysam.qualitystring_to_array("IIIIIIIIII")
383 output.write(read)
384```
386Construct records against the destination header so numeric reference IDs map
387correctly. Sequence length, qualities, and query-consuming CIGAR operations
388must agree.

Construct a new record

header = pysam.AlignmentHeader.from_dict(
    {
        "HD": {"VN": "1.6", "SO": "coordinate"},
        "SQ": [{"SN": "chr1", "LN": 248_956_422}],
    }
)

with pysam.AlignmentFile("new.bam", "wb", header=header) as output:
    read = pysam.AlignedSegment(output.header)
    read.query_name = "read001"
    read.query_sequence = "ACGTACGTAA"
    read.flag = 0
    read.reference_id = output.get_tid("chr1")
    read.reference_start = 100
    read.mapping_quality = 60
    read.cigartuples = [(pysam.CIGAR_OPS.CMATCH, 10)]
    read.query_qualities = pysam.qualitystring_to_array("IIIIIIIIII")
    output.write(read)

Construct records against the destination header so numeric reference IDs map correctly. Sequence length, qualities, and query-consuming CIGAR operations must agree.

Raw lines 363 to 389
390## Validation
392For BAM/CRAM:
394```python
395pysam.samtools.quickcheck("-v", "filtered.bam")
396pysam.samtools.index("filtered.bam", catch_stdout=False)
397```
399`quickcheck` checks basic headers and EOF markers, not biological correctness.
400Reopen the file, verify header/reference compatibility, and inspect expected
401regions. Read `cram_and_performance.md` for CRAM-specific validation.

Validation

For BAM/CRAM:

pysam.samtools.quickcheck("-v", "filtered.bam")
pysam.samtools.index("filtered.bam", catch_stdout=False)

quickcheck checks basic headers and EOF markers, not biological correctness. Reopen the file, verify header/reference compatibility, and inspect expected regions. Read cram_and_performance.md for CRAM-specific validation.

Raw lines 390 to 401