Correct & Readable
Scientific
Code

(Beginner Version)

Lesson 1:

Why spend the time?


TipFor the best experience, use our desktop version!
Some of our examples include longer lines of code that are easier to navigate on desktop, but this unit is still fully accessible on mobile and tablet screens.

1.1 The Wiles and Woes of Code

Anyone who has worked with scientific code – be it their own or that of others in their field – has likely experienced the frustration of trying to get that code to work as intended, if at all. Worse yet, you may even have found yourself perplexed by cases in which scripts that previously worked just fine suddenly throw errors when you attempt to revisit or reuse them. While such experiences may make you question your sanity (or your talent for programming), they are actually incredibly common.

Beyond the hours spent frustratingly inching your way towards a functional script, there is the additional concern of incorrect results, or results whose correctness is obscured by poor reproducibility and opaque code.

This is a risk that is not exclusive to trainees and students. Even experts with years of programming experience can fall into the trap of writing – and even publishing – code whose results are irreproducible:

Activity 1

Though the idea of being unable to prove the validity of your research due to lacking standards in your programming might be harrowing, you could convince yourself that this is something you can control for just before publication – well after the bulk of the research process is done.

However, the cost of poorly written code extends well beyond publication alone, and includes an often invisible degradation of the efficiency with which scientists work. This is true even if you primarily write shorter scripts with less complex logic.

To illustrate, let’s take a look at some examples.

Embedded Webpage
Click anywhere to start

Post-activity questions:

  • These were all very short and simple scripts. How do you suppose the impact of readability issues translates to bigger, more complex projects?
  • Were some scripts more or less affected by the readability issues? Why?

Suffice to say that the way you write your code impacts not only the validity and reproducibility of your findings, but also the transparency and workability of the code that produced them. It’s easy to dismiss these issues as a natural part of the scientific programming experience, and to an extent, they are – however, a significant portion of them are actually the result of improper adherence to best practice and perfectly avoidable.

Why, then, does poorly written code appear to be so common in the sciences?

1.2 The Source of the Problem

The reason these frustrations are such commonplace experiences is three-fold.

First, the rise of computational techniques has been so rapid and all-encompassing that many scientists have found themselves learning programming on the fly – often missing out on courses that provide formal introductions to best practices.

This lack of proper training then serves to fuel the natural insecurities experienced in fields rife with imposter syndrome: there is a natural concern for how the code we write is perceived by others and what it might reveal about our competence and experience, so we don’t communicate our failures to the degree we should.

However, particularly if you’re an interdisciplinary scientist with little to no formal background in programming and computer science, it’s important to remember that coding is just one skill in your scientific toolkit, and it’s a highly technical one at that.

Just as you would not be any less of a scientist for taking a little longer with your injections or needing to redo your gel electrophoresis, you are not any less of a scientist for not writing the cleverest code in the fewest lines possible. The importance lies in the truth and functionality of the results you produce, not in the speed – or the number of lines – with which you got there.

TipThink this doesn't apply to you? Think again!
Even if you’re the only one using your code, odds are that between debugging, updating, and revisiting it, any one line is read far more often than it is written. This means that it is more important that your code be easy to read and understand than it is for it to take up as little space as possible.

Finally, none of this is helped by the plain fact that scientific code is often written under pressure, usually in the form of approaching deadlines or an impatient supervisor. This often drives us to focus first and foremost on functionality – that is, we have the singular goal of making the code in front of us do the thing that we want it to do: load a file, process an image, or do fancy math.

That’s an understandable approach, but it allows us to forget that code, like most tools, only works as well as its user understands it, and it sets both the programmer and the users up to waste time & produce incorrect or irreproducible results.

Uh-oh!Feeling called out?
If that describes the kind of code you write, don’t feel too bad: Scientists are rarely taught how to write code that not only works, but also conveys understanding, and in reading this lesson, you are taking the first step towards becoming a better programmer – and by extension, a better, more transparent scientist.

The key to writing code which prevents frustrations & inefficiencies, and which ensures reproducible results, lies in making readability and thoughtful design a priority.

1.3 Code & Communication

In addition to its primary function, code serves the secondary purpose of communication: Every line of code that you write has the opportunity to convey some information to the user beyond its syntax.

With that in mind, what changes can we make to our line-by-line syntax to ensure that every script we write communicates as much information as it reasonably can in the space it is given?

Takeaways:

  • Unreadable and undisciplined research code is a common issue in science.
  • While we often shrug off readability issues as minor inconveniences, their impact is real and costly.
  • These problems are exacerbated by systemic issues like poor training, imposter syndrome, and time pressure.

Reflection:

  • Looking back on your own experience, can you think of a time when you struggled with code, but didn't tell anyone for fear of appearing unskilled or novice?
  • What forms do you suppose "opportunities to communicate" might take in the context of code?

Lesson 2:

Writing Clean Scripts

2.1 Descriptive Names & Explicit Variables

The first and most important thing that we can do is ensure that we use meaningful names for all variables that we use. Consider the following script:

100%

f = ND2Reader('20191010_tail_01.nd2')
d = np.max(np.transpose(f, (1, 2, 0)), 2)
d = zoom((d - np.min(d)) / (np.max(d) - np.min(d)), (1, 1))
a = gaussian_filter(d, 0.02)

Here, variables like f, d, and a don’t tell us much, and what little information we get from the names of the functions that are imported and called is largely unhelpful. A minor adjustment can clarify the script’s logic:

100%

nd2_file = ND2Reader('20191010_tail_01.nd2')
maximum_intensity_projection = np.max(np.transpose(nd2_file, (1, 2, 0)), 2)
downsampled_img = zoom((maximum_intensity_projection - np.min(maximum_intensity_projection)) / (np.max(maximum_intensity_projection) - np.min(maximum_intensity_projection)), (1, 1))
smoothed_img = gaussian_filter(maximum_intensity_projection , 0.02)

Note that you should generally try to ensure that your variable names are descriptive nouns that can be pronounced by your readers.

An exception to this can, however, be made in cases where not doing so enables you to convey additional information. One such example might be when a variable name describes the dimensional order of an array: it is okay to name a variable xyzc_image if that name is reflective of its contents and you'd like to specify the order of the width (x), height (y), depth (z), and color (c) dimensions in the array contained therein.

QuestionWhat's a "MIP"?
The maximum intensity projection of a microscopy image is essentially a way of compressing one of its dimensions. If you have a 3D image with x width, y height, and z depth, you would calculate its MIP by creating a 2D image with x width and y height where each pixel is the greatest pixel value (i.e. maximum) it features along the entire depth dimension.
Tinker with some toy examples using this MIP widget:
Slice #12
MIP of slices 1 to 12
112

These variable names have improved the readability of our script. However, unless you’re intimately familiar with the functions that are being called and given input arguments here, you’ll be left confused as to what some of these numbers mean – and how, exactly, they impact the script’s logic and the results that it produces.

100%

nd2_file = ND2Reader('20191010_tail_01.nd2')
maximum_intensity_projection = np.max(np.transpose(nd2_file, (1, 2, 0)), 2)
downsampled_img = zoom((maximum_intensity_projection - np.min(maximum_intensity_projection)) / (np.max(maximum_intensity_projection) - np.min(maximum_intensity_projection)), (1, 1))
smoothed_img = gaussian_filter(maximum_intensity_projection , 0.02)

Free-floating numbers with no associated names are often referred to as magic numbers. Experienced coders avoid these altogether, because every magic number is a missed opportunity to communicate the logic of your code and makes it more difficult to change that number as it begins to pop up in multiple places.

QuestionWon't this slow my code?
It’s a common misconception among new coders that explicitly defining your variables in this way degrades your code’s performance – usually because of the assumption that something now lives in memory that previously would not have.

However, with very few specialized exceptions, the impact of binding a name to a value is inconsequential. What really impacts performance at this level of programming is a combination of input/output operations (e.g. loading a file) and complex matrix maths for large arrays (e.g. calculating the MIP).

With every bit of data defined, our example script is now much more self-explanatory, and you no longer need to know every function that it calls in order to get a rough sense of what the script is attempting to do:

100%

nd2_file = ND2Reader('20191010_tail_01.nd2')

z_index = 2
xyz_order = (1, 2, 0)
maximum_intensity_projection = np.max(np.transpose(nd2_file, xyz_order), z_index)

downsampling_factor = (1, 1)
downsampled_img = zoom((maximum_intensity_projection - np.min(maximum_intensity_projection)) / (np.max(maximum_intensity_projection) - np.min(maximum_intensity_projection)), downsampling_factor)

smoothing_factor = 0.02
smoothed_img = gaussian_filter(downsampled_img, smoothing_factor)

However, the script currently increases the risk of misunderstanding by making some of its image processing steps implicit rather than explicit. This is something we need to address.

2.2 One Step At A Time

In Python – and most modern programming languages – it is generally better to be explicit rather than implicit, meaning that you should try to transparently communicate your structure and your operations. For example, consider the line in which we define the maximum intensity projection:

100%

z_index = 2
xyz_order = (1, 2, 0)
maximum_intensity_projection = np.max(np.transpose(nd2_file, xyz_order), z_index)

If you are not familiar with the operation the microscopy data is being subjected to,  you could be confused as to what exactly is happening in the implicit processing step – or not realize that one is occurring at all.

In order to communicate both the existence and the purpose of this additional processing step to readers of our code, we need render it explicit by separating it out and assigning its result a variable name of its own:

100%

nd2_file = ND2Reader('20191010_tail_01.nd2')

z_index = 2
xyz_order = (1, 2, 0)
xyz_img = np.transpose(nd2_file, xyz_order)
maximum_intensity_projection = np.max(xyz_img, axis=z_index)

minimum_pixel_value = np.min(maximum_intensity_projection)
maximum_pixel_value = np.max(maximum_intensity_projection)
pixel_value_range = maximum_pixel_value - minimum_pixel_value
normalized_img = (maximum_intensity_projection - minimum_pixel_value) / pixel_value_range

downsampling_factor = (1, 1)
downsampled_img = zoom(normalized_img, downsampling_factor)

smoothing_factor = 0.02
smoothed_img = gaussian_filter(downsampled_img, sigma=smoothing_factor)

Now anyone who reads this script should be able to understand that the data is being normalized prior to computing the maximum intensity projection, regardless of whether they recognize the normalization equation or not.

Making steps embedded in a separate operation more explicit is also a tool in your communication kit: It’s a way to highlight a particular nuance of your code and draw the attention of anyone reading it to what you are doing.

There is, of course, some level of negotiation between giving every single operation a line of its own and keeping the logical steps your code takes explicit. Generally speaking, you will want to make explicit every step which transforms data in a significant way, and which could affect logic down the road.

For example, normalizing the data means that what was previously an array of values between 0 and 65,535 (the maximum value of a 16 bit pixel) is now an array whose values fall between 0 and 1. That could have a significant impact on downstream processing, so it’s important that users realize that this is happening.

Looking at our example, you may also notice that we did not separate out the definition of the dividend and the divisor:

100%

minimum_pixel_value = np.min(maximum_intensity_projection)
maximum_pixel_value = np.max(maximum_intensity_projection)
pixel_value_range = maximum_pixel_value - minimum_pixel_value
normalized_img = (maximum_intensity_projection - minimum_pixel_value) / pixel_value_range

This, too, is an exercise in communication, because it preserves the actual equation we’re using for normalization in a single line. Writing your code with this approach in mind – that is, defining the individual components of equations, but keeping the equations themselves legible and recognizable – allows subject matter experts to quickly review the math underlying your code.

2.3 Keep It Short And Simple

It is a common misconception among new coders that clever code is concise. To an extent, that can be true. You can shorten your code through the use of intelligent structure. However, the kinds of scripts that tend to be written early on in your programming journey are far likelier to suffer from being excessively convoluted and broadly undisciplined.

As a novice programmer, it’s important not to confuse brevity for elegance. When writing code, try to strike a balance between simplicity and clarity. Stuffing as much logic as possible into a few lines of code can sacrifice logical clarity and human readability, which is ultimately counterproductive.

Activity: Time to integrate.

We've established three essential guidelines:

  1. Use descriptive, pronounceable variable names
  2. Make each procedural steps explicit
  3. Avoid overly convoluted logic

Let's now practice integrating and applying them to a real-life script with multiple readability issues.

Click anywhere to start

Post-activity questions:

  1. Which readability guidelines do you think the script you fixed violate?
  2. Did you encounter any issues in trying to improve the readability of a script you did not expect?
  3. Did your solution differ from our example? How so?

2.4 Working with Artificial Intelligence

With the rise and rapid development of large language models, the use of artificial intelligence in programming has undeniably become commonplace. This introduces a number of fantastic advantages, and responsible use of AI can increase both the quantity and the quality of your code.

If you’ve used LLMs to generate, edit, or augment your work, then you’ve no doubt encountered cases in which it made both obvious and subtle mistakes. Thus, as with any tool, you need to know how to use AI – and just as importantly, how to not use it – in order to ensure that its impact remains net positive.

Examples of good uses of AI-assisted coding would be syntax completion, or adjusting or polishing plotting routines: The code you use to generate your figures is usually boilerplate, easy to double-check for correctness, and used to illustrate your results rather than define them. Similarly, certain front-end tasks, such as generating a simple documentation page, are largely trivial, relatively easy to inspect and test, and unlikely to result in publishing incorrect findings.

AI can also be a good sounding board that you can use to help you generate and interrogate ideas. It can tell you about useful libraries you might not have known about, highlight common implementation strategies, or outline some of the steps that are likely to be involved in your prospective project’s development process. LLMs are a good resource for interrogating some of your own blind spots and greasing the mental wheels – but you need to be careful that it does not replace those wheels altogether. This is all the more important if you’re a novice programmer, because thinking through computational problems and devising programmatic solutions is a skill that is developed through practice.

These are precisely the risks that you run when you use AI to accomplish complex tasks like designing algorithms, implementing deeply embedded components of complicated systems, or writing performance-critical routines. These are all things that AI isn’t equipped to handle, because such tasks rely on many different moving parts both scientifically and programmatically, and understanding and reasoning about them requires both expert knowledge and experience applying it.

Another issue you’ve probably seen is that commercial LLMs tend to be better at performing like an expert than actually being one (even in the latest updates). 

The work they produce could look convincing and even produce functional programs, but feature mistakes or nuanced functionalities that the untrained eye could miss.

A 2024 study by Perry, Srivastava, Kumar, & Boneh found that users that use AI assistants tend to be more confident in the quality of their code (in this case, its security), even though it was actually significantly less secure than code written without the use of AI assistants. The higher vulnerability of AI generated code is well documented in LLM literature.

This is further complicated by the fact that AI code generation often involves some degree of back-and-forth between the AI and its users, throughout which the generated code is iterated upon. This is a problem because that iteration isn’t necessarily resulting in more robust code – for example, in a preprint posted in May of 2025, researchers found that code that had been iterated upon tended to degrade in its security. The preprint found that this effect could be mitigated with mindful prompting that prioritized code quality over features, but not only did degradation occur even in prompts that were written with a focus on security, iterations involving such prompts were actually more likely to introduce certain types of errors than their more feature-focused counterparts. 

So the risks of using artificial intelligence to generate code not only remain, but they may even be exacerbated by iterative generation and targeted prompt engineering. There is no way to immunize your AI-assisted code against these risks except to:

  1. Limit your use of AI to idea generation and simple, transparent tasks.
  2. Avoid outsourcing actual thinking and decision-making to AI.
  3. Always use robust human validation.

There will be more to say about the responsible integration of artificial intelligence in scientific programming as this unit proceeds and we cover different aspects of code design, so be sure to keep an eye out for AI-specific sections in upcoming lessons.

Takeaways:

  • Readable code uses descriptive, pronounceable variable names, makes each of its procedural steps explicit, and avoids overly convoluted logic.
  • While AI can make us more efficient programmers, its use is best limited to transparent tasks that can be easily validated, and which don't outsource actual thinking.

Reflection:

  • Can you think of times when it may be warranted to make an exception to one of our three guidelines?
  • Have you ever used a large language model to tackle a challenge or question beyond its means?