Topic 6.2
Writing Your Own Modules
In one line
Any .py file is a module you can import. The line if __name__ == "__main__": lets one file work both as a program and as a library, and sys.path is the list of folders Python searches, which is why a file named after a standard module can break your program.
Think of it like this
A box of recipe cards. Each card is one file. You can cook straight from a card, or another recipe can say "for the sauce, use the card called Tomato Sauce". A card that is also a full meal can have a line at the bottom: "if you are cooking this card directly, make the whole meal". That last line is if __name__ == "__main__":.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- __name__
- A variable every module has. It holds the module's name, or
"__main__"if the file is the program being run. - Main guard
- The pattern
if __name__ == "__main__":. Its block runs only when the file is run directly. - Script
- A file you run directly, with
python file.py. - Library module
- A file meant to be imported by others. It should do nothing visible when imported.
- sys.path
- The list of folders Python searches, in order, to find a module to import.
- Shadowing
- When a name in an earlier-searched place hides one with the same name in a later place. A local
random.pyshadows the standardrandom. - __file__
- A module variable holding the path of the file the module was loaded from.
- runpy
- A standard-library module that runs a file or module the way
python file.pyorpython -m namewould.
Step by step
01A file is a module
Create calc.py next to your main file. Everything you define in it becomes an attribute of the module.
Nothing else is needed: no registration, no special header. The only rule is that the file lives somewhere Python searches (Step 5).
def add(a, b):
return a + b
TAX_RATE = 0.1802Import it from another file
In a second file in the same folder, import it with its file name (no .py). You reach its contents with a dot. Python also creates a __pycache__ folder to store the compiled bytecode (Topic 0.8) for faster loading next time; you can ignore that folder and keep it out of git.
import calc
print(calc.add(2, 3))
print(calc.TAX_RATE)03The two lives of a file: __name__
Add a print of __name__ to calc.py. Run python calc.py and it shows __main__. Import it from main.py and it shows calc. Same file, two different values, decided by *how it was started*.
That is the whole mechanism. The main-guard is an ordinary if that compares this variable to a string.
def add(a, b):
return a + b
print("calc: my __name__ is", __name__)
if __name__ == "__main__":
print("calc: run directly, self-test:", add(2, 3))04The standard shape of a script
Put the work in functions, a main() at the bottom, and call it under the guard. The file is then safe to import (for tests, or reuse), and python report.py still does its job. The guard is the line between "definitions" and "actually doing something".
sys.exit(main()) is a common refinement. An exit code is a number a program hands back to the terminal when it ends: 0 means "all went well", anything else means "something failed". Here main returns that number, and sys.exit passes it on.
import sys
def build_report(rows):
return f"{len(rows)} rows"
def main():
print(build_report([1, 2, 3]))
return 0
if __name__ == "__main__":
sys.exit(main())05Where Python looks: sys.path
When you write import notes, Python walks through sys.path and, in each folder, looks for notes.py or a notes package. The first hit wins and the search stops. Your script's folder is first, then the standard library, then site-packages (where pip and uv put installed packages).
If the file is not in any of those folders, you get ModuleNotFoundError, even though the file exists on your disk. The fix is to put the module where Python looks, not to hope Python finds it.
06Name shadowing: the file called statistics.py
Suppose a learner saves a practice file as statistics.py and, in the same folder, writes import statistics to use statistics.median. Python finds their own file first. It never reaches the standard library. The error says the module has no attribute median, and the learner stares at a module that clearly has one in the documentation.
Clues that you are shadowing: an AttributeError on a standard module, __file__ pointing at your own folder, or weird behaviour right after you created a new file. Rename your file (and delete the __pycache__ folder beside it).
import statistics
print(statistics.__file__) # points at YOUR statistics.py if it shadows
print(statistics.median([1, 2, 3]))07Reading a module's own variables
A module is an object you can inspect. module.__name__, module.__file__ and dir(module) (the list of its names) are the quickest ways to find out what you really imported and where it came from. When an import behaves strangely, print __file__ first.
import calc
print(calc.__name__) # calc
print(calc.__file__) # the path of calc.py
print([n for n in dir(calc) if not n.startswith("_")]) # ['TAX_RATE', 'add']Try it yourself
- 1
Remove the guard
In the first example, change the module text so the line
print("calc: run directly, self-test:", add(2, 3))is not inside theif. Run it. Now the self-test message appears on import too. That is the exact bug the guard prevents. - 2
Shadow something else
In the second example, change
statisticstocalendar(and the function name tomonth). It behaves the same way, as long as the real module was not imported earlier in the program. Tryjsonand see why it fails differently: it is already loaded, sosys.modulesanswers the import first. - 3
Predict the path
In the third example, predict the last item of
sys.pathbefore you run it, then changeappendtosys.path.insert(0, "tools")and predict again. Insert at the front means your folder is searched first.
Code & diagrams
The file `calc.py` is written from the program itself, so you can see both lives. `runpy.run_path(..., run_name="__main__")` runs the file exactly like `python calc.py` would, so `__name__` becomes `__main__` and the guarded block runs.
Expected output
main program: my __name__ is __main__
calc: my __name__ is calc
imported, add(4, 5) = 9
--- now run calc.py like 'python calc.py' ---
calc: my __name__ is __main__
calc: run directly, self-test: 5The example writes its own `statistics.py` before the real one is ever imported, so Python finds the local file first. That is exactly what happens when you save a practice file with a standard module's name. (`try`/`except AttributeError` means: try the call, and if the name is missing, print the error instead of crashing. Topic 7.1 teaches it.)
Expected output
statistics loaded yet: False
file used: statistics.py
mean: not the real mean
AttributeError: module 'statistics' has no attribute 'median'Before the `sys.path.append`, the module in `tools/` is invisible. After it, the import works. This is a quick fix in scripts and a poor design in real projects (Topic 6.8 shows the better way).
Expected output
ModuleNotFoundError: No module named 'notes'
found me in tools/
search path ends with: toolsBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Name your practice file after a standard module
Save a file called statistics.py containing only print("practice"), then in another file in the same folder write import statistics and call statistics.mean([1, 2, 3]).
import statistics
print(statistics.mean([1, 2, 3]))Break #2
Run a package module like a script
Inside a package (Topic 6.3), write from .util import X in main.py and run python pkg/main.py.
from .util import X
print(X)Myth vs fact
Myth
if __name__ == "__main__": is required in every script.
Fact
It is optional. It matters when a file may also be imported, and it is good habit because it makes a file safe to import and test.
Myth
Python searches the whole computer for modules.
Fact
It searches only the folders in sys.path, in order. A file elsewhere on your disk is invisible.
Myth
Naming my file random.py is fine because I wrote it first.
Fact
Your folder is searched before the standard library, so your file hides the real module for your whole program, and for any library that imports it.
Myth
__main__ is a special keyword.
Fact
It is an ordinary string. Python sets __name__ to that string for the file it was asked to run.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
python -m package.moduleruns a module by its import name. It puts the current directory onsys.pathand sets up the package context, so relative imports work. That is whypython -m pytestcan import your project's modules from the current folder while a barepytestcommand sometimes cannot. Prefer-mfor anything inside a package. - ▸
A subtle trap: the file you run is registered as
__main__, not under its own name. Ifpython app.pyruns a file that another module later imports asimport app, Python loads the file a second time as a separate moduleapp. You then have two copies of every class and global, andisinstancechecks between them fail. This is one more reason to keep the main file thin and put real code in importable modules. - ▸
Search order in full: modules already in
sys.modules, then built-in modules compiled into the interpreter (sys.builtin_module_names, such assysandbuiltins), then the entries ofsys.pathin order. The script folder (or current directory for-m) comes first, then anyPYTHONPATHfolders, then the standard library, thensite-packages. Because built-in modules are found before the path is searched, a file namedsys.pycannot shadowsys, though one namedrandom.pycan shadowrandom. - ▸
Since Python 3.11 you can set
PYTHONSAFEPATH=1or runpython -Pto stop the script's folder from being added to the front ofsys.path. It exists precisely to avoid shadowing surprises and unwanted imports in security-sensitive setups. - ▸
When a module seems to be the wrong one,
python -c "import x; print(x.__file__)"andpython -v(verbose imports) show what was loaded and from where.importlib.util.find_spec("x")tells you where a module *would* load from without importing it. - ▸
Stale
.pycfiles are rarely a problem: Python checks the source file's timestamp and size recorded in the.pyc(or a hash, if the file was built that way) and recompiles when they differ. A.pycinside__pycache__whose source is gone is simply ignored, so deleting__pycache__is tidying, not a repair. The one exception is a legacy.pycplaced directly beside where the source used to be, which Python will still import.
Remember this
- 1
Writing a module needs no special step. Save
calc.pywith a functionadd, and in another file in the same folder writeimport calcand callcalc.add(2, 3). The module name is the file name without.py, so choose names that are lowercase, short and valid as Python names (no spaces, no dashes, not starting with a digit). - 2
Every module has a built-in variable
__name__. If the file is imported,__name__is the module's name ("calc"). If the file is run directly (python calc.py), Python sets__name__to the special string"__main__". The guardif __name__ == "__main__":therefore means "only when run as the main program". Code inside it is skipped on import, which is how a file can offer both functions to others and a command-line behaviour for you. - 3
Without the guard, anything at the top level of a module runs on import. A module that prints, reads input or starts a server at top level is a trap for everyone who imports it. The usual shape is: imports, constants, functions and classes at the top, and a small
main()function called under the guard at the bottom. - 4
Recall from Topic 6.1: Python first checks the
sys.modulescache. If the module is not there, it searches the listsys.path, in order, and uses the first match. The list normally starts with the folder of the script you ran (or the current folder in the interactive shell, Topic 0.5), then the standard library folders, thensite-packageswhere installed packages live. The order is why your own folder wins over the standard library. - 5
That winning order is the source of the most confusing beginner bug: a file you named
random.py,statistics.py,email.pyorjson.pyshadows the standard module of the same name, because your folder is searched first.import randomthen loads your file, and everything in the real module "disappears". The error messages talk about names missing from a module you never wrote, which is why it feels so strange. (An "attribute" is a name that lives inside another object, such assqrtinsidemath.) Never name your files after standard-library or popular package names. - 6
You can add a folder to the search path with
sys.path.append("tools")or with thePYTHONPATHenvironment variable (a named setting that the operating system hands to every program you start). Both work, but both are a warning sign in a real project. They make the program depend on setup that is not written in the code. Topic 6.8 shows the clean way, installing your project as a package so imports work from anywhere.
Explain it without notes
What does if __name__ == "__main__": do, and why do people use it?
How does Python decide which file to load for import foo?
Why can naming your file random.py break a program, and how do you diagnose it?
What is the difference between python calc.py and python -m calc?
Practice
Write a file temperature.py with a function to_fahrenheit(c) that turns degrees Celsius into Fahrenheit. Add a main guard that prints to_fahrenheit(100) only when the file is run directly. Then, from your program, import it and call to_fahrenheit(37).
Write a program that prints I am the main program when it is run directly, and I was imported when another file imports it. Use __name__ and an if/else.
Make a folder libs containing a module greeting.py with one function hello() that returns "hello". Add the folder libs to sys.path, import greeting, and print greeting.hello().
Trade-offs
- ↔
A script with the guard vs one without. The guard costs two extra lines and an indent and makes the file importable and testable. Without it, a file is fine as a throwaway but can never be reused safely.
- ↔
**Editing
sys.pathvs installing the project as a package.**sys.path.appendfixes imports in one line but only for that run and that machine. Installing the project (Topic 6.8) fixes it everywhere, including tests and other people's computers.
Done when you can
Done when you can write a module, import it from another file, and use its functions.
Done when you can explain what
__name__is in both cases and write the main-guard from memory.Done when you can name the first three places Python looks for a module.
Done when you can recognise name shadowing and fix it by renaming.
Done when you can say why
python -m pkg.moduleexists.