diff --git a/CHANGELOG.md b/CHANGELOG.md
index 588c254..8ddbc7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.0.53 Nov 23, 2018
+
+Fixed [#650](https://github.com/acorg/dark-matter/issues/650),
+exception in `SAMFilter` when quality is `*` in a SAM file.
+
 ## 3.0.52 Nov 23, 2018
 
 Added hard-clipping to CIGAR in SAM created by `convert-diamond-to-sam.py`.
diff --git a/dark/__init__.py b/dark/__init__.py
index 147b204..2f33ce1 100644
--- a/dark/__init__.py
+++ b/dark/__init__.py
@@ -7,4 +7,4 @@ if sys.version_info < (2, 7):
 # will not be found by the version() function in ../setup.py
 #
 # Remember to update ../CHANGELOG.md describing what's new in each version.
-__version__ = '3.0.52'
+__version__ = '3.0.53'
diff --git a/dark/sam.py b/dark/sam.py
index f6052ca..b86dbb2 100644
--- a/dark/sam.py
+++ b/dark/sam.py
@@ -70,8 +70,9 @@ def _hardClip(sequence, quality, cigartuples):
     Hard clip (if necessary) a sequence.
 
     @param sequence: A C{str} nucleotide sequence.
-    @param quality: A C{str} quality string or a C{list} of C{int} quality
-        values as returned by pysam.
+    @param quality: A C{str} quality string, or a C{list} of C{int} quality
+        values as returned by pysam, or C{None} if the SAM file had a '*'
+        for the quality string (which pysam converts to C{None}).
     @param cigartuples: An iterable of (operation, length) tuples, detailing
         the alignment, as per the SAM specification.
     @return: A 3-tuple consisting of
@@ -90,7 +91,8 @@ def _hardClip(sequence, quality, cigartuples):
         cigarLength += length if operation in _CONSUMES_QUERY else 0
 
     sequenceLength = len(sequence)
-    assert sequenceLength == len(quality)
+    if quality is not None:
+        assert sequenceLength == len(quality)
     clipLeft = clipRight = 0
     clippedSequence = sequence
     clippedQuality = quality
@@ -109,12 +111,14 @@ def _hardClip(sequence, quality, cigartuples):
             if not alreadyClipped:
                 clipLeft = cigartuples[0][1]
                 clippedSequence = sequence[clipLeft:]
-                clippedQuality = quality[clipLeft:]
+                if quality is not None:
+                    clippedQuality = quality[clipLeft:]
         elif cigartuples[-1][0] == CHARD_CLIP:
             if not alreadyClipped:
                 clipRight = cigartuples[-1][1]
                 clippedSequence = sequence[:-clipRight]
-                clippedQuality = quality[:-clipRight]
+                if quality is not None:
+                    clippedQuality = quality[:-clipRight]
         else:
             raise ValueError(
                 'Invalid CIGAR tuples (%s) contains hard-clipping operation '
@@ -126,7 +130,8 @@ def _hardClip(sequence, quality, cigartuples):
         if not alreadyClipped:
             clipLeft, clipRight = cigartuples[0][1], cigartuples[-1][1]
             clippedSequence = sequence[clipLeft:-clipRight]
-            clippedQuality = quality[clipLeft:-clipRight]
+            if quality is not None:
+                clippedQuality = quality[clipLeft:-clipRight]
     else:
         raise ValueError(
             'Invalid CIGAR tuples (%s) specifies hard-clipping %d times (2 '
@@ -147,7 +152,9 @@ def _hardClip(sequence, quality, cigartuples):
                  abs(len(sequence) - len(clippedSequence)),
                  clipLeft + clipRight, clipLeft, clipRight, cigartuples))
     else:
-        assert len(clippedSequence) == len(clippedQuality) == sequenceLength
+        assert len(clippedSequence) == sequenceLength
+        if quality is not None:
+            assert len(clippedQuality) == sequenceLength
 
     return clippedSequence, clippedQuality, weClipped
 

SAMFilter raises if pysam returns an alignment with a None quality string
If a SAM file has `*` for a quality string, `pysam` gives back a `None` field and this causes an exception in `dark/sam.py`:

```
$ read-sam.py 180426-18-mouse-L1.sam
Traceback (most recent call last):
  File "./read-sam.py", line 8, in <module>
    for alignment in sam.alignments():
  File "/Users/terry/dark-matter/dark-matter/dark/sam.py", line 365, in alignments
    alignment.cigartuples)
  File "/Users/terry/dark-matter/dark-matter/dark/sam.py", line 93, in _hardClip
    assert sequenceLength == len(quality)
TypeError: object of type 'NoneType' has no len()
```
diff --git a/test/test_sam.py b/test/test_sam.py
index ca2e4d0..42ef202 100644
--- a/test/test_sam.py
+++ b/test/test_sam.py
@@ -265,6 +265,23 @@ class TestSAMFilter(TestCase):
             self.assertEqual('ZZZZZZ', ''.join(
                 map(lambda x: chr(x + 33), alignment.query_qualities)))
 
+    def testNoQuality(self):
+        """
+        If an alignment has * for the quality string, the filter must
+        return an alignment with a C{None} quality value.
+        """
+        data = '\n'.join([
+            '@SQ SN:ref LN:10',
+            'query1 4 * 0 0 6M * 0 0 TCTAGG *',
+        ]).replace(' ', '\t')
+
+        with dataFile(data) as filename:
+            sf = SAMFilter(filename)
+            (alignment,) = list(sf.alignments())
+            self.assertEqual('query1', alignment.query_name)
+            self.assertEqual('TCTAGG', alignment.query_sequence)
+            self.assertIsNone(alignment.query_qualities)
+
 
 class TestPaddedSAM(TestCase):
     """

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
backcall==0.2.0
biopython==1.79
bz2file==0.98
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
cycler==0.11.0
-e git+https://github.com/acorg/dark-matter.git@e48f39aded59ac675d9b250f3cb0c7677968fbe3#egg=dark_matter
decorator==5.1.1
ete3==3.1.3
flake8==5.0.4
idna==3.10
importlib-metadata==4.2.0
iniconfig==1.1.1
ipython==7.16.3
ipython-genutils==0.2.0
jedi==0.17.2
kiwisolver==1.3.1
matplotlib==3.3.4
mccabe==0.7.0
mysql-connector-python==8.0.11
numpy==1.19.5
packaging==21.3
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
Pillow==8.4.0
pluggy==1.0.0
prompt-toolkit==3.0.36
protobuf==3.19.6
ptyprocess==0.7.0
py==1.11.0
pycodestyle==2.9.1
pycparser==2.21
pyfaidx==0.5.0
pyflakes==2.5.0
Pygments==2.14.0
pyparsing==3.1.4
pysam==0.23.0
pytest==7.0.1
python-dateutil==2.9.0.post0
pyzmq==25.1.2
requests==2.27.1
simplejson==3.20.1
six==1.17.0
tomli==1.2.3
traitlets==4.3.3
typing_extensions==4.1.1
urllib3==1.26.20
wcwidth==0.2.13
zipp==3.6.0

name: dark-matter
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - backcall==0.2.0
      - biopython==1.79
      - bz2file==0.98
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - cycler==0.11.0
      - decorator==5.1.1
      - ete3==3.1.3
      - flake8==5.0.4
      - idna==3.10
      - importlib-metadata==4.2.0
      - iniconfig==1.1.1
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - jedi==0.17.2
      - kiwisolver==1.3.1
      - matplotlib==3.3.4
      - mccabe==0.7.0
      - mysql-connector-python==8.0.11
      - numpy==1.19.5
      - packaging==21.3
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pillow==8.4.0
      - pluggy==1.0.0
      - prompt-toolkit==3.0.36
      - protobuf==3.19.6
      - ptyprocess==0.7.0
      - py==1.11.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pyfaidx==0.5.0
      - pyflakes==2.5.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pysam==0.23.0
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pyzmq==25.1.2
      - requests==2.27.1
      - simplejson==3.20.1
      - six==1.17.0
      - tomli==1.2.3
      - traitlets==4.3.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - zipp==3.6.0
prefix: /opt/conda/envs/dark-matter

test/test_sam.py::TestSAMFilter::testNoQuality
test/test_sam.py::TestSAMFilter::testAlignmentCount test/test_sam.py::TestSAMFilter::testCloseButNoCIGAR test/test_sam.py::TestSAMFilter::testDropDuplicates test/test_sam.py::TestSAMFilter::testDropSecondary test/test_sam.py::TestSAMFilter::testDropSupplementary test/test_sam.py::TestSAMFilter::testKeepQualityControlFailures test/test_sam.py::TestSAMFilter::testMaxScore test/test_sam.py::TestSAMFilter::testMaxScoreNoScores test/test_sam.py::TestSAMFilter::testMinAndMaxScore test/test_sam.py::TestSAMFilter::testMinLength test/test_sam.py::TestSAMFilter::testMinScore test/test_sam.py::TestSAMFilter::testMinScoreNoScores test/test_sam.py::TestSAMFilter::testStoreQueryIds test/test_sam.py::TestSAMFilter::testUnknownReferences test/test_sam.py::TestPaddedSAM::testAllMMatch test/test_sam.py::TestPaddedSAM::testAllowDuplicateIds test/test_sam.py::TestPaddedSAM::testAlsoYieldAlignments test/test_sam.py::TestPaddedSAM::testDropDuplicates test/test_sam.py::TestPaddedSAM::testDropSecondary test/test_sam.py::TestPaddedSAM::testDropSupplementary test/test_sam.py::TestPaddedSAM::testDuplicateIdDisambiguation test/test_sam.py::TestPaddedSAM::testHardClipLeft test/test_sam.py::TestPaddedSAM::testHardClipRight test/test_sam.py::TestPaddedSAM::testHardClippingInCIGARButQueryNotHardClipped test/test_sam.py::TestPaddedSAM::testKF414679SoftClipLeft test/test_sam.py::TestPaddedSAM::testKeepQualityControlFailures test/test_sam.py::TestPaddedSAM::testMinLength test/test_sam.py::TestPaddedSAM::testMixedMatch test/test_sam.py::TestPaddedSAM::testMixedMatchSpecificReference test/test_sam.py::TestPaddedSAM::testMixedMatchSpecificReferenceButNoMatches test/test_sam.py::TestPaddedSAM::testNotSecondaryAndNotSupplementaryWithNoSequence test/test_sam.py::TestPaddedSAM::testPrimaryAndSecondaryReferenceInsertion test/test_sam.py::TestPaddedSAM::testQueryHardClipAndSoftClipProtrudesBothSides test/test_sam.py::TestPaddedSAM::testQuerySoftClipLeft test/test_sam.py::TestPaddedSAM::testQuerySoftClipProtrudesBothSides test/test_sam.py::TestPaddedSAM::testQuerySoftClipProtrudesLeft test/test_sam.py::TestPaddedSAM::testQuerySoftClipProtrudesRight test/test_sam.py::TestPaddedSAM::testQuerySoftClipReachesLeftEdge test/test_sam.py::TestPaddedSAM::testQuerySoftClipReachesRightEdge test/test_sam.py::TestPaddedSAM::testQuerySoftClipRight test/test_sam.py::TestPaddedSAM::testQueryTooLong test/test_sam.py::TestPaddedSAM::testRcNeeded test/test_sam.py::TestPaddedSAM::testRcSuffix test/test_sam.py::TestPaddedSAM::testReferenceDeletion test/test_sam.py::TestPaddedSAM::testReferenceDeletionAlternateChars test/test_sam.py::TestPaddedSAM::testReferenceInsertion test/test_sam.py::TestPaddedSAM::testReferenceSkip test/test_sam.py::TestPaddedSAM::testReferenceSkipAlternateChars test/test_sam.py::TestPaddedSAM::testSecondaryAlignmentHasQuery test/test_sam.py::TestPaddedSAM::testSecondaryWithNoPreviousSequence test/test_sam.py::TestPaddedSAM::testSecondaryWithNoSequence test/test_sam.py::TestPaddedSAM::testSupplementaryAlignmentHasQuery test/test_sam.py::TestPaddedSAM::testSupplementaryWithNoPreviousSequence test/test_sam.py::TestPaddedSAM::testSupplementaryWithNoSequence test/test_sam.py::TestPaddedSAM::testUnequalReferenceLengths test/test_sam.py::TestSamReferencesToStr::testIndent test/test_sam.py::TestSamReferencesToStr::testSimple test/test_sam.py::TestHardClip::testClipBoth test/test_sam.py::TestHardClip::testClipBothAlreadyDone test/test_sam.py::TestHardClip::testClipLeft test/test_sam.py::TestHardClip::testClipLeftAlreadyDone test/test_sam.py::TestHardClip::testClipRight test/test_sam.py::TestHardClip::testClipRightAlreadyDone test/test_sam.py::TestHardClip::testHardClipInMiddle test/test_sam.py::TestHardClip::testNoClip test/test_sam.py::TestHardClip::testThreeHardClips
dark/__init__.py dark/sam.py CHANGELOG.md
dark/__init__.py dark/sam.py CHANGELOG.md