Efficient parsing of FASTQ
1. Bugs

All the bugs in this section are to do with handling of bad input; see §2.1 below for more about this issue.


If there are no sequences in the input, your function wrongly yields an output sequence:

&gt;&gt;&gt; from StringIO import StringIO
&gt;&gt;&gt; list(read_fastq(StringIO('')))
[(None, None, None)]

If the input is wrongly formatted, your program can go into an infinite loop waiting for a line starting with a plus sign:

&gt;&gt;&gt; list(read_fastq(StringIO('@ID\nSEQUENCE\n')))
^C
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
  File "cr32897.py", line 155, in read_fastq
    sequence += line.rstrip().replace(' ', '')
KeyboardInterrupt

If the input is wrongly formatted, your program can go into an infinite loop waiting for the end of the quality data:

&gt;&gt;&gt; list(read_fastq(StringIO('@ID\nSEQUENCE\n+ID\nSCORE')))
^C
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
  File "cr32897.py", line 162, in read_fastq
    if len(score) &gt;= len(sequence):
KeyboardInterrupt



2. Other comments on your code


You don't detect or report problems in the input. You just ignore or suppress them. This leads to outright bugs, as noted in §1 above, but it also means that you don't detect other problems with the input, like these:


missing sequence id;
junk lines before the first sequence or between sequences;
sequence id on + line fails to match sequence id on @ line;
length of quality data fails to match length of sequence data;
quality data contains characters outside the correct range.


You shouldn't ignore or suppress errors in the input, because you need to know about these errors in order to discover problems with your processing pipeline. What if you are being fed the wrong input? What if the input is in the wrong format (for example, FASTA instead of FASTQ)? What if there is a bug in the code that generated the input? If you don't detect errors then you will end up generating bogus output and causing trouble for whatever process is next in your processing pipeline.

When writing code that parses input data, the code for detecting and reporting errors typically constitutes the majority of the code. See §3 below for how I would go about writing this kind of parser.
The first item of each output tuple (the sequence id) starts with the @ sign, but the @ sign is not part of the id.
Your claim, "Deciding what is a quality score and what is an id is a tricky endeavor with many pitfalls" is a bit exaggerated. As far as I can see there is just one pitfall, namely that you might incorrectly stop reading the quality data when you encounter a line starting with @. If there are other pitfalls, you should probably mention them in a comment in the code so that we can check that they are all avoided.
It's not clear to me what the H means in the variable name fileH. Does it stand for "handle"? If that's what you mean, you should write it out for clarity: file_handle. But really there's no reason not just to use the name file here.
Your code contains two docstrings, but only the first is actually available to a user via the help function. The second docstring should be a comment instead — or better still, break it up into small comments that appear next to the relevant bits of code.
The actual comments could be better. There is really no need to write comments like this:

line = fileH.readline()

#break if we hit the end of the file
if not line:
    break


You can expect Python programmers to know that file.readline returns an empty string to indicate end-of-file; or if not, they can find out by running help(file.readline).

Some of the comments are wrong, for example here:

#get our first idSeq
idSeq = line.rstrip()


This only gets the "first" idSeq if there was no previous line starting with an @.
For an infinite loop, you write:

while 1:


but it's conventional to write:

while True:

In Python, a file object is also an iterator that yields the lines in the file, so instead of

line = fileH.readline()

#break if we hit the end of the file
if not line:
    break


you can write:

line = next(fileH)


This raises StopIteration when there are no more lines.
You build up the sequence string by repeated addition:

sequence = ""
while not line.startswith('+'):
    sequence += line.rstrip().replace(' ', '')
    line = fileH.readline()


In Python this is an anti-pattern that results in quadratic space and time consumption. The efficient way to build up a string from components is to put the components in a list and then call str.join:

sequence_lines = []
while not line.startswith('+'):
    sequence_lines.append(line.rstrip().replace(' ', ''))
    line = fileH.readline()
sequence = ''.join(sequence_lines)

You build up the score in the form of a list rather than a string:

score = []
while 1:
    score += line.rstrip().replace(' ', '')
    ...


which means that your results come out like this:

('@ID', 'GATTTGGG', ['!', "'", "'", '*', '(', '(', '(', '('])


Is that really what you want? I would have expected score to come out as a string, like this:

('ID', 'GATTTGGG', "!''*((((")



3. Revised code

Here's some revised code that fixes the problems above, and is much more robust in its detection and reporting of errors in the input.

class Error(Exception):
    pass

class Line(str):
    """A line of text with associated filename and line number."""
    def error(self, message):
        """Return an error relating to this line."""
        return Error("{0}({1}): {2}\n{3}"
                     .format(self.filename, self.lineno, message, self))

class Lines(object):
    """Lines(filename, iterator) wraps 'iterator' so that it yields Line
    objects, with line numbers starting from 1. 'filename' is used in
    error messages.

    """
    def __init__(self, filename, iterator):
        self.filename = filename
        self.lines = enumerate(iterator, start=1)

    def __iter__(self):
        return self

    def __next__(self):
        lineno, s = next(self.lines)
        line = Line(s)
        line.filename = self.filename
        line.lineno = lineno
        return line

    # For compatibility with Python 2.
    next = __next__

def read_fastq(filename, iterator):
    """Read FASTQ data from 'iterator' (which may be a file object or any
    other iterator that yields strings) and generate tuples (sequence
    name, sequence data, quality data). 'filename' is used in error
    messages.

    """
    # This implementation follows the FASTQ specification given here:
    # &lt;http://nar.oxfordjournals.org/content/38/6/1767.full&gt;
    import re
    at_seqname_re = re.compile(r'@(.+)$')
    sequence_re = re.compile(r'[!-*,-~]*$')
    plus_seqname_re = re.compile(r'\+(.*)$')
    quality_re = re.compile(r'[!-~]*$')

    lines = Lines(filename, iterator)
    for line in lines:
        # First line of block is @&lt;seqname&gt;.
        m = at_seqname_re.match(line)
        if not m:
            raise line.error("Expected @&lt;seqname&gt; but found:")
        seqname = m.group(1)
        try:
            # One or more lines of sequence data.
            sequence = []
            for line in lines:
                m = sequence_re.match(line)
                if not m:
                    break
                sequence.append(m.group(0))
            if not sequence:
                raise line.error("Expected &lt;sequence&gt; but found:")

            # The line following the sequence data consists of a plus
            # sign and an optional sequence name (if supplied, it must
            # match the sequence name from the start of the block).
            m = plus_seqname_re.match(line)
            if not m:
                raise line.error("Expected +[&lt;seqname&gt;] but found:")
            if m.group(1) not in ['', seqname]:
                raise line.error("Expected +{} but found:".format(seqname))

            # One or more lines of quality data, containing the same
            # number of characters as the sequence data.
            quality = []
            n = sum(map(len, sequence))
            while n &gt; 0:
                line = next(lines)
                m = quality_re.match(line)
                if not m:
                    raise line.error("Expected &lt;quality&gt; but found:")
                n -= len(m.group(0))
                if n &lt; 0:
                    raise line.error("&lt;quality&gt; is longer than &lt;sequence&gt;:")
                quality.append(m.group(0))

            yield seqname, ''.join(sequence), ''.join(quality)

        except StopIteration:
            raise line.error("End of input before sequence was complete:")