…which is everything in the GC calculator you wrote:
name = "BRCA1_exon3"
sequence = "ATGCGTACGTAGGCTA"
length = len(sequence)
gc_count = sequence.count("G") + sequence.count("C")
gc_pct = round(gc_count / length * 100, 1)
print(f"Gene: {name}")
print(f"Length: {length} bases")
print(f"GC content: {gc_pct}%")
She can analyse one sequence. Now her supervisor hands her a whole folder of them. Making a separate variable — seq1, seq2, seq3… — for each one would be madness.
How do we move from handling one piece of data to handling many similar pieces at once? Python's answer is a container — and the simplest one is a list.
So far each variable held one value. Real data comes in collections — folders of sequences, tables of results. A container is a value that holds many other values. Today's three all share one idea: you reach inside with [ ].
An ordered collection — hold many sequences at once.
A string is a sequence of characters — reach in to pull out codons.
A lookup table — maps each codon to its amino acid.
Write the items between [ ], separated by commas. The order you write them in is the order they keep. len() tells you how many there are.
Run it, then add a gene of your own to the list and try running it again.
genes[0] is the first item, genes[-1] the last. This trips up everyone at first — the first item is at position zero.
genes[1:3] gives items 1 and 2 — the end is not included.
Leave out the start (genes[:2]) or end (genes[2:]) to slice from the beginning or to the end of the list.
Run it — then try genes[2] and genes[1:-1]
The most common way a list grows: by adding items one-by-one to the end. This is exactly how we'll collect results inside a loop next session.
The same + you used to add numbers also joins lists end to end — just like it joins two strings together.
Run it, then try appending another gene before joining the lists.
Assign to a position — genes[1] = "KRAS" — and that item is swapped out. The list keeps its identity; only the contents change.
del genes[0] drops the first item; everything after it shuffles up. The list is now one shorter.
Run it — watch the positions shift after del. What happens if you del genes[-1] and then print the list?
A list doesn't care what it holds — strings, ints, floats, booleans — it can hold them all.
We could use one to hold a gene record: its name, length, GC fraction, and a Quality Control (QC) flag.
An item can itself be a list. Stack rows and you have a table; reach in twice — plate[1][0] — to get one cell. This is the shape of the data tables we'll meet later.
Run it, then try reaching into the table with plate[2][1] — which row and column does it print?
When a list holds numbers — replicate measurements, read counts, colony numbers — sum() totals them in one step. No need to add them by hand.
Pair sum() with len() and you have the average — sum(readings) / len(readings). The everyday workhorse of any analysis.
Run it — then add a sixth reading and watch the mean shift.
You've collected 20 readings from a growth assay. One was a pipetting error and a late replicate has just come in. Let's clean them up and report the mean together.
readings = [42, 17, 88, 63, 29, 55, 71, 34, 90, 12,
47, 68, 23, 81, 59, 36, 74, 50, 28, 65]Warm-up: which line gives you the third reading, 88?
readings = [42, 17, 88, 63, 29, ...]
readings[3]readings[2]readings[1]readings(2)The 2nd reading (17) was a pipetting error. Which line removes it?
readings = [42, 17, 88, 63, 29, ...]
del readings[2]readings[1] = 0del readings[1]del readings(1)A late replicate came in. Which line adds 19 to the end?
readings.append(19)readings.append[19]readings + 19readings[20] = 19We started with 20 readings. After both edits, what does this print?
del readings[1] readings.append(19) print(len(readings))
19212018Which line gives the mean, rounded to 1 decimal place?
round(sum(readings) // len(readings), 1)round(sum(readings) / len(readings), 1)round(sum(readings) / 20)readings.sum() / readings.len()Try this after the lecture. Write the five steps you just voted on as code. Press Run and each step is checked for you.
A string holding a DNA sequence is just an ordered collection of bases. Reach in by position exactly as you did with lists:
dna[0] is the first base, dna[-1] the last, and len(dna) counts them.
Run it — then guess what dna[1] and dna[-2] will output. Try running them and see if you were right.
When text is saved or sent, each character is encoded as a pattern of bits. The worldwide standard is UTF-8, and it uses more bits for fancier characters.
With just four options, one base could be packed into 2 bits (A=00, T=01, G=10, C=11). But as an ordinary text character it takes 8 bits — four times bigger than it needs to be. Across a 3-billion-base genome, that waste adds up.
For squeezing whole genomes into memory, specialised tools pack 4 bases into each byte with bit operations. But for learning — and for almost every working biologist — a plain, readable ATGC string is exactly the right tool. Clarity beats cleverness.
A codon is a 3-base slice: seq[0:3] grabs the first three bases. Just like with lists, we slice with [start:end], and the end is not included, so 0:3 gives positions 0, 1, 2.
Run it — then can you figure out how to slice the 3rd codon?
The same operators you used on numbers and lists also build strings — primer + seq joins them, and "N" * 5 repeats the string.
Unlike a list, you can't edit a string in place — seq[0] = "T" is a TypeError. To “change” a string you build a new one with slicing and +.
Run it — then uncomment the last line to see the TypeError when you try to edit one character of the string in place.
Strings carry their own tools, called with a dot: seq.upper(). You met .count() last week — here are the ones you'll use the most.
Methods can be chained together — the output of one becomes the input of the next. seq.upper().find("GAATTC") makes the string uppercase, then searches for the EcoRI motif.
Run it — then try .find("TTT") to look for a motif that isn't there. How many "AT"s are in the sequence?
dna.replace("T", "U") returns a new string with every T turned into U — DNA transcription in one line.
Chaining .replace() to make a complement breaks: turn every A into T, then every T into A, and the A's you just made flip straight back. To complement each base independently we need a loop — that's Session 3.
Run it — if the final line made the complement, it would print TACGCA, but it doesn't. We need loops for this — next week!
One messy sequence in, a tidy summary out. Clean it first, then measure it, read its first codon, count its GC and find the EcoRI site.
# A sequence straight from a file — messy lower-case dna = "atgcgtacggaattcaaatag"
Which line stores an upper-case copy of the sequence in clean?
dna = "atgcgtacggaattcaaatag"
clean = dna.upperclean = upper(dna)clean = dna.upper()clean = dna.UPPER()How would you find the number of bases in the sequence?
clean = "ATGCGTACGGAATTCAAATAG"
print(len(clean))print(clean.length())print(length(clean))print(clean.count())Which slice gives you the first codon, ATG?
clean = "ATGCGTACGGAATTCAAATAG"
clean[1:4]clean[0:3]clean[0:2]clean[3]Which line counts all the G and C bases?
clean.count("GC")dna.count("G") + dna.count("C")clean.count("G", "C")clean.count("G") + clean.count("C")How can you locate the starting position of the EcoRI site?
clean = "ATGCGTACGGAATTCAAATAG"
"GAATTC" in cleanclean.search("GAATTC")clean.find("GAATTC")find(clean, "GAATTC")Try this after the lecture. Write the five steps you just voted on as code. Press Run and each step is checked for you.
A list finds things by position (0, 1, 2). A dictionary finds each value it holds via a paired key — here, a codon. Write the pairs in { } as key: value.
The genetic code is a lookup table: each three-base codon maps to one amino acid. That's a dictionary, exactly.
Run it — then add a fourth codon of your own (you can make it up or find one online!).
Any time data comes with labels, a dictionary fits. It's the most common way to store structured data in Python — config files, web data (JSON) and results tables are all dictionaries underneath. And the value can be anything:
{
"red_fox": 12,
"rabbit": 340,
"buzzard": 5,
}Species → count. The value is a number.
{
"neuron_1": [12, 45, 78],
"neuron_2": [9, 33],
}Neuron → spike times. The value is a list.
{
"p_01": {"age": 34, "bp": 128},
"p_02": {"age": 51, "bp": 142},
}Patient → record. The value is another dict.
Numbers, strings, lists, even other dictionaries — that flexibility is why dictionaries turn up in every field.
What if we want to look up just the keys or the values in our codon table? We can use the .keys() and .values() methods to do that.
Sometimes we want to see both the key and the value together. The .items() method returns all of the key–value pairs in the dictionary. We'll be using it in the next lecture.
Remember, each item in a dictionary maps a key to a value.
The same [ ] you used to reach into lists and strings — but instead of a position you give the key, and Python hands back its value. One step, instant answer.
Run it — then look up "GGT".
codon_table["TTT"] stops your program if TTT was never added. Real data is full of surprises — you need a safer way to ask.
If the key exists you get its value; if not, you get the fallback you chose — "?" here. Perfect for an unknown or malformed codon.
Run it — then uncomment the ["TTT"] line to meet the KeyError.
table["TAA"] = "Stop" adds a new entry. If you use a key that already exists, it overwrites the old value — no error, no duplicate.
"ATG" in codon_table answers True/False — your loop-free way to search for a key. len() counts the entries.
Run it — watch GGT get updated, not duplicated. Then try adding a new codon!
Every question uses this codon table. The last one puts today together: slice a codon out of the sequence, then look it up.
codon_table = {"ATG": "Met", "GGT": "Gly", "TAA": "Stop"}
seq = "ATGGGTTAA"Which line makes a dictionary that maps "ATG" to "Met"?
["ATG": "Met"]{"ATG", "Met"}{"ATG": "Met"}{"ATG" = "Met"}What does this print?
codon_table = {"ATG": "Met", "GGT": "Gly", "TAA": "Stop"}
print(len(codon_table))631Which line gives you "Gly"?
codon_table = {"ATG": "Met", "GGT": "Gly", "TAA": "Stop"}codon_table["GGT"]codon_table[1]codon_table["Gly"]codon_table("GGT")Which line shows all the amino acids in the table?
codon_table = {"ATG": "Met", "GGT": "Gly", "TAA": "Stop"}codon_table.keys()codon_table.items()codon_table["values"]codon_table.values()Slice, then look up. What does this print?
codon_table = {"ATG": "Met", "GGT": "Gly", "TAA": "Stop"}
seq = "ATGGGTTAA"
print(codon_table.get(seq[3:6], "???"))Met???GlyKeyErrorTry this after the lecture. It puts both halves of today together: slice each codon out of the sequence, then look it up in the table. Press Run and each step is checked for you.
You started with one sequence and ended up translating it by hand — slicing out codons and looking each one up in a table. Three new containers got you there.
Today you translated three codons by hand. But a real gene has hundreds — you can't write a line for each. Next time we hand the repetition to the computer, so it can run through a whole sequence on its own.
You now have every piece a translator needs — a sequence, a way to slice it, and a table to look codons up in. Control flow is what turns those pieces into one autonomous program.
See you next week!