Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 26 of 898 min

Modules: splitting a script across files without breaking it

Every .py file is already a module

Put this in money.py:

python
TAX_RATE = 0.18

def with_tax(amount):
    return round(amount * (1 + TAX_RATE), 2)

And in main.py, in the same folder:

python
import money

print(money.with_tax(100))     # 118.0

That is the whole mechanism. No registration, no build step. The file name without .py is the module name.

Three ways to bring things in:

python
import money                       # money.with_tax(...)
from money import with_tax         # with_tax(...)
import money as m                  # m.with_tax(...)

The first keeps the source of the name visible at every call, which is worth more than the characters it costs. The second is fine for a handful of names you use constantly. Avoid from money import *: it dumps every public name into your file, so nobody can tell where anything came from, and a new name in money can silently shadow one of yours.

What import actually does

The first time a module is imported, Python finds the file, executes the whole file top to bottom, and stores the resulting module object in sys.modules. Every later import of the same name finds it there and does not re-run anything.

Two consequences.

Top-level code runs on import. If money.py ends with print(with_tax(100)), that line runs the moment anybody imports the module — including a test runner and a documentation tool. Which is why every script that is also importable ends with:

python
def main():
    print(with_tax(100))

if __name__ == "__main__":
    main()

__name__ is "__main__" when the file is the one you ran, and the module's own name when it was imported. The guard means "only do this when I am the program, not when I am a library".

Re-importing does not pick up edits. In a long-running REPL or notebook, editing money.py and re-running import money changes nothing, because the module is cached. Restart the interpreter, or use importlib.reload(money), which has enough caveats that restarting is usually quicker.

Where Python looks

sys.path is the list of folders searched, in order: the folder containing the script you ran, then anything in the PYTHONPATH environment variable, then the installed packages of the current environment.

python
import sys; print(sys.path)

Two failures come straight out of that list.

ModuleNotFoundError for your own file usually means you ran Python from a different folder than the one the file is in. The first entry of sys.path is the script's folder, not your current directory — so python3 tools/run.py can import things next to run.py but not things next to you.

Your file shadows a real module. Name a file random.py, json.py, email.py or types.py and your file wins, because the script's own folder is searched first. The error that follows is bizarre — AttributeError: module 'random' has no attribute 'randint' — and the fix is to rename your file and delete the __pycache__ folder that still holds the compiled shadow.

Circular imports

a.py imports b, and b.py imports a. The second import finds a half-executed module and fails with ImportError: cannot import name 'x' from partially initialized module.

This is a design signal, not a puzzle to solve. Either the two modules are really one, or a third module holds what they share. Moving the import inside the function that needs it defers the problem and works, and it is a patch rather than a fix.

Two commands that end most import arguments

python
import money
print(money.__file__)      # exactly which file got loaded
print(dir(money))          # every name the module defines

__file__ settles "which version am I running" in one line — the answer is frequently a copy in a different folder, or a package installed in another environment. dir() shows what is actually available, which beats guessing at a name from a tutorial written for an older release.

The standard library is large and already installed

Before adding a dependency, check whether Python ships it: json, csv, pathlib, datetime, re, math, random, collections, itertools, statistics, sqlite3, urllib, unittest, logging, argparse, subprocess, zipfile, hashlib, secrets. A working SQL database, a JSON parser and a web client all come free with the interpreter.

Every dependency you do not add is one you never have to upgrade, audit or explain.

Try this now

Split any script you have written into two files: one with the functions, one that imports them and runs. Add the __name__ guard to both. Then rename the function file to json.py and run it, so you see the shadowing failure once, on purpose, when it is harmless.

The one thing to keep

An import executes the module once and caches it, and Python searches the script's own folder first, which is why naming a file `json.py` breaks the real one.

Before you move on

A colleague adds `analysis.py` to a project folder and the previously working `import numpy` in another file starts failing with an AttributeError deep inside numpy. They swear they changed nothing else. What is the first thing to check?

Pick the one you would defend. Nobody sees your answer.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly