Day 3 — Basic Python

UBDS 2026: Basic Python

Day 3: Take control of your code

Main contributor: @vvsbiocode

Second contributor: ChatGPT & Codex

Topics covered / Сьогоднішні теми

  1. Loop comprehension / …
  2. Writing custom functions / …
  3. Reading errors and debugging in Jupyter/ …
  4. Good practices in Python project structure / …

We will use the biopython package to work with a codon table and pairwise sequence alignment. / …

Import the tools we need

  • biopython helps in processing biological sequences / …
  • logging helps handle messages a program wants the user to know about during execution / …
  • warnings helps to raise custom warnings / …
  • os handles directories / …
from Bio.Data import CodonTable
from Bio import pairwise2
from Bio.Align import substitution_matrices
from Bio.Data import IUPACData
import logging
import warnings
import os

PROBLEM 1: How to fit same amount of code into less lines?

Loop comprehension

To illustrate the principle, we’ll do a typical bioinformatics task:

Transform DNA sequence into a protein!

# Load a standard codon table
standart_codon_table = CodonTable.unambiguous_dna_by_name["Standard"]

Q: What are a triplet and a codon table?

print(standart_codon_table)
# Set a DNA sequence, which encodes a short protein
dna_sequence = "ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"

# Initialize a variable to store the protein sequence
protein1 = ""

# Loop through the sequence in steps of 3
for i in range(0, len(dna_sequence), 3):

    # Select one consecutive codon
    codon = dna_sequence[i:i+3]

    # Retrieve the amino acid encoded by the codon. If the codon is atypical, insert "?"
    # Add amino acid to a protein string
    protein1 += standart_codon_table.forward_table.get(codon, "?")
protein2 = "".join([standart_codon_table.forward_table.get(dna_sequence[i:i+3], "?") for i in range(0, len(dna_sequence), 3)])

Q: Are protein1 and protein2 different?

print("Protein 1", protein1)
print("Protein 2", protein2)
print("Are they the same?", protein1==protein2)

Let’s unfold what just happened.

It is possible to write a loop in one line.

  • Assign a list to a variable.
protein2 = []
  • Include “for” statement.
protein2 = [for i in range(0, len(dna_sequence), 3)]
  • Add execution of the function before “for” statement. Note, how instead of creating codon variable, its value is passed directly.
protein2 = [
    standart_codon_table.forward_table.get(dna_sequence[i:i+3], "?") 
    for i in range(0, len(dna_sequence), 3)
    ]
  • For our case, we have to add another trick. Now, protein2 is a list of amino acids. To convert it into a string, pass the whole statement to the function join() with an empty string “” as the delimiter. This concatenates all string elements of the list.
protein2 = "".join([...])

Of course, you do not have to follow the exact order of the steps. But the final result always looks the same.

Something does not make sense.

Why does this sequence contain unrecognized triplets? Maybe the DNA sequence is not divisible by 3?

We can incorporate a condition inside a loop comprehension to demand that all checked sequences are triplets.

A single condition always follows the for statement.

# Here we again unfold loop comprehension for clarity, but it can be written in one line
protein3 = "".join(
    [
        standart_codon_table.forward_table.get(dna_sequence[i:i+3], "?") 
        for i in range(0, len(dna_sequence), 3)
        if len(dna_sequence[i:i+3]) == 3 
    ]
)

Q: How added condition makes protein3 different from protein2?

print("Protein 2 (w/o condition) length: ", len(protein2))
print("Protein 3 (w/ condition) length: ", len(protein3))
print(protein2)
print(protein3)

There is still one unrecognised triplet in the sequence.

Q: Does codon table include stop codons?

stop_codon = "TGA"
print(standart_codon_table.forward_table.get(stop_codon, "?"))

We can add multiple conditions.

As in full loops, a loop comprehension condition can not only filter out the elements, but also control the output.

Wrap the function call statement into () and insert a condition. Whatever has to be executed if the condition is fulfilled goes before if and second statement goes after else.

protein4 = "".join(
    [
        (
            standart_codon_table.forward_table.get(dna_sequence[i:i+3], "?")
            if dna_sequence[i:i+3] not in standart_codon_table.stop_codons
            else
            "*"
        )
        for i in range(0, len(dna_sequence), 3)
        if len(dna_sequence[i:i+3]) == 3 
    ]
)
print("Protein 3 (w/ one condition) length: ", len(protein3))
print("Protein 4 (w/ two conditions) length: ", len(protein4))
print(protein3)
print(protein4)

From a biological perspective, Protein 4 does not make sense. The protein sequence has to stop after a stop codon.

Note, you can also put several conditions one after another, where the second statement going after the first else is executed when the second if is fulfilled.

Q: Can we put a condition for a loop to stop inside a comprehension?

# This nested condition tries to stop the loop when a stop codon is met
protein5 = "".join(
    [
        (
            standart_codon_table.forward_table.get(dna_sequence[i:i+3], "?")
            if dna_sequence[i:i+3] not in standart_codon_table.stop_codons
            else 
            break 
            if dna_sequence[i:i+3] in standart_codon_table.stop_codons
            else
            ""
        )
        for i in range(0, len(dna_sequence), 3)
        if len(dna_sequence[i:i+3]) == 3 
    ]
)

It is impossible to stop a loop comprehension until it runs out of things to loop through. Also, note how we had to write dna_sequence[i:i+3] four times.

Loop comprehension is meant to shrink simple tasks into one line of code.

To accommodate more complex tasks, either use full loops or write a custom function.

PROBLEM 2: I cannot find a package that does what I want.

Similarly to how biopython functions work for general bioinformatics purposes, we can create our own functions that suit the specific needs of our analysis.

Another advantage of writing a function - you do not need to repeat the same big chunk of code anymore.

Let’s create a function that performs codon-to-amino acid conversion and is aware of the triplet condition and stop codons. Meet a custom function, real_translation!

Q: What do we want a custom function to take as input and give as output?

Similar to variables, a custom function has to be defined before execution.

protein5 = "".join([real_translation(dna_sequence[i:i+3]) for i in range(0, len(dna_sequence), 3)])

Custom functions can take nothing as input, one or more arguments.

They can return or not return objects.

# It is a good practice to describe inside the function what it does
def real_translation(dna_sequence):
    """
    This function translates dna_sequence in real protein sequence.
    Input:
        dna_sequence: string of nucleotides
    Output:
        protein: string of amino acids
    """
    protein = ""
    # Loop through the sequence in steps of 3
    for i in range(0, len(dna_sequence), 3):
        codon = dna_sequence[i:i+3]
        
        # Make sure codon has length 3
        if len(codon) < 3:
            break
        # Stop when a stop codon was found
        if codon in standart_codon_table.stop_codons:
            break
        else:
            # Retrieve the amino acid encoded by the codon. If the codon is atypical, insert "?"
            protein += standart_codon_table.forward_table.get(codon, "?")

Attempt N 1

dna_sequence = "ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"
real_translation()
print(protein)

If a custom function was designed to receive arguments, it will demand them.

Attempt N 2

real_translation(dna_sequence)
print(protein)

If a variable was defined inside a custom function - it is not accessible from outside.

To make a variable accessible, add this to the end of the function real_translation.

return protein

Note how below we define a target variable (protein) as storage for the real_translation output.

dna_sequence = "ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"
protein = real_translation(dna_sequence)
print(protein)

Let’s say that you want to use a different codon table for different parts of your analysis. Usually you use a standard one, but sometimes one from mitochondria.

# Find the codon table of any mitochondria
for id, table in CodonTable.unambiguous_dna_by_id.items():
    if "mitochondria" in table.names[0].lower():
        print(id, "-", table.names)

Q: Are the mitochondria codon table and the standard codon table different?

CodonTable.unambiguous_dna_by_name["Standard"] == CodonTable.unambiguous_dna_by_name["Vertebrate Mitochondrial"]

To make our function applicable to any organism, we could pass a codon_table_name as an argument and it will be retrieved automatically.

What if we want a standard codon table to be used by default?

To do so, give a value to the argument directly in function definition. In this case, the user does not have to pass the codon_table_name="Standart" argument, only if a table different from “Standart” is required.

def real_translation(dna_sequence, codon_table_name="Standard"):
    """
    This function translates dna_sequence in real protein sequence.
    Input:
        dna_sequence: string of nucleotides
        codon_table_name: string name
    Output:
        protein: string of amino acids
    """
    # Retrieve a codon table
    codon_table = CodonTable.unambiguous_dna_by_name[codon_table_name]
    protein = ""
    # Loop through the sequence in steps of 3
    for i in range(0, len(dna_sequence), 3):
        codon = dna_sequence[i:i+3]
        
        # Make sure codon has length 3
        if len(codon) < 3:
            break
        # Stop when a stop codon was found
        if codon in codon_table.stop_codons:
            break
        else:
            # Retrieve the amino acid encoded by the codon. If the codon is atypical, insert "?"
            protein += codon_table.forward_table.get(codon, "?")
    return protein

We can also define an argument by calling its name.

Note, keyword arguments (with default value) have to be always called after positional arguments (without default).

dna_sequence = "ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"
protein = real_translation(codon_table_name = "Vertebrate Mitochondrial", dna_sequence)
print(protein)

Q: Does our function finally work?

It is good practice to test a custom function on various scenarios. This way you will know if it works as expected.

Test N 1 normal dna sequence

dna_sequence = "ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"
protein = real_translation(dna_sequence)
print(protein)

Test N 2

dna_sequence = ["ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"]
protein = real_translation(dna_sequence)
print(protein)

Test N 3

dna_sequence = "AUGGCCAUUGUAAUGGGCCGCUGAAAGGGUGCCCGUA"
protein = real_translation(dna_sequence)
print(protein)

Test N 4

dna_sequence = "AT"
protein = real_translation(dna_sequence)
print(protein)

Test N 5

dna_sequence = "GGGCTAGCCATTGTAATGGGCCGCAAGGGTGCCCGTA"
protein = real_translation(dna_sequence)
print(protein)

Test N 6

dna_sequence = "TGATAA"
protein = real_translation(dna_sequence)
print(protein)

Test N 7

dna_sequence = "GGGCTAATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"
codon_table_name = "Dragon"
protein = real_translation(dna_sequence, codon_table_name)
print(protein)

We identified several problematic cases: - input is not a string - non-DNA sequence - non-triplet sequence - sequence starting with either no start or stop codon - sequence contains only non-coding triplets - unknown codon table name

To avoid these cases, we can warn a user (you or your colleagues) about weird input or program behavior.

What code can communicate to us?

INFO -> “What is the program doing overall?” (for casual usage)

DEBUG -> “What exactly is happening step-by-step?” (for developers)

WARNING -> “Signal something is not ideal.” (program continues)

EXCEPTION -> “Signal something is wrong, but handleable.” (program stops, you can correct)

ERROR -> “Signal of code or system issues.” (program stops, you cannot correct, unless you are a developer)

Note how the warnings and exceptions are embedded in if statements, while info and debug are not necessarily. It makes sense to disturb a user or stop a program only in special conditions.

Info and debug messages can simply be written as print statements.

def simple_translation(codon):
    print(f"Received codon {codon}")
    amino_acid = CodonTable.unambiguous_dna_by_name["Standard"].forward_table.get(codon, "?")
    return amino_acid

amino_acid1 = simple_translation("ATT")
print(amino_acid1)

For invoking warnings, use the warnings package. Note that first the function is executed and then the warning is printed out.

def simple_translation(codon):
    amino_acid = CodonTable.unambiguous_dna_by_name["Standard"].forward_table.get(codon, "?")
    if amino_acid == "?":
        warnings.warn(f"Non-DNA or non-coding triplet was given.")
    return amino_acid

amino_acid2 = simple_translation("AUA")
print(amino_acid2)

There are two ways to handle exceptions.

One, when you want to raise a custom exception before the code crashes.

def simple_translation(codon):
    if len(codon)<3:
        raise ValueError("Codon has to be 3 letters long.")
    amino_acid = CodonTable.unambiguous_dna_by_name["Standard"].forward_table.get(codon, "?")
    return amino_acid

amino_acid3 = simple_translation("TC")
print(amino_acid3)

Two, when you want to work around exception that is raised by different opperation.

Note, you can add several return statements in a custom function.

def simple_translation(codon):
    try:
        amino_acid = CodonTable.unambiguous_dna_by_name["Standard"].forward_table.get(codon, "?")
        return amino_acid
    except TypeError:
        print("TypeError was caught. Codon is expected to be a string.")
        return None
    

simple_translation(["ATT"])

There are many types of exceptions. Each of their names tries to precisely describe what went wrong.

Often name = what went wrong + “Error” (why not “Exception”? Idk, historical reasons…)

If you do not understand meaning of exception - look it up on the Internet!

Here are some of the most often encountered:

IndexError List index out of range

KeyError Dictionary key not found

FileNotFoundError File doesn’t exist

PermissionError No access to file or directory

ModuleNotFoundError Module (imported package) doesn’t exist

KeyboardInterrupt Ctrl+C

Exception Generic exception, can mean anything

etc.

You can also write your own exceptions! (not covered here)

Logging

Logging is good practice when you develop a script, a tool, or a pipeline for internal usage.

# set up logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    datefmt="%H:%M:%S",
    force=True 
)

Logging works similarly to print statements. But you can easily write the output to a .log file (not shown here). Substitute info with other kinds of messages to apply logging for different purposes.

logging.info("Welcome at Day 3 of python track!")
logging.warning("Did you eat today?!")

It is unnecessary to combine logging of info, debug, and warnings with the above functions (print and warnings.warn), as they do not stop a program and would just duplicate a message. However, logging.exception cannot raise exception on its own, so we combine it with raise.

Let’s put it all into practice.

Q: Any ideas on how to overcome issues in real_translation()?

This function looks huge, but only because it tries to be both super user- and developer-friendly.

def real_translation(dna_sequence, codon_table_name="Standard"):
    """
    This function translates dna_sequence in real protein sequence.
    Input:
        dna_sequence: string of nucleotides
        codon_table_name: string name
    Output:
        protein: string of amino acids
    """
    logging.info(f"Start execution of real_translation()")

    # Check codon_table_name value
    logging.debug(f"Use codon table {codon_table_name}")
    table_names = list(CodonTable.unambiguous_dna_by_name.keys())
    if codon_table_name not in table_names:
        logging.exception(f"Codon table name is invalid. Try one of these: {table_names}")
        raise ValueError
    
    # Retrieve a codon table
    codon_table = CodonTable.unambiguous_dna_by_name[codon_table_name]
    logging.info(f"Successfully retrieved codon table.")

    # Check dna_sequence type
    logging.debug(f"Type of dna_sequence argument {type(dna_sequence)}")
    if not isinstance(dna_sequence, str):
        logging.exception(f"Argument dna_sequence type invalid. It should be a string (str).")
        raise TypeError
    
    # Check dna_sequence alphabet
    expected_alphabet = set("ATGC")
    sequence_alphabet = set(dna_sequence)
    logging.debug(f"String dna_sequence contains letters {sequence_alphabet}")
    if expected_alphabet != sequence_alphabet:
        logging.warning(f"Function real_translation() is suboptimal for non-DNA sequences. Use {expected_alphabet} alphabet.")
    
    # Check if dna_sequence contains at least one codon
    logging.debug(f"String dna_sequence legth {len(dna_sequence)}")
    if len(dna_sequence)<3:
        logging.exception(f"String dna_sequence is shorter than one codon. Minimal acceptable length is 3.")
        raise ValueError


    protein = ""
    codons = []
    cdna = 0 ; real_stop_codon = False
    # Loop through the sequence in steps of 3
    for i in range(0, len(dna_sequence), 3):

        codon = dna_sequence[i:i+3]

        # Make sure codon has length 3
        if len(codon) < 3:
            break

        # Check that sequence is coding from the start
        if i==0:
            print(codon)
            cdna = 1 if codon == "ATG" else 0
            if not cdna:
                logging.warning(f"String dna_sequence starts as non-coding. Skipping non-coding triplets.")
                continue
        
        # Check that start codon was found
        if codon == "ATG":
            cdna +=1
            if cdna==1:
                logging.info(f"Start codon was found on position {i}.")
        
        if cdna>1 and not real_stop_codon:
            real_stop_codon = codon in codon_table.stop_codons
            if real_stop_codon:
                logging.info(f"Stop codon was found on position {i}.")
        
        # Recod unique codons
        if codon not in codons:
            codons.append(codon)

        # Stop when a stop codon was found
        if codon in codon_table.stop_codons:
            break
        else:
            # Retrieve the amino acid encoded by the codon. If the codon is atypical, insert "?"
            protein += codon_table.forward_table.get(codon, "?")

    if protein=="":
        logging.debug(f"String dna_sequence unique codons {codons}")
        logging.warning(f"Protein sequence is empty. Probably dna_sequence consisted only of non-coding triplets.")
    elif protein!="" and not real_stop_codon:
        logging.exception(f"Stop codon was never found. Corrupted coding sequence")
        raise ValueError

    return protein
dna_sequence = "AAAATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGTA"
protein = real_translation(dna_sequence)
print(protein)

You just learned from the inside about the biggest enemy of programmers: bugs.

PROBLEM 3: These red messages drive me crazy!

QUOTE

Debugging is a process of solving (removing) bugs. They often feel like a brain teaser, and they consume most of the time of bioinformaticians.

So you encounter one or multiple errors (red text). What’s next?

Your goal is not to get rid of errors, but to understand them.

Q: What is the difference between handling a bug in two solutions below?

Error

-> RNA sequence was passed instead of expected protein.

def align_sequences(seq1, seq2, matrix_name="BLOSUM62"):
    matrix = substitution_matrices.load(matrix_name)
    alignments = pairwise2.align.globalds(
        seq1,
        seq2,
        matrix,        
        -10,
        -0.5
    )
    best_alignment = alignments[0]
    return best_alignment

seq1 = "UUUUUUUU"
seq2 = "UUUUUUUU"
print("Are compared sequences the same?",seq1==seq2)
best_alignment = align_sequences(seq1, seq2)
print(best_alignment)

Solution N 1

-> The developer assumed that if the error arose from two identical sequences, then pairwise2.align.globalds probably cannot handle perfect alignments.

def align_sequences(seq1, seq2, matrix_name="BLOSUM62"):
    matrix = substitution_matrices.load(matrix_name)
    try:
        alignments = pairwise2.align.globalds(
            seq1,
            seq2,
            matrix,        
            -10,
            -0.5
        )
        best_alignment = alignments[0]
        return best_alignment
    except:
        print("perfect alignment")
        return None

seq1 = "UUUUUUUU"
seq2 = "UUUUUUUU"
print("Are compared sequences the same?",seq1==seq2)
best_alignment = align_sequences(seq1, seq2)
print(best_alignment)

Solution N 2

-> The developer identified that the substitution matrix "BLOSUM62" concerns proteins, meaning that the align_sequences() function by default expects protein sequences as input. Thus, the developer passed a custom matrix_name value and made sure that all nucleotides are DNA and not RNA.

def align_sequences(seq1, seq2, matrix_name="BLOSUM62"):
    amino_acids = list(IUPACData.protein_letters)
    alphabet = set(seq1+seq2)

    if matrix_name=="BLOSUM62" and any(s not in amino_acids for s in alphabet):
        raise ValueError("Non-protein sequence was passed. Use different matrix_name to align DNA.")
    
    matrix = substitution_matrices.load(matrix_name)

    alignments = pairwise2.align.globalds(
        seq1,
        seq2,
        matrix,        
        -10,
        -0.5
    )
    best_alignment = alignments[0]
    return best_alignment

seq1 = "UUUUUUUU"
seq2 = "UUUUUUUU"
seq1 = seq1.replace("U","T")
seq2 = seq2.replace("U","T")
print("Are compared sequences the same?",seq1==seq2)
best_alignment = align_sequences(seq1, seq2, "NUC.4.4")
print(best_alignment)

You understand the bug through understanding what input function expects, what it does and what are its limitations.

Common steps in debugging.

Reproduce -> Reduce -> Inspect -> Fix -> Verify

  • Forsee the bugs.

    What can go wrong with XYZ data input? Wrap such cases into exceptions.

  • Read the error message carefully (traceback).

    Pay attention to the error type, line number, and what variables are involved in the error.

  • Check if the bug is reproducible every time you run the code.

    It can happen that you accidentally used the wrong object with same name.

  • Check if assumptions about your input or function output are correct. Write quick tests.

    Especially in a long pipeline, you may lose the logic of data transformation and function design. If you assume that a variable is of type X or contains data in format Y, check by printing it out.

  • Reduce your code to the smallest failing case. Isolate failing part.

    If you use big data objects or perform complicated operations, extract representative data point(s) and/or write seperately the smallest part of code that could be failing.

  • Change one thing at a time.

    Test code after each code alteration. Do not rewrite everything from scratch (unless its the easiest way to fix the code).

  • Trace the flow of code step-by-step.

    If it is unclear what code is failing, why, or if it is impossible or hard to isolate it.

Modern IDEs usually have a “debugging mode,” which lets you see all variable values without printing them.

Let’s see how it works for Jupyter Notebook.

# Two bugs are unanticipated: one throws an error and one is silent
def debug_me(seq1, seq2):

    matrix = substitution_matrices.load("NUC.4.4")

    alignment = pairwise2.align.globalds(seq1, seq2, matrix, -10, 0.5)[0]

    seqA, seqB, score, start, end = alignment

    matches = 0
    for a, b in zip(seqA, seqB):
        if a != b:
            matches +=1

    identity = matches / len(seqA)

    return identity

# Difference between sequences is in one nucleotide -> expect high identity
seq1 = "ATGCT"
seq2 = "ATGTT"
identity = debug_me(seq1, seq2)
print("Identity:", identity)
# Test debugger here
...

Debugger

Option 1: Use the built-in %debug

  1. Run a cell that throws an exception.
  2. Immediately after the error, run:
%debug

This will launch an interactive post-mortem debugger.

Command Meaning
n Next line
s Step into a function
c Continue to the end
p variable Print variable value
q Quit debugger

Option 2: Use %pdb on to automatically enter debugger

%pdb on
  • Now, anytime an exception occurs, Jupyter will automatically drop you into the debugger.
  • %pdb off disables automatic debugging
Command Meaning
n next line
s step into function
c continue until next breakpoint
l list code around current line
p var print variable value
q quit debugger

Option 3: Use pdb manually inside code

You can set breakpoints anywhere:

import pdb

def my_function(x, y):
    pdb.set_trace()  # execution will pause here
    result = x + y
    return result

my_function(3, 5)
  • The notebook cell will enter interactive debugging mode at the set_trace() line.
  • You can inspect variables, step through code, etc.

EXERCISE 1: Debug race.

Find and tackle as many bugs as possible in 15 minutes in the function align_sequences(). Use debug mode, if possible. To get hints, scroll down.

def align_sequences(seq1, seq2, matrix_name="BLOSUM62"):
    """
    Perform pairwise alignment between two sequences.
    """

    logging.info("Starting alignment")

    if type(seq1) != str or type(seq2) != str:
        logging.warning("Sequences should be strings")

    matrix = substitution_matrices.load(matrix_name)

    gap_open = -10
    gap_extend = -0.5

    alignments = pairwise2.align.globalxx(
        seq1,
        seq2,
        matrix,        
        gap_open,
        gap_extend
    )

    best_alignment = alignments[0]

    aligned_seq1, aligned_seq2, score = best_alignment

    logging.info(f"Alignment score: {scores}")

    if score > "10":
        logging.info("High score alignment")

    return alignment

seq1 = "MTEYKLVVVG123"
seq2 = "MTEYKLVVVGA"

result = align_sequences(seq1, seq2)

print(result)

Unanticipated bugs in align_sequences() - wrong type check (should allow str only) - not normalizing case, like

    seq1 = seq1.upper()
    seq2 = seq2.upper()
  • wrong matrix loading (wrong variable name later)
  • typo in variable name
  • wrong function for matrix (should use globalds, matrix is ignored by globalxx)
  • assumes at least one alignment exists
  • wrong unpacking (structure is different)
  • logging wrong variable
  • logic bug (score comparison always False)
  • returning wrong variable
  • invalid input sequence (contains numbers)

If you are done debugging, execute this line.

CUTE

PROBLEM 4: I am lost in my code, help…

Does it sound familiar?

Too long notebooks (like this one XD). Tired of scrolling. Takes ages to rerun. The kernel crashes due to memory overload.

Many notebooks with unclear names and purposes. Cannot find the right one.

Running out of creative names for distinct variables and custom functions. Start putting 1, 2, 3… at the end of the names.

Follow best practices of project structure to never struggle again!


# Print directory tree
def print_tree(startpath, prefix=""):
    files = sorted(os.listdir(startpath))
    for i, f in enumerate(files):
        path = os.path.join(startpath, f)
        connector = "├── " if i < len(files) - 1 else "└── "
        print(prefix + connector + f)
        if os.path.isdir(path):
            extension = "│   " if i < len(files) - 1 else "    "
            print_tree(path, prefix + extension)

# This is a typical basic structure
print_tree("./real_alignment")

Exercise: write your answer in this editable Python cell.