47  AI: The docs are the test

You have just met BioPython, and the note on reading it as a catalog of contracts mentioned, in passing, the assistant’s weakest habit around libraries, which is that it sometimes invents a method that sounds exactly right and does not exist. Here is that failure in the flesh, so that you recognize it the first time it happens to you for real, and so that you know what to reach for when it does.

A method that does not exist

Ask an assistant for the GC content of a BioPython Seq object and you may well get back something like this.

from Bio.Seq import Seq

dna = Seq('ATGGCCTGA')
print(dna.gc_content())

Read that before you run it. It looks completely unremarkable. The method name matches the concept exactly, it is called the same way .translate() and .reverse_complement() were in the last note, and it is called on the same kind of object. It has every surface feature of a correct call. Run it anyway.

AttributeError: 'Seq' object has no attribute 'gc_content'

There is no .gc_content() method on a Seq object. The assistant did not look this up. It predicted a plausible next method name given everything it has seen written about sequences and GC content, in the same way it predicts a plausible next word, and gc_content was a very good guess by that measure. It is simply not the guess BioPython’s actual authors made. The real function lives elsewhere, as Bio.SeqUtils.gc_fraction, called on the sequence rather than as a method of it.

from Bio.SeqUtils import gc_fraction

print(gc_fraction(dna))

What actually caught it

Notice exactly what caught this, because it generalizes to every library and every interface you will ever touch. It was not that the code looked wrong, because it looked fine right up until it ran. It was not a hunch either. It was the documentation, checked, which had no entry for Seq.gc_content and did have an entry for SeqUtils.gc_fraction, with a stated contract that matched what you actually wanted.

The real documentation is the test an invented call fails, exactly the way a hand-written assert is a test a wrong function fails. It is the only thing that reliably catches this particular failure, because reading the call a second time, more carefully, will not help. It was written to look right.

Checking from the machine, not just the browser

Checking the documentation so far has meant opening a browser tab and reading a page, and that works. It is not the only way, and the other way puts the proving half of “the assistant predicts, the machine proves” back in your own hands instead of a search engine’s. From inside the interpreter itself, dir(dna) lists every attribute and method the object actually has, so 'gc_content' in dir(dna) would have told you, before you ever ran anything, that the method the assistant offered simply is not there. In an interactive session or a notebook, typing dna. and pressing tab does the same job, showing you the real list rather than a remembered or guessed one. And help(gc_fraction), once you have imported it, prints the same contract the web page would have shown you: what it takes, what it returns, and any assumptions stated in its description, without leaving the file you are already working in.

Reading what either of those shows you is its own small skill. A signature such as gc_fraction(seq, ambiguous="remove") is telling you two things in one line: seq is required, since it has no default and the call fails without it, and ambiguous is optional, since ="remove" is exactly what happens if you say nothing about it. An assistant’s call that quietly drops a required argument, or supplies an optional one you never asked it to set, becomes visible the moment you hold it up against this line, which is the actual reason to read a signature yourself rather than trust that a call must be fine because it runs. The last line of what help() shows you, usually a short description of the return value, is the one students skip first and need most. It is exactly what would have told you, before a single test, that gc_fraction hands back a fraction rather than a percentage.

The call can be right and the assumption still wrong

Fix the call from earlier and you are not quite finished. Once you know to reach for gc_fraction instead of the invented .gc_content(), the natural next step is to call it and believe whatever number comes back, because the call itself is now correct: real function, real object, no AttributeError anywhere in sight.

from Bio.SeqUtils import gc_fraction

dna = Seq('ATGGCCTGA')
print(gc_fraction(dna))

Run that and you get 0.5555555555555556. If you have spent any time at all around sequence data, GC content is a number people say out loud as a percentage, “fifty-six percent GC,” and it would be entirely natural to read 0.556 and assume the function rounded strangely, or to multiply it by 100 out of habit and get a number that looks right without ever checking whether doing so was necessary. Nothing about this crashes. Nothing about it looks wrong. It is the quietest of the ways an assistant, or you, can be mistaken about a real function: the call is exactly right, and the assumption about what the returned value means is not. The signature’s return line would have settled this immediately: it names a fraction, not a percentage.

This is exactly where a documentation check earns its keep by becoming a test rather than staying a fact you read once and might misremember under pressure two weeks from now. You already know how to do this from Chapter 43: work out the answer to a small case by hand, and pin the contract down with assert before you build anything else on top of it.

assert gc_fraction(Seq('GGCC')) == 1.0
assert gc_fraction(Seq('ATAT')) == 0.0

Written this way, the fact that the function returns a fraction is no longer something to remember correctly under pressure. It is enforced, the same way every contract in this course eventually becomes an assert instead of a memory, and the next person to read your code, including you in a month, inherits the corrected assumption for free.

Four ways to be wrong, and the traceback each one leaves

Step back from the two examples so far and name the whole shape of the problem, because “an invented function” is actually four different failures wearing the same fluent surface, and each one leaves a different piece of evidence behind.

The first is the one that opened this note: a method that does not exist at all, on this object or on any other. Python’s answer is an AttributeError, naming the object and the missing attribute exactly, 'Seq' object has no attribute 'gc_content'. Take that message at face value: it is Python telling you, correctly, that no such thing has ever existed on this object.

The second is a real function called with an argument that does not exist. Ask for the second-frame translation of a coding sequence and you might get back a call like this, which reads perfectly, because plenty of other bioinformatics tools do take a frame argument:

from Bio.Seq import Seq

dna = Seq('ATGGCCTGA')
dna.translate(frame=2)

Seq.translate() is not one of those tools. Its signature holds table, stop_symbol, to_stop, cds, and gap, nothing named frame anywhere in it, so Python raises a different exception this time:

TypeError: translate() got an unexpected keyword argument 'frame'

The fix is a different approach rather than a different argument: slice the sequence to the frame you want before translating it, dna[1:].translate() for frame two. A TypeError on a call that otherwise looks entirely right is almost always this mode, the right idea aimed at an argument the real function was never given.

The third is the one the last section just walked through: a real function, called correctly, whose return value means something other than what you assumed. This is the quietest of the four, because it raises no exception at all. Nothing crashes, so nothing prompts you to go looking, which is exactly why the read-the-signature habit has to run every time, not only when something visibly breaks.

The fourth is a real function that used to exist and does not anymore. Bio.SeqUtils.GC is the example with an exact paper trail: for years it was the standard way to get GC content, deprecated in BioPython’s 1.80 release in favor of gc_fraction, and removed outright in release 1.82. Try this on a current install:

from Bio.SeqUtils import GC
ImportError: cannot import name 'GC' from 'Bio.SeqUtils'

This is the most unsettling of the four to meet as a beginner, because the assistant was not guessing and was not wrong about syntax. It was reporting something that used to be true, about a version of the library that no longer matches the one sitting in your own environment. An AttributeError means the thing was never real. An ImportError on a name that clearly used to mean something means it was real once, and calls for a different next step than the other three: search the documentation for your installed version specifically, since the page a search engine hands you first may still be describing the GC that used to exist rather than the gc_fraction that replaced it. A search engine has no way of knowing which version sits in your own environment, so before you trust what any page says a function does, check your own copy from inside the interpreter itself, import Bio; print(Bio.__version__), and read the documentation for that exact version rather than whichever one happened to load first.

The same failure outside libraries

The same failure shows up with web services, not just libraries. An assistant will confidently name an endpoint, a query parameter, or a field in a returned record that the actual service does not offer, with exactly the same fluent certainty whether the field exists or not.

The habit that catches it is identical. Before you trust a call the assistant produced against a library or a service you did not write, find that exact function or endpoint in the real documentation and confirm three things, which are that it exists, that it takes what the assistant assumed it takes, and that it returns what the assistant assumed it returns. If you cannot find it, it very likely is not real, no matter how natural it looked.

Exercise 47-1

AI: Drafter

Ask the assistant for a one-line way to reverse-translate a protein string back into a possible DNA coding sequence using BioPython. Before running anything, search the real BioPython documentation for whatever function or method it names. Does it exist, under that name, taking those arguments? Record what you found either way, because a call that turns out to be real is as much a result as one that turns out to be invented.

Exercise 47-2

SOLO

Before opening the assistant at all, find Bio.SeqUtils.molecular_weight in the real BioPython documentation and answer the three questions from Chapter 46: what it takes, what it returns, and what it assumes. Pay particular attention to any argument that has a default value. Then decide, from the documentation alone, what molecular_weight('AGC') should return, and what would change if the same three letters were meant as a protein sequence rather than a DNA one. Only after you have written your answer down, run it both ways and check yourself against it.

Exercise 47-3

AI: Comparer

Open two separate, fresh conversations. In each, ask for a one-line way to get the amino acid a single codon codes for using BioPython, without writing a codon table by hand yourself. Compare the two answers before you run either one. Do they name the same object and the same call, or did the assistant reach into two different corners of the library, such as Seq(codon).translate() in one answer and something from Bio.Data.CodonTable in the other? Check whatever each one named against the real documentation, the way this note has been doing throughout, and only then run both on a codon whose amino acid you already know by heart. If the two conversations disagreed, at least one of them was a guess dressed as an answer, and checking is how you find out which.

For your logbook this week, note whether you have caught the assistant inventing a function or parameter yet, in this course or elsewhere, and if not, say specifically what you now intend to check before you next call a library method you did not write yourself.