Introduction
Python is an interpreted language which means that anything fed to the Python interpreter will be instantly interpreted or executed. For very simple scripts, that’s all we need.
For example, here’s a script that takes an input string and returns whether the string is an isogram. An isogram is a word with no repeating letters, no matter the letter case.
Yuri
and Jim
are isograms.
Jerry
and Bob
are not isograms.
# isogram.py
s = input("Enter a word: ")
letters_without_duplicates = set(s.lower())
if len(s) == len(letters_without_duplicates):
print(f"'{s}' is an isogram")
else:
print("Not an isogram")
Here’s a sample execution
> python isogram.py
Enter a word: Yuri
'Yuri' is an isogram
> python isogram.py
Enter a word: Bob
Not an isogram
In the script, we are grabbing input from the terminal and storing it into the variable s
.
Since we want the test to be case insensitive, we can get the all lowercase version of the string by calling s.lower()
.
The result of s.lower()
is passed to the set()
function which returns a set
. A set is like a list where all duplicates are removed.
We then compare the length of our original string s
and the set
of all letters. If equal, we have an isogram.
What if you want to test whether a string is an isogram from some other script?
You could copy paste these lines into that script but then you’ve duplicated the logic in two places which is not good practice. If the logic ever needs to change to allow case sensitive isogram tests, then that change now has to be made in two places instead of one.
A good way to organize the steps to do something in a reusable way is to put those steps inside a named function.
def is_isogram(s):
letters_without_duplicates = set(s.lower())
return len(s) == len(letters_without_duplicates)
s = input("Enter a word: ")
if is_isogram(s):
print(f"'{s}' is an isogram")
else:
print("Not an isogram")
Notice how putting the isogram testing inside a function makes the main logic of the program more readable.
But something strange happens when one tries to import the is_isogram
function into another script to use it.
# other.py
import isogram
print(isogram.is_isogram("Yuri"))
One sees the prompt Enter a word
show up!
> python other.py
Enter a word:
This brings us to something every python developer should be aware of.
Whenever you import something, all the top level statements in that module are executed by the interpreter.
Top level means everything that is not under a class or a function. In isogram.py
, the function definition, the input assigment into s and the conditional statements are all top level so they are executed.
The function definition is something we want the python interpreter to know about since we want to eventually use that function. But we don’t want the input part to run when we import the module.
To solve this problem, we use a main function.
The Main Function
Python, unlike other languages, doesn’t force the programmer to use a main function as a program’s entry point. But we can easily design our scripts along that convention.
Sometimes we just want to be able to import a function or a variable from another python script for re-use, but without actually running the main work of that script.
To do this, the script needs some way of knowing the context of its own execution.
Am I being used to import stuff? Am I being run as the main process?
Python provides the __name__
special variable to answer this question.
> python
Python 3.9.1 (tags/v3.9.1:1e5d33e, Dec 7 2020, 17:08:21) [MSC v.1927 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> __name__
'__main__'
The variable just holds a string indicating the context.
Lets add a print(__name__)
statement at the bottom of isogram.py
and other.py
and comment out all other statements except the function definition and import statement.
def is_isogram(s):
letters_without_duplicates = set(s.lower())
return len(s) == len(letters_without_duplicates)
# s = input("Enter a word: ")
# if is_isogram(s):
# print(f"'{s}' is an isogram")
# else:
# print("Not an isogram")
print(__name__)
import isogram
# print(isogram.is_isogram("isogram"))
print(__name__)
If you run isogram.py
directly, you’ll see
> python isogram.py
__main__
Running other.py
gives
> python other.py
isogram
__main__
Since the import statement runs before the print, isogram
is being printed by isogram.py
and __main__
is printed from other.py
.
__name__
will be __main__
only inside the script you’re directly passing to the python interpreter!
We can now use it to establish our main function. The main function only runs when we directly invoke the script by passing it to the python interpreter.
def main():
s = input("Enter a word: ")
if is_isogram(s):
print(f"'{s}' is an isogram")
else:
print("Not an isogram")
def is_isogram(s):
letters_without_duplicates = set(s.lower())
return len(s) == len(letters_without_duplicates)
if __name__ == "__main__":
main() # runs only when script directly invoked!
By this simple change, we can run other.py
and see the expected output.
# other .py
import isogram
print(isogram.is_isogram("Yuri")) # prints True
Script Scaffolding
Whenever creating a python script that contains anything that may be imported in the future for re-use by other scripts, always use this standard layout:
# scaffolding.py
# import statements
def main():
pass # placeholder
# what the script does when directly invoked
# helper functions
# def a():
#...
# def b():
# ...
# ...
if __name__ == "__main__":
main()
With this script layout, one can avoid the unexpected side effects of top level statements when importing the module.