The Code That Almost Led to Disaster • A Starbase Story
A Python pitfall • Maybe it's not quite Episode IV level of drama, but drama it is nonetheless…
"Mind where you're going," yelled the supervisor. And he's normally one of the kinder people at Central Command. But not today. Everyone's stressed and tired today. And that's never a great combination.
Trision didn't apologise. This, also, was out of character for him. But there was no time to waste. He had the solution, but time was running out.
"Status?" Trision's tone wasn't what he was aiming for as he entered the engine room.
He didn't need an answer. He didn't wait for one.
"I've got it. I've run simulations. This will work."
"Here it is." Trision handed over the pad with the code. "I figured out we need to randomise the output power, but also ensure the mean power doesn't exceed our max op temp."
"I don't need a full presentation. Does it work?" Kwamian snapped. She was in charge in the engine room. The thing is, she always wanted to know even the smallest details of every operational change anyone suggested. After all, the primary engine was her baby. But she wasn't used to working under this sort of pressure.
Trision was sure about this code even though he didn't have the time to test it as thoroughly as he normally would:
Trision was confident this varying power output will disrupt the systems on board the enemy starjets, which now were on the verge of breaching the starbase’s outer shields.
[Footnote that's not at the foot of the page: Yes, this code is relatively trivial for what you'd expect on a 29th-century starbase. Just play along. Please!]
[Another footnote: Note also that apply_power_to_main_engine()
and pause_process()
are functions defined elsewhere in the code used to control the starbase's primary engine. Comment them out when you need to try out this code! But don't try the code now—you won't see anything. Unless you're on the starbase!]
"Do you want me to insert the code in the primary system?" asked Trision.
"No, I'll do it. I need you to monitor the secondary systems."
"You need to insert this segment…"
"I know where it goes." Of course, she knew. Trision climbed the steps towards the secondary system interface and took over the console.
"TRISIOOON," Kwamian screamed over the loud alarm siren, "Get. Here. Now."
The attacking starjets were retreating. The interference from the fluctuating power was working. But the main engine was overloading.
If they stop randomising the power output, the enemy starjets will carry on their attack. If they keep they primary engine running in its current state, it will blow up, and…
…and that's not an option they can contemplate.
"Give me 30 seconds. Let me turn on the diagnostics."
"You told me you're sure this works…"
"Just 30 seconds."
Trision toggled CHECK_MEAN_OUTPUT
to True
. He couldn't believe what he saw when he ran the code (remember to comment out the lines with the missing functions to run this code):
...
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
...
The maximum operating power is 5.5 TW. He had checked and double checked his code before rushing it to the engine room. He was sure he hadn’t made any mistake. What's gone wrong? He couldn't understand…
Dear reader,
This is a good time to have the intermission in this story. It's your turn to look at this code again and make suggestions to Trision. He only has a few seconds to figure out what's wrong. If he can't, it's not good news for the starbase.
Trision didn't make any mistakes in his simulation. And neither Trision nor Kwamian, when she added this code segment to the main code, introduced any typos.
What else could it be?
Once you have a suggestion for Trision, erm, well, he's eight centuries in the future, so there's nothing much you can do, I'm afraid. Just read on…
Ten seconds had gone already. Kwamian wasn't going to give him more than the thirty he asked for. She wasn't going to risk the engine blowing up. The whole starbase would blow up.
Trision closed his eyes. He took two quick, deep breaths. That's all he could afford.
He knew these few lines of code well. Kwamian hadn't messed them up. Had she placed them in the wrong place? He hit the shortcut to go to the top of the code…
…and he saw it.
One of the lines at the top was:
That's it. That's the problem. Luckily, it's an easy fix. Trision knew about this Python pitfall.
Seven minutes earlier, when Trision had barged into the engine room with his precious lines of code, Kwamian took charge of adding them to the main code. She did, and she got this error when she ran the code:
NameError: name 'randint' is not defined
Kwamian was a decent programmer. She wasn't as proficient as Trision, but she had no problems dealing with this error message. The code hadn't relied on random numbers until now. She needed to add an import
statement. And since the code used randint(3, 9)
, she knew she had to import this function from the module:
Error message gone. The engine reset to run on this code. Seconds later, the enemy starjets started struggling. A couple of minutes later, many were retreating. All was going well until the engine overload alarm went off.
Kwamian was standing behind Trision. "Time's up, we need to shut the engine down and revert to our standard operating parameters."
"No, I got it." Trision replaced the import
statement with the following one:
He had no time to run tests. He ran the code.
"Reset the alarm. It will be fine now. Trust me."
Kwamian had to make a decision. But she didn't have time to think.
"OK," is all she said. She trusted Trision. She trusted his judgment call.
Pop! This was the last bottle of champagne they had. But that's a good thing. They were all exhausted after the day's events. And they had already finished quite a few bottles.
They were all heroes today. For thirteen hours and twenty-eight minutes, every person on the starbase had performed admirably. But Kwamian, from the other side of the hall, caught Trision's eye. She put on a tired smile and raised a glass towards Trision. Trision also raised his glass and nodded to acknowledge. Everyone was a hero today. But Trision knew what Kwamian meant. He had done well today.
The debrief will have to wait until tomorrow.
A minute of your time. Thank you to all of you who read these articles. And thank you to the paid subscribers and those who contribute from time to time. As you can imagine, a lot of time and effort goes into these articles, all in the evenings and weekends.
If you want to contribute, you can upgrade to a paid subscription, either just for a few months ($5 per month) or using the yearly option ($50).
Alternatively, you can make a one-off contribution whenever you can.
Now, back to the article.
So, what had gone wrong? And how did Trision recognise the problem?
Trision used the randint()
function in his code to generate a random number within a specified range. However, randint
is just a name, a reference. It could refer to different functions. In fact, there's more than one randint()
function in the Python ecosystem.
The most common and well-known one is the one in the random
module. The random
module is part of Python's standard library.
Another well-known function that's also named randint()
is the one in numpy.random
, which is NumPy's own random
submodule.
But they're not the same function. And their behaviour is different. See whether you can spot this difference. Here's the one from the standard library's random
first:
And now here's the one from NumPy. You’ll need to install NumPy if you don’t already have it in your environment:
Did you see that? There are no 3's in this version, but there are 3's in the previous one. The randint()
in the standard library's random
module includes both the start and end of the range included. In this example, the code chooses a random integer between 1 and 3, including 3. This is not the usual behaviour in Python since, in most places, ranges exclude the endpoint. However, random.randint()
doesn't follow this convention.
But the randint()
function in numpy.random
follows the default Python convention and does not include the endpoint. Therefore, NumPy's randint(1, 3)
only picks 1's and 2's.
In the rush to save the starbase, Trision copied the main code snippet he had added to the test version of the primary engine's code. He didn't include the import
statement he had used. He was using NumPy:
The output from this code, after the first few iterations of the loop, settled on the following mean power output:
...
Mean power: 5.50
Mean power: 5.50
Mean power: 5.50
Mean power: 5.50
Mean power: 5.50
Mean power: 5.50
...
The power
variable only included the values from 3 up to and including 8, but not 9. The mean value of these numbers is 5.5, and 5.5 TW was the absolute maximum mean power output for the engine. He knew he could go higher in the short-lived bursts as long as the mean value never went above this maximum rating.
But when Kwamian used the standard library's random
module, the name randint
now referred to a different function, the one in the standard library, which includes the endpoint:
The output of this version, as Trision saw in horror in those interminable seconds, is the following:
...
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
Mean power: 6.00
...
This is the mean value of the integers from 3 up to and including 9. And 6.0 TW is way too high.
What are the lessons learnt for Trision and the team?
Sure, he could have included the import
statement in the code he copied on his pad to take to the engine room. That would work. But…
The Dangers of Importing from
a Module
But he could also have used a different style of importing that would avoid all ambiguity:
Instead of importing randint()
from numpy.random
, he could have imported the whole module using import numpy
. This forces him to use the expression numpy.random.randint(3, 9)
in the code rather than simply randint(3, 9)
. Sure, it's more typing. But it wouldn't have put the starbase in danger!
Note that we often use the alias np
and import numpy as np
. But I'll stick to the full version in this article.
When you import a name from a module, such as from random import randint
, you're bringing the randint
name into the global namespace. Therefore, you can simply use the name randint
. If the module is a box of tools, you're taking just one tool out of the box and placing it directly on the workbench with all the other main tools.
But when you import the whole module, import random
, you're putting the whole box on the workbench, and you keep all the tools in random
inside the box. When you need a tool from the box, you fetch it directly from the box.
In this scenario, it's the module name random
that's in the global namespace and not its contents.
Trision Saved the Day, But He Made Another "Mistake"
There's another issue Kwamian and Trision identified in the debriefing session the day after! It's not quite a mistake, but it's still worth pointing out to make sure their code follows best practices.
NumPy's numpy.random.randint()
is no longer the recommended way to generate random numbers using NumPy. There's a newer, preferred way:
You create a random number generator using default_rng()
and then you call the distribution you need. In this case, you use .integers()
to generate random integers from a uniform distribution.
But this is a topic for another day, for another article.
Final Words
There's nothing wrong with using from <module> import <name>
in your imports. But beware of the consequences.
There aren't many disadvantages to importing the whole module instead. Yes, you'll need to type a bit more in your code as you'll need to reference the module name each time you need to use something from the module. But that's a small price to pay to save a starbase from annihilation, don't you think?!
Code in this article uses Python 3.13
The code images used in this article are created using Snappify. [Affiliate link]
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
And you can find out more about me at stephengruppetta.com
Further reading related to this article’s topic:
Appendix: Code Blocks
Code Block #1
# For optimal primary engine performance, set flag to False
CHECK_MEAN_OUTPUT = False
if CHECK_MEAN_OUTPUT:
mean_power = 0
count = 0
while True:
if CHECK_MEAN_OUTPUT:
count += 1
# Set and apply new power output
power = randint(3, 9)
apply_power_to_main_engine(power)
pause_process(0.5)
if CHECK_MEAN_OUTPUT:
mean_power = ((count - 1) * mean_power + power) / count
print(f"Mean power: {mean_power:.2f}")
Code Block #2
from random import randint
Code Block #3
from random import randint
Code Block #4
from numpy.random import randint
Code Block #5
from random import randint
for _ in range(10):
print(randint(1, 3))
# 2
# 1
# 2
# 2
# 2
# 3
# 3
# 1
# 3
# 2
Code Block #6
from numpy.random import randint
for _ in range(10):
print(randint(1, 3))
# 2
# 1
# 1
# 2
# 2
# 1
# 2
# 2
# 1
# 1
Code Block #7
from numpy.random import randint
# For optimal primary engine performance, set flag to False
CHECK_MEAN_OUTPUT = True
if CHECK_MEAN_OUTPUT:
mean_power = 0
count = 0
while True:
if CHECK_MEAN_OUTPUT:
count += 1
# Set and apply new power output
power = randint(3, 9)
# apply_power_to_main_engine(power)
# pause_process(0.5)
if CHECK_MEAN_OUTPUT:
mean_power = ((count - 1) * mean_power + power) / count
print(f"Mean power: {mean_power:.2f}")
Code Block #8
from random import randint
# For optimal primary engine performance, set flag to False
CHECK_MEAN_OUTPUT = True
if CHECK_MEAN_OUTPUT:
mean_power = 0
count = 0
while True:
if CHECK_MEAN_OUTPUT:
count += 1
# Set and apply new power output
power = randint(3, 9)
# apply_power_to_main_engine(power)
# pause_process(0.5)
if CHECK_MEAN_OUTPUT:
mean_power = ((count - 1) * mean_power + power) / count
print(f"Mean power: {mean_power:.2f}")
Code Block #9
import numpy # Note that we normally use the alias np
# For optimal primary engine performance, set flag to False
CHECK_MEAN_OUTPUT = True
if CHECK_MEAN_OUTPUT:
mean_power = 0
count = 0
while True:
if CHECK_MEAN_OUTPUT:
count += 1
# Set and apply new power output
power = numpy.random.randint(3, 9)
# apply_power_to_main_engine(power)
# pause_process(0.5)
if CHECK_MEAN_OUTPUT:
mean_power = ((count - 1) * mean_power + power) / count
print(f"Mean power: {mean_power:.2f}")
Code Block #10
import numpy # Note that we normally use the alias np
# For optimal primary engine performance, set flag to False
CHECK_MEAN_OUTPUT = True
if CHECK_MEAN_OUTPUT:
mean_power = 0
count = 0
rng = numpy.random.default_rng()
while True:
if CHECK_MEAN_OUTPUT:
count += 1
# Set and apply new power output
power = rng.integers(3, 9)
# apply_power_to_main_engine(power)
# pause_process(0.5)
if CHECK_MEAN_OUTPUT:
mean_power = ((count - 1) * mean_power + power) / count
print(f"Mean power: {mean_power:.2f}")
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
And you can find out more about me at stephengruppetta.com