Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~29 min· topic 2 of 9

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.py shadows the standard random.
__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.py or python -m name would.

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).

calc.pywhole filepython
def add(a, b):
    return a + b


TAX_RATE = 0.18

02Import 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.

main.pywhole filepython
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.

calc.pywhole filepython
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))
terminal
$ python calc.py
── expected output ──
calc: my __name__ is __main__
calc: run directly, self-test: 5

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.

report.pywhole filepython
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.

Where Python looks: sys.pathdiagram
Rendering diagram…

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).

main.pywhole filepython
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.

main.pywhole filepython
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. 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 the if. Run it. Now the self-test message appears on import too. That is the exact bug the guard prevents.

  2. 2

    Shadow something else

    In the second example, change statistics to calendar (and the function name to month). It behaves the same way, as long as the real module was not imported earlier in the program. Try json and see why it fails differently: it is already loaded, so sys.modules answers the import first.

  3. 3

    Predict the path

    In the third example, predict the last item of sys.path before you run it, then change append to sys.path.insert(0, "tools") and predict again. Insert at the front means your folder is searched first.

Code & diagrams

One file, two lives New tab

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.

Sign in to run this example in your browser.

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: 5
Shadowing the standard library New tab

The 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.)

Sign in to run this example in your browser.

Expected output

statistics loaded yet: False
file used: statistics.py
mean: not the real mean
AttributeError: module 'statistics' has no attribute 'median'
Putting a folder on sys.path New tab

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).

Sign in to run this example in your browser.

Expected output

ModuleNotFoundError: No module named 'notes'
found me in tools/
search path ends with: tools

Break 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]).

main.pywhole filepython
import statistics

print(statistics.mean([1, 2, 3]))
terminal
$ python main.py
── what you'll see ──
practice
Traceback (most recent call last):
File "main.py", line 3, in <module>
print(statistics.mean([1, 2, 3]))
^^^^^^^^^^^^^^^
AttributeError: module 'statistics' has no attribute 'mean'

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.

pkg/main.pywhole filepython
from .util import X

print(X)
terminal
$ python pkg/main.py
── what you'll see ──
Traceback (most recent call last):
File "pkg/main.py", line 1, in <module>
from .util import X
ImportError: attempted relative import with no known parent package

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.module runs a module by its import name. It puts the current directory on sys.path and sets up the package context, so relative imports work. That is why python -m pytest can import your project's modules from the current folder while a bare pytest command sometimes cannot. Prefer -m for anything inside a package.

  • ▸

    A subtle trap: the file you run is registered as __main__, not under its own name. If python app.py runs a file that another module later imports as import app, Python loads the file a second time as a separate module app. You then have two copies of every class and global, and isinstance checks 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 as sys and builtins), then the entries of sys.path in order. The script folder (or current directory for -m) comes first, then any PYTHONPATH folders, then the standard library, then site-packages. Because built-in modules are found before the path is searched, a file named sys.py cannot shadow sys, though one named random.py can shadow random.

  • ▸

    Since Python 3.11 you can set PYTHONSAFEPATH=1 or run python -P to stop the script's folder from being added to the front of sys.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__)" and python -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 .pyc files 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 .pyc inside __pycache__ whose source is gone is simply ignored, so deleting __pycache__ is tidying, not a repair. The one exception is a legacy .pyc placed directly beside where the source used to be, which Python will still import.

Remember this

  1. 1

    Writing a module needs no special step. Save calc.py with a function add, and in another file in the same folder write import calc and call calc.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. 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 guard if __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. 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. 4

    Recall from Topic 6.1: Python first checks the sys.modules cache. If the module is not there, it searches the list sys.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, then site-packages where installed packages live. The order is why your own folder wins over the standard library.

  5. 5

    That winning order is the source of the most confusing beginner bug: a file you named random.py, statistics.py, email.py or json.py shadows the standard module of the same name, because your folder is searched first. import random then 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 as sqrt inside math.) Never name your files after standard-library or popular package names.

  6. 6

    You can add a folder to the search path with sys.path.append("tools") or with the PYTHONPATH environment 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

01

What does if __name__ == "__main__": do, and why do people use it?

02

How does Python decide which file to load for import foo?

03

Why can naming your file random.py break a program, and how do you diagnose it?

04

What is the difference between python calc.py and python -m calc?

Practice

01

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).

02

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.

03

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.path vs installing the project as a package.** sys.path.append fixes 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.module exists.